Package com.graphaware.test.unit

Source Code of com.graphaware.test.unit.GraphUnit

/*
* Copyright (c) 2014 GraphAware
*
* This file is part of GraphAware.
*
* GraphAware is free software: you can redistribute it and/or modify it under the terms of
* the GNU General Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
*  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of
* the GNU General Public License along with this program.  If not, see
* <http://www.gnu.org/licenses/>.
*/

package com.graphaware.test.unit;

import com.graphaware.common.policy.InclusionPolicies;
import org.neo4j.cypher.javacompat.ExecutionEngine;
import org.neo4j.graphdb.*;
import org.neo4j.test.TestGraphDatabaseFactory;
import org.neo4j.tooling.GlobalGraphOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

import static com.graphaware.common.util.PropertyContainerUtils.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.neo4j.graphdb.Direction.OUTGOING;
import static org.neo4j.helpers.collection.Iterables.count;
import static org.neo4j.tooling.GlobalGraphOperations.at;

/**
* A set of assertion methods useful for writing tests for Neo4j. Uses the {@link org.junit.Assert} class from JUnit
* to throw {@link AssertionError}s.
* <p/>
* Note: This class is well-tested functionally, but it is not designed for production use, mainly because it hasn't been
* optimised for performance. The performance will be poor on large graphs - the problem it is solving is computationally
* quite hard!
*/
public final class GraphUnit {

    private static final Logger LOG = LoggerFactory.getLogger(GraphUnit.class);

    /**
     * Private constructor - this class is a utility and should not be instantiated.
     */
    private GraphUnit() {
    }

    /**
     * Assert that the graph in the given database is exactly the same as the graph that would be created by the given
     * Cypher query. The only thing that can be different in those two graphs are IDs of nodes and relationships.
     * Properties values are converted to {@link String} before comparison, which means 123L (long) is equal to 123 (int),
     * but also "123" (String) is considered equal to 123 (int).
     *
     * @param database        first graph, typically the one that has been created by some code that is being tested by this
     *                        method.
     * @param sameGraphCypher second graph expressed as a Cypher create statement, which communicates the desired state
     *                        of the database (first parameter) iff the code that created it is correct.
     * @throws AssertionError in case the graphs are not the same.
     */
    public static void assertSameGraph(GraphDatabaseService database, String sameGraphCypher) {
        GraphDatabaseService otherDatabase = new TestGraphDatabaseFactory().newImpermanentDatabase();

        new ExecutionEngine(otherDatabase).execute(sameGraphCypher);

        try {
            assertSameGraph(database, otherDatabase, InclusionPolicies.all());
        } finally {
            otherDatabase.shutdown();
        }
    }

    /**
     * Assert that the graph in the given database is exactly the same as the graph that would be created by the given
     * Cypher query. The only thing that can be different in those two graphs are IDs of nodes and relationships
     * and nodes/relationships/properties explicitly excluded from comparisons by provided inclusionPolicies.
     * Properties values are converted to {@link String} before comparison, which means 123L (long) is equal to 123 (int),
     * but also "123" (String) is considered equal to 123 (int).
     *
     * @param database          first graph, typically the one that has been created by some code that is being tested by this
     *                          method.
     * @param sameGraphCypher   second graph expressed as a Cypher create statement, which communicates the desired state
     *                          of the database (first parameter) iff the code that created it is correct.
     * @param inclusionPolicies {@link com.graphaware.common.policy.InclusionPolicies} deciding whether to include nodes/relationships/properties in the comparisons.
     * @throws AssertionError in case the graphs are not the same.
     */
    public static void assertSameGraph(GraphDatabaseService database, String sameGraphCypher, InclusionPolicies inclusionPolicies) {
        GraphDatabaseService otherDatabase = new TestGraphDatabaseFactory().newImpermanentDatabase();

        new ExecutionEngine(otherDatabase).execute(sameGraphCypher);

        try {
            assertSameGraph(database, otherDatabase, inclusionPolicies);
        } finally {
            otherDatabase.shutdown();
        }
    }

