diff --git a/README.md b/README.md index b662bad47..440dd758a 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,36 @@ Run it like: ```bash java -jar graphwalker-studio/target/graphwalker-studio-.jar ``` + +=================== + +Predefined Path +=================== +This fork of the GraphWalker project enables the user to define an edge sequence in the input graph, along which the machine should execute. + +## Graph input format + +Currently a predefined path can only be specified in JSON GW3 input graphs. + +To define an edge sequence, the model has to contain an array element called *predefinedPathEdgeIds* containing the edge IDs in the sequence. + +The generator and stop condition has to be specified in the *generator* element. + +Example: + +```JSON +{ + "models": [ + { + "generator": "predefined_path(predefined_path)", + ... + "edges": [ + { "id": "e0", ... }, + { "id": "e1", ... }, + { "id": "e2", ... } + ], + "predefinedPathEdgeIds": [ "e0", "e1", "e2", "e0" ] + } + ] +} +``` \ No newline at end of file diff --git a/graphwalker-core/README.md b/graphwalker-core/README.md index 68a67ecb7..df86652a1 100644 --- a/graphwalker-core/README.md +++ b/graphwalker-core/README.md @@ -104,6 +104,7 @@ Are used by path generators. The [algorithm] provides the generators the logic f ### Stop conditions Used by path generators to determine when to stop generating a path. Stop conditions can be logically AND'ed and OR'ed. When a stop condition (or a combination of several) evaluates to true, the generator stops. Examples are: + * **Edge coverage** - The condition is a percentage number. When, during execution, the percentage of traversed edges are reached, the test is stopped. If an edge is traversed more than one time, it still counts as 1, when calculating the percentage coverage. * **Length** - The condition is a number, representing the total numbers of pairs of vertices and edges generated by a generator. For example, if the number is 110, the test sequence would be 220 lines long. (including 110 pairs of edges and vertices). * **Never** - This special condition will never halt the generator. @@ -112,6 +113,7 @@ Used by path generators to determine when to stop generating a path. Stop condit * **Requirement coverage** - The condition is a percentage number. When, during execution, the percentage of traversed requirements is reached, the test is stopped. If an requirement is traversed more than one time, it still counts as 1, when calculating the percentage coverage. * **Time duration** - The condition is a time, representing the number of seconds that the test generator is allowed to execute. * **Vertex coverage** - The condition is a percentage number. When, during execution, the percentage of traversed vertices are reached, the test is stopped. If a vertex is traversed more than one time, it still counts as 1, when calculating the percentage coverage. +* **PredefinedPath** - The condition is the end of the predefined edge sequence. When, during execution, the last edge and its target vertex is reached, the test is stopped. The stop condition only works when a predefined path is present in the model. ### Events @@ -131,6 +133,7 @@ A generator used an algorithm to decide how to traverse a model. Different gener The algorithm works well an very large models, and generates reasonably short sequences. The downside is when used in conjunction with EFSM, the algorithm can choose a path which is blocked by a guard. * **RandomPath** - Navigate through the model in a completely random manor. Also called "Drunkard’s walk", or "Random walk". This algorithm selects an out-edge from a vertex by random, and repeats the process in the next vertex. +* **PredefinedPath** - Navigate through the model deterministically along a predefined edge sequence. This generator only works with its corresponding PredefinedPath stop condition. ### Machines diff --git a/graphwalker-core/src/main/java/org/graphwalker/core/condition/PredefinedPathStopCondition.java b/graphwalker-core/src/main/java/org/graphwalker/core/condition/PredefinedPathStopCondition.java new file mode 100644 index 000000000..81fdebeee --- /dev/null +++ b/graphwalker-core/src/main/java/org/graphwalker/core/condition/PredefinedPathStopCondition.java @@ -0,0 +1,35 @@ +package org.graphwalker.core.condition; + +import org.graphwalker.core.model.Edge; +import org.graphwalker.core.model.Vertex; + +public class PredefinedPathStopCondition extends StopConditionBase { + + public PredefinedPathStopCondition() { + super("PredefinedPath"); + } + + @Override + public boolean isFulfilled() { + return getCurrentStepCount() == getTotalStepCount(); + } + + @Override + public double getFulfilment() { + return (double) getCurrentStepCount() / getTotalStepCount(); + } + + private int getCurrentStepCount() { + // *2 because each index increment corresponds to a vertex-edge step pair + int currentStepCount = getContext().getPredefinedPathCurrentEdgeIndex() * 2; + if (getContext().getCurrentElement() instanceof Vertex.RuntimeVertex) { + return currentStepCount + 1; + } + return currentStepCount; + } + + private int getTotalStepCount() { + return (getContext().getModel().getPredefinedPath().size() * 2) + 1; + } + +} diff --git a/graphwalker-core/src/main/java/org/graphwalker/core/generator/PredefinedPath.java b/graphwalker-core/src/main/java/org/graphwalker/core/generator/PredefinedPath.java new file mode 100644 index 000000000..9decf45ab --- /dev/null +++ b/graphwalker-core/src/main/java/org/graphwalker/core/generator/PredefinedPath.java @@ -0,0 +1,71 @@ +package org.graphwalker.core.generator; + +import org.graphwalker.core.condition.PredefinedPathStopCondition; +import org.graphwalker.core.condition.StopCondition; +import org.graphwalker.core.condition.StopConditionException; +import org.graphwalker.core.machine.Context; +import org.graphwalker.core.model.Edge; +import org.graphwalker.core.model.Element; +import org.graphwalker.core.model.Vertex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class PredefinedPath extends PathGeneratorBase { + + private static final Logger LOG = LoggerFactory.getLogger(PredefinedPath.class); + + public PredefinedPath(StopCondition stopCondition) { + if (!(stopCondition instanceof PredefinedPathStopCondition)) { + throw new StopConditionException("PredefinedPath generator can only work with a PredefinedPathStopCondition instance"); + } + setStopCondition(stopCondition); + } + + @Override + public Context getNextStep() { + Context context = super.getNextStep(); + Element currentElement = context.getCurrentElement(); + List elements = context.filter(context.getModel().getElements(currentElement)); + if (elements.isEmpty()) { + LOG.error("currentElement: " + currentElement); + LOG.error("context.getModel().getElements(): " + context.getModel().getElements()); + throw new NoPathFoundException(context.getCurrentElement()); + } + Element nextElement; + if (currentElement instanceof Edge.RuntimeEdge) { + nextElement = getNextElementFromEdge(context, elements, (Edge.RuntimeEdge) currentElement); + } else if (currentElement instanceof Vertex.RuntimeVertex) { + nextElement = getNextElementFromVertex(context, elements, (Vertex.RuntimeVertex) currentElement); + context.setPredefinedPathCurrentElementIndex(context.getPredefinedPathCurrentEdgeIndex() + 1); + } else { + LOG.error("Current element is neither an edge or a vertex"); + throw new NoPathFoundException(context.getCurrentElement()); + } + context.setCurrentElement(nextElement); + return context; + } + + private Element getNextElementFromEdge(Context context, List reachableElements, Edge.RuntimeEdge currentElement) { + if (reachableElements.size() != 1) { + LOG.error("Next vertex of predefined path is ambiguous (after step " + context.getPredefinedPathCurrentEdgeIndex() + ", from edge with id \"" + currentElement.getId() + "\")"); + throw new NoPathFoundException(currentElement); + } + return reachableElements.get(0); + } + + private Element getNextElementFromVertex(Context context, List reachableElements, Vertex.RuntimeVertex currentElement) { + Element nextElement = context.getModel().getPredefinedPath().get(context.getPredefinedPathCurrentEdgeIndex()); + if (!reachableElements.contains(nextElement)) { + LOG.error("Next edge with id \"" + nextElement.getId() + "\" from predefined path is unreachable (either the guarding condition was not met or the edge has a different source vertex."); + throw new NoPathFoundException(currentElement); + } + return nextElement; + } + + @Override + public boolean hasNextStep() { + return !getStopCondition().isFulfilled(); + } +} diff --git a/graphwalker-core/src/main/java/org/graphwalker/core/machine/Context.java b/graphwalker-core/src/main/java/org/graphwalker/core/machine/Context.java index 719193bc6..4f78f6c39 100644 --- a/graphwalker-core/src/main/java/org/graphwalker/core/machine/Context.java +++ b/graphwalker-core/src/main/java/org/graphwalker/core/machine/Context.java @@ -76,6 +76,10 @@ public interface Context { Context setNextElement(Element nextElement); + Integer getPredefinedPathCurrentEdgeIndex(); + + Context setPredefinedPathCurrentElementIndex(Integer predefinedPathCurrentElementIndex); + List getRequirements(); List getRequirements(RequirementStatus status); diff --git a/graphwalker-core/src/main/java/org/graphwalker/core/machine/ExecutionContext.java b/graphwalker-core/src/main/java/org/graphwalker/core/machine/ExecutionContext.java index 5bcff1354..ec0a685d5 100644 --- a/graphwalker-core/src/main/java/org/graphwalker/core/machine/ExecutionContext.java +++ b/graphwalker-core/src/main/java/org/graphwalker/core/machine/ExecutionContext.java @@ -70,6 +70,7 @@ public abstract class ExecutionContext implements Context { private Element currentElement; private Element nextElement; private Element lastElement; + private Integer predefinedPathCurrentEdgeIndex; private String REGEXP_GLOBAL = "global\\."; @@ -80,6 +81,7 @@ public abstract class ExecutionContext implements Context { public ExecutionContext() { executionEnvironment = org.graalvm.polyglot.Context.newBuilder().allowAllAccess(true).build(); executionEnvironment.getBindings("js").putMember(getClass().getSimpleName(), this); + predefinedPathCurrentEdgeIndex = 0; } public ExecutionContext(Model model, PathGenerator pathGenerator) { @@ -179,6 +181,15 @@ public Context setNextElement(Element nextElement) { return this; } + public Integer getPredefinedPathCurrentEdgeIndex() { + return predefinedPathCurrentEdgeIndex; + } + + public Context setPredefinedPathCurrentElementIndex(Integer predefinedPathCurrentElementIndex) { + this.predefinedPathCurrentEdgeIndex = predefinedPathCurrentElementIndex; + return this; + } + public Context setRequirementStatus(Requirement requirement, RequirementStatus requirementStatus) { requirements.put(requirement, requirementStatus); return this; diff --git a/graphwalker-core/src/main/java/org/graphwalker/core/model/Model.java b/graphwalker-core/src/main/java/org/graphwalker/core/model/Model.java index ca593f153..1ea3b4c37 100644 --- a/graphwalker-core/src/main/java/org/graphwalker/core/model/Model.java +++ b/graphwalker-core/src/main/java/org/graphwalker/core/model/Model.java @@ -27,8 +27,10 @@ */ import org.graphwalker.core.common.Objects; +import org.graphwalker.core.machine.MachineException; import java.util.*; +import java.util.stream.Collectors; import static org.graphwalker.core.common.Objects.*; import static org.graphwalker.core.model.Edge.RuntimeEdge; @@ -54,6 +56,7 @@ public class Model extends BuilderBase { private List vertices = new ArrayList<>(); private List edges = new ArrayList<>(); private List actions = new ArrayList<>(); + private List predefinedPath = new ArrayList<>(); /** * Create a new Model @@ -97,6 +100,19 @@ public Model(RuntimeModel model) { edge.setProperties(runtimeEdge.getProperties()); this.edges.add(edge); } + for (RuntimeEdge runtimeEdge : model.getPredefinedPath()) { + Edge edge = new Edge(); + edge.setId(runtimeEdge.getId()); + edge.setName(runtimeEdge.getName()); + edge.setSourceVertex(cache.get(runtimeEdge.getSourceVertex())); + edge.setTargetVertex(cache.get(runtimeEdge.getTargetVertex())); + edge.setGuard(runtimeEdge.getGuard()); + edge.setActions(runtimeEdge.getActions()); + edge.setRequirements(runtimeEdge.getRequirements()); + edge.setWeight(runtimeEdge.getWeight()); + edge.setProperties(runtimeEdge.getProperties()); + this.predefinedPath.add(edge); + } } /** @@ -141,6 +157,9 @@ public Model addEdge(Edge edge) { * @return The model. */ public Model deleteEdge(Edge edge) { + if (isNotNull(predefinedPath) && predefinedPath.contains(edge)) { + throw new RuntimeException("Cannot remove edge contained in predefined path"); + } edges.remove(edge); return this; } @@ -156,6 +175,10 @@ public Model deleteEdge(Edge edge) { * @return The model. */ public Model deleteVertex(Vertex vertex) { + if (isNotNull(predefinedPath) + && predefinedPath.stream().anyMatch(edge -> vertex.equals(edge.getSourceVertex()) || vertex.equals(edge.getTargetVertex()))) { + throw new RuntimeException("Cannot remove vertex with connecting edge contained in predefined path"); + } edges.removeIf(edge -> vertex.equals(edge.getSourceVertex()) || vertex.equals(edge.getTargetVertex())); vertices.remove(vertex); return this; @@ -224,6 +247,23 @@ public List getEdges() { return edges; } + public Model setPredefinedPath(List predefinedPath) { + this.predefinedPath = predefinedPath.stream() + .map(predefinedPathEdge -> getEdges().stream() + .filter(localEdge -> localEdge.getId().equals(predefinedPathEdge.getId())) + .findFirst().orElseThrow(() -> new RuntimeException("Not all edges from predefined path exist in the model"))) + .collect(Collectors.toList()); + return this; + } + + public List getPredefinedPath() { + return predefinedPath; + } + + public boolean hasPredefinedPath() { + return isNotNullOrEmpty(predefinedPath); + } + /** * Creates an immutable model from this model. * @@ -253,6 +293,7 @@ public static class RuntimeModel extends RuntimeBase { private final Map> inEdgesByVertexCache; private final Map> outEdgesByVertexCache; private final Map> sharedStateCache; + private final List predefinedPath; private RuntimeModel(Model model) { super(model.getId(), model.getName(), model.getActions(), model.getRequirements(), model.getProperties()); @@ -266,6 +307,11 @@ private RuntimeModel(Model model) { this.elementsByNameCache = createElementsByNameCache(); this.elementsByElementCache = createElementsByElementCache(elementsCache, outEdgesByVertexCache); this.sharedStateCache = createSharedStateCache(); + this.predefinedPath = model.getPredefinedPath().stream() + .map(predefinedPathEdge -> this.edges.stream() + .filter(localEdge -> localEdge.getId().equals(predefinedPathEdge.getId())) + .findFirst().orElseThrow(() -> new RuntimeException("Not all edges from predefined path exist in the model"))) + .collect(Collectors.toList()); } /** @@ -379,6 +425,14 @@ public List findEdges(String name) { return edgesByNameCache.get(name); } + public List getPredefinedPath() { + return predefinedPath; + } + + public boolean hasPredefinedPath() { + return isNotNullOrEmpty(predefinedPath); + } + /** * Searches the model for any element matching the search string. *

