Add tests for HabitCardListCache; refactor TaskRunners

pull/151/head
Alinson S. Xavier 9 years ago
parent d54de9df89
commit 37a9e793e7

@ -23,6 +23,7 @@ import android.content.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.tasks.*;
import javax.inject.*;
@ -56,4 +57,11 @@ public class AndroidModule
{
return HabitsApplication.getContext();
}
@Provides
@Singleton
static TaskRunner provideTaskRunner()
{
return new AndroidTaskRunner();
}
}

@ -89,7 +89,7 @@ public abstract class HabitList implements Iterable<Habit>
* @return the habit at that position
* @throws IndexOutOfBoundsException when the position is invalid
*/
@Nullable
@NonNull
public abstract Habit getByPosition(int position);
/**

@ -0,0 +1,120 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks;
import android.os.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
@Singleton
public class AndroidTaskRunner implements TaskRunner
{
private final LinkedList<CustomAsyncTask> activeTasks;
@Inject
public AndroidTaskRunner()
{
activeTasks = new LinkedList<>();
}
@Override
public void execute(Task task)
{
task.onAttached(this);
new CustomAsyncTask(task).execute();
}
@Override
public void publishProgress(Task task, int progress)
{
for (CustomAsyncTask asyncTask : activeTasks)
if (asyncTask.getTask() == task) asyncTask.publish(progress);
}
@Override
public void waitForTasks(long timeout)
throws TimeoutException, InterruptedException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
throw new UnsupportedOperationException("waitForTasks requires API 16+");
int poolInterval = 100;
while(timeout > 0)
{
if(activeTasks.isEmpty()) return;
timeout -= poolInterval;
Thread.sleep(poolInterval);
}
throw new TimeoutException();
}
private class CustomAsyncTask extends AsyncTask<Void, Integer, Void>
{
private final Task task;
public CustomAsyncTask(Task task)
{
this.task = task;
}
public Task getTask()
{
return task;
}
public void publish(int progress)
{
publishProgress(progress);
}
@Override
protected Void doInBackground(Void... params)
{
task.doInBackground();
return null;
}
@Override
protected void onPostExecute(Void aVoid)
{
task.onPostExecute();
activeTasks.remove(this);
}
@Override
protected void onProgressUpdate(Integer... values)
{
task.onProgressUpdate(values[0]);
}
@Override
protected void onPreExecute()
{
activeTasks.add(this);
task.onPreExecute();
}
}
}

@ -19,22 +19,29 @@
package org.isoron.uhabits.tasks;
/**
* Simple progress bar, used to indicate the progress of a task.
*/
public interface ProgressBar
{
/**
* Hides the progress bar.
*/
default void hide() {}
import java.util.concurrent.*;
default void setCurrent(int current) {}
public class SingleThreadTaskRunner implements TaskRunner
{
@Override
public void execute(Task task)
{
task.onAttached(this);
task.onPreExecute();
task.doInBackground();
task.onPostExecute();
}
default void setTotal(int total) {}
@Override
public void publishProgress(Task task, int progress)
{
task.onProgressUpdate(progress);
}
/**
* Shows the progress bar.
*/
default void show() {}
@Override
public void waitForTasks(long timeout)
throws TimeoutException, InterruptedException
{
// NOP
}
}

@ -19,99 +19,14 @@
package org.isoron.uhabits.tasks;
import android.os.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
@Singleton
public class TaskRunner
public interface TaskRunner
{
private final LinkedList<CustomAsyncTask> activeTasks;
@Inject
public TaskRunner()
{
activeTasks = new LinkedList<>();
}
public void execute(Task task)
{
task.onAttached(this);
new CustomAsyncTask(task).execute();
}
public void setCurrentProgress(Task task, int progress)
{
for (CustomAsyncTask asyncTask : activeTasks)
if (asyncTask.getTask() == task) asyncTask.publish(progress);
}
public void waitForTasks(long timeout)
throws TimeoutException, InterruptedException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
throw new UnsupportedOperationException("waitForTasks requires API 16+");
int poolInterval = 100;
while(timeout > 0)
{
if(activeTasks.isEmpty()) return;
timeout -= poolInterval;
Thread.sleep(poolInterval);
}
throw new TimeoutException();
}
private class CustomAsyncTask extends AsyncTask<Void, Integer, Void>
{
private final Task task;
public CustomAsyncTask(Task task)
{
this.task = task;
}
public Task getTask()
{
return task;
}
public void publish(int progress)
{
publishProgress(progress);
}
@Override
protected Void doInBackground(Void... params)
{
task.doInBackground();
return null;
}
@Override
protected void onPostExecute(Void aVoid)
{
task.onPostExecute();
activeTasks.remove(this);
}
void execute(Task task);
@Override
protected void onProgressUpdate(Integer... values)
{
task.onProgressUpdate(values[0]);
}
void publishProgress(Task task, int progress);
@Override
protected void onPreExecute()
{
activeTasks.add(this);
task.onPreExecute();
}
}
void waitForTasks(long timeout)
throws TimeoutException, InterruptedException;
}