    /**
     * Assert that the graph that would be created by the given Cypher query is a subgraph of the graph in the given
     * database. This means that every node and every relationship in the (Cypher) subgraph must be present in the
     * (database) graph.
     * <p/>
     * Nodes are considered equal if they have the exact same labels and properties. Relationships
     * are considered equal if they have the same type and properties. IDs of nodes and relationships are not taken
     * into account.  Properties are included for comparison based on the InclusionPolicies.
     * Properties values are converted to {@link String} before comparison, which means 123L (long) is
     * equal to 123 (int), but also "123" (String) is considered equal to 123 (int).
     * <p/>
     * This method is useful for testing that some portion of a graph has been created correctly without the need to
     * express the entire graph structure in Cypher.
     *
     * @param database       first graph, typically the one that has been created by some code that is being tested by
     *                       this method.
     * @param subgraphCypher second graph expressed as a Cypher create statement, which communicates the desired state
     *                       of the database (first parameter) iff the code that created it is correct.
     * @throws AssertionError in case the "cypher" graph is not a subgraph of the "database" graph.
     */
    public static void assertSubgraph(GraphDatabaseService database, String subgraphCypher) {
        GraphDatabaseService otherDatabase = new TestGraphDatabaseFactory().newImpermanentDatabase();

        new ExecutionEngine(otherDatabase).execute(subgraphCypher);

        try {
            assertSubgraph(database, otherDatabase, InclusionPolicies.all());
        } finally {
            otherDatabase.shutdown();
        }
    }

    /**
     * Assert that the graph that would be created by the given Cypher query is a subgraph of the graph in the given
     * database. This means that every node and every relationship in the (Cypher) subgraph included as specified by
     * the inclusionPolicies must be present in the (database) graph.
     * <p/>
     * Nodes are considered equal if they have the exact same labels and properties. Relationships
     * are considered equal if they have the same type and properties. IDs of nodes and relationships are not taken
     * into account. Properties are included for comparison based on the inclusionPolicies.
     * Properties values are converted to {@link String} before comparison, which means 123L (long) is
     * equal to 123 (int), but also "123" (String) is considered equal to 123 (int).
     * <p/>
     * This method is useful for testing that some portion of a graph has been created correctly without the need to
     * express the entire graph structure in Cypher.
     *
     * @param database          first graph, typically the one that has been created by some code that is being tested by
     *                          this method.
     * @param subgraphCypher    second graph expressed as a Cypher create statement, which communicates the desired state
     *                          of the database (first parameter) iff the code that created it is correct.
     * @param inclusionPolicies {@link com.graphaware.common.policy.InclusionPolicies} deciding whether to include nodes/relationships/properties or not.
     * @throws AssertionError in case the "cypher" graph is not a subgraph of the "database" graph.
     */
    public static void assertSubgraph(GraphDatabaseService database, String subgraphCypher, InclusionPolicies inclusionPolicies) {
        GraphDatabaseService otherDatabase = new TestGraphDatabaseFactory().newImpermanentDatabase();

        new ExecutionEngine(otherDatabase).execute(subgraphCypher);

        try {
            assertSubgraph(database, otherDatabase, inclusionPolicies);
        } finally {
            otherDatabase.shutdown();
        }
    }

    /**
     * Clear the graph by deleting all nodes and relationships.
     *
     * @param database graph, typically the one that has been created by some code that is being tested.
     */
    public static void clearGraph(GraphDatabaseService database) {
        clearGraph(database, InclusionPolicies.all());
    }

    /**
     * Clear the graph by deleting all nodes and relationships specified by inclusionPolicies
     *
     * @param database          graph, typically the one that has been created by some code that is being tested.
     * @param inclusionPolicies {@link com.graphaware.common.policy.InclusionPolicies} deciding whether to include nodes/relationships or not.
     *                          Note that {@link com.graphaware.common.policy.PropertyInclusionPolicy}s are ignored when clearing the graph.
     */
    public static void clearGraph(GraphDatabaseService database, InclusionPolicies inclusionPolicies) {
        for (Relationship rel : GlobalGraphOperations.at(database).getAllRelationships()) {
            if (isRelationshipIncluded(rel, inclusionPolicies)) {
                rel.delete();
            }
        }

        for (Node node : GlobalGraphOperations.at(database).getAllNodes()) {
            if (isNodeIncluded(node, inclusionPolicies)) {
                node.delete();
            }
        }
    }

    private static void assertSameGraph(GraphDatabaseService database, GraphDatabaseService otherDatabase, InclusionPolicies InclusionPolicies) {
        try (Transaction tx = database.beginTx()) {
            try (Transaction tx2 = otherDatabase.beginTx()) {
                doAssertSubgraph(database, otherDatabase, InclusionPolicies, "existing database");
                doAssertSubgraph(otherDatabase, database, InclusionPolicies, "Cypher-created database");
                tx2.failure();
            }
            tx.failure();
        }
    }

    private static void assertSubgraph(GraphDatabaseService database, GraphDatabaseService otherDatabase, InclusionPolicies InclusionPolicies) {
        try (Transaction tx = database.beginTx()) {
            try (Transaction tx2 = otherDatabase.beginTx()) {
                doAssertSubgraph(database, otherDatabase, InclusionPolicies, "existing database");
                tx2.failure();
            }
            tx.failure();
        }
    }

