History tests

This commit is contained in:
serso 2016-02-23 17:23:54 +01:00
parent cff8a24885
commit b83d5f68e9
7 changed files with 246 additions and 55 deletions

View File

@ -28,22 +28,25 @@ import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.squareup.otto.Bus; import com.squareup.otto.Bus;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.solovyev.android.Check; 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.Json;
import org.solovyev.android.calculator.json.Jsonable; import org.solovyev.android.calculator.json.Jsonable;
import org.solovyev.android.io.FileSaver; import org.solovyev.android.io.FileSaver;
import org.solovyev.android.io.FileSystem;
import org.solovyev.common.JBuilder; import org.solovyev.common.JBuilder;
import org.solovyev.common.math.MathEntity; import org.solovyev.common.math.MathEntity;
import org.solovyev.common.math.MathRegistry; 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -52,6 +55,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor; 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<T extends MathEntity> implements EntitiesRegistry<T> { public abstract class BaseEntitiesRegistry<T extends MathEntity> implements EntitiesRegistry<T> {
@Nonnull @Nonnull
@ -73,6 +81,8 @@ public abstract class BaseEntitiesRegistry<T extends MathEntity> implements Enti
@Inject @Inject
public ErrorReporter errorReporter; public ErrorReporter errorReporter;
@Inject @Inject
public FileSystem fileSystem;
@Inject
@Named(AppModule.THREAD_BACKGROUND) @Named(AppModule.THREAD_BACKGROUND)
public Executor backgroundThread; public Executor backgroundThread;
@Inject @Inject
@ -132,7 +142,7 @@ public abstract class BaseEntitiesRegistry<T extends MathEntity> implements Enti
return Collections.emptyList(); return Collections.emptyList();
} }
try { try {
return Json.load(file, creator); return Json.load(file, fileSystem, creator);
} catch (IOException | JSONException e) { } catch (IOException | JSONException e) {
errorReporter.onException(e); errorReporter.onException(e);
} }

View File