@ -1,67 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.view.*;
import org.isoron.uhabits.tasks.*;
/**
* Android implementation of {@link ProgressBar}.
*/
public class AndroidProgressBar implements ProgressBar
{
private final android.widget.ProgressBar progressBar;
public AndroidProgressBar(android.widget.ProgressBar progressBar)
{
this.progressBar = progressBar;
}
@Override
public void hide()
{
progressBar.setVisibility(View.GONE);
}
@Override
public void setTotal(int total)
{
if(total == 0)
progressBar.setIndeterminate(true);
else
{
progressBar.setIndeterminate(false);
progressBar.setMax(total);
}
}
@Override
public void setCurrent(int current)
{
progressBar.setProgress(current);
}
@Override
public void show()
{
progressBar.setVisibility(View.VISIBLE);
}
}

@ -31,7 +31,6 @@ import android.support.v7.widget.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
@ -100,21 +99,6 @@ public abstract class BaseScreen
selectionMenu.finish();
}
/**
* Returns the progress bar that is currently visible on the screen.
* <p>
* If the root view attached to the screen does not provide any progress
* bars, returns null.
*
* @return current progress bar, or null if there are none.
*/
@Nullable
public ProgressBar getProgressBar()
{
if (rootView == null) return null;
return new AndroidProgressBar(rootView.getProgressBar());
}
/**
* Notifies the screen that its contents should be updated.
*/

@ -67,8 +67,6 @@ public class ListHabitsActivity extends BaseActivity
selectionMenu = new ListHabitsSelectionMenu(habits, screen, adapter);
controller = new ListHabitsController(habits, screen, system, adapter);
adapter.setProgressBar(
new AndroidProgressBar(rootView.getProgressBar()));
screen.setMenu(menu);
screen.setController(controller);
screen.setSelectionMenu(selectionMenu);

