From b83d5f68e9fe53b2b6c520ecde1149ba4d08c503 Mon Sep 17 00:00:00 2001 From: serso Date: Tue, 23 Feb 2016 17:23:54 +0100 Subject: [PATCH] History tests --- .../entities/BaseEntitiesRegistry.java | 22 ++- .../android/calculator/history/History.java | 52 +++---- .../calculator/history/OldHistory.java | 8 +- .../android/calculator/json/Json.java | 13 +- .../org/solovyev/android/io/FileSystem.java | 42 +++++ .../solovyev/android/calculator/Tests.java | 18 +++ .../calculator/history/HistoryTest.java | 146 ++++++++++++++++-- 7 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/org/solovyev/android/io/FileSystem.java create mode 100644 app/src/test/java/org/solovyev/android/calculator/Tests.java diff --git a/app/src/main/java/org/solovyev/android/calculator/entities/BaseEntitiesRegistry.java b/app/src/main/java/org/solovyev/android/calculator/entities/BaseEntitiesRegistry.java index e2264e3b..aa13c7d0 100644 --- a/app/src/main/java/org/solovyev/android/calculator/entities/BaseEntitiesRegistry.java +++ b/app/src/main/java/org/solovyev/android/calculator/entities/BaseEntitiesRegistry.java @@ -28,22 +28,25 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Handler; import android.support.annotation.NonNull; + import com.squareup.otto.Bus; + import org.json.JSONArray; import org.json.JSONException; import org.solovyev.android.Check; -import org.solovyev.android.calculator.*; +import org.solovyev.android.calculator.App; +import org.solovyev.android.calculator.AppModule; +import org.solovyev.android.calculator.CalculatorApplication; +import org.solovyev.android.calculator.EntitiesRegistry; +import org.solovyev.android.calculator.ErrorReporter; import org.solovyev.android.calculator.json.Json; import org.solovyev.android.calculator.json.Jsonable; import org.solovyev.android.io.FileSaver; +import org.solovyev.android.io.FileSystem; import org.solovyev.common.JBuilder; import org.solovyev.common.math.MathEntity; import org.solovyev.common.math.MathRegistry; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Named; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -52,6 +55,11 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Executor; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Named; + public abstract class BaseEntitiesRegistry implements EntitiesRegistry { @Nonnull @@ -73,6 +81,8 @@ public abstract class BaseEntitiesRegistry implements Enti @Inject public ErrorReporter errorReporter; @Inject + public FileSystem fileSystem; + @Inject @Named(AppModule.THREAD_BACKGROUND) public Executor backgroundThread; @Inject @@ -132,7 +142,7 @@ public abstract class BaseEntitiesRegistry implements Enti return Collections.emptyList(); } try { - return Json.load(file, creator); + return Json.load(file, fileSystem, creator); } catch (IOException | JSONException e) { errorReporter.onException(e); } diff --git a/app/src/main/java/org/solovyev/android/calculator/history/History.java b/app/src/main/java/org/solovyev/android/calculator/history/History.java index 11cc7029..1f64332d 100644 --- a/app/src/main/java/org/solovyev/android/calculator/history/History.java +++ b/app/src/main/java/org/solovyev/android/calculator/history/History.java @@ -22,27 +22,32 @@ package org.solovyev.android.calculator.history; +import static android.text.TextUtils.isEmpty; + import android.app.Application; import android.content.SharedPreferences; import android.os.Handler; import android.support.annotation.NonNull; import android.text.TextUtils; + import com.google.common.base.Strings; import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; + import org.json.JSONArray; import org.json.JSONException; import org.solovyev.android.Check; -import org.solovyev.android.calculator.*; +import org.solovyev.android.calculator.AppModule; +import org.solovyev.android.calculator.Calculator; +import org.solovyev.android.calculator.Display; +import org.solovyev.android.calculator.DisplayState; +import org.solovyev.android.calculator.Editor; +import org.solovyev.android.calculator.EditorState; import org.solovyev.android.calculator.Engine.Preferences; +import org.solovyev.android.calculator.ErrorReporter; import org.solovyev.android.calculator.json.Json; -import org.solovyev.android.io.FileSaver; +import org.solovyev.android.io.FileSystem; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -51,12 +56,15 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executor; -import static android.text.TextUtils.isEmpty; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; @Singleton public class History { - public static final String TAG = App.subTag("History"); public static final String OLD_HISTORY_PREFS_KEY = "org.solovyev.android.calculator.CalculatorModel_history"; private static final ClearedEvent CLEARED_EVENT_RECENT = new ClearedEvent(true); private static final ClearedEvent CLEARED_EVENT_SAVED = new ClearedEvent(false); @@ -84,6 +92,8 @@ public class History { @Inject ErrorReporter errorReporter; @Inject + FileSystem fileSystem; + @Inject @Named(AppModule.THREAD_BACKGROUND) Executor backgroundThread; @Inject @@ -91,7 +101,7 @@ public class History { File filesDir; @Nullable - static List convertOldHistory(@NonNull String xml) { + static List convertOldHistory(@NonNull String xml) throws Exception { final OldHistory history = OldHistory.fromXml(xml); if (history == null) { // strange, history seems to be broken. Avoid clearing the preference @@ -172,12 +182,12 @@ public class History { } @NonNull - private File getSavedHistoryFile() { + File getSavedHistoryFile() { return new File(filesDir, "history-saved.json"); } @NonNull - private File getRecentHistoryFile() { + File getRecentHistoryFile() { return new File(filesDir, "history-recent.json"); } @@ -192,9 +202,9 @@ public class History { return; } final JSONArray json = Json.toJson(states); - FileSaver.save(getSavedHistoryFile(), json.toString()); + fileSystem.write(getSavedHistoryFile(), json.toString()); preferences.edit().remove(OLD_HISTORY_PREFS_KEY).apply(); - } catch (IOException e) { + } catch (Exception e) { errorReporter.onException(e); } } @@ -218,7 +228,7 @@ public class History { @Nonnull private List tryLoadStates(@NonNull File file) { try { - return Json.load(file, HistoryState.JSON_CREATOR); + return Json.load(file, fileSystem, HistoryState.JSON_CREATOR); } catch (IOException | JSONException e) { errorReporter.onException(e); } @@ -333,12 +343,6 @@ public class History { onSavedChanged(new RemovedEvent(state, false)); } - public void removeRecent(@Nonnull HistoryState state) { - Check.isMainThread(); - recent.remove(state); - onSavedChanged(new RemovedEvent(state, true)); - } - @Subscribe public void onDisplayChanged(@Nonnull Display.ChangedEvent e) { final EditorState editorState = editor.getState(); @@ -403,11 +407,7 @@ public class History { public void run() { final File file = recent ? getRecentHistoryFile() : getSavedHistoryFile(); final JSONArray array = Json.toJson(states); - try { - FileSaver.save(file, array.toString()); - } catch (IOException e) { - errorReporter.onException(e); - } + fileSystem.writeSilently(file, array.toString()); } }); } diff --git a/app/src/main/java/org/solovyev/android/calculator/history/OldHistory.java b/app/src/main/java/org/solovyev/android/calculator/history/OldHistory.java index d5c1ad5e..8bd79a12 100644 --- a/app/src/main/java/org/solovyev/android/calculator/history/OldHistory.java +++ b/app/src/main/java/org/solovyev/android/calculator/history/OldHistory.java @@ -44,16 +44,12 @@ class OldHistory { } @Nullable - public static OldHistory fromXml(@Nullable String xml) { + public static OldHistory fromXml(@Nullable String xml) throws Exception { if (xml == null) { return null; } final Serializer serializer = new Persister(); - try { - return serializer.read(OldHistory.class, xml); - } catch (Exception e) { - return null; - } + return serializer.read(OldHistory.class, xml); } @Nonnull diff --git a/app/src/main/java/org/solovyev/android/calculator/json/Json.java b/app/src/main/java/org/solovyev/android/calculator/json/Json.java index efc2b6d5..e65ad179 100644 --- a/app/src/main/java/org/solovyev/android/calculator/json/Json.java +++ b/app/src/main/java/org/solovyev/android/calculator/json/Json.java @@ -1,20 +1,22 @@ package org.solovyev.android.calculator.json; +import static android.text.TextUtils.isEmpty; + import android.support.annotation.NonNull; import android.util.Log; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.solovyev.android.io.FileLoader; +import org.solovyev.android.io.FileSystem; -import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static android.text.TextUtils.isEmpty; +import javax.annotation.Nonnull; public final class Json { @@ -56,11 +58,12 @@ public final class Json { } @Nonnull - public static List load(@Nonnull File file, @NonNull Creator creator) throws IOException, JSONException { + public static List load(@Nonnull File file, @NonNull FileSystem fileSystem, + @NonNull Creator creator) throws IOException, JSONException { if (!file.exists()) { return Collections.emptyList(); } - final CharSequence json = FileLoader.load(file); + final CharSequence json = fileSystem.read(file); if (isEmpty(json)) { return Collections.emptyList(); } diff --git a/app/src/main/java/org/solovyev/android/io/FileSystem.java b/app/src/main/java/org/solovyev/android/io/FileSystem.java new file mode 100644 index 00000000..0bfbf225 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/io/FileSystem.java @@ -0,0 +1,42 @@ +package org.solovyev.android.io; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.solovyev.android.calculator.ErrorReporter; + +import java.io.File; +import java.io.IOException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class FileSystem { + + @Inject + ErrorReporter errorReporter; + + @Inject + public FileSystem() { + } + + public boolean writeSilently(@NonNull File file, @NonNull String data) { + try { + write(file, data); + return true; + } catch (IOException e) { + errorReporter.onException(e); + } + return false; + } + + public void write(@NonNull File file, @NonNull String data) throws IOException { + FileSaver.save(file, data); + } + + @Nullable + public CharSequence read(File file) throws IOException { + return FileLoader.load(file); + } +} diff --git a/app/src/test/java/org/solovyev/android/calculator/Tests.java b/app/src/test/java/org/solovyev/android/calculator/Tests.java new file mode 100644 index 00000000..5a5fb37d --- /dev/null +++ b/app/src/test/java/org/solovyev/android/calculator/Tests.java @@ -0,0 +1,18 @@ +package org.solovyev.android.calculator; + +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; + +public class Tests { + + @NonNull + public static Executor sameThreadExecutor() { + return new Executor() { + @Override + public void execute(@NonNull Runnable command) { + command.run(); + } + }; + } +} diff --git a/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java b/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java index 35ffb3bc..f33b2302 100644 --- a/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java +++ b/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java @@ -22,28 +22,50 @@ package org.solovyev.android.calculator.history; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.solovyev.android.calculator.Engine.Preferences.groupingSeparator; +import static org.solovyev.android.calculator.Tests.sameThreadExecutor; +import static org.solovyev.android.calculator.jscl.JsclOperation.numeric; + import android.content.SharedPreferences; import android.os.Handler; +import android.os.Looper; + import com.squareup.otto.Bus; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.solovyev.android.CalculatorTestRunner; -import org.solovyev.android.calculator.*; -import org.solovyev.android.calculator.jscl.JsclOperation; +import org.solovyev.android.calculator.BuildConfig; +import org.solovyev.android.calculator.Display; +import org.solovyev.android.calculator.DisplayState; +import org.solovyev.android.calculator.Editor; +import org.solovyev.android.calculator.EditorState; +import org.solovyev.android.calculator.ErrorReporter; import org.solovyev.android.calculator.json.Json; +import org.solovyev.android.io.FileSystem; -import javax.annotation.Nonnull; import java.io.File; import java.util.List; -import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -import static org.solovyev.android.calculator.Engine.Preferences.groupingSeparator; +import javax.annotation.Nonnull; @Config(constants = BuildConfig.class, sdk = CalculatorTestRunner.SUPPORTED_SDK) @RunWith(RobolectricGradleTestRunner.class) @@ -54,13 +76,25 @@ public class HistoryTest { @Before public void setUp() throws Exception { history = new History(); + history.backgroundThread = sameThreadExecutor(); history.application = RuntimeEnvironment.application; history.bus = mock(Bus.class); - history.handler = mock(Handler.class); + history.errorReporter = mock(ErrorReporter.class); + history.fileSystem = mock(FileSystem.class); + history.handler = new Handler(Looper.getMainLooper()); history.preferences = mock(SharedPreferences.class); + final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class); + when(history.preferences.edit()).thenReturn(editor); + when(editor.remove(anyString())).thenReturn(editor); history.editor = mock(Editor.class); } + @After + public void tearDown() throws Exception { + history.getSavedHistoryFile().delete(); + history.getRecentHistoryFile().delete(); + } + @Test public void testGetStates() throws Exception { addState("1"); @@ -231,10 +265,14 @@ public class HistoryTest { assertNull(state.display.getResult()); states = History.convertOldHistory(oldXml2); + checkOldXml2States(states); + } + + private void checkOldXml2States(List states) { assertNotNull(states); assertEquals(4, states.size()); - state = states.get(0); + HistoryState state = states.get(0); assertEquals(100000000, state.time); assertEquals("boom", state.comment); assertEquals("1+11", state.editor.getTextString()); @@ -253,12 +291,32 @@ public class HistoryTest { assertNull(state.display.getResult()); } + @Test + public void testShouldMigrateOldHistory() throws Exception { + history.fileSystem = new FileSystem(); + when(history.preferences.getString(eq(History.OLD_HISTORY_PREFS_KEY), anyString())).thenReturn(oldXml2); + history.init(sameThreadExecutor()); + Robolectric.flushForegroundThreadScheduler(); + checkOldXml2States(history.getSaved()); + } + + @Test + public void testShouldWriteNewHistoryFile() throws Exception { + history.fileSystem = mock(FileSystem.class); + when(history.preferences.getString(eq(History.OLD_HISTORY_PREFS_KEY), anyString())) + .thenReturn(oldXml1); + history.init(sameThreadExecutor()); + Robolectric.flushForegroundThreadScheduler(); + verify(history.fileSystem).write(eq(history.getSavedHistoryFile()), eq( + "[{\"e\":{\"t\":\"1+1\",\"s\":3},\"d\":{\"t\":\"Error\"},\"t\":100000000}]")); + } + @Test public void testShouldAddStateIfEditorAndDisplayAreInSync() throws Exception { final EditorState editorState = EditorState.create("editor", 2); when(history.editor.getState()).thenReturn(editorState); - final DisplayState displayState = DisplayState.createError(JsclOperation.numeric, "test", editorState.sequence); + final DisplayState displayState = DisplayState.createError(numeric, "test", editorState.sequence); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); final List states = history.getRecent(); @@ -272,16 +330,35 @@ public class HistoryTest { final EditorState editorState = EditorState.create("editor", 2); when(history.editor.getState()).thenReturn(editorState); - final DisplayState displayState = DisplayState.createError(JsclOperation.numeric, "test", editorState.sequence - 1); + final DisplayState displayState = DisplayState.createError(numeric, "test", editorState.sequence - 1); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); final List states = history.getRecent(); assertEquals(0, states.size()); } + @Test + public void testShouldReportOnMigrateException() throws Exception { + when(history.preferences.getString(eq(History.OLD_HISTORY_PREFS_KEY), anyString())).thenReturn( + "boom"); + history.init(sameThreadExecutor()); + + verify(history.errorReporter).onException(any(Throwable.class)); + } + + @Test + public void testShouldNotRemoveOldHistoryOnError() throws Exception { + when(history.preferences.getString(eq(History.OLD_HISTORY_PREFS_KEY), anyString())).thenReturn("boom"); + history.init(sameThreadExecutor()); + + verify(history.preferences, never()).edit(); + verify(history.errorReporter).onException(any(Throwable.class)); + } + @Test public void testShouldLoadStates() throws Exception { - final List states = Json.load(new File(HistoryTest.class.getResource("recent-history.json").getFile()), HistoryState.JSON_CREATOR); + final List states = Json.load(new File(HistoryTest.class.getResource("recent-history.json").getFile()), + new FileSystem(), HistoryState.JSON_CREATOR); assertEquals(8, states.size()); HistoryState state = states.get(0); @@ -305,4 +382,49 @@ public class HistoryTest { assertEquals(2, state.editor.selection); assertEquals("52", state.display.text); } + + @Test + public void testShouldClearSaved() throws Exception { + history.updateSaved(HistoryState.builder(EditorState.create("text", 0), + DisplayState.createValid(numeric, null, "result", 0)).build()); + Robolectric.flushForegroundThreadScheduler(); + assertTrue(!history.getSaved().isEmpty()); + + // renew counter + history.fileSystem = mock(FileSystem.class); + history.clearSaved(); + Robolectric.flushForegroundThreadScheduler(); + + assertTrue(history.getSaved().isEmpty()); + verify(history.fileSystem).writeSilently(eq(history.getSavedHistoryFile()), eq("[]")); + } + + @Test + public void testShouldClearRecent() throws Exception { + history.addRecent(HistoryState.builder(EditorState.create("text", 0), + DisplayState.createValid(numeric, null, "result", 0)).build()); + Robolectric.flushForegroundThreadScheduler(); + assertTrue(!history.getRecent().isEmpty()); + + // renew counter + history.fileSystem = mock(FileSystem.class); + history.clearRecent(); + Robolectric.flushForegroundThreadScheduler(); + + assertTrue(history.getRecent().isEmpty()); + verify(history.fileSystem).writeSilently(eq(history.getRecentHistoryFile()), eq("[]")); + } + + @Test + public void testShouldUpdateSaved() throws Exception { + final HistoryState state = HistoryState.builder(EditorState.create("text", 0), + DisplayState.createValid(numeric, null, "result", 0)).build(); + history.updateSaved(state); + assertTrue(history.getSaved().size() == 1); + assertEquals(state.time, history.getSaved().get(0).time); + + history.updateSaved(HistoryState.builder(state, false).withTime(10).build()); + assertTrue(history.getSaved().size() == 1); + assertEquals(10, history.getSaved().get(0).time); + } }