    private static void doAssertSubgraph(GraphDatabaseService database, GraphDatabaseService otherDatabase, InclusionPolicies inclusionPolicies, String firstDatabaseName) {
        Map<Long, Long[]> sameNodesMap = buildSameNodesMap(database, otherDatabase, inclusionPolicies, "existing database");
        Set<Map<Long, Long>> nodeMappings = buildNodeMappingPermutations(sameNodesMap, otherDatabase);

        if (nodeMappings.size() == 1) {
            assertRelationshipsMappingExistsForSingleNodeMapping(database, otherDatabase, nodeMappings.iterator().next(), inclusionPolicies, "existing database");
            return;
        }

        for (Map<Long, Long> nodeMapping : nodeMappings) {
            if (relationshipsMappingExists(database, otherDatabase, nodeMapping, inclusionPolicies)) {
                return;
            }
        }

        fail("There is no corresponding relationship mapping for any of the possible node mappings");
    }

    private static Map<Long, Long[]> buildSameNodesMap(GraphDatabaseService database, GraphDatabaseService otherDatabase, InclusionPolicies inclusionPolicies, String firstDatabaseName) {
        Map<Long, Long[]> sameNodesMap = new HashMap<>()//map of nodeID and IDs of nodes that match

        for (Node node : at(otherDatabase).getAllNodes()) {
            if (!isNodeIncluded(node, inclusionPolicies)) {
                continue;
            }
            Iterable<Node> sameNodes = findSameNodes(database, node, inclusionPolicies);    //List of all nodes that match this

            //fail fast
            if (!sameNodes.iterator().hasNext()) {
                fail("There is no corresponding node to " + nodeToString(node) + " in " + firstDatabaseName);
            }

            Set<Long> sameNodeIds = new HashSet<>();
            for (Node sameNode : sameNodes) {
                sameNodeIds.add(sameNode.getId());
            }
            sameNodesMap.put(node.getId(), sameNodeIds.toArray(new Long[sameNodeIds.size()]));
        }

        return sameNodesMap;
    }

    private static Set<Map<Long, Long>> buildNodeMappingPermutations(Map<Long, Long[]> sameNodesMap, GraphDatabaseService otherDatabase) {
        Set<Map<Long, Long>> result = new HashSet<>();
        result.add(new HashMap<Long, Long>());

        for (Map.Entry<Long, Long[]> entry : sameNodesMap.entrySet()) {

            Set<Map<Long, Long>> newResult = new HashSet<>();

            for (Long target : entry.getValue()) {
                for (Map<Long, Long> mapping : result) {
                    if (!mapping.values().contains(target)) {
                        Map<Long, Long> newMapping = new HashMap<>(mapping);
                        newMapping.put(entry.getKey(), target);
                        newResult.add(newMapping);
                    }
                }
            }

            if (newResult.isEmpty()) {
                fail("Could not find a node corresponding to: " + nodeToString(otherDatabase.getNodeById(entry.getKey()))
                        + ". There are most likely more nodes with the same characteristics (labels, properties) in your " +
                        "cypher CREATE statement but fewer in the database.");
            }

            result = newResult;
        }

        return result;
    }

    private static boolean relationshipsMappingExists(GraphDatabaseService database, GraphDatabaseService otherDatabase, Map<Long, Long> mapping, InclusionPolicies inclusionPolicies) {
        LOG.debug("Attempting a node mapping...");

        Set<Long> usedRelationships = new HashSet<>();
        for (Relationship relationship : at(otherDatabase).getAllRelationships()) {
            if (!relationshipMappingExists(database, relationship, mapping, usedRelationships, inclusionPolicies)) {
                LOG.debug("Failure... No corresponding relationship found to: " + relationshipToString(relationship));
                return false;
            }
        }

        LOG.debug("Success...");
        return true;
    }

    private static void assertRelationshipsMappingExistsForSingleNodeMapping(GraphDatabaseService database, GraphDatabaseService otherDatabase, Map<Long, Long> mapping, InclusionPolicies inclusionPolicies, String firstDatabaseName) {
        Set<Long> usedRelationships = new HashSet<>();
        for (Relationship relationship : at(otherDatabase).getAllRelationships()) {
            if (!relationshipMappingExists(database, relationship, mapping, usedRelationships, inclusionPolicies)) {
                fail("No corresponding relationship found to " + relationshipToString(relationship) + " in " + firstDatabaseName);
            }
        }
    }