@ -25,7 +25,6 @@ import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.ui.habits.list.views.*;
import java.util.*;
@ -254,11 +253,6 @@ public class HabitCardListAdapter
this.listView = listView;
}
public void setProgressBar(ProgressBar progressBar)
{
cache.setProgressBar(progressBar);
}
/**
* Selects or deselects the item at a given position.
*

@ -43,7 +43,7 @@ public class HabitCardListCache implements CommandRunner.Listener
private Task currentFetchTask;
@Nullable
@NonNull
private Listener listener;
@NonNull
@ -63,6 +63,7 @@ public class HabitCardListCache implements CommandRunner.Listener
{
this.allHabits = allHabits;
this.filteredHabits = allHabits;
this.listener = new Listener() {};
data = new CacheData();
BaseComponent component = HabitsApplication.getComponent();
@ -145,7 +146,7 @@ public class HabitCardListCache implements CommandRunner.Listener
data.checkmarks.remove(id);
data.scores.remove(id);
if (listener != null) listener.onItemRemoved(position);
listener.onItemRemoved(position);
}
public void reorder(int from, int to)
@ -153,7 +154,7 @@ public class HabitCardListCache implements CommandRunner.Listener
Habit fromHabit = data.habits.get(from);
data.habits.remove(from);
data.habits.add(to, fromHabit);
if (listener != null) listener.onItemMoved(from, to);
listener.onItemMoved(from, to);
}
public void setCheckmarkCount(int checkmarkCount)
@ -166,29 +167,24 @@ public class HabitCardListCache implements CommandRunner.Listener
filteredHabits = allHabits.getFiltered(matcher);
}
public void setListener(@Nullable Listener listener)
public void setListener(@NonNull Listener listener)
{
this.listener = listener;
}
public void setProgressBar(@NonNull ProgressBar progressBar)
{
}
/**
* Interface definition for a callback to be invoked when the data on the
* cache has been modified.
*/
public interface Listener
{
void onItemChanged(int position);
default void onItemChanged(int position) {}
void onItemInserted(int position);
default void onItemInserted(int position) {}
void onItemMoved(int oldPosition, int newPosition);
default void onItemMoved(int oldPosition, int newPosition) {}
void onItemRemoved(int position);
default void onItemRemoved(int position) {}
}
private class CacheData
@ -267,7 +263,7 @@ public class HabitCardListCache implements CommandRunner.Listener
isCancelled = false;
}
public RefreshTask(Long targetId)
public RefreshTask(long targetId)
{
newData = new CacheData();
this.targetId = targetId;
@ -286,11 +282,11 @@ public class HabitCardListCache implements CommandRunner.Listener
newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data);
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long day = DateUtils.millisecondsInOneDay;
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * day;
runner.setCurrentProgress(this, -1);
runner.publishProgress(this, -1);
for (int position = 0; position < newData.habits.size(); position++)
{
@ -304,7 +300,7 @@ public class HabitCardListCache implements CommandRunner.Listener
newData.checkmarks.put(id,
habit.getCheckmarks().getValues(dateFrom, dateTo));
runner.setCurrentProgress(this, position);
runner.publishProgress(this, position);
}
}
@ -334,22 +330,32 @@ public class HabitCardListCache implements CommandRunner.Listener
data.id_to_habit.put(id, habit);
data.scores.put(id, newData.scores.get(id));
data.checkmarks.put(id, newData.checkmarks.get(id));
if (listener != null) listener.onItemInserted(position);
listener.onItemInserted(position);
}
private void performMove(Habit habit, int fromPosition, int toPosition)
{
data.habits.remove(fromPosition);
data.habits.add(toPosition, habit);
if (listener != null)
listener.onItemMoved(fromPosition, toPosition);
}
private void performUpdate(Long id, int position)
{
data.scores.put(id, newData.scores.get(id));
data.checkmarks.put(id, newData.checkmarks.get(id));
if (listener != null) listener.onItemChanged(position);
Integer oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id);
Integer newScore = newData.scores.get(id);
int[] newCheckmarks = newData.checkmarks.get(id);
boolean unchanged = true;
if (!oldScore.equals(newScore)) unchanged = false;
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false;
if(unchanged) return;
data.scores.put(id, newScore);
data.checkmarks.put(id, newCheckmarks);
listener.onItemChanged(position);
}
private void processPosition(int currentPosition)

@ -19,6 +19,7 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
@ -60,6 +61,9 @@ public class BaseUnitTest
@Inject
protected DirFinder dirFinder;
@Inject
protected CommandRunner commandRunner;
protected TestComponent testComponent;
protected HabitFixtures fixtures;

@ -19,11 +19,11 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.memory.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.ui.common.dialogs.*;
import org.isoron.uhabits.utils.*;
@ -36,13 +36,6 @@ import static org.mockito.Mockito.*;
@Module
public class TestModule
{
@Singleton
@Provides
CommandRunner provideCommandRunner()
{
return mock(CommandRunner.class);
}
@Provides
@Singleton
DialogFactory provideDialogFactory()
@ -125,4 +118,11 @@ public class TestModule
{
return mock(WidgetPreferences.class);
}
@Provides
@Singleton
TaskRunner provideTaskRunner()
{
return new SingleThreadTaskRunner();
}
}

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
@RunWith(JUnit4.class)
public class SingleThreadTaskRunnerTest extends BaseUnitTest
{
private SingleThreadTaskRunner runner;
private Task task;
@Override
public void setUp()
{
super.setUp();
runner = new SingleThreadTaskRunner();
task = mock(Task.class);
}
@Test
public void test()
{
runner.execute(task);
InOrder inOrder = inOrder(task);
inOrder.verify(task).onAttached(runner);
inOrder.verify(task).onPreExecute();
inOrder.verify(task).doInBackground();
inOrder.verify(task).onPostExecute();
}
}

