/* * Copyright 2013 serso aka se.solovyev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Contact details * * Email: se.solovyev@gmail.com * Site: http://se.solovyev.org */ package org.solovyev.android.calculator.history; 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.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.Engine; import org.solovyev.android.calculator.ErrorReporter; import org.solovyev.android.calculator.json.Json; import org.solovyev.android.io.FileSystem; import java.io.File; import java.util.List; import javax.annotation.Nonnull; 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.Tests.sameThreadExecutor; import static org.solovyev.android.calculator.jscl.JsclOperation.numeric; @Config(constants = BuildConfig.class, sdk = CalculatorTestRunner.SUPPORTED_SDK) @RunWith(RobolectricGradleTestRunner.class) public class HistoryTest { private History history; @Before public void setUp() throws Exception { history = new History(); history.backgroundThread = sameThreadExecutor(); history.application = RuntimeEnvironment.application; history.bus = mock(Bus.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); history.setLoaded(true); } @After public void tearDown() throws Exception { history.getSavedHistoryFile().delete(); history.getRecentHistoryFile().delete(); } @Test public void testGetStates() throws Exception { addState("1"); addState("12"); addState("123"); addState("123+"); addState("123+3"); addState(""); addState("2"); addState("23"); addState("235"); addState("2355"); addState("235"); addState("2354"); addState("23547"); final List<HistoryState> states = history.getRecent(); assertEquals(3, states.size()); assertEquals("23547", states.get(0).editor.getTextString()); // intermediate state assertEquals("235", states.get(1).editor.getTextString()); assertEquals("123+3", states.get(2).editor.getTextString()); } @Test public void testRecentHistoryShouldTakeIntoAccountGroupingSeparator() throws Exception { when(history.preferences.contains(eq(Engine.Preferences.Output.separator.getKey()))).thenReturn(true); when(history.preferences.getString(eq(Engine.Preferences.Output.separator.getKey()), anyString())).thenReturn(" "); addState("1"); addState("12"); addState("123"); addState("1 234"); addState("12 345"); List<HistoryState> states = history.getRecent(); assertEquals(1, states.size()); assertEquals("12 345", states.get(0).editor.getTextString()); history.clearRecent(); when(history.preferences.getString(eq(Engine.Preferences.Output.separator.getKey()), anyString())).thenReturn("'"); addState("1"); addState("12"); addState("123"); addState("1'234"); addState("12'345"); addState("12 345"); states = history.getRecent(); assertEquals(2, states.size()); assertEquals("12 345", states.get(0).editor.getTextString()); assertEquals("12'345", states.get(1).editor.getTextString()); } @Test public void testRecentHistoryShouldNotContainEmptyStates() throws Exception { addState(""); addState("1"); addState("12"); addState(""); addState(""); addState("34"); addState(""); final List<HistoryState> states = history.getRecent(); assertEquals(2, states.size()); assertEquals("34", states.get(0).editor.getTextString()); assertEquals("12", states.get(1).editor.getTextString()); } private void addState(@Nonnull String text) { history.addRecent(HistoryState.builder(EditorState.create(text, 3), DisplayState.empty()).build()); } private static final String oldXml1 = "<history>\n" + " <historyItems class=\"java.util.ArrayList\">\n" + " <calculatorHistoryState>\n" + " <time>100000000</time>\n" + " <editorState>\n" + " <cursorPosition>3</cursorPosition>\n" + " <text>1+1</text>\n" + " </editorState>\n" + " <displayState>\n" + " <editorState>\n" + " <cursorPosition>0</cursorPosition>\n" + " <text>Error</text>\n" + " </editorState>\n" + " <jsclOperation>simplify</jsclOperation>\n" + " </displayState>\n" + " </calculatorHistoryState>\n" + " </historyItems>\n" + "</history>"; private static final String oldXml2 = "<history>\n" + " <historyItems class=\"java.util.ArrayList\">\n" + " <calculatorHistoryState>\n" + " <time>100000000</time>\n" + " <comment>boom</comment>\n" + " <editorState>\n" + " <cursorPosition>3</cursorPosition>\n" + " <text>1+11</text>\n" + " </editorState>\n" + " <displayState>\n" + " <editorState>\n" + " <cursorPosition>0</cursorPosition>\n" + " <text>Error</text>\n" + " </editorState>\n" + " <jsclOperation>simplify</jsclOperation>\n" + " </displayState>\n" + " </calculatorHistoryState>\n" + " <calculatorHistoryState>\n" + " <time>100000000</time>\n" + " <editorState>\n" + " <cursorPosition>2</cursorPosition>\n" + " <text>5/6</text>\n" + " </editorState>\n" + " <displayState>\n" + " <editorState>\n" + " <cursorPosition>3</cursorPosition>\n" + " <text>5/6</text>\n" + " </editorState>\n" + " <jsclOperation>numeric</jsclOperation>\n" + " </displayState>\n" + " </calculatorHistoryState>\n" + " <calculatorHistoryState>\n" + " <time>100000000</time>\n" + " <editorState>\n" + " <cursorPosition>1</cursorPosition>\n" + " <text></text>\n" + " </editorState>\n" + " <displayState>\n" + " <editorState>\n" + " <cursorPosition>0</cursorPosition>\n" + " <text>Error</text>\n" + " </editorState>\n" + " <jsclOperation>elementary</jsclOperation>\n" + " </displayState>\n" + " </calculatorHistoryState>\n" + " <calculatorHistoryState>\n" + " <time>1</time>\n" + " <editorState>\n" + " <cursorPosition>0</cursorPosition>\n" + " <text>4+5/35sin(41)+dfdsfsdfs</text>\n" + " </editorState>\n" + " <displayState>\n" + " <editorState>\n" + " <cursorPosition>1</cursorPosition>\n" + " <text>4+5/35sin(41)+dfdsfsdfs</text>\n" + " </editorState>\n" + " <jsclOperation>numeric</jsclOperation>\n" + " </displayState>\n" + " </calculatorHistoryState>\n" + " </historyItems>\n" + "</history>"; @Test public void testShouldConvertOldHistory() throws Exception { List<HistoryState> states = History.convertOldHistory(oldXml1); assertNotNull(states); assertEquals(1, states.size()); HistoryState state = states.get(0); assertEquals(100000000, state.time); assertEquals("", state.comment); assertEquals("1+1", state.editor.getTextString()); assertEquals(3, state.editor.selection); assertEquals("Error", state.display.text); assertEquals(true, state.display.valid); assertNull(state.display.getResult()); states = History.convertOldHistory(oldXml2); checkOldXml2States(states); } private void checkOldXml2States(List<HistoryState> states) { assertNotNull(states); assertEquals(4, states.size()); HistoryState state = states.get(0); assertEquals(100000000, state.time); assertEquals("boom", state.comment); assertEquals("1+11", state.editor.getTextString()); assertEquals(3, state.editor.selection); assertEquals("Error", state.display.text); assertEquals(true, state.display.valid); assertNull(state.display.getResult()); state = states.get(3); assertEquals(1, state.time); assertEquals("", state.comment); assertEquals("4+5/35sin(41)+dfdsfsdfs", state.editor.getTextString()); assertEquals(0, state.editor.selection); assertEquals("4+5/35sin(41)+dfdsfsdfs", state.display.text); assertEquals(true, state.display.valid); 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\",\"v\":true},\"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(numeric, "test", editorState.sequence); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); final List<HistoryState> states = history.getRecent(); assertEquals(1, states.size()); assertSame(editorState, states.get(0).editor); assertSame(displayState, states.get(0).display); } @Test public void testShouldNotAddStateIfEditorAndDisplayAreOutOfSync() throws Exception { final EditorState editorState = EditorState.create("editor", 2); when(history.editor.getState()).thenReturn(editorState); final DisplayState displayState = DisplayState.createError(numeric, "test", editorState.sequence - 1); history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); final List<HistoryState> 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<HistoryState> 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); assertEquals(1452770652381L, state.time); assertEquals("", state.comment); assertEquals("01 234 567 890 123 456 789", state.editor.getTextString()); assertEquals(26, state.editor.selection); assertEquals("1 234 567 890 123 460 000", state.display.text); state = states.get(4); assertEquals(1452770626394L, state.time); assertEquals("", state.comment); assertEquals("985", state.editor.getTextString()); assertEquals(3, state.editor.selection); assertEquals("985", state.display.text); state = states.get(7); assertEquals(1452770503823L, state.time); assertEquals("", state.comment); assertEquals("52", state.editor.getTextString()); 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); } }