|
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) {
// }
|