    private static boolean relationshipMappingExists(GraphDatabaseService database, Relationship relationship, Map<Long, Long> nodeMapping, Set<Long> usedRelationships, InclusionPolicies inclusionPolicies) {
        if (!isRelationshipIncluded(relationship, inclusionPolicies)) {
            return true;
        }

        for (Relationship candidate : database.getNodeById(nodeMapping.get(relationship.getStartNode().getId())).getRelationships(OUTGOING)) {
            if (nodeMapping.get(relationship.getEndNode().getId()).equals(candidate.getEndNode().getId())) {
                if (areSame(candidate, relationship, inclusionPolicies) && !usedRelationships.contains(candidate.getId())) {
                    usedRelationships.add(candidate.getId());
                    return true;
                }
            }
        }

        return false;
    }

    private static Iterable<Node> findSameNodes(GraphDatabaseService database, Node node, InclusionPolicies inclusionPolicies) {
        Iterator<Label> labels = node.getLabels().iterator();
        if (labels.hasNext()) {
            return findSameNodesByLabel(database, node, labels.next(), inclusionPolicies);
        }

        return findSameNodesWithoutLabel(database, node, inclusionPolicies);
    }

    private static Iterable<Node> findSameNodesByLabel(GraphDatabaseService database, Node node, Label label, InclusionPolicies inclusionPolicies) {
        Set<Node> result = new HashSet<>();

        for (Node candidate : GlobalGraphOperations.at(database).getAllNodesWithLabel(label)) {
            if (isNodeIncluded(candidate, inclusionPolicies)) {
                if (areSame(node, candidate, inclusionPolicies)) {
                    result.add(candidate);
                }
            }
        }

        return result;
    }

    private static Iterable<Node> findSameNodesWithoutLabel(GraphDatabaseService database, Node node, InclusionPolicies inclusionPolicies) {
        Set<Node> result = new HashSet<>();

        for (Node candidate : GlobalGraphOperations.at(database).getAllNodes()) {
            if (isNodeIncluded(candidate, inclusionPolicies)) {
                if (areSame(node, candidate, inclusionPolicies)) {
                    result.add(candidate);
                }
            }
        }

        return result;
    }

    private static boolean areSame(Node node1, Node node2, InclusionPolicies inclusionPolicies) {
        return haveSameLabels(node1, node2) && haveSameProperties(node1, node2, inclusionPolicies);

    }

    private static boolean areSame(Relationship relationship1, Relationship relationship2, InclusionPolicies inclusionPolicies) {
        return haveSameType(relationship1, relationship2) && haveSameProperties(relationship1, relationship2, inclusionPolicies);

    }

    private static boolean haveSameLabels(Node node1, Node node2) {
        if (count(node1.getLabels()) != count(node2.getLabels())) {
            return false;
        }

        for (Label label : node1.getLabels()) {
            if (!node2.hasLabel(label)) {
                return false;
            }
        }

        return true;
    }

    private static boolean haveSameType(Relationship relationship1, Relationship relationship2) {
        return relationship1.isType(relationship2.getType());
    }

    private static boolean haveSameProperties(PropertyContainer pc1, PropertyContainer pc2, InclusionPolicies inclusionPolicies) {
        int pc1KeyCount = 0, pc2KeyCount = 0;
        for (String key : pc1.getPropertyKeys()) {
            if (isPropertyIncluded(pc1, key, inclusionPolicies)) {
                pc1KeyCount++;
                if (!pc2.hasProperty(key)) {
                    return false;
                }
                if (!valueToString(pc1.getProperty(key)).equals(valueToString(pc2.getProperty(key)))) {
                    return false;
                }
            }
        }
        for (String key : pc2.getPropertyKeys()) {
            if (isPropertyIncluded(pc2, key, inclusionPolicies)) {
                pc2KeyCount++;
            }
        }
        return pc1KeyCount == pc2KeyCount;
    }

    private static boolean isNodeIncluded(Node node, InclusionPolicies inclusionPolicies) {
        return inclusionPolicies.getNodeInclusionPolicy().include(node);
    }

    private static boolean isRelationshipIncluded(Relationship rel, InclusionPolicies inclusionPolicies) {
        return inclusionPolicies.getRelationshipInclusionPolicy().include(rel);
    }

    private static boolean isPropertyIncluded(PropertyContainer propertyContainer, String propertyKey, InclusionPolicies inclusionPolicies) {
        if (propertyContainer instanceof Node) {
            return inclusionPolicies.getNodePropertyInclusionPolicy().include(propertyKey, (Node) propertyContainer);
        }

        if (propertyContainer instanceof Relationship) {
            return inclusionPolicies.getRelationshipPropertyInclusionPolicy().include(propertyKey, (Relationship) propertyContainer);
        }

        throw new IllegalStateException("Property container is not a Node or Relationship!");
    }
}
TOP

Related Classes of com.graphaware.test.unit.GraphUnit

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.