diff --git a/graphwalker-core/src/test/java/org/graphwalker/core/condition/PredefinedPathStopConditionTest.java b/graphwalker-core/src/test/java/org/graphwalker/core/condition/PredefinedPathStopConditionTest.java new file mode 100644 index 000000000..f876fa777 --- /dev/null +++ b/graphwalker-core/src/test/java/org/graphwalker/core/condition/PredefinedPathStopConditionTest.java @@ -0,0 +1,171 @@ +package org.graphwalker.core.condition; + +import org.graphwalker.core.generator.PredefinedPath; +import org.graphwalker.core.machine.Context; +import org.graphwalker.core.machine.TestExecutionContext; +import org.graphwalker.core.model.*; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +public class PredefinedPathStopConditionTest { + + private Model createSimpleModel() { + Vertex v0 = new Vertex().setId("v0"); + Vertex v1 = new Vertex().setId("v1"); + Edge e0 = new Edge() + .setId("e0") + .setSourceVertex(v0) + .setTargetVertex(v1) + .setActions(Arrays.asList( + new Action("i = 1"), + new Action("b = false") + )); + Edge e1 = new Edge() + .setId("e1") + .setSourceVertex(v1) + .setTargetVertex(v0) + .setGuard(new Guard("i > (1 + 1) * 1.5 && b")); + Edge e2 = new Edge() + .setId("e2") + .setSourceVertex(v1) + .setTargetVertex(v1) + .setActions(Arrays.asList( + new Action("i = i + 1"), + new Action("b = i > 4") + )); + List predefinedPath = Arrays.asList(e0, e2, e2, e2, e2, e1); + return new Model() + .addVertex(v0) + .addVertex(v1) + .addEdge(e0) + .addEdge(e1) + .addEdge(e2) + .setPredefinedPath(predefinedPath); + } + + @Test() + public void testFulfilment() { + Model model = createSimpleModel(); + Vertex v0 = model.getVertices().stream().filter(vertex -> "v0".equals(vertex.getId())).findFirst().orElseThrow(() -> new RuntimeException("Vertex v0 not found")); + Vertex v1 = model.getVertices().stream().filter(vertex -> "v1".equals(vertex.getId())).findFirst().orElseThrow(() -> new RuntimeException("Vertex v1 not found")); + Edge e0 = model.getEdges().stream().filter(edge -> "e0".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e0 not found")); + Edge e1 = model.getEdges().stream().filter(edge -> "e1".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e1 not found")); + Edge e2 = model.getEdges().stream().filter(edge -> "e2".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e2 not found")); + + StopCondition condition = new PredefinedPathStopCondition(); + Context context = new TestExecutionContext(model, new PredefinedPath(condition)); + + context.setPredefinedPathCurrentElementIndex(0); + + context.setCurrentElement(v0.build()); + assertThat(condition.getFulfilment(), is((double) 1/13)); + + context.setCurrentElement(e0.build()); + context.setPredefinedPathCurrentElementIndex(1); + assertThat(condition.getFulfilment(), is((double) 2/13)); + + context.setCurrentElement(v1.build()); + assertThat(condition.getFulfilment(), is((double) 3/13)); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(2); + assertThat(condition.getFulfilment(), is((double) 4/13)); + + context.setCurrentElement(v1.build()); + assertThat(condition.getFulfilment(), is((double) 5/13)); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(3); + assertThat(condition.getFulfilment(), is((double) 6/13)); + + context.setCurrentElement(v1.build()); + assertThat(condition.getFulfilment(), is((double) 7/13)); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(4); + assertThat(condition.getFulfilment(), is((double) 8/13)); + + context.setCurrentElement(v1.build()); + assertThat(condition.getFulfilment(), is((double) 9/13)); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(5); + assertThat(condition.getFulfilment(), is((double) 10/13)); + + context.setCurrentElement(v1.build()); + assertThat(condition.getFulfilment(), is((double) 11/13)); + + context.setCurrentElement(e1.build()); + context.setPredefinedPathCurrentElementIndex(6); + assertThat(condition.getFulfilment(), is((double) 12/13)); + + context.setCurrentElement(v0.build()); + assertThat(condition.getFulfilment(), is((double) 13/13)); + } + + @Test() + public void testIsFulfilled() { + Model model = createSimpleModel(); + Vertex v0 = model.getVertices().stream().filter(vertex -> "v0".equals(vertex.getId())).findFirst().orElseThrow(() -> new RuntimeException("Vertex v0 not found")); + Vertex v1 = model.getVertices().stream().filter(vertex -> "v1".equals(vertex.getId())).findFirst().orElseThrow(() -> new RuntimeException("Vertex v1 not found")); + Edge e0 = model.getEdges().stream().filter(edge -> "e0".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e0 not found")); + Edge e1 = model.getEdges().stream().filter(edge -> "e1".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e1 not found")); + Edge e2 = model.getEdges().stream().filter(edge -> "e2".equals(edge.getId())).findFirst().orElseThrow(() -> new RuntimeException("Edge e2 not found")); + + StopCondition condition = new PredefinedPathStopCondition(); + Context context = new TestExecutionContext(model, new PredefinedPath(condition)); + + context.setPredefinedPathCurrentElementIndex(0); + + context.setCurrentElement(v0.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e0.build()); + context.setPredefinedPathCurrentElementIndex(1); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v1.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(2); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v1.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(3); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v1.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(4); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v1.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e2.build()); + context.setPredefinedPathCurrentElementIndex(5); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v1.build()); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(e1.build()); + context.setPredefinedPathCurrentElementIndex(6); + assertFalse(condition.isFulfilled()); + + context.setCurrentElement(v0.build()); + assertTrue(condition.isFulfilled()); + } + +} diff --git a/graphwalker-core/src/test/java/org/graphwalker/core/generator/PredefinedPathTest.java b/graphwalker-core/src/test/java/org/graphwalker/core/generator/PredefinedPathTest.java new file mode 100644 index 000000000..e3d63f07f --- /dev/null +++ b/graphwalker-core/src/test/java/org/graphwalker/core/generator/PredefinedPathTest.java @@ -0,0 +1,100 @@ +package org.graphwalker.core.generator; + +import org.graphwalker.core.condition.Never; +import org.graphwalker.core.condition.PredefinedPathStopCondition; +import org.graphwalker.core.condition.StopConditionException; +import org.graphwalker.core.condition.VertexCoverage; +import org.graphwalker.core.machine.*; +import org.graphwalker.core.model.Edge; +import org.graphwalker.core.model.Model; +import org.graphwalker.core.model.Vertex; +import org.junit.Test; + +import java.util.Arrays; + +import static org.graphwalker.core.Models.*; +import static org.junit.Assert.*; + +public class PredefinedPathTest { + + @Test + public void simpleTest() { + // Model + Model model = fourEdgesModel(); + Edge edgeAB = model.getEdges().stream().filter(edge -> "ab".equals(edge.getId())).findFirst().get(); + Edge edgeAB_2 = model.getEdges().stream().filter(edge -> "ab_2".equals(edge.getId())).findFirst().get(); + Edge edgeAB_3 = model.getEdges().stream().filter(edge -> "ab_3".equals(edge.getId())).findFirst().get(); + Edge edgeBA = model.getEdges().stream().filter(edge -> "ba".equals(edge.getId())).findFirst().get(); + model.setPredefinedPath(Arrays.asList(edgeAB, edgeBA, edgeAB_2, edgeBA, edgeAB_3)); + + // Runtime model + Model.RuntimeModel runtimeModel = model.build(); + Vertex.RuntimeVertex source = findVertex(runtimeModel, "A"); + Vertex.RuntimeVertex target = findVertex(runtimeModel, "B"); + Edge.RuntimeEdge runtimeEdgeAB = findEdge(runtimeModel, "ab"); + Edge.RuntimeEdge runtimeEdgeAB_2 = findEdge(runtimeModel, "ab_2"); + Edge.RuntimeEdge runtimeEdgeAB_3 = findEdge(runtimeModel, "ab_3"); + Edge.RuntimeEdge runtimeEdgeBA = findEdge(runtimeModel, "ba"); + + // Context, generator and machine + Context context = new TestExecutionContext().setModel(runtimeModel).setNextElement(source); + PathGenerator generator = new PredefinedPath(new PredefinedPathStopCondition()); + context.setPathGenerator(generator); + Machine machine = new SimpleMachine(context); + + // Tests + assertTrue(machine.hasNextStep()); + assertEquals(machine.getNextStep().getCurrentElement(), source); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeAB); + assertEquals(machine.getNextStep().getCurrentElement(), target); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeBA); + assertEquals(machine.getNextStep().getCurrentElement(), source); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeAB_2); + assertEquals(machine.getNextStep().getCurrentElement(), target); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeBA); + assertEquals(machine.getNextStep().getCurrentElement(), source); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeAB_3); + assertEquals(machine.getNextStep().getCurrentElement(), target); + assertFalse(machine.hasNextStep()); + } + + @Test(expected = MachineException.class) + public void testUnreachableEdge() throws Exception { + // Model + Model model = fourEdgesModel(); + Edge edgeAB = model.getEdges().stream().filter(edge -> "ab".equals(edge.getId())).findFirst().get(); + Edge edgeAB_2 = model.getEdges().stream().filter(edge -> "ab_2".equals(edge.getId())).findFirst().get(); + Edge edgeAB_3 = model.getEdges().stream().filter(edge -> "ab_3".equals(edge.getId())).findFirst().get(); + Edge edgeBA = model.getEdges().stream().filter(edge -> "ba".equals(edge.getId())).findFirst().get(); + model.setPredefinedPath(Arrays.asList(edgeAB, edgeAB_2)); + + // Runtime model + Model.RuntimeModel runtimeModel = model.build(); + Vertex.RuntimeVertex source = findVertex(runtimeModel, "A"); + Vertex.RuntimeVertex target = findVertex(runtimeModel, "B"); + Edge.RuntimeEdge runtimeEdgeAB = findEdge(runtimeModel, "ab"); + Edge.RuntimeEdge runtimeEdgeAB_2 = findEdge(runtimeModel, "ab_2"); + Edge.RuntimeEdge runtimeEdgeAB_3 = findEdge(runtimeModel, "ab_3"); + Edge.RuntimeEdge runtimeEdgeBA = findEdge(runtimeModel, "ba"); + + // Context, generator and machine + Context context = new TestExecutionContext().setModel(runtimeModel).setNextElement(source); + PathGenerator generator = new PredefinedPath(new PredefinedPathStopCondition()); + context.setPathGenerator(generator); + Machine machine = new SimpleMachine(context); + + // Tests + assertTrue(machine.hasNextStep()); + assertEquals(machine.getNextStep().getCurrentElement(), source); + assertEquals(machine.getNextStep().getCurrentElement(), runtimeEdgeAB); + assertEquals(machine.getNextStep().getCurrentElement(), target); + assertTrue(machine.hasNextStep()); + machine.getNextStep(); // should fail + } + + @Test(expected = StopConditionException.class) + public void testIncompatibleStopConditionInstance() { + new PredefinedPath(new Never()); // should fail + } + +} diff --git a/graphwalker-core/src/test/java/org/graphwalker/core/machine/SimpleMachineTest.java b/graphwalker-core/src/test/java/org/graphwalker/core/machine/SimpleMachineTest.java index cba7d426b..5452ea70a 100644 --- a/graphwalker-core/src/test/java/org/graphwalker/core/machine/SimpleMachineTest.java +++ b/graphwalker-core/src/test/java/org/graphwalker/core/machine/SimpleMachineTest.java @@ -32,17 +32,12 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import org.graphwalker.core.condition.EdgeCoverage; -import org.graphwalker.core.condition.ReachedVertex; -import org.graphwalker.core.condition.StopConditionException; -import org.graphwalker.core.condition.VertexCoverage; -import org.graphwalker.core.generator.AStarPath; -import org.graphwalker.core.generator.RandomPath; -import org.graphwalker.core.generator.ShortestAllPaths; -import org.graphwalker.core.generator.SingletonRandomGenerator; +import org.graphwalker.core.condition.*; +import org.graphwalker.core.generator.*; import org.graphwalker.core.model.Action; import org.graphwalker.core.model.Edge; import org.graphwalker.core.model.Element; @@ -506,4 +501,37 @@ public void resetMachine() throws Exception { .map(Execution::getElement).collect(Collectors.toList()); assertThat(expectedPath, is(path)); } + + @Test + public void predefinedPathCurrentEdgeIndex() { + // Model + Vertex v0 = new Vertex().setId("v0"); + Vertex v1 = new Vertex().setId("v1"); + Edge e0 = new Edge().setId("e0").setSourceVertex(v0).setTargetVertex(v1); + Edge e1 = new Edge().setId("e1").setSourceVertex(v0).setTargetVertex(v1); + Edge e2 = new Edge().setId("e2").setSourceVertex(v0).setTargetVertex(v1); + Edge e3 = new Edge().setId("e3").setSourceVertex(v1).setTargetVertex(v0); + List predefinedPath = Arrays.asList(e0, e3, e1, e3, e2); + Model model = new Model() + .addVertex(v0).addVertex(v1) + .addEdge(e0).addEdge(e1).addEdge(e2).addEdge(e3) + .setPredefinedPath(predefinedPath); + + // Context and machine + Context context = new TestExecutionContext(model, new PredefinedPath(new PredefinedPathStopCondition())); + context.setNextElement(v0); + Machine machine = new SimpleMachine(context); + + // Tests + for (int stepCount = 0; stepCount < 5; ++stepCount) { + // Current element is a vertex + assertEquals((Integer) stepCount, context.getPredefinedPathCurrentEdgeIndex()); + machine.getNextStep(); + // Current element is an edge + assertEquals((Integer) stepCount, context.getPredefinedPathCurrentEdgeIndex()); + machine.getNextStep(); + } + // Last element is a vertex + assertEquals((Integer) 5, context.getPredefinedPathCurrentEdgeIndex()); + } } diff --git a/graphwalker-core/src/test/java/org/graphwalker/core/model/ModelTest.java b/graphwalker-core/src/test/java/org/graphwalker/core/model/ModelTest.java index 5ca17d197..6cac985ab 100644 --- a/graphwalker-core/src/test/java/org/graphwalker/core/model/ModelTest.java +++ b/graphwalker-core/src/test/java/org/graphwalker/core/model/ModelTest.java @@ -32,8 +32,10 @@ import static org.junit.Assert.*; import java.util.Arrays; +import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.graphwalker.core.model.Model.RuntimeModel; import org.junit.Test; @@ -352,4 +354,102 @@ public void testInequality() throws Exception { assertFalse(model1.build().equals(null)); assertThat(model1.build(), not(model1)); } + + @Test + public void setPredefinedPath() { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + Model model = new Model() + .addVertex(vertex) + .addEdge(edge) + .setPredefinedPath(predefinedPath); + assertEquals(predefinedPath, model.getPredefinedPath()); + } + + @Test + public void buildModelWithPredefinedPath() { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + Model model = new Model() + .addVertex(vertex) + .addEdge(edge) + .setPredefinedPath(predefinedPath); + RuntimeModel runtimeModel = model.build(); + String[] predefinedPathEdgeIds = predefinedPath.stream().map(Edge::getId).toArray(String[]::new); + String[] runtimeModelPredefinedPathEdgeIds = runtimeModel.getPredefinedPath().stream().map(RuntimeBase::getId).toArray(String[]::new); + assertArrayEquals(predefinedPathEdgeIds, runtimeModelPredefinedPathEdgeIds); + } + + @Test + public void recreateModelWithPredefinedPath() throws Exception { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + Model model1 = new Model() + .addVertex(vertex) + .addEdge(edge) + .setPredefinedPath(predefinedPath); + Model model2 = new Model(model1.build()); + String[] model1PredefinedPathEdgeIds = model1.getPredefinedPath().stream().map(Edge::getId).toArray(String[]::new); + String[] model2PredefinedPathEdgeIds = model2.getPredefinedPath().stream().map(Edge::getId).toArray(String[]::new); + assertArrayEquals(model1PredefinedPathEdgeIds, model2PredefinedPathEdgeIds); + } + + @Test(expected = RuntimeException.class) + public void testSetPredefinedPathWithUnknownEdge() throws Exception { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + new Model().setPredefinedPath(predefinedPath); // should fail + } + + @Test(expected = RuntimeException.class) + public void testRemoveEdgeContainedInPredefinedPath() throws Exception { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + Model model = new Model() + .addVertex(vertex) + .addEdge(edge) + .setPredefinedPath(predefinedPath); + model.deleteEdge(edge); // should fail + } + + @Test(expected = RuntimeException.class) + public void testRemoveVertexWithConnectingEdgeContainedInPredefinedPath() throws Exception { + Vertex vertex = new Vertex() + .setId("v"); + Edge edge = new Edge() + .setId("v") + .setSourceVertex(vertex) + .setTargetVertex(vertex); + List predefinedPath = Arrays.asList(edge, edge); + Model model = new Model() + .addVertex(vertex) + .addEdge(edge) + .setPredefinedPath(predefinedPath); + model.deleteVertex(vertex); // should fail + } } diff --git a/graphwalker-dsl/src/main/java/org/graphwalker/dsl/antlr/generator/GeneratorLoader.java b/graphwalker-dsl/src/main/java/org/graphwalker/dsl/antlr/generator/GeneratorLoader.java index 0d49eb2b2..98fa33e79 100644 --- a/graphwalker-dsl/src/main/java/org/graphwalker/dsl/antlr/generator/GeneratorLoader.java +++ b/graphwalker-dsl/src/main/java/org/graphwalker/dsl/antlr/generator/GeneratorLoader.java @@ -32,13 +32,7 @@ import java.util.concurrent.TimeUnit; import org.graphwalker.core.condition.*; -import org.graphwalker.core.generator.AStarPath; -import org.graphwalker.core.generator.CombinedPath; -import org.graphwalker.core.generator.PathGenerator; -import org.graphwalker.core.generator.QuickRandomPath; -import org.graphwalker.core.generator.RandomPath; -import org.graphwalker.core.generator.ShortestAllPaths; -import org.graphwalker.core.generator.WeightedRandomPath; +import org.graphwalker.core.generator.*; import org.graphwalker.dsl.generator.GeneratorParser; import org.graphwalker.dsl.generator.GeneratorParserBaseListener; @@ -83,6 +77,8 @@ public void exitStopCondition(GeneratorParser.StopConditionContext ctx) { stopConditions.add(new RequirementCoverage(Integer.parseInt(ctx.getChild(2).getText()))); } else if ("length".equals(conditionName)) { stopConditions.add(new Length(Integer.parseInt(ctx.getChild(2).getText()))); + } else if ("predefined_path".equals(conditionName) || "predefinedpath".equals(conditionName)) { + stopConditions.add(new PredefinedPathStopCondition()); } } @@ -111,6 +107,8 @@ public void exitGenerator(GeneratorParser.GeneratorContext context) { pathGenerators.add(new AStarPath((ReachedStopCondition) stopCondition)); } else if ("shortest_all_paths".equals(generatorName) || "shortestallpaths".equals(generatorName)) { pathGenerators.add(new ShortestAllPaths(stopCondition)); + } else if ("predefined_path".equals(generatorName) || "predefinedpath".equals(generatorName)) { + pathGenerators.add(new PredefinedPath(stopCondition)); } else { Class generatorClass = GeneratorFactoryScanner.get(generatorName); try { diff --git a/graphwalker-dsl/src/test/java/org/graphwalker/dsl/GeneratorFactoryTest.java b/graphwalker-dsl/src/test/java/org/graphwalker/dsl/GeneratorFactoryTest.java index c1a5635f9..fc3940e2b 100644 --- a/graphwalker-dsl/src/test/java/org/graphwalker/dsl/GeneratorFactoryTest.java +++ b/graphwalker-dsl/src/test/java/org/graphwalker/dsl/GeneratorFactoryTest.java @@ -30,24 +30,8 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; -import org.graphwalker.core.condition.AlternativeCondition; -import org.graphwalker.core.condition.CombinedCondition; -import org.graphwalker.core.condition.DependencyEdgeCoverage; -import org.graphwalker.core.condition.EdgeCoverage; -import org.graphwalker.core.condition.Length; -import org.graphwalker.core.condition.ReachedEdge; -import org.graphwalker.core.condition.ReachedVertex; -import org.graphwalker.core.condition.RequirementCoverage; -import org.graphwalker.core.condition.StopCondition; -import org.graphwalker.core.condition.TimeDuration; -import org.graphwalker.core.condition.VertexCoverage; -import org.graphwalker.core.generator.AStarPath; -import org.graphwalker.core.generator.CombinedPath; -import org.graphwalker.core.generator.PathGenerator; -import org.graphwalker.core.generator.QuickRandomPath; -import org.graphwalker.core.generator.RandomPath; -import org.graphwalker.core.generator.ShortestAllPaths; -import org.graphwalker.core.generator.WeightedRandomPath; +import org.graphwalker.core.condition.*; +import org.graphwalker.core.generator.*; import org.graphwalker.dsl.antlr.DslException; import org.graphwalker.dsl.antlr.generator.GeneratorFactory; import org.graphwalker.dsl.antlr.generator.GeneratorFactoryException; @@ -314,6 +298,14 @@ public void multipleAlternativeStopConditions() { assertThat(((AlternativeCondition) generator.getStopCondition()).getStopConditions().size(), is(3)); } + @Test + public void predefinedPath_predefinedPathStopCondition() { + PathGenerator generator = GeneratorFactory.parse("predefined_path(predefined_path)"); + assertThat(generator, instanceOf(PredefinedPath.class)); + assertThat(generator.getStopCondition(), instanceOf(PredefinedPathStopCondition.class)); + assertThat(generator.getStopCondition().getValue(), is("PredefinedPath")); + } + /** * Tries to load a plugin generator that should be found in the classpath */ diff --git a/graphwalker-io/src/main/java/org/graphwalker/io/factory/json/JsonModel.java b/graphwalker-io/src/main/java/org/graphwalker/io/factory/json/JsonModel.java index 3dd257764..953ca38d4 100644 --- a/graphwalker-io/src/main/java/org/graphwalker/io/factory/json/JsonModel.java +++ b/graphwalker-io/src/main/java/org/graphwalker/io/factory/json/JsonModel.java @@ -30,11 +30,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.graphwalker.core.model.Action; -import org.graphwalker.core.model.Edge; -import org.graphwalker.core.model.Model; -import org.graphwalker.core.model.Requirement; -import org.graphwalker.core.model.Vertex; +import java.util.stream.Collectors; + +import org.graphwalker.core.model.*; +import org.graphwalker.io.factory.ContextFactoryException; /** * @author Nils Olsson @@ -50,6 +49,7 @@ public class JsonModel { private Map properties; private List vertices; private List edges; + private List predefinedPathEdgeIds; public String getId() { return id; @@ -123,6 +123,14 @@ public void setEdges(List edges) { this.edges = edges; } + public List getPredefinedPathEdgeIds() { + return predefinedPathEdgeIds; + } + + public void setPredefinedPathEdgeIds(List predefinedPathEdgeIds) { + this.predefinedPathEdgeIds = predefinedPathEdgeIds; + } + public boolean isModel() { return !(null == name || null == generator || null == edges || null == vertices); } @@ -162,6 +170,14 @@ public Model getModel() { model.addEdge(edge); } } + + if (predefinedPathEdgeIds != null) { + List predefinedPath = getPredefinedPathEdgeIds().stream() + .map(this::findEdgeById) + .collect(Collectors.toList()); + model.setPredefinedPath(predefinedPath); + } + return model; } @@ -201,6 +217,12 @@ public void setModel(Model.RuntimeModel model) { jsonEdge.setEdge(edge); edges.add(jsonEdge); } + + if (model.hasPredefinedPath()) { + predefinedPathEdgeIds = model.getPredefinedPath().stream() + .map(RuntimeBase::getId).collect(Collectors.toList()); + } + } public void setModel(Model model) { @@ -233,4 +255,10 @@ public void copyValuesTo(Model model) { model.getProperties().putAll(properties); } } + + private Edge findEdgeById(String edgeId) { + return getEdges().stream().filter(edge -> edgeId.equals(edge.getEdge().getId())) + .findFirst().orElseThrow(() -> new ContextFactoryException("Edge with id \"" + edgeId + "\" could not be found in JsonModel")) + .getEdge(); + } } diff --git a/graphwalker-io/src/test/java/org/graphwalker/io/factory/json/JsonContextFactoryTest.java b/graphwalker-io/src/test/java/org/graphwalker/io/factory/json/JsonContextFactoryTest.java index 56680190d..0dcea0ae5 100644 --- a/graphwalker-io/src/test/java/org/graphwalker/io/factory/json/JsonContextFactoryTest.java +++ b/graphwalker-io/src/test/java/org/graphwalker/io/factory/json/JsonContextFactoryTest.java @@ -345,4 +345,28 @@ public void petClinicWithSeed() throws IOException { assertThat(SingletonRandomGenerator.nextInt(), is(-2090749135)); assertThat(SingletonRandomGenerator.nextInt(), is(-287790814)); } + + @Test + public void predefinedPath() throws IOException { + List contexts = new JsonContextFactory().create(Paths.get("json/ModelWithPredefinedPath.json")); + assertNotNull(contexts); + assertThat(contexts.size(), is(1)); + + Context context = contexts.get(0); + + assertThat(context.getModel().getVertices().size(), is(2)); + assertThat(context.getModel().getEdges().size(), is(4)); + assertThat(context.getModel().getPredefinedPath().size(), is(5)); + + assertThat(context.getModel().getPredefinedPath().get(0).getId(), is("e0")); + assertThat(context.getModel().getPredefinedPath().get(1).getId(), is("e3")); + assertThat(context.getModel().getPredefinedPath().get(2).getId(), is("e1")); + assertThat(context.getModel().getPredefinedPath().get(3).getId(), is("e3")); + assertThat(context.getModel().getPredefinedPath().get(4).getId(), is("e2")); + } + + @Test(expected = ContextFactoryException.class) + public void testPredefinedPathWithUnknownEdge() throws Exception { + new JsonContextFactory().create(Paths.get("json/ModelWithPredefinedPath_unknown_edge.json")); // should fail + } } diff --git a/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath.json b/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath.json new file mode 100644 index 000000000..2b02c2e06 --- /dev/null +++ b/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath.json @@ -0,0 +1,46 @@ +{ + "models": [ + { + "name": "Small model", + "generator": "predefined_path(predefined_path)", + "startElementId": "n0", + "vertices": [ + { + "name": "n0", + "id": "n0" + }, + { + "name": "n1", + "id": "n1" + } + ], + "edges": [ + { + "name": "e0", + "id": "e0", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e1", + "id": "e1", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e2", + "id": "e2", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e3", + "id": "e3", + "sourceVertexId": "n1", + "targetVertexId": "n0" + } + ], + "predefinedPathEdgeIds": ["e0", "e3", "e1", "e3", "e2"] + } + ] +} diff --git a/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath_unknown_edge.json b/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath_unknown_edge.json new file mode 100644 index 000000000..71ecf7e89 --- /dev/null +++ b/graphwalker-io/src/test/resources/json/ModelWithPredefinedPath_unknown_edge.json @@ -0,0 +1,46 @@ +{ + "models": [ + { + "name": "Small model", + "generator": "predefined_path(predefined_path)", + "startElementId": "n0", + "vertices": [ + { + "name": "n0", + "id": "n0" + }, + { + "name": "n1", + "id": "n1" + } + ], + "edges": [ + { + "name": "e0", + "id": "e0", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e1", + "id": "e1", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e2", + "id": "e2", + "sourceVertexId": "n0", + "targetVertexId": "n1" + }, + { + "name": "e3", + "id": "e3", + "sourceVertexId": "n1", + "targetVertexId": "n0" + } + ], + "predefinedPathEdgeIds": ["e0", "e3", "e1", "e3", "e2", "e4"] + } + ] +}