@ -22,27 +22,32 @@
package org.solovyev.android.calculator.history; package org.solovyev.android.calculator.history;
import static android.text.TextUtils.isEmpty;
import android.app.Application; import android.app.Application;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.squareup.otto.Bus; import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.solovyev.android.Check; 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.Engine.Preferences;
import org.solovyev.android.calculator.ErrorReporter;
import org.solovyev.android.calculator.json.Json; 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -51,12 +56,15 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; 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 @Singleton
public class History { 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"; 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_RECENT = new ClearedEvent(true);
private static final ClearedEvent CLEARED_EVENT_SAVED = new ClearedEvent(false); private static final ClearedEvent CLEARED_EVENT_SAVED = new ClearedEvent(false);
@ -84,6 +92,8 @@ public class History {
@Inject @Inject
ErrorReporter errorReporter; ErrorReporter errorReporter;
@Inject @Inject
FileSystem fileSystem;
@Inject
@Named(AppModule.THREAD_BACKGROUND) @Named(AppModule.THREAD_BACKGROUND)
Executor backgroundThread; Executor backgroundThread;
@Inject @Inject
@ -91,7 +101,7 @@ public class History {
File filesDir; File filesDir;
@Nullable @Nullable
static List<HistoryState> convertOldHistory(@NonNull String xml) { static List<HistoryState> convertOldHistory(@NonNull String xml) throws Exception {
final OldHistory history = OldHistory.fromXml(xml); final OldHistory history = OldHistory.fromXml(xml);
if (history == null) { if (history == null) {
// strange, history seems to be broken. Avoid clearing the preference // strange, history seems to be broken. Avoid clearing the preference
@ -172,12 +182,12 @@ public class History {
} }
@NonNull @NonNull
private File getSavedHistoryFile() { File getSavedHistoryFile() {
return new File(filesDir, "history-saved.json"); return new File(filesDir, "history-saved.json");
} }
@NonNull @NonNull
private File getRecentHistoryFile() { File getRecentHistoryFile() {
return new File(filesDir, "history-recent.json"); return new File(filesDir, "history-recent.json");
} }
@ -192,9 +202,9 @@ public class History {
return; return;
} }
final JSONArray json = Json.toJson(states); final JSONArray json = Json.toJson(states);
FileSaver.save(getSavedHistoryFile(), json.toString()); fileSystem.write(getSavedHistoryFile(), json.toString());
preferences.edit().remove(OLD_HISTORY_PREFS_KEY).apply(); preferences.edit().remove(OLD_HISTORY_PREFS_KEY).apply();
} catch (IOException e) { } catch (Exception e) {
errorReporter.onException(e); errorReporter.onException(e);
} }
} }
@ -218,7 +228,7 @@ public class History {
@Nonnull @Nonnull
private List<HistoryState> tryLoadStates(@NonNull File file) { private List<HistoryState> tryLoadStates(@NonNull File file) {
try { try {
return Json.load(file, HistoryState.JSON_CREATOR); return Json.load(file, fileSystem, HistoryState.JSON_CREATOR);
} catch (IOException | JSONException e) { } catch (IOException | JSONException e) {
errorReporter.onException(e); errorReporter.onException(e);
} }
@ -333,12 +343,6 @@ public class History {
onSavedChanged(new RemovedEvent(state, false)); onSavedChanged(new RemovedEvent(state, false));
} }
public void removeRecent(@Nonnull HistoryState state) {
Check.isMainThread();
recent.remove(state);
onSavedChanged(new RemovedEvent(state, true));
}
@Subscribe @Subscribe
public void onDisplayChanged(@Nonnull Display.ChangedEvent e) { public void onDisplayChanged(@Nonnull Display.ChangedEvent e) {
final EditorState editorState = editor.getState(); final EditorState editorState = editor.getState();
@ -403,11 +407,7 @@ public class History {
public void run() { public void run() {
final File file = recent ? getRecentHistoryFile() : getSavedHistoryFile(); final File file = recent ? getRecentHistoryFile() : getSavedHistoryFile();
final JSONArray array = Json.toJson(states); final JSONArray array = Json.toJson(states);
try { fileSystem.writeSilently(file, array.toString());
FileSaver.save(file, array.toString());
} catch (IOException e) {
errorReporter.onException(e);
}
} }
}); });
} }

View File

@ -44,16 +44,12 @@ class OldHistory {
} }
@Nullable @Nullable
public static OldHistory fromXml(@Nullable String xml) { public static OldHistory fromXml(@Nullable String xml) throws Exception {
if (xml == null) { if (xml == null) {
return null; return null;
} }
final Serializer serializer = new Persister(); final Serializer serializer = new Persister();
try { return serializer.read(OldHistory.class, xml);
return serializer.read(OldHistory.class, xml);
} catch (Exception e) {
return null;
}
} }
@Nonnull @Nonnull

View File

@ -1,20 +1,22 @@
package org.solovyev.android.calculator.json; package org.solovyev.android.calculator.json;
import static android.text.TextUtils.isEmpty;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static android.text.TextUtils.isEmpty; import javax.annotation.Nonnull;
public final class Json { public final class Json {
@ -56,11 +58,12 @@ public final class Json {
} }
@Nonnull @Nonnull
public static <T> List<T> load(@Nonnull File file, @NonNull Creator<T> creator) throws IOException, JSONException { public static <T> List<T> load(@Nonnull File file, @NonNull FileSystem fileSystem,
@NonNull Creator<T> creator) throws IOException, JSONException {
if (!file.exists()) { if (!file.exists()) {
return Collections.emptyList(); return Collections.emptyList();
} }
final CharSequence json = FileLoader.load(file); final CharSequence json = fileSystem.read(file);
if (isEmpty(json)) { if (isEmpty(json)) {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -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);
}
}

View File

@ -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();
}
};
}
}

View File

