From 1a18bb939d3cf20f81e5dcb886213e04db883a7f Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 13:55:16 -0400 Subject: [PATCH] Refactor and write unit tests for RepetitionList --- .../unit/models/CheckmarkListTest.java | 31 +--- .../uhabits/unit/models/HabitFixtures.java | 60 +++++++ .../isoron/uhabits/unit/models/HabitTest.java | 3 +- .../unit/models/RepetitionListTest.java | 162 ++++++++++++++++++ .../uhabits/HabitBroadcastReceiver.java | 3 +- .../isoron/uhabits/models/RepetitionList.java | 51 ++++-- 6 files changed, 268 insertions(+), 42 deletions(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java index a952f2887..5c017da75 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java @@ -42,33 +42,13 @@ public class CheckmarkListTest Habit nonDailyHabit; private Habit emptyHabit; - public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) - @Before public void prepare() { - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME); - createNonDailyHabit(); - - emptyHabit = new Habit(); - emptyHabit.save(); - } - - private void createNonDailyHabit() - { - nonDailyHabit = new Habit(); - nonDailyHabit.freqNum = 2; - nonDailyHabit.freqDen = 3; - nonDailyHabit.save(); - - boolean check[] = { true, false, false, true, true, true, false, false, true, true }; - - long timestamp = DateHelper.getStartOfToday(); - for(boolean c : check) - { - if(c) nonDailyHabit.repetitions.toggle(timestamp); - timestamp -= DateHelper.millisecondsInOneDay; - } + HabitFixtures.purgeHabits(); + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME); + nonDailyHabit = HabitFixtures.createNonDailyHabit(); + emptyHabit = HabitFixtures.createEmptyHabit(); } @After @@ -162,6 +142,7 @@ public class CheckmarkListTest private void travelInTime(int days) { - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME + days * DateHelper.millisecondsInOneDay); + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME + + days * DateHelper.millisecondsInOneDay); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java new file mode 100644 index 000000000..3cbea7a0d --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.models; + +import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.models.Habit; + +public class HabitFixtures +{ + public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) + public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false, + false, true, true }; + + static Habit createNonDailyHabit() + { + Habit habit = new Habit(); + habit.freqNum = 2; + habit.freqDen = 3; + habit.save(); + + long timestamp = DateHelper.getStartOfToday(); + for(boolean c : NON_DAILY_HABIT_CHECKS) + { + if(c) habit.repetitions.toggle(timestamp); + timestamp -= DateHelper.millisecondsInOneDay; + } + + return habit; + } + + static Habit createEmptyHabit() + { + Habit habit = new Habit(); + habit.save(); + return habit; + } + + static void purgeHabits() + { + for(Habit h : Habit.getAll(true)) + h.cascadeDelete(); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java index 7d2f2b3c3..d46826974 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java @@ -40,8 +40,7 @@ public class HabitTest @Before public void prepare() { - for(Habit h : Habit.getAll(true)) - h.cascadeDelete(); + HabitFixtures.purgeHabits(); } @Test diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java new file mode 100644 index 000000000..1c30c28d0 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.models; + +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.models.Habit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Random; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RepetitionListTest +{ + Habit habit; + private Habit emptyHabit; + + @Before + public void prepare() + { + HabitFixtures.purgeHabits(); + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME); + habit = HabitFixtures.createNonDailyHabit(); + emptyHabit = HabitFixtures.createEmptyHabit(); + } + + @After + public void tearDown() + { + DateHelper.setFixedLocalTime(null); + } + + @Test + public void contains_testNonDailyHabit() + { + long current = DateHelper.getStartOfToday(); + + for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS) + { + assertThat(habit.repetitions.contains(current), equalTo(b)); + current -= DateHelper.millisecondsInOneDay; + } + + for(int i = 0; i < 3; i++) + { + assertThat(habit.repetitions.contains(current), equalTo(false)); + current -= DateHelper.millisecondsInOneDay; + } + } + + @Test + public void delete_test() + { + long timestamp = DateHelper.getStartOfToday(); + assertThat(habit.repetitions.contains(timestamp), equalTo(true)); + + habit.repetitions.delete(timestamp); + assertThat(habit.repetitions.contains(timestamp), equalTo(false)); + } + + @Test + public void toggle_test() + { + long timestamp = DateHelper.getStartOfToday(); + assertThat(habit.repetitions.contains(timestamp), equalTo(true)); + + habit.repetitions.toggle(timestamp); + assertThat(habit.repetitions.contains(timestamp), equalTo(false)); + + habit.repetitions.toggle(timestamp); + assertThat(habit.repetitions.contains(timestamp), equalTo(true)); + } + + @Test + public void getWeekDayFrequency_test() + { + Random random = new Random(); + Integer weekdayCount[][] = new Integer[12][7]; + Integer monthCount[] = new Integer[12]; + + Arrays.fill(monthCount, 0); + for(Integer row[] : weekdayCount) + Arrays.fill(row, 0); + + GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); + + // Sets the current date to the end of November + day.set(2015, 10, 30); + DateHelper.setFixedLocalTime(day.getTimeInMillis()); + + // Add repetitions randomly from January to December + // Leaves the month of March empty, to check that it returns null + day.set(2015, 0, 1); + for(int i = 0; i < 365; i ++) + { + if(random.nextBoolean()) + { + int month = day.get(Calendar.MONTH); + int week = day.get(Calendar.DAY_OF_WEEK) % 7; + + if(month != 2) + { + if (month <= 10) + { + weekdayCount[month][week]++; + monthCount[month]++; + } + emptyHabit.repetitions.toggle(day.getTimeInMillis()); + } + } + + day.add(Calendar.DAY_OF_YEAR, 1); + } + + HashMap freq = emptyHabit.repetitions.getWeekdayFrequency(); + + // Repetitions until November should be counted correctly + for(int month = 0; month < 11; month++) + { + day.set(2015, month, 1); + Integer actualCount[] = freq.get(day.getTimeInMillis()); + if(monthCount[month] == 0) + assertThat(actualCount, equalTo(null)); + else + assertThat(actualCount, equalTo(weekdayCount[month])); + } + + // Repetitions in December should be discarded + day.set(2015, 11, 1); + assertThat(freq.get(day.getTimeInMillis()), equalTo(null)); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index a43757c84..c4969b835 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -38,6 +38,7 @@ import android.support.v4.content.LocalBroadcastManager; import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.ReminderHelper; +import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; import java.util.Date; @@ -145,7 +146,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday()); - if (habit.repetitions.hasImplicitRepToday()) return; + if (habit.checkmarks.getTodayValue() != Checkmark.UNCHECKED) return; habit.highlight = 1; habit.save(); diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index fe7b72879..ebdf35c66 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -47,6 +47,7 @@ public class RepetitionList { return new Select().from(Repetition.class) .where("habit = ?", habit.getId()) + .and("timestamp <= ?", DateHelper.getStartOfToday()) .orderBy("timestamp"); } @@ -55,12 +56,23 @@ public class RepetitionList return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo); } + /** + * Checks whether there is a repetition at a given timestamp. + * + * @param timestamp the timestamp to check + * @return true if there is a repetition + */ public boolean contains(long timestamp) { int count = select().where("timestamp = ?", timestamp).count(); return (count > 0); } + /** + * Deletes the repetition at a given timestamp, if it exists. + * + * @param timestamp the timestamp of the repetition to delete + */ public void delete(long timestamp) { new Delete().from(Repetition.class) @@ -69,11 +81,12 @@ public class RepetitionList .execute(); } - public Repetition getOldestNewerThan(long timestamp) - { - return select().where("timestamp > ?", timestamp).limit(1).executeSingle(); - } - + /** + * Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists + * or creates one if it does not. + * + * @param timestamp the timestamp of the repetition to toggle + */ public void toggle(long timestamp) { timestamp = DateHelper.getStartOfDay(timestamp); @@ -95,18 +108,27 @@ public class RepetitionList habit.streaks.deleteNewerThan(timestamp); } + /** + * Returns the oldest repetition for the habit. If there is no repetition, returns null. + * Repetitions in the future are discarded. + * + * @return oldest repetition for the habit + */ public Repetition getOldest() { return (Repetition) select().limit(1).executeSingle(); } - public boolean hasImplicitRepToday() - { - long today = DateHelper.getStartOfToday(); - int reps[] = habit.checkmarks.getValues(today - DateHelper.millisecondsInOneDay, today); - return (reps[0] > 0); - } - + /** + * Returns the total number of repetitions for each month, from the first repetition until + * today, grouped by day of week. The repetitions are returned in a HashMap. The key is the + * timestamp for the first day of the month, at midnight (00:00). The value is an integer + * array with 7 entries. The first entry contains the total number of repetitions during + * the specified month that occurred on a Saturday. The second entry corresponds to Sunday, + * and so on. If there are no repetitions during a certain month, the value is null. + * + * @return total number of repetitions by month versus day of week + */ public HashMap getWeekdayFrequency() { Repetition oldestRep = getOldest(); @@ -116,10 +138,11 @@ public class RepetitionList "strftime('%m', timestamp / 1000, 'unixepoch') as month," + "strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " + "count(*) from repetitions " + - "where habit = ? " + + "where habit = ? and timestamp <= ? " + "group by year, month, weekday"; - String[] params = { habit.getId().toString() }; + String[] params = { habit.getId().toString(), + Long.toString(DateHelper.getStartOfToday()) }; SQLiteDatabase db = Cache.openDatabase(); Cursor cursor = db.rawQuery(query, params);