org.apache.tinkerpop.gremlin.AbstractGremlinSuite.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.tinkerpop.gremlin.AbstractGremlinSuite.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin;

import org.apache.tinkerpop.gremlin.process.TraversalEngine;
import org.apache.tinkerpop.gremlin.process.traversal.engine.StandardTraversalEngine;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.commons.configuration.Configuration;
import org.javatuples.Pair;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Base Gremlin test suite from which different classes of tests can be exposed to implementers.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public abstract class AbstractGremlinSuite extends Suite {

    // todo: perhaps there is a test that validates against the implementations to be sure that the Graph constructed matches what's defined???
    private static final Set<Class> STRUCTURE_INTERFACES = new HashSet<Class>() {
        {
            add(Edge.class);
            add(Edge.Iterators.class);
            add(Element.class);
            add(Element.Iterators.class);
            add(Graph.class);
            add(Graph.Variables.class);
            add(Property.class);
            add(Vertex.class);
            add(Vertex.Iterators.class);
            add(VertexProperty.class);
            add(VertexProperty.Iterators.class);
        }
    };

    /**
     * The GraphProvider instance that will be used to generate a Graph instance.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited
    public @interface GraphProviderClass {
        /**
         * The class of the {@link Graph} that will be returned by the {@link GraphProvider}
         */
        public Class<? extends Graph> graph();

        /**
         * The class of the {@link GraphProvider} implementation to use to generate the {@link Graph} specified by
         * {@link #graph()}
         */
        public Class<? extends GraphProvider> provider();
    }

    /**
     * Indicates that this suite is for testing a gremlin flavor and is therefore not responsible for validating
     * the suite against what the Graph implementation opts-in for.
     */
    private final boolean gremlinFlavorSuite;

    public AbstractGremlinSuite(final Class<?> klass, final RunnerBuilder builder, final Class<?>[] testsToExecute)
            throws InitializationError {
        this(klass, builder, testsToExecute, null);
    }

    public AbstractGremlinSuite(final Class<?> klass, final RunnerBuilder builder, final Class<?>[] testsToExecute,
            final Class<?>[] testsToEnforce) throws InitializationError {
        this(klass, builder, testsToExecute, testsToEnforce, false);
    }

    public AbstractGremlinSuite(final Class<?> klass, final RunnerBuilder builder, final Class<?>[] testsToExecute,
            final Class<?>[] testsToEnforce, final boolean gremlinFlavorSuite) throws InitializationError {
        this(klass, builder, testsToExecute, testsToEnforce, gremlinFlavorSuite, StandardTraversalEngine.standard);
    }

    public AbstractGremlinSuite(final Class<?> klass, final RunnerBuilder builder, final Class<?>[] testsToExecute,
            final Class<?>[] testsToEnforce, final boolean gremlinFlavorSuite, TraversalEngine traversalEngine)
            throws InitializationError {
        super(builder, klass, enforce(testsToExecute, testsToEnforce));

        this.gremlinFlavorSuite = gremlinFlavorSuite;

        // figures out what the implementer assigned as the GraphProvider class and make it available to tests.
        // the klass is the Suite that implements this suite (e.g. GroovyTinkerGraphProcessStandardTest).
        // this class should be annotated with GraphProviderClass.  Failure to do so will toss an InitializationError
        final Pair<Class<? extends GraphProvider>, Class<? extends Graph>> pair = getGraphProviderClass(klass);

        // validate public acknowledgement of the test suite and filter out tests ignored by the implementation
        validateOptInToSuite(pair.getValue1());
        validateOptInAndOutAnnotationsOnGraph(pair.getValue1());
        registerOptOuts(pair.getValue1());

        try {
            final GraphProvider graphProvider = pair.getValue0().newInstance();
            validateStructureInterfacesRegistered(graphProvider);
            validateHelpersNotImplemented(graphProvider);

            GraphManager.setGraphProvider(graphProvider);
            GraphManager.setTraversalEngine(traversalEngine);
        } catch (Exception ex) {
            throw new InitializationError(ex);
        }
    }

    /**
     * Need to validate that structure interfaces are implemented so that checks to {@link Graph.Helper} can be
     * properly enforced.
     */
    private void validateStructureInterfacesRegistered(final GraphProvider graphProvider) throws Exception {
        final Set<Class> implementations = graphProvider.getImplementations();
        final Set<Class> noImplementationRegistered = new HashSet<>();

        final Configuration conf = graphProvider.newGraphConfiguration("prototype", AbstractGremlinSuite.class,
                "validateStructureInterfacesRegistered");
        final Graph g = graphProvider.openTestGraph(conf);
        final Set<Class> structureInterfaces = new HashSet<>(STRUCTURE_INTERFACES);

        // not all graphs implement all features and therefore may not have implementations of certain "core" interfaces
        if (!g.features().graph().variables().supportsVariables())
            structureInterfaces.remove(Graph.Variables.class);
        if (!g.features().vertex().supportsMultiProperties())
            structureInterfaces.remove(VertexProperty.Iterators.class);

        graphProvider.clear(g, conf);

        final boolean missingImplementations = structureInterfaces.stream().anyMatch(iface -> {
            final boolean noneMatch = implementations.stream().noneMatch(c -> iface.isAssignableFrom(c));
            if (noneMatch)
                noImplementationRegistered.add(iface);
            return noneMatch;
        });

        if (missingImplementations)
            throw new RuntimeException(String.format(
                    "Implementations must register their implementations for the following interfaces %s",
                    String.join(",",
                            noImplementationRegistered.stream().map(Class::getName).collect(Collectors.toList()))));
    }

    private void validateHelpersNotImplemented(final GraphProvider graphProvider) {
        final List<String> overridenMethods = new ArrayList<>();
        graphProvider.getImplementations().forEach(clazz -> Stream.of(clazz.getDeclaredMethods())
                .filter(AbstractGremlinSuite::isHelperMethodOverriden)
                .map(m -> m.getDeclaringClass().getName() + "." + m.getName()).forEach(overridenMethods::add));

        if (overridenMethods.size() > 0)
            throw new RuntimeException(String.format(
                    "Implementations cannot override methods marked by @Helper annotation - check the following methods [%s]",
                    String.join(",", overridenMethods)));
    }

    private void validateOptInToSuite(final Class<? extends Graph> klass) throws InitializationError {
        final Graph.OptIn[] optIns = klass.getAnnotationsByType(Graph.OptIn.class);
        if (!gremlinFlavorSuite && !Arrays.stream(optIns)
                .anyMatch(optIn -> optIn.value().equals(this.getClass().getCanonicalName())))
            throw new InitializationError(
                    "The suite will not run for this Graph until it is publicly acknowledged with the @OptIn annotation on the Graph instance itself");
    }

    private void registerOptOuts(final Class<? extends Graph> klass) throws InitializationError {
        final Graph.OptOut[] optOuts = klass.getAnnotationsByType(Graph.OptOut.class);

        if (optOuts != null && optOuts.length > 0) {
            // validate annotation - test class and reason must be set
            if (!Arrays.stream(optOuts).allMatch(
                    ignore -> ignore.test() != null && ignore.reason() != null && !ignore.reason().isEmpty()))
                throw new InitializationError(
                        "Check @IgnoreTest annotations - all must have a 'test' and 'reason' set");

            try {
                filter(new OptOutTestFilter(optOuts));
            } catch (NoTestsRemainException ex) {
                throw new InitializationError(ex);
            }
        }
    }

    private static Class<?>[] enforce(final Class<?>[] testsToExecute, final Class<?>[] testsToEnforce) {
        if (null == testsToEnforce)
            return testsToExecute;

        // examine each test to enforce and ensure an instance of it is in the list of testsToExecute
        final List<Class<?>> notSupplied = Stream.of(testsToEnforce)
                .filter(t -> Stream.of(testsToExecute).noneMatch(t::isAssignableFrom)).collect(Collectors.toList());

        if (notSupplied.size() > 0)
            System.err.println(String.format(
                    "Review the testsToExecute given to the test suite as the following are missing: %s",
                    notSupplied));

        return testsToExecute;
    }

    public static boolean isHelperMethodOverriden(final Method myMethod) {
        final Class<?> declaringClass = myMethod.getDeclaringClass();
        for (Class<?> iface : declaringClass.getInterfaces()) {
            try {
                return iface.getMethod(myMethod.getName(), myMethod.getParameterTypes())
                        .isAnnotationPresent(Graph.Helper.class);
            } catch (NoSuchMethodException ignored) {
            }
        }

        return false;
    }

    public static Pair<Class<? extends GraphProvider>, Class<? extends Graph>> getGraphProviderClass(
            final Class<?> klass) throws InitializationError {
        final GraphProviderClass annotation = klass.getAnnotation(GraphProviderClass.class);
        if (null == annotation)
            throw new InitializationError(
                    String.format("class '%s' must have a GraphProviderClass annotation", klass.getName()));
        return Pair.with(annotation.provider(), annotation.graph());
    }

    public static void validateOptInAndOutAnnotationsOnGraph(final Class<? extends Graph> klass)
            throws InitializationError {
        // sometimes test names change and since they are String representations they can easily break if a test
        // is renamed. this test will validate such things.  it is not possible to @OptOut of this test.
        final Graph.OptOut[] optOuts = klass.getAnnotationsByType(Graph.OptOut.class);
        for (Graph.OptOut optOut : optOuts) {
            final Class testClass;
            try {
                testClass = Class.forName(optOut.test());
            } catch (Exception ex) {
                throw new InitializationError(String.format(
                        "Invalid @OptOut on Graph instance.  Could not instantiate test class (it may have been renamed): %s",
                        optOut.test()));
            }

            if (!optOut.method().equals("*")
                    && !Arrays.stream(testClass.getMethods()).anyMatch(m -> m.getName().equals(optOut.method())))
                throw new InitializationError(String.format(
                        "Invalid @OptOut on Graph instance.  Could not match @OptOut test name %s on test class %s (it may have been renamed)",
                        optOut.method(), optOut.test()));
        }
    }

    @Override
    protected void runChild(final Runner runner, final RunNotifier notifier) {
        if (beforeTestExecution((Class<? extends AbstractGremlinTest>) runner.getDescription().getTestClass()))
            super.runChild(runner, notifier);
        afterTestExecution((Class<? extends AbstractGremlinTest>) runner.getDescription().getTestClass());
    }

    /**
     * Called just prior to test class execution.  Return false to ignore test class. By default this always returns
     * true.
     */
    public boolean beforeTestExecution(final Class<? extends AbstractGremlinTest> testClass) {
        return true;
    }

    /**
     * Called just after test class execution.
     */
    public void afterTestExecution(final Class<? extends AbstractGremlinTest> testClass) {
    }

    /**
     * Filter for tests in the suite which is controlled by the {@link Graph.OptOut} annotation.
     */
    public static class OptOutTestFilter extends Filter {

        private final List<Description> individualTestsToIgnore;
        private final List<Graph.OptOut> entireTestsToIgnore;

        public OptOutTestFilter(final Graph.OptOut[] optOuts) {
            // split the tests to filter into two groups - true represents those that should ignore a whole
            final Map<Boolean, List<Graph.OptOut>> split = Arrays.stream(optOuts)
                    .collect(Collectors.groupingBy(optOut -> optOut.method().equals("*")));

            final List<Graph.OptOut> optOutsOfIndividualTests = split.getOrDefault(Boolean.FALSE,
                    Collections.emptyList());
            individualTestsToIgnore = optOutsOfIndividualTests.stream()
                    .filter(ignoreTest -> !ignoreTest.method().equals("*"))
                    .<Pair>map(ignoreTest -> Pair.with(ignoreTest.test(),
                            ignoreTest.specific().isEmpty() ? ignoreTest.method()
                                    : String.format("%s[%s]", ignoreTest.method(), ignoreTest.specific())))
                    .<Description>map(p -> Description.createTestDescription(p.getValue0().toString(),
                            p.getValue1().toString()))
                    .collect(Collectors.toList());

            entireTestsToIgnore = split.getOrDefault(Boolean.TRUE, Collections.emptyList());
        }

        @Override
        public boolean shouldRun(final Description description) {
            // first check if all tests from a class should be ignored
            if (!entireTestsToIgnore.isEmpty() && entireTestsToIgnore.stream()
                    .anyMatch(optOut -> optOut.test().equals(description.getClassName()))) {
                return false;
            }

            if (description.isTest()) {
                return !individualTestsToIgnore.contains(description);
            }

            // explicitly check if any children want to run
            for (Description each : description.getChildren()) {
                if (shouldRun(each)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String describe() {
            return String.format("Method %s", String.join(",", individualTestsToIgnore.stream()
                    .map(Description::getDisplayName).collect(Collectors.toList())));
        }
    }
}