@ -0,0 +1,179 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.model;
import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*;
import static org.mockito.Mockito.*;
public class HabitCardListCacheTest extends BaseUnitTest
{
private HabitCardListCache cache;
private HabitCardListCache.Listener listener;
@Override
public void setUp()
{
super.setUp();
fixtures.purgeHabits();
for (int i = 0; i < 10; i++)
{
if (i == 3) fixtures.createLongHabit();
else fixtures.createShortHabit();
}
cache = new HabitCardListCache(habitList);
cache.setCheckmarkCount(10);
cache.refreshAllHabits();
cache.onAttached();
listener = mock(HabitCardListCache.Listener.class);
cache.setListener(listener);
}
@Override
public void tearDown()
{
cache.onDetached();
super.tearDown();
}
@Test
public void testCommandListener_all()
{
assertThat(cache.getHabitCount(), equalTo(10));
Habit h = habitList.getByPosition(0);
commandRunner.execute(
new DeleteHabitsCommand(habitList, Collections.singletonList(h)),
null);
verify(listener).onItemRemoved(0);
assertThat(cache.getHabitCount(), equalTo(9));
}
@Test
public void testCommandListener_single()
{
Habit h2 = habitList.getByPosition(2);
long today = DateUtils.getStartOfToday();
commandRunner.execute(new ToggleRepetitionCommand(h2, today),
h2.getId());
verify(listener).onItemChanged(2);
verifyNoMoreInteractions(listener);
}
@Test
public void testGet()
{
assertThat(cache.getHabitCount(), equalTo(10));
Habit h = habitList.getByPosition(3);
assertNotNull(h.getId());
int score = h.getScores().getTodayValue();
assertThat(cache.getHabitByPosition(3), equalTo(h));
assertThat(cache.getScore(h.getId()), equalTo(score));
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
int[] actualCheckmarks = cache.getCheckmarks(h.getId());
int[] expectedCheckmarks =
h.getCheckmarks().getValues(today - 9 * day, today);
assertThat(actualCheckmarks, equalTo(expectedCheckmarks));
}
@Test
public void testRemoval()
{
removeHabitAt(0);
removeHabitAt(3);
cache.refreshAllHabits();
verify(listener).onItemRemoved(0);
verify(listener).onItemRemoved(3);
assertThat(cache.getHabitCount(), equalTo(8));
}
@Test
public void testReorder_onCache()
{
Habit h2 = cache.getHabitByPosition(2);
Habit h3 = cache.getHabitByPosition(3);
Habit h7 = cache.getHabitByPosition(7);
cache.reorder(2, 7);
assertThat(cache.getHabitByPosition(2), equalTo(h3));
assertThat(cache.getHabitByPosition(7), equalTo(h2));
assertThat(cache.getHabitByPosition(6), equalTo(h7));
verify(listener).onItemMoved(2, 7);
verifyNoMoreInteractions(listener);
}
@Test
public void testReorder_onList()
{
Habit h2 = habitList.getByPosition(2);
Habit h3 = habitList.getByPosition(3);
Habit h7 = habitList.getByPosition(7);
assertThat(cache.getHabitByPosition(2), equalTo(h2));
assertThat(cache.getHabitByPosition(7), equalTo(h7));
reset(listener);
habitList.reorder(h2, h7);
cache.refreshAllHabits();
assertThat(cache.getHabitByPosition(2), equalTo(h3));
assertThat(cache.getHabitByPosition(7), equalTo(h2));
assertThat(cache.getHabitByPosition(6), equalTo(h7));
verify(listener).onItemMoved(3, 2);
verify(listener).onItemMoved(4, 3);
verify(listener).onItemMoved(5, 4);
verify(listener).onItemMoved(6, 5);
verify(listener).onItemMoved(7, 6);
verifyNoMoreInteractions(listener);
}
protected void removeHabitAt(int position)
{
Habit h = habitList.getByPosition(position);
assertNotNull(h);
habitList.remove(h);
}
}
Loading…
Cancel
Save