package dgm.graphs; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.tinkerpop.blueprints.*; import dgm.*; import dgm.exceptions.DegraphmalizerException; import org.neo4j.helpers.collection.Iterables; import org.testng.annotations.*; import java.io.IOException; import java.util.*; import static dgm.GraphUtilities.*; import static org.fest.assertions.Assertions.assertThat; public class SubgraphManagerTest { final Random random = new Random(System.currentTimeMillis()); LocalGraph lg; final ObjectMapper om = new ObjectMapper(); @BeforeMethod public void clearGraph() { lg = LocalGraph.localNode(); } @AfterMethod public void shutdownGraph() { lg.G.shutdown(); } /** * It should not be possible to create an edge that has the same id for head and tail */ @Test(expectedExceptions = IllegalArgumentException.class) void testCommitOrUpdateEdgeWithHeadEqualToTailFails() throws DegraphmalizerException { final MutableSubgraph sg = new MutableSubgraph(); final ID id = randomVersionedID(); sg.beginEdge("test", getSymbolicID(id), Subgraph.Direction.INWARDS); lg.sgm.commitSubgraph(id, sg); } /** * If you commitAndFindCentralVertex a subgraph, All the mandatory properties should be created, plus * the extra properties that you set on the subgraph. * * @throws Exception */ @Test void testCommitSubgraphCreateVertex() throws Exception { // commit subgraph and verify final Props p = new Props("[1,2,3]", "{\"a\":2}", "3", "\"abc\"", "{}"); commitSubgraphAndVerifyProperties(p, randomVersionedID()); } /** * One should not be allowed to commit a subgraph to a symbolic ID (ie. version == 0) * */ @Test(expectedExceptions = IllegalArgumentException.class) void testVersionZeroNotAllowed() { final ID symbolicID = randomSymbolicID(); lg.sgm.commitSubgraph(symbolicID, Subgraphs.EMPTY_SUBGRAPH); } /** * Thou shall not create edges referring to other vertices by version! */ @Test(expectedExceptions = IllegalArgumentException.class) void testNotSymbolicEdgeAllowed() throws DegraphmalizerException { final MutableSubgraph sg = new MutableSubgraph(); sg.beginEdge("label", randomVersionedID(), Subgraph.Direction.INWARDS); lg.sgm.commitSubgraph(randomVersionedID(), sg); } /** * if you create a subgraph with an id for which a symbolic node already exists, it * should claim that node. * @throws Exception */ @Test void testCommitSubgraphWithEdgeClaimsExistingSymbolicVertexForEdge() throws Exception { // first create some node in the graph final ID id0 = randomVersionedID(); final Props p0 = new Props("[1,2,3]", "{\"a\":2}", "3", "\"abc\"", "{}"); commitSubgraphAndVerifyProperties(p0, id0); // now we create a subgraph referring the node above final ID id1 = randomVersionedID(); final MutableSubgraph sg1 = new MutableSubgraph(); final Props p1 = new Props("\"foo\""); p1.modifySubgraph(sg1); sg1.beginEdge("label", getSymbolicID(id0), Subgraph.Direction.INWARDS); lg.sgm.commitSubgraph(id1, sg1); // there should now be two nodes, and one edge assertThat(lg.G.getVertices()).hasSize(2); assertThat(lg.G.getEdges()).hasSize(1); final Vertex v0 = resolveVertex(om, lg.G, id0); final Vertex v1 = resolveVertex(om, lg.G, id1); assertThat(getID(om, v0)).isEqualTo(id0); assertThat(getID(om, v1)).isEqualTo(id1); p0.assertOK(v0); p1.assertOK(v1); final EdgeID edgeID = new EdgeID(id0, "label", id1); final Edge e = findEdge(om, lg.G, edgeID); assertThat(e).isNotNull(); assertThat(e.getVertex(Direction.IN)).isEqualTo(v1); assertThat(e.getVertex(Direction.OUT)).isEqualTo(v0); } /** * If you create a subgraph for which a symbolic vertex exists, it should claim that * vertex, like there is no tomorrow! * Also should it adopt the edges from that id */ @Test void testCommitSubgraphClaimsExistingSymbolicVertex() throws DegraphmalizerException { //first create a graph with two vertices and an edge. One vertex is symbolic, the other is not. final ID id1 = new ID("a", "b", "c1", 0); final ID id2 = new ID("a", "b", "c2", 2); addVertexWithId(id1, true); addVertexWithId(id2, false); createEdge(om, lg.G, new EdgeID(id1, "edge", id2)); //now create a subgraph for id1, but with a version final ID id1a = new ID("a", "b", "c1", 1); Vertex v1 = commitAndFindCentralVertex(id1a); //now assert we still have the edge assertThat(Iterables.toList(v1.getEdges(Direction.BOTH)).size()).isEqualTo(1); assertThat(v1.getEdges(Direction.BOTH).iterator().next().getId().equals(id2)); } @Test void testCommitSubgraphWithExistingIdThrows() { //TODO: implement } /** * if you create a subgraph with edges then the central vertex, the edges and symbolic vertices should be created * in the graph. * * @throws Exception */ @Test void testCommitSubgraphCreateEdgesAndCreatesSymbolicVertices() throws Exception { final ID sourceId = randomVersionedID(); final ID targetId = randomSymbolicID(); // create a subgraph that has an edge. the other node of the edge doesn't exist yet. final MutableSubgraph sg = new MutableSubgraph(); // create an edge: sourceId ---> targetId final Props p = new Props("true", "\"twee\""); final MutableSubgraph.Edge e = sg.beginEdge("edge1", targetId, Subgraph.Direction.OUTWARDS); for(Map.Entry<String,JsonNode> pe : p.properties.entrySet()) e.property(pe.getKey(), pe.getValue()); final Vertex v = commitAndFindCentralVertex(sg, sourceId); assertThat(lg.G.getEdges()).hasSize(1); assertThat(lg.G.getVertices()).hasSize(2); // test if the edge is created, and the other node as well. It should be a symbolic node. Edge edge = v.getEdges(Direction.OUT).iterator().next(); //edge -> IN == v2 :: edge -> OUT == v1 final Vertex head = edge.getVertex(Direction.IN); assertThat(getID(om, head).version()).isZero(); assertThat(getID(om, head)).isEqualTo(targetId); assertThat(getID(om, v)).isEqualTo(sourceId); } /** * If you create a subgraph with edges to symbolic vertices that are already in the graph, these should be * used to create the edges to. * @throws Exception */ @Test void testCommitSubgraphCreateEdgesUsesExistingSymbolicVertices() throws Exception { //first create an existing virtual node ID targetId = randomSymbolicID(); addVertexWithId(targetId, true); //then create a subgraph, that creates an edge to this existing symbolic vertex final ID id = randomVersionedID(); final MutableSubgraph sg = new MutableSubgraph(); sg.beginEdge("edge1", targetId, Subgraph.Direction.OUTWARDS); lg.sgm.commitSubgraph(id, sg); Vertex v = commitAndFindCentralVertex(sg, id); //test if the edge has been created properly Edge edge = v.getEdges(Direction.OUT).iterator().next(); Vertex vout = edge.getVertex(Direction.IN); assertThat(getID(om, vout)).isEqualTo(targetId); //test if exactly two vertices are created. assertThat(Iterables.toList(lg.G.getVertices()).size()).isEqualTo(2); } /** * When you create a subgraph with an edge to an existing vertex, and this vertex is not symbolic, its properties *should remain intact. * In practice you may not know what the version is for the target vertex. Perhaps the version field should be * ignored for 'other' vertices for edges? */ @Test public void creatingAnEdgeToAVertexShouldLeaveItsPropertiesIntact() throws Exception { //create an existing vertex in the graph, with some properties, and a real version. ID targetID = new ID("a", "b", "c", 1); Vertex v = addVertexWithId(targetID, false); v.setProperty("foo", "bar"); v.setProperty("too", "bad"); //create a subgraph that links to this vertex, and commit it. final MutableSubgraph sg = new MutableSubgraph(); sg.beginEdge("edge1", getSymbolicID(targetID), Subgraph.Direction.INWARDS); lg.sgm.commitSubgraph(randomVersionedID(), sg); //test that the vertex still has the properties. Vertex targetVertex = findVertex(om, lg.G, targetID); assertThat(targetVertex).isEqualTo(v); assertThat(targetVertex.getPropertyKeys().size()).isEqualTo(5); checkElementProperty(targetVertex, "foo", "bar"); checkElementProperty(targetVertex, "too", "bad"); } /** * Updating a subgraph should remove the properties of the old version that are not in the new version too. * @throws Exception */ @Test void testUpdateSubgraphReplacesProperties() throws Exception { //first create a graph with a vertex for the id, with some properties. This is not a symbolic vertex, but //the previous version. final ID ID = randomVersionedID(); Vertex v = addVertexWithId(ID, false); v.setProperty("foo", "bar"); //now create a subgraph for this id, and set different properties. ID newID = new ID(ID.index(), ID.type(), ID.id(), ID.version()+1); final MutableSubgraph sg = new MutableSubgraph(); new Props("\"een\"", "\"twee\"").modifySubgraph(sg); Vertex v1 = commitAndFindCentralVertex(sg, newID); //now check if all the old properties are gone. checkElementProperty(v1, "prop0", "een"); checkElementProperty(v1, "prop1", "twee"); assertThat(v1.getPropertyKeys().contains("foo")).isFalse(); assertThat(getID(om, v1).version()).isEqualTo(newID.version()); } /** * Creating a subgraph for an existing vertex with a higher version should work ok. * @throws DegraphmalizerException */ @Test void testUpdateSubgraphWithHigherVersion() throws DegraphmalizerException { testUpgradeSubgraphToVersion(2, 3); } /** * Creating a subgraph for an existing vertex with an equal version should work ok. * @throws DegraphmalizerException */ @Test void testUpdateSubgraphWithEqualVersion() throws DegraphmalizerException { testUpgradeSubgraphToVersion(2, 2); } private void testUpgradeSubgraphToVersion(long preveousVersion, long nextVersion) throws DegraphmalizerException { //first create a graph with a vertex for the id This is not a symbolic vertex, but //the previous version. final ID id = new ID("a", "b", "c", preveousVersion); addVertexWithId(id, false); //now create a subgraph for the same id, with a new version. //If we commit the subgraph and the version is not equal or higher, a DegraphmalizerException should be thrown final ID nextVersionId = new ID("a", "b", "c", nextVersion); lg.sgm.commitSubgraph(nextVersionId, new MutableSubgraph()); } /** * If you update a subgraph: * - edges in the previous version but not in the new one should be removed * - target vertices of those obsolete edges that are symbolic should be removed. * - target vertices of those obsolete edges that are not symbolic should be maintained; */ @Test void testUpdateSubgraphShouldRemovePreviousEdgesAndSymbolicTargetVertices() throws DegraphmalizerException { //first create a subgraph with a bunch of edges ID rootId = new ID("a", "b", "c0", 1); final ID realVertexIncoming = new ID("a", "c", "c1", 3); final ID realVertexOutgoing = new ID("a", "d", "c2", 3); final ID symbolicVertexIncoming = new ID("a", "c", "c3", 0); /* symbolic nodes can be claimed by the subgraph. */ final ID symbolicVertexOutGoing = new ID("a", "c", "c4", 0); addEdgeAndVertices(rootId, realVertexOutgoing, "e1", Direction.OUT); addEdgeAndVertices(rootId, symbolicVertexOutGoing, "e2", Direction.OUT); addEdgeAndVertices(rootId, realVertexIncoming, "e3", Direction.IN); addEdgeAndVertices(rootId, symbolicVertexIncoming, "e4", Direction.IN); lg.G.stopTransaction(TransactionalGraph.Conclusion.SUCCESS); //let's check we actually have the edges. final Vertex v1 = findVertex(om, lg.G, rootId); assertThat(Iterables.toList(v1.getEdges(Direction.OUT))).hasSize(2); assertThat(Iterables.toList(v1.getEdges(Direction.IN))).hasSize(2); //and now update the subgraph to a version without edges. rootId = updateIDVersion(rootId, 2); final MutableSubgraph sg = new MutableSubgraph(); lg.sgm.commitSubgraph(rootId, sg); //assert that the new subgraph has no edges, final Vertex v2 = findVertex(om, lg.G, rootId); assertThat(Iterables.toList(v2.getEdges(Direction.BOTH))).isEmpty(); //the two symbolic vertices are gone, assertThat(findVertex(om, lg.G, symbolicVertexIncoming)).isNull(); assertThat(findVertex(om, lg.G, symbolicVertexOutGoing)).isNull(); //and the two versioned vertices are still there. assertThat(findVertex(om, lg.G, realVertexIncoming)).isNotNull(); assertThat(findVertex(om, lg.G, realVertexOutgoing)).isNotNull(); } @Test void testDeleteSubgraph() throws DegraphmalizerException { // Setup first subgraph final ID v0 = new ID("a", "b", "v0", 1); final ID v1s = new ID("a", "b", "v1", 0); final EdgeID e0 = new EdgeID(v0, "e0", v1s); final MutableSubgraph sg0 = new MutableSubgraph(); sg0.beginEdge(e0.label(), v1s, Subgraph.Direction.OUTWARDS); lg.sgm.commitSubgraph(v0, sg0); // Setup second subgraph final ID v1 = new ID("a", "b", "v1", 1); final ID v2s = new ID("a", "b", "v2", 0); final EdgeID e1 = new EdgeID(v1, "e1", v2s); final MutableSubgraph sg1 = new MutableSubgraph(); sg1.beginEdge(e1.label(), v2s, Subgraph.Direction.OUTWARDS); lg.sgm.commitSubgraph(v1, sg1); // Setup third subgraph final ID v2 = new ID("a", "b", "v2", 1); lg.sgm.commitSubgraph(v2, new MutableSubgraph()); // DELETE second subgraph! lg.sgm.deleteSubgraph(v1); // Check vertices Vertex vertex0 = findVertex(om, lg.G, v0); Vertex vertex1 = findVertex(om, lg.G, v1s); Vertex vertex2 = findVertex(om, lg.G, v2); assertThat(vertex0).isNotNull(); assertThat(vertex1).isNotNull(); assertThat(vertex2).isNotNull(); assertThat(GraphUtilities.isSymbolic(om, vertex0)).isFalse(); assertThat(GraphUtilities.isSymbolic(om, vertex1)).isTrue(); assertThat(GraphUtilities.isSymbolic(om, vertex2)).isFalse(); // Check edges final Edge edge0 = findEdge(om, lg.G, e0); final Edge edge1 = findEdge(om, lg.G, e1); assertThat(edge0).isNotNull(); assertThat(edge1).isNull(); } private Vertex addVertexWithId(ID id, boolean isSymbolic) { final Vertex vertex = lg.G.addVertex(id); final ObjectMapper om = new ObjectMapper(); final String identifier = toJSON(om, isSymbolic ? getSymbolicID(id) : id).toString(); final String symbolicIdentifier = toJSON(om, getSymbolicID(id)).toString(); vertex.setProperty(IDENTIFIER, identifier); vertex.setProperty(OWNER, identifier); vertex.setProperty(SYMBOLIC_IDENTIFER, symbolicIdentifier); return vertex; } /** * Creates an edge and also creates the vertices. The vertex id is used for the vertex owner, and the * graph id is used for the edges ownership * * @param graphID id of the center vertex of the subgraph * @param targetID id of the target Vertex * @param label the label for the edge * @param direction the edge direction. */ private void addEdgeAndVertices(final ID graphID, final ID targetID, final String label, Direction direction) { for (final ID id : Arrays.asList(graphID, targetID)) { if (findVertex(om, lg.G, id) == null) { setOwner(om, createVertex(om, lg.G, id), id); } } if (direction == Direction.IN) { setOwner(om, createEdge(om, lg.G, new EdgeID(targetID, label, graphID)), graphID); }else{ setOwner(om, createEdge(om, lg.G, new EdgeID(graphID, label, targetID)), graphID); } } private void checkElementProperty(Element elt, String key, String expectedValue) { assertThat(elt.getPropertyKeys()).contains(key); assertThat(elt.getProperty(key)).isEqualTo(expectedValue); } private void checkElementProperty(Element elt, String key, Long expectedValue) { assertThat(elt.getPropertyKeys()).contains(key); assertThat(elt.getProperty(key)).isEqualTo(expectedValue); } /** * this method: * - creates a subgraph with given id. * - puts the props on the subgraph * - commits the subgraph into the graph * - checks the properties on the created vertex. * @param p properties * @param id id of the center vertex of the subgraph * @return the committed subgraph * @throws DegraphmalizerException * @throws IOException */ private Subgraph commitSubgraphAndVerifyProperties(Props p, ID id) throws DegraphmalizerException, IOException { final MutableSubgraph sg = new MutableSubgraph(); p.modifySubgraph(sg); final Vertex center = commitAndFindCentralVertex(sg, id); p.assertOK(center); final ObjectMapper om = new ObjectMapper(); final String identifier = toJSON(om, id).toString(); final String symbolicidentifier = toJSON(om, getSymbolicID(id)).toString(); checkElementProperty(center, IDENTIFIER, identifier); checkElementProperty(center, OWNER, identifier); checkElementProperty(center, SYMBOLIC_OWNER, symbolicidentifier); checkElementProperty(center, SYMBOLIC_IDENTIFER, symbolicidentifier); checkElementProperty(center, KEY_INDEX, id.index()); checkElementProperty(center, KEY_TYPE, id.type()); checkElementProperty(center, KEY_ID, id.id()); checkElementProperty(center, KEY_VERSION, id.version()); assertThat(center.getPropertyKeys()).hasSize(RESERVED_COUNT + p.properties.size()); return sg; } private Vertex commitAndFindCentralVertex(Subgraph sg, ID id) throws DegraphmalizerException { lg.sgm.commitSubgraph(id, sg); return findVertex(om, lg.G, id); } private Vertex commitAndFindCentralVertex(ID id) throws DegraphmalizerException { return commitAndFindCentralVertex(new MutableSubgraph(), id); } private ID randomSymbolicID() { return generateRandomID(0); } private ID randomVersionedID() { return generateRandomID(random.nextInt(10000) +1); } private ID generateRandomID(long version) { return new ID(randomString(), randomString(), randomString(), version); } private String randomString() { final String s = UUID.randomUUID().toString(); return s.substring(0, random.nextInt(s.length())); } private ID updateIDVersion(final ID id, final long version) { return new ID(id.index(), id.type(), id.id(), version); } } class Props { final ObjectMapper om = new ObjectMapper(); final HashMap<String,JsonNode> properties = new HashMap<String, JsonNode>(); Props(String... propVals) throws IOException { int i = 0; for(String v : propVals) properties.put("prop" + (i++), om.readTree(v)); } void modifySubgraph(MutableSubgraph sg) { for(Map.Entry<String,JsonNode> e : properties.entrySet()) sg.property(e.getKey(), e.getValue()); } void assertOK(Element v) throws IOException { for(Map.Entry<String,JsonNode> e : properties.entrySet()) { final String name = e.getKey(); final JsonNode expected = e.getValue(); final JsonNode n = getProperty(om, v, name); assertThat(n.equals(expected)).isTrue(); } } }