From: Stefan F. <ste...@us...> - 2012-07-31 17:14:59
|
junit/rails/game/state/ModelImpl.java | 22 ++- junit/rails/game/state/ModelTest.java | 40 +++++ junit/rails/game/state/StateImpl.java | 6 junit/rails/game/state/StateManagerTest.java | 186 +++++++++++++++++++++++++++ junit/rails/game/state/StateTest.java | 8 - junit/rails/game/state/StateTestUtils.java | 4 src/rails/game/state/StateManager.java | 167 +++++++----------------- 7 files changed, 305 insertions(+), 128 deletions(-) New commits: commit f4f887ca0517fe84536f6840c01efd5125093426 Author: Stefan Frey <ste...@we...> Date: Tue Jul 31 19:14:04 2012 +0200 Added StateManagerTest and ModelTest Refactored StateManager topological sorting diff --git a/junit/rails/game/state/ModelImpl.java b/junit/rails/game/state/ModelImpl.java index e029de0..886f2e3 100644 --- a/junit/rails/game/state/ModelImpl.java +++ b/junit/rails/game/state/ModelImpl.java @@ -3,15 +3,27 @@ package rails.game.state; //An implementation only for testing class ModelImpl extends Model { - private final String text; + private final StringState text = StringState.create(this, "text"); - ModelImpl(Item parent, String id, String text) { + private ModelImpl(Item parent, String id, String text) { super(parent, id); - this.text = text; + this.text.set(text); } - @Override - public String toString() { + static ModelImpl create(Item parent, String id, String text) { + return new ModelImpl(parent, id, text); + } + + void changeText(String text) { + this.text.set(text); + } + + State getState() { return text; } + + @Override + public String cachedText() { + return text.value(); + } } diff --git a/junit/rails/game/state/ModelTest.java b/junit/rails/game/state/ModelTest.java new file mode 100644 index 0000000..ae16b1b --- /dev/null +++ b/junit/rails/game/state/ModelTest.java @@ -0,0 +1,40 @@ +package rails.game.state; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ModelTest { + + private static final String MODEL_ID = "Model"; + private static final String MODEL_TEXT_INIT = "Init"; + private static final String MODEL_TEXT_CHANGE = "Change"; + + private Root root; + private ModelImpl model; + @Mock private Observer observer; + + @Before + public void setUp() { + root = StateTestUtils.setUpRoot(); + model = ModelImpl.create(root, MODEL_ID, MODEL_TEXT_INIT); + StateTestUtils.startActionChangeSet(root); + model.addObserver(observer); + } + + @Test + public void testModel() { + assertEquals(MODEL_TEXT_INIT, model.observerText()); + model.changeText(MODEL_TEXT_CHANGE); + StateTestUtils.close(root); + assertEquals(MODEL_TEXT_CHANGE, model.observerText()); + verify(observer).update(MODEL_TEXT_CHANGE); + } + +} diff --git a/junit/rails/game/state/StateImpl.java b/junit/rails/game/state/StateImpl.java index 4d4e732..95cae54 100644 --- a/junit/rails/game/state/StateImpl.java +++ b/junit/rails/game/state/StateImpl.java @@ -5,11 +5,15 @@ class StateImpl extends State { private final String text; - StateImpl(Item parent, String id, String text) { + private StateImpl(Item parent, String id, String text) { super(parent, id); this.text = text; } + static StateImpl create(Item parent, String id, String text) { + return new StateImpl(parent, id, text); + } + @Override public String observerText() { return text; diff --git a/junit/rails/game/state/StateManagerTest.java b/junit/rails/game/state/StateManagerTest.java new file mode 100644 index 0000000..9816a5d --- /dev/null +++ b/junit/rails/game/state/StateManagerTest.java @@ -0,0 +1,186 @@ +package rails.game.state; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +@RunWith(MockitoJUnitRunner.class) +public class StateManagerTest { + + private final static List<String> ID = + ImmutableList.of("A1" , "A2" , "A3", "B1", "B2", "C1", "C2", "C3", "D", "E", "F"); + + private Root root; + private StateManager sm; + + @Mock private State state; + @Mock private Observer observer; + @Mock private Model model; + private ModelImpl m_A1, m_A2, m_A3, m_B1, m_B2, m_C1, m_C2, m_C3, m_D, m_E, m_F; + @Mock private Observer o_A1, o_A2, o_A3, o_B1, o_B2, o_C1, o_C2, o_C3; + + @Before + public void setUp() { + root = StateTestUtils.setUpRoot(); + sm = root.getStateManager(); + + // initialize the models + m_A1 = ModelImpl.create(root, ID.get(0), ID.get(0)); + m_A2 = ModelImpl.create(root, ID.get(1), ID.get(1)); + m_A3 = ModelImpl.create(root, ID.get(2), ID.get(2)); + m_B1 = ModelImpl.create(root, ID.get(3), ID.get(3)); + m_B2 = ModelImpl.create(root, ID.get(4), ID.get(4)); + m_C1 = ModelImpl.create(root, ID.get(5), ID.get(5)); + m_C2 = ModelImpl.create(root, ID.get(6), ID.get(6)); + m_C3 = ModelImpl.create(root, ID.get(7), ID.get(7)); + m_D = ModelImpl.create(root, ID.get(8), ID.get(8)); + m_E = ModelImpl.create(root, ID.get(9), ID.get(9)); + m_F = ModelImpl.create(root, ID.get(10), ID.get(10)); + + // define connections + m_A1.addModel(m_B1); + m_A1.addModel(m_B2); + m_A1.addModel(m_C2); + m_A2.addModel(m_B2); + m_A3.addModel(m_C3); + m_B1.addModel(m_C1); + m_B2.addModel(m_C2); + + // D is special that it depends on the state in A3 directly + m_A3.getState().addModel(m_D); + m_D.addModel(m_A3); + + // E and F form a cycle + m_E.addModel(m_F); + m_F.addModel(m_E); + + // observers + m_A1.addObserver(o_A1); + m_A2.addObserver(o_A2); + m_A3.addObserver(o_A3); + m_B1.addObserver(o_B1); + m_B2.addObserver(o_B2); + m_C1.addObserver(o_C1); + m_C2.addObserver(o_C2); + m_C3.addObserver(o_C3); + } + + @Test + public void testRegisterState() { + sm.registerState(state); + assertThat(sm.getAllStates()).contains(state); + sm.deRegisterState(state); + assertThat(sm.getAllStates()).doesNotContain(state); + } + + @Test + public void testAddObserver() { + sm.addObserver(observer, state); + assertThat(sm.getObservers(state)).contains(observer); + sm.removeObserver(observer, state); + assertThat(sm.getObservers(state)).doesNotContain(observer); + } + + @Test + public void testAddModel() { + sm.addModel(model, state); + assertThat(sm.getModels(state)).contains(model); + sm.removeModel(model, state); + assertThat(sm.getModels(state)).doesNotContain(model); + } + + private void checkOrderings(List<Model> updates) { + for (Model m:updates) { + for (Model dep_m: m.getModels()) { + assertThat(updates.indexOf(dep_m)).isGreaterThan(updates.indexOf(m)); + } + } + } + + + private void assertObservables(List<? extends Model> expected, Set<ModelImpl> updated) { + // get all embedded states that are included + Set<State> states = Sets.newHashSet(); + for (ModelImpl m:updated) { + states.add(m.getState()); + } + // get all observables that are updated + List<Model> toUpdate = sm.getModelsToUpdate(states); + // check that all non-states observables are the updated models and the expected models + assertThat(toUpdate).containsAll(expected); + // ... and does not have duplicates + assertThat(toUpdate).doesNotHaveDuplicates(); + // ... and has the same size + assertEquals(expected.size(), toUpdate.size()); + // and check ordering + checkOrderings(toUpdate); + } + + @Test + public void testObservablesToUpdate() { + // nothing <= nothing + assertObservables(ImmutableList.<Model>of(),ImmutableSet.<ModelImpl>of()); + // A1, B1, B2, C1, C2 <= A1 + assertObservables(ImmutableList.of(m_A1, m_B1, m_B2, m_C1, m_C2),ImmutableSet.of(m_A1)); + // A2, B2, C2 <= A2 + assertObservables(ImmutableList.of(m_A2, m_B2, m_C2),ImmutableSet.of(m_A2)); + // A3, C3 <= A3 + assertObservables(ImmutableList.of(m_A3, m_C3, m_D),ImmutableSet.of(m_A3)); + // B1, C1 <= B1 + assertObservables(ImmutableList.of(m_B1, m_C1),ImmutableSet.of(m_B1)); + // B2, C2 <= B2 + assertObservables(ImmutableList.of(m_B2, m_C2),ImmutableSet.of(m_B2)); + // C1, C2 <= C1, C2 + assertObservables(ImmutableList.of(m_C1, m_C2),ImmutableSet.of(m_C1, m_C2)); + // Combinations: + // A1, A2, B1, B2, C1, C2 <= A1, A2 + assertObservables(ImmutableList.of(m_A1, m_A2, m_B1, m_B2, m_C1, m_C2),ImmutableSet.of(m_A1, m_A2)); + // A1, A2, A3, B1, B2, C1, C2, C3 <= A1, A2, A3 + assertObservables(ImmutableList.of(m_A1, m_A2, m_A3, m_B1, m_B2, m_C1, m_C2, m_C3, m_D),ImmutableSet.of(m_A1, m_A2, m_A3)); + // A2, B1, B2, C1, C2 <= A2, B1 + assertObservables(ImmutableList.of(m_A2, m_B1, m_B2, m_C1, m_C2),ImmutableSet.of(m_A2, m_B1)); + + try{ + assertObservables(ImmutableList.<Model>of(), ImmutableSet.of(m_E)); + failBecauseExceptionWasNotThrown(IllegalStateException.class); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class); + } + } + + @Test + public void testUpdateObservers() { + sm.updateObservers(ImmutableSet.of(m_A1.getState())); + verify(o_A1).update(ID.get(0)); + verify(o_B1).update(ID.get(3)); + verify(o_B2).update(ID.get(4)); + verify(o_C1).update(ID.get(5)); + verify(o_C2).update(ID.get(6)); + verifyZeroInteractions(o_A2, o_A3, o_C3); + } + + @Test + public void testGetChangeStack() { + assertNotNull(sm.getChangeStack()); + } + + @Test + public void testGetPortfolioManager() { + assertNotNull(sm.getPortfolioManager()); + } + +} diff --git a/junit/rails/game/state/StateTest.java b/junit/rails/game/state/StateTest.java index dcc2bef..0b2dc1d 100644 --- a/junit/rails/game/state/StateTest.java +++ b/junit/rails/game/state/StateTest.java @@ -21,10 +21,10 @@ public class StateTest { @Before public void setUp() { root = Root.create(); - state = new StateImpl(root, STATE_ID, null); - model = new ModelImpl(root, MODEL_ID, null); - state_model = new StateImpl(model, STATE_ID, null); - state_wo_id = new StateImpl(model, null, STATE_TEXT); + state = StateImpl.create(root, STATE_ID, null); + model = ModelImpl.create(root, MODEL_ID, null); + state_model = StateImpl.create(model, STATE_ID, null); + state_wo_id = StateImpl.create(model, null, STATE_TEXT); } @Test diff --git a/junit/rails/game/state/StateTestUtils.java b/junit/rails/game/state/StateTestUtils.java index b47b9ac..b9b580c 100644 --- a/junit/rails/game/state/StateTestUtils.java +++ b/junit/rails/game/state/StateTestUtils.java @@ -40,6 +40,10 @@ class StateTestUtils { root.getStateManager().getChangeStack().redo(); } + public static ChangeSet getCurrentChangeSet(Root root) { + return root.getStateManager().getChangeStack().getCurrentChangeSet(); + } + public static ChangeSet getLastClosedChangeSet(Root root) { return root.getStateManager().getChangeStack().getLastClosedChangeSet(); diff --git a/src/rails/game/state/StateManager.java b/src/rails/game/state/StateManager.java index 26b9187..0b95760 100644 --- a/src/rails/game/state/StateManager.java +++ b/src/rails/game/state/StateManager.java @@ -2,15 +2,20 @@ package rails.game.state; import static com.google.common.base.Preconditions.checkArgument; -import java.util.List; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; @@ -39,30 +44,21 @@ public final class StateManager extends Manager implements DelayedItem { return new StateManager(parent, id); } /** - * Register states - * Remark: Portfolios and Wallets get added from their respective managers automatically + * Register states (usually done automatically at state creation) */ void registerState(State state) { allStates.add(state); -// if (state instanceof Portfolio) { -// return portfolioManager.addPortfolio((Portfolio<?>) state); -// } else if (state instanceof Wallet) { -// return walletManager.addWallet((Wallet<?>) state); -// } } /** - * De-Register states - * Remark: Portfolios and Wallets are removed from their respective managers automatically + * De-Register states */ boolean deRegisterState(State state) { - if (!allStates.remove(state)) return false; -// if (state instanceof PortfolioMap) { -// return portfolioManager.removePortfolio((PortfolioMap<?>) state); -// } else if (state instanceof Wallet) { -// return walletManager.removeWallet((Wallet<?>) state); -// } - return true; + return allStates.remove(state); + } + + ImmutableSet<State> getAllStates() { + return allStates.view(); } /** @@ -84,6 +80,8 @@ public final class StateManager extends Manager implements DelayedItem { /** * Adds the combination of model to observable + * @param Model the model that tracks the observable + * @param Observable the observable to monitor */ void addModel(Model model, Observable observable) { models.put(observable, model); @@ -96,128 +94,61 @@ public final class StateManager extends Manager implements DelayedItem { public ImmutableSet<Model> getModels(Observable observable) { return models.get(observable); } - + /** * A set of states is given as input * and then calculates all observer to update in the correct sequence * - * It uses a topological sort algorithm (Kahn 1962) + * It uses a topological sort based on DFS * - * @param states Set of states - * @return sorted list of all observables (states and models) + * @param states that have changed + * @return sorted list of all models to be updated */ - List<Observable> getSortedObservables(Set<State> states) { - - // 1: define all models - Set<Model> models = getModels(states); - - // 2: define graph - Multimap<Model, Observable> edges = HashMultimap.create(); - - // 2a: add edges that start from states - for (State s:states) { - for (Model m:s.getModels()) { - edges.put(m, s); - } - } - - // 2b: add edges that start from models - for (Model m1:models) { - for (Model m2:m1.getModels()) { - edges.put(m2, m1); - } - } - - // 3: run topological sort - List<Observable> sortedList = Lists.newArrayList(); - List<Observable> startNodes = Lists.newArrayList(); - startNodes.addAll(states); - - while (!startNodes.isEmpty()) { - // remove node n - Observable n = startNodes.remove(0); - // insert node into sortedList - sortedList.add(n); - for (Model m:n.getModels()) { - edges.remove(m, n); - // check if m is now a start node - if (!edges.containsKey(m)) { - startNodes.add(m); - } - } - } + private static enum Color {WHITE, GREY, BLACK} + ImmutableList<Model> getModelsToUpdate(Collection<State> states) { + // Topological sort + // Initialize (we do not use WHITE explicitly, but implicit) + Map<Observable, Color> colors = Maps.newHashMap(); + LinkedList<Model> topoList = Lists.newLinkedList(); - // if graph is not empty => cyclical graph - if (!edges.isEmpty()) { - log.debug("StateManager: Cyclical graph detected in State/Model relations."); - // add remaining models to the end - sortedList.addAll(edges.keySet()); + // For all states + for (State s: states) { + topoSort(s, colors, topoList); } - - return sortedList; + log.debug("Observables to Update = " + topoList.toString()); + return ImmutableList.copyOf(topoList); } - /** - * @param states Set of states - * @return all observers to be updated from states (either directly or via Models) - */ - private Set<Observer> getObservers(Set<State> states){ - - Set<Observer> observers = Sets.newHashSet(); - - // all direct observers - for (State s:states){ - observers.addAll(s.getObservers()); - } - - // all indirect observers - for (Model m:getModels(states)){ - observers.addAll(m.getObservers()); + private void topoSort(Observable v, Map<Observable, Color> colors, LinkedList<Model> topoList) { + colors.put(v, Color.GREY); + for (Model m:v.getModels()) { + if (!colors.containsKey(m)) { + topoSort(m, colors, topoList); + } else if (colors.get(m) == Color.GREY) { + throw new IllegalStateException("Graph of Observables contains Cycle"); + } } - - return observers; + colors.put(v, Color.BLACK); + if (v instanceof Model) topoList.addFirst((Model)v); } + void updateObservers(Set<State> states) { - for (Observable observable:getSortedObservables(states)) { - for (Observer observer:observable.getObservers()) { - observer.update(observable.observerText()); + // all direct observers + for (State s:states){ + for (Observer o:s.getObservers()) { + o.update(s.observerText()); } } - } - - /** - * @param states Set of states - * @return all models to be updated from states - */ - Set<Model> getModels(Set<State> states) { - - Set<Model> allModels = Sets.newHashSet(); - - // add all models updated from states directly - for (State s:states) { - allModels.addAll(s.getModels()); - } - // then add models called indirectly - ImmutableSet<Model> checkModels = ImmutableSet.copyOf(allModels); - Set<Model> newModels = Sets.newHashSet(); - while (!checkModels.isEmpty()) { - for (Model m1:checkModels) { - for (Model m2:m1.getModels()) { - if (!allModels.contains(m2)) { - allModels.add(m2); - newModels.add(m2); - } - } + // all indirect observers + for (Model m:getModelsToUpdate(states)) { + for (Observer o:m.getObservers()) { + o.update(m.observerText()); } - checkModels = ImmutableSet.copyOf(newModels); - newModels.clear(); } - return allModels; } - // void registerReceiver(Triggerable receiver, State toState) { // } |