@ -22,28 +22,50 @@
package org.solovyev.android.calculator.history; 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.content.SharedPreferences;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import com.squareup.otto.Bus; import com.squareup.otto.Bus;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.solovyev.android.CalculatorTestRunner; import org.solovyev.android.CalculatorTestRunner;
import org.solovyev.android.calculator.*; import org.solovyev.android.calculator.BuildConfig;
import org.solovyev.android.calculator.jscl.JsclOperation; 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.calculator.json.Json;
import org.solovyev.android.io.FileSystem;
import javax.annotation.Nonnull;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import static org.junit.Assert.*; import javax.annotation.Nonnull;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import static org.solovyev.android.calculator.Engine.Preferences.groupingSeparator;
@Config(constants = BuildConfig.class, sdk = CalculatorTestRunner.SUPPORTED_SDK) @Config(constants = BuildConfig.class, sdk = CalculatorTestRunner.SUPPORTED_SDK)
@RunWith(RobolectricGradleTestRunner.class) @RunWith(RobolectricGradleTestRunner.class)
@ -54,13 +76,25 @@ public class HistoryTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
history = new History(); history = new History();
history.backgroundThread = sameThreadExecutor();
history.application = RuntimeEnvironment.application; history.application = RuntimeEnvironment.application;
history.bus = mock(Bus.class); 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); 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); history.editor = mock(Editor.class);
} }
@After
public void tearDown() throws Exception {
history.getSavedHistoryFile().delete();
history.getRecentHistoryFile().delete();
}
@Test @Test
public void testGetStates() throws Exception { public void testGetStates() throws Exception {
addState("1"); addState("1");
@ -231,10 +265,14 @@ public class HistoryTest {
assertNull(state.display.getResult()); assertNull(state.display.getResult());
states = History.convertOldHistory(oldXml2); states = History.convertOldHistory(oldXml2);
checkOldXml2States(states);
}
private void checkOldXml2States(List<HistoryState> states) {
assertNotNull(states); assertNotNull(states);
assertEquals(4, states.size()); assertEquals(4, states.size());
state = states.get(0); HistoryState state = states.get(0);
assertEquals(100000000, state.time); assertEquals(100000000, state.time);
assertEquals("boom", state.comment); assertEquals("boom", state.comment);
assertEquals("1+11", state.editor.getTextString()); assertEquals("1+11", state.editor.getTextString());
@ -253,12 +291,32 @@ public class HistoryTest {
assertNull(state.display.getResult()); 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 @Test
public void testShouldAddStateIfEditorAndDisplayAreInSync() throws Exception { public void testShouldAddStateIfEditorAndDisplayAreInSync() throws Exception {
final EditorState editorState = EditorState.create("editor", 2); final EditorState editorState = EditorState.create("editor", 2);
when(history.editor.getState()).thenReturn(editorState); 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)); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState));
final List<HistoryState> states = history.getRecent(); final List<HistoryState> states = history.getRecent();
@ -272,16 +330,35 @@ public class HistoryTest {
final EditorState editorState = EditorState.create("editor", 2); final EditorState editorState = EditorState.create("editor", 2);
when(history.editor.getState()).thenReturn(editorState); 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)); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState));
final List<HistoryState> states = history.getRecent(); final List<HistoryState> states = history.getRecent();
assertEquals(0, states.size()); 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 @Test
public void testShouldLoadStates() throws Exception { public void testShouldLoadStates() throws Exception {
final List<HistoryState> states = Json.load(new File(HistoryTest.class.getResource("recent-history.json").getFile()), HistoryState.JSON_CREATOR); final List<HistoryState> states = Json.load(new File(HistoryTest.class.getResource("recent-history.json").getFile()),
new FileSystem(), HistoryState.JSON_CREATOR);
assertEquals(8, states.size()); assertEquals(8, states.size());
HistoryState state = states.get(0); HistoryState state = states.get(0);
@ -305,4 +382,49 @@ public class HistoryTest {
assertEquals(2, state.editor.selection); assertEquals(2, state.editor.selection);
assertEquals("52", state.display.text); 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);
}
} }