This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-2601 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit bab2606df0c6e1337e884fc0b3b459c1013cb6ed Author: Stephen Mallette <[email protected]> AuthorDate: Mon Feb 8 10:05:24 2021 -0500 TINKERPOP-2601 Basics in place for Gherkin test to run on JVM Fully working pattern for gherkin tests running on TinkerGraph and using gremlin-language to parse to a Traversal object. --- gremlin-core/pom.xml | 9 - .../language/grammar/TraversalMethodVisitor.java | 12 +- gremlin-javascript/build/generate.groovy | 2 +- gremlin-language/pom.xml | 14 +- .../language/corpus}/DocumentationReader.java | 4 +- .../gremlin/language/corpus/FeatureReader.java | 135 +++++++ .../language/corpus/DocumentationReaderTest.java | 13 +- .../language/corpus}/FeatureReaderTest.java | 23 +- .../gremlin/language/grammar/FeatureReader.java | 95 ----- .../language/grammar/ReferenceGrammarTest.java | 37 +- gremlin-python/build/generate.groovy | 2 +- gremlin-test/features/map/Map.feature | 1 + gremlin-test/features/map/Vertex.feature | 18 +- gremlin-test/pom.xml | 21 + .../tinkerpop/gremlin/features/FeatureReader.java | 79 ---- .../tinkerpop/gremlin/features/StepDefinition.java | 433 +++++++++++++++++++++ .../apache/tinkerpop/gremlin/features/World.java | 57 +++ pom.xml | 1 + tinkergraph-gremlin/pom.xml | 6 + .../tinkergraph/TinkerGraphFeatureTest.java | 89 +++++ .../src/test/resources/cucumber.properties | 1 + 21 files changed, 830 insertions(+), 222 deletions(-) diff --git a/gremlin-core/pom.xml b/gremlin-core/pom.xml index 6c10b85..cef750b 100644 --- a/gremlin-core/pom.xml +++ b/gremlin-core/pom.xml @@ -52,20 +52,11 @@ limitations under the License. <artifactId>commons-lang3</artifactId> </dependency> <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-text</artifactId> - </dependency> - <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>${snakeyaml.version}</version> </dependency> <dependency> - <groupId>org.javatuples</groupId> - <artifactId>javatuples</artifactId> - <version>${java.tuples.version}</version> - </dependency> - <dependency> <groupId>com.carrotsearch</groupId> <artifactId>hppc</artifactId> <version>0.7.1</version> diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java index 9b67447..598dd45 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java @@ -940,7 +940,7 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> */ @Override public GraphTraversal visitTraversalMethod_option_Object_Traversal(final GremlinParser.TraversalMethod_option_Object_TraversalContext ctx) { - return graphTraversal.option(GenericLiteralVisitor.getInstance().visitGenericLiteral(ctx.genericLiteral()), + return graphTraversal.option(new GenericLiteralVisitor(antlr).visitGenericLiteral(ctx.genericLiteral()), antlr.tvisitor.visitNestedTraversal(ctx.nestedTraversal())); } @@ -1118,8 +1118,8 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> */ @Override public GraphTraversal visitTraversalMethod_property_Object_Object_Object(final GremlinParser.TraversalMethod_property_Object_Object_ObjectContext ctx) { - return graphTraversal.property(GenericLiteralVisitor.getInstance().visitGenericLiteral(ctx.genericLiteral(0)), - GenericLiteralVisitor.getInstance().visitGenericLiteral(ctx.genericLiteral(1)), + return graphTraversal.property(new GenericLiteralVisitor(antlr).visitGenericLiteral(ctx.genericLiteral(0)), + new GenericLiteralVisitor(antlr).visitGenericLiteral(ctx.genericLiteral(1)), GenericLiteralVisitor.getGenericLiteralList(ctx.genericLiteralList())); } @@ -1504,6 +1504,12 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> return graphTraversal.math(GenericLiteralVisitor.getStringLiteral(ctx.stringLiteral())); } + @Override + public Traversal visitTraversalMethod_option_Predicate_Traversal(final GremlinParser.TraversalMethod_option_Predicate_TraversalContext ctx) { + return graphTraversal.option(TraversalPredicateVisitor.getInstance().visitTraversalPredicate(ctx.traversalPredicate()), + antlr.tvisitor.visitNestedTraversal(ctx.nestedTraversal())); + } + public GraphTraversal[] getNestedTraversalList(final GremlinParser.NestedTraversalListContext ctx) { return ctx.nestedTraversalExpr().nestedTraversal() .stream() diff --git a/gremlin-javascript/build/generate.groovy b/gremlin-javascript/build/generate.groovy index 7a374c0..404f14a 100644 --- a/gremlin-javascript/build/generate.groovy +++ b/gremlin-javascript/build/generate.groovy @@ -42,7 +42,7 @@ gremlinGroovyScriptEngine = new GremlinGroovyScriptEngine(new GroovyCustomizer() } }) translator = JavascriptTranslator.of('g') -g = traversal().withGraph(EmptyGraph.instance()) +g = traversal().withEmbedded(EmptyGraph.instance()) bindings = new SimpleBindings() bindings.put('g', g) diff --git a/gremlin-language/pom.xml b/gremlin-language/pom.xml index aff53a3..a48e6ed 100644 --- a/gremlin-language/pom.xml +++ b/gremlin-language/pom.xml @@ -32,6 +32,15 @@ limitations under the License. <artifactId>antlr4</artifactId> <version>${antlr4.version}</version> </dependency> + <dependency> + <groupId>org.javatuples</groupId> + <artifactId>javatuples</artifactId> + <version>${java.tuples.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-text</artifactId> + </dependency> <!-- TESTING --> <dependency> <groupId>junit</groupId> @@ -43,11 +52,6 @@ limitations under the License. <artifactId>hamcrest</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-text</artifactId> - <scope>test</scope> - </dependency> </dependencies> <properties> diff --git a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/DocumentationReader.java b/gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReader.java similarity index 98% rename from gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/DocumentationReader.java rename to gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReader.java index 93bbccd..b657cae 100644 --- a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/DocumentationReader.java +++ b/gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReader.java @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.tinkerpop.gremlin.language.grammar; - -import org.apache.commons.text.StringEscapeUtils; +package org.apache.tinkerpop.gremlin.language.corpus; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReader.java b/gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReader.java new file mode 100644 index 0000000..07457bd --- /dev/null +++ b/gremlin-language/src/main/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReader.java @@ -0,0 +1,135 @@ +/* + * 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.language.corpus; + +import org.apache.commons.text.StringEscapeUtils; +import org.javatuples.Pair; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Reads the Gherkin feature files of {@code gremlin-test} and extracts Gremlin examples. + */ +public class FeatureReader { + + private static final Pattern generalParameterPattern = Pattern.compile("And using the parameter (.+) (defined as|of) (.*)"); + + /** + * Parses Gremlin to a {@code Map} structure of the test name as the key with a {@code List} of Gremlin strings + * for the value. + * @param projectRoot the root directory of the TinkerPop project source code + */ + public static Map<String, List<String>> parse(final String projectRoot) throws IOException { + return parse(projectRoot, Collections.emptyList()); + } + + /** + * Parses Gremlin to a {@code Map} structure of the test name as the key with a {@code List} of Gremlin strings + * for the value. + * @param projectRoot the root directory of the TinkerPop project source code + * @param parameterMatchers list of pattern/functions that will transform a parameter from its Gherkin form to + * another format triggering that new formatted string to be inserted into the Gremlin + * itself + */ + public static Map<String, List<String>> parse(final String projectRoot, + final List<Pair<Pattern, BiFunction<String, String, String>>> parameterMatchers) throws IOException { + final Map<String, List<String>> gremlins = new LinkedHashMap<>(); + Files.find(Paths.get(projectRoot, "gremlin-test", "features"), + Integer.MAX_VALUE, + (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().endsWith(".feature")). + sorted(). + forEach(f -> { + String currentGremlin = ""; + boolean openTriples = false; + boolean skipIgnored = false; + String scenarioName = ""; + Map<String,String> parameters = new HashMap<>(); + + try { + final List<String> lines = Files.readAllLines(f, StandardCharsets.UTF_8); + for (String line : lines) { + String cleanLine = line.trim(); + if (cleanLine.startsWith("Scenario:")) { + scenarioName = cleanLine.split(":")[1].trim(); + skipIgnored = false; + parameters.clear(); + } else if (!parameterMatchers.isEmpty() && cleanLine.startsWith("And using the parameter")) { + final Matcher m = generalParameterPattern.matcher(cleanLine); + if (m.matches()) { + parameters.put(m.group(1), matchAndTransform(m.group(1), StringEscapeUtils.unescapeJava(m.group(3)), parameterMatchers)); + } else { + throw new IllegalStateException(String.format("Could not read parameters at: %s", cleanLine)); + } + } else if (cleanLine.startsWith("Then nothing should happen because")) { + skipIgnored = true; + } else if (cleanLine.startsWith("And the graph should return")) { + gremlins.computeIfAbsent(scenarioName, k -> new ArrayList<>()).add(applyParametersToGremlin(StringEscapeUtils.unescapeJava(cleanLine.substring(cleanLine.indexOf("\"") + 1, cleanLine.lastIndexOf("\""))), parameters)); + } else if (cleanLine.startsWith("\"\"\"")) { + openTriples = !openTriples; + if (!skipIgnored && !openTriples) { + currentGremlin = applyParametersToGremlin(currentGremlin, parameters); + gremlins.computeIfAbsent(scenarioName, k -> new ArrayList<>()).add(currentGremlin); + currentGremlin = ""; + } + } else if (openTriples && !skipIgnored) { + currentGremlin += cleanLine; + } + } + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + }); + + return gremlins; + } + + private static String applyParametersToGremlin(String currentGremlin, Map<String, String> parameters) { + for (Map.Entry<String,String> kv : parameters.entrySet()) { + currentGremlin = currentGremlin.replace(kv.getKey(), kv.getValue()); + } + return currentGremlin; + } + + private static String matchAndTransform(final String k, final String v, + final List<Pair<Pattern, BiFunction<String, String, String>>> parameterMatchers) { + for (Pair<Pattern,BiFunction<String,String,String>> matcherConverter : parameterMatchers) { + final Pattern pattern = matcherConverter.getValue0(); + final Matcher matcher = pattern.matcher(v); + if (matcher.find()) { + final BiFunction<String,String,String> converter = matcherConverter.getValue1(); + // when there are no groups there is a direct match + return converter.apply(k, matcher.groupCount() == 0 ? "" : matcher.group(1)); + } + } + + throw new IllegalStateException(String.format("Could not match the parameter [%s] pattern of %s", k, v)); + } +} diff --git a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReaderTest.java similarity index 80% copy from gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java copy to gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReaderTest.java index 8d67347..ab131c4 100644 --- a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java +++ b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/DocumentationReaderTest.java @@ -16,27 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.tinkerpop.gremlin.features; +package org.apache.tinkerpop.gremlin.language.corpus; import org.junit.Test; import java.io.IOException; -import java.util.List; -import java.util.Map; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.number.OrderingComparison.greaterThan; import static org.junit.Assert.assertEquals; -public class FeatureReaderTest { +public class DocumentationReaderTest { @Test public void shouldParseInSameOrder() throws IOException { final String projectRoot = "../"; - final Map<String,List<String>> gremlins = FeatureReader.parse(projectRoot); + final Set<String> gremlins = DocumentationReader.parse(projectRoot); assertThat(gremlins.size(), greaterThan(0)); - assertEquals(gremlins, - FeatureReader.parse(projectRoot)); - + assertEquals(gremlins, DocumentationReader.parse(projectRoot)); } } diff --git a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReaderTest.java similarity index 57% rename from gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java rename to gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReaderTest.java index 8d67347..6639ea8 100644 --- a/gremlin-test/src/test/java/org/apache/tinkerpop/gremlin/features/FeatureReaderTest.java +++ b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/corpus/FeatureReaderTest.java @@ -16,15 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.tinkerpop.gremlin.features; +package org.apache.tinkerpop.gremlin.language.corpus; +import org.javatuples.Pair; import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.regex.Pattern; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.number.OrderingComparison.greaterThan; import static org.junit.Assert.assertEquals; @@ -35,8 +41,19 @@ public class FeatureReaderTest { final String projectRoot = "../"; final Map<String,List<String>> gremlins = FeatureReader.parse(projectRoot); assertThat(gremlins.size(), greaterThan(0)); - assertEquals(gremlins, - FeatureReader.parse(projectRoot)); + assertEquals(gremlins, FeatureReader.parse(projectRoot)); + } + + @Test + public void shouldParseAndEmbed() throws IOException { + final String replaceToken = "****replaced****"; + final List<Pair<Pattern, BiFunction<String, String, String>>> parameterMatchers = new ArrayList<>(); + parameterMatchers.add(Pair.with(Pattern.compile("(.*)"), (k, v) -> replaceToken)); + final String projectRoot = "../"; + final Map<String,List<String>> gremlins = FeatureReader.parse(projectRoot, parameterMatchers); + // at least one of these things must have the "replaced" token + assertThat(gremlins.values().stream(). + flatMap(Collection::stream).anyMatch(gremlin -> gremlin.contains(replaceToken)), is(true)); } } diff --git a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/FeatureReader.java b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/FeatureReader.java deleted file mode 100644 index 6006c7b..0000000 --- a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/FeatureReader.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.language.grammar; - -import org.apache.commons.text.StringEscapeUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * Reads the feature files and extracts Gremlin to help build the parsing test corpus. - */ -public class FeatureReader { - - public static Set<String> parse(final String projectRoot) throws IOException { - final Set<String> gremlins = new LinkedHashSet<>(); - Files.find(Paths.get(projectRoot, "gremlin-test", "features"), - Integer.MAX_VALUE, - (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().endsWith(".feature")). - sorted(). - forEach(f -> { - String currentGremlin = ""; - boolean openTriples = false; - boolean skipIgnored = false; - - try { - final List<String> lines = Files.readAllLines(f, StandardCharsets.UTF_8); - for (String line : lines) { - String cleanLine = line.trim(); - if (cleanLine.startsWith("Then nothing should happen because")) { - skipIgnored = true; - } else if (cleanLine.startsWith("And the graph should return")) { - gremlins.add(replaceVariables( - StringEscapeUtils.unescapeJava( - cleanLine.substring(cleanLine.indexOf("\"") + 1, cleanLine.lastIndexOf("\""))))); - } else if (cleanLine.startsWith("\"\"\"")) { - openTriples = !openTriples; - if (!skipIgnored && !openTriples) { - gremlins.add(replaceVariables(currentGremlin)); - currentGremlin = ""; - } - } else if (openTriples && !skipIgnored) { - currentGremlin += cleanLine; - } - } - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - }); - - return gremlins; - } - - /** - * Variables can't be parsed by the grammar so they must be replaced with something concrete. - */ - private static String replaceVariables(final String gremlin) { - return gremlin.replace("xx1", "\"1\""). - replace("xx2", "\"2\""). - replace("xx3", "\"3\""). - replace("vid1", "1"). - replace("vid2", "2"). - replace("vid3", "3"). - replace("vid4", "4"). - replace("vid5", "5"). - replace("vid6", "6"). - replace("eid7", "7"). - replace("eid8", "8"). - replace("eid9", "9"). - replace("eid10", "10"). - replace("eid11", "11"). - replace("eid12", "12"); - } -} diff --git a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ReferenceGrammarTest.java b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ReferenceGrammarTest.java index 2246fd3..f1f2e4f 100644 --- a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ReferenceGrammarTest.java +++ b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/ReferenceGrammarTest.java @@ -18,14 +18,23 @@ */ package org.apache.tinkerpop.gremlin.language.grammar; +import org.apache.tinkerpop.gremlin.language.corpus.DocumentationReader; +import org.apache.tinkerpop.gremlin.language.corpus.FeatureReader; +import org.javatuples.Pair; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; +import java.util.function.BiFunction; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.Matchers.is; import static org.junit.Assume.assumeThat; @@ -40,10 +49,31 @@ public class ReferenceGrammarTest extends AbstractGrammarTest { private static final Pattern vertexPattern = Pattern.compile(".*v\\d.*"); private static final Pattern edgePattern = Pattern.compile(".*e\\d.*"); + private static final List<Pair<Pattern, BiFunction<String,String,String>>> stringMatcherConverters = new ArrayList<Pair<Pattern, BiFunction<String,String,String>>>() {{ + add(Pair.with(Pattern.compile("l\\[\\]"), (k,v) -> "[]")); + add(Pair.with(Pattern.compile("l\\[(.*)\\]"), (k,v) -> { + final String[] items = v.split(","); + final String listItems = Stream.of(items).map(String::trim).map(x -> String.format("\"%s\"", x)).collect(Collectors.joining(",")); + return String.format("[%s]", listItems); + })); + add(Pair.with(Pattern.compile("v\\[(.+)\\]"), (k,v) -> "\"1\"")); + add(Pair.with(Pattern.compile("e\\[(.+)\\]"), (k,v) -> "\"1\"")); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.?.*"), (k,v) -> v)); + add(Pair.with(Pattern.compile("m\\[(.*)\\]"), (k,v) -> v.replace('{','[').replace('}', ']'))); + add(Pair.with(Pattern.compile("t\\[(.*)\\]"), (k,v) -> String.format("T.%s", v))); + add(Pair.with(Pattern.compile("D\\[(.*)\\]"), (k,v) -> String.format("Direction.%s", v))); + + // the grammar doesn't support all the Gremlin we have in the gherkin set, so try to coerce it into + // something that can be parsed so that we get maximum exercise over the parser itself. + add(Pair.with(Pattern.compile("c\\[(.*)\\]"), (k,v) -> k.equals("c1") || k.equals("c2") ? "Order.desc" : "__.identity()")); // closure -> Comparator || Traversal + add(Pair.with(Pattern.compile("s\\[\\]"), (k,v) -> "[]")); // set -> list + add(Pair.with(Pattern.compile("s\\[(.*)\\]"), (k,v) -> "[]")); // set -> list + }}; + @Parameterized.Parameters(name = "{0}") public static Iterable<String> queries() throws IOException { final Set<String> gremlins = new LinkedHashSet<>(DocumentationReader.parse("../")); - gremlins.addAll(FeatureReader.parse("../")); + gremlins.addAll(FeatureReader.parse("../", stringMatcherConverters).values().stream().flatMap(Collection::stream).collect(Collectors.toList())); return gremlins; } @@ -52,11 +82,6 @@ public class ReferenceGrammarTest extends AbstractGrammarTest { @Test public void test_parse() { - assumeThat("Lambdas are not supported", query.contains("l1"), is(false)); - assumeThat("Lambdas are not supported", query.contains("l2"), is(false)); - assumeThat("Lambdas are not supported", query.contains("pred1"), is(false)); - assumeThat("Lambdas are not supported", query.contains("c1"), is(false)); - assumeThat("Lambdas are not supported", query.contains("c2"), is(false)); assumeThat("Lambdas are not supported", query.contains("Lambda.function("), is(false)); // start of a closure assumeThat("Lambdas are not supported", query.contains("{"), is(false)); diff --git a/gremlin-python/build/generate.groovy b/gremlin-python/build/generate.groovy index 05507d5..4ad84dc 100644 --- a/gremlin-python/build/generate.groovy +++ b/gremlin-python/build/generate.groovy @@ -42,7 +42,7 @@ gremlinGroovyScriptEngine = new GremlinGroovyScriptEngine(new GroovyCustomizer() } }) translator = PythonTranslator.of('g') -g = traversal().withGraph(EmptyGraph.instance()) +g = traversal().withEmbedded(EmptyGraph.instance()) bindings = new SimpleBindings() bindings.put('g', g) diff --git a/gremlin-test/features/map/Map.feature b/gremlin-test/features/map/Map.feature index d9acfed..58f5d37 100644 --- a/gremlin-test/features/map/Map.feature +++ b/gremlin-test/features/map/Map.feature @@ -61,6 +61,7 @@ Feature: Step - map() | d[5].i | | d[4].i | + @RemoteOnly Scenario: g_VX1X_out_mapXlambdaXnameXX_mapXlambdaXlengthXX Given the modern graph And using the parameter vid1 defined as "v[marko].id" diff --git a/gremlin-test/features/map/Vertex.feature b/gremlin-test/features/map/Vertex.feature index 62ac798..d1715d1 100644 --- a/gremlin-test/features/map/Vertex.feature +++ b/gremlin-test/features/map/Vertex.feature @@ -135,7 +135,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter eid11 defined as "e[josh-created->lop].id" And the traversal of - """ + """ g.E(eid11) """ When iterated to list @@ -147,7 +147,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter eid11 defined as "e[josh-created->lop].sid" And the traversal of - """ + """ g.E(eid11) """ When iterated to list @@ -159,7 +159,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter e11 defined as "e[josh-created->lop]" And the traversal of - """ + """ g.E(e11) """ When iterated to list @@ -172,7 +172,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() And using the parameter e7 defined as "e[marko-knows->vadas]" And using the parameter e11 defined as "e[josh-created->lop]" And the traversal of - """ + """ g.E(e7,e11) """ When iterated to list @@ -185,7 +185,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter xx1 defined as "l[e[marko-knows->vadas],e[josh-created->lop]]" And the traversal of - """ + """ g.E(xx1) """ When iterated to list @@ -198,7 +198,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter vid1 defined as "v[marko].id" And the traversal of - """ + """ g.V(vid1).outE() """ When iterated to list @@ -212,7 +212,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter vid2 defined as "v[vadas].id" And the traversal of - """ + """ g.V(vid2).inE() """ When iterated to list @@ -224,7 +224,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter vid4 defined as "v[josh].id" And the traversal of - """ + """ g.V(vid4).bothE("created") """ When iterated to list @@ -237,7 +237,7 @@ Feature: Step - V(), E(), out(), in(), both(), inE(), outE(), bothE() Given the modern graph And using the parameter vid4 defined as "v[josh].id" And the traversal of - """ + """ g.V(vid4).bothE() """ When iterated to list diff --git a/gremlin-test/pom.xml b/gremlin-test/pom.xml index 343159c..0ae28f7 100644 --- a/gremlin-test/pom.xml +++ b/gremlin-test/pom.xml @@ -48,6 +48,27 @@ limitations under the License. <artifactId>hamcrest</artifactId> </dependency> <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-java</artifactId> + <version>6.11.0</version> + </dependency> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-junit</artifactId> + <version>6.11.0</version> + </dependency> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-guice</artifactId> + <version>6.11.0</version> + </dependency> + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <version>4.2.3</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <optional>true</optional> diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/FeatureReader.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/FeatureReader.java deleted file mode 100644 index 763cb0e..0000000 --- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/FeatureReader.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.features; - -import org.apache.commons.text.StringEscapeUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Reads the feature files and extracts Gremlin to a {@code Map} structure of the test name as the key with a - * {@code List} of Gremlin strings for the value. - */ -public class FeatureReader { - - public static Map<String, List<String>> parse(final String projectRoot) throws IOException { - final Map<String, List<String>> gremlins = new LinkedHashMap<>(); - Files.find(Paths.get(projectRoot, "gremlin-test", "features"), - Integer.MAX_VALUE, - (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().endsWith(".feature")). - sorted(). - forEach(f -> { - String currentGremlin = ""; - boolean openTriples = false; - boolean skipIgnored = false; - String scenarioName = ""; - - try { - final List<String> lines = Files.readAllLines(f, StandardCharsets.UTF_8); - for (String line : lines) { - String cleanLine = line.trim(); - if (cleanLine.startsWith("Scenario:")) { - scenarioName = cleanLine.split(":")[1].trim(); - skipIgnored = false; - } else if (cleanLine.startsWith("Then nothing should happen because")) { - skipIgnored = true; - } else if (cleanLine.startsWith("And the graph should return")) { - gremlins.computeIfAbsent(scenarioName, k -> new ArrayList<>()).add(StringEscapeUtils.unescapeJava(cleanLine.substring(cleanLine.indexOf("\"") + 1, cleanLine.lastIndexOf("\"")))); - } else if (cleanLine.startsWith("\"\"\"")) { - openTriples = !openTriples; - if (!skipIgnored && !openTriples) { - gremlins.computeIfAbsent(scenarioName, k -> new ArrayList<>()).add(currentGremlin); - currentGremlin = ""; - } - } else if (openTriples && !skipIgnored) { - currentGremlin += cleanLine; - } - } - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - }); - - return gremlins; - } -} diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java new file mode 100644 index 0000000..e1023ab --- /dev/null +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java @@ -0,0 +1,433 @@ +/* + * 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.features; + +import com.google.inject.Inject; +import io.cucumber.datatable.DataTable; +import io.cucumber.guice.ScenarioScoped; +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.tinkerpop.gremlin.language.grammar.GremlinAntlrToJava; +import org.apache.tinkerpop.gremlin.language.grammar.GremlinLexer; +import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; +import org.apache.tinkerpop.shaded.jackson.databind.JsonNode; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; +import org.javatuples.Pair; +import org.javatuples.Triplet; +import org.junit.AssumptionViolatedException; + +import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData; +import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.inV; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIn.in; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.core.Every.everyItem; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@ScenarioScoped +public final class StepDefinition { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private final World world; + private GraphTraversalSource g; + private final Map<String, String> stringParameters = new HashMap<>(); + private Traversal traversal; + private Object result; + private static Pattern edgeTriplet = Pattern.compile("(.+)-(.+)->(.+)"); + private List<Pair<Pattern, Function<String,String>>> stringMatcherConverters = new ArrayList<Pair<Pattern, Function<String,String>>>() {{ + // expects json so that should port to the Gremlin script form - replace curly json braces with square ones + // for Gremlin sake. + add(Pair.with(Pattern.compile("m\\[(.*)\\]"), s -> s.replace('{','[').replace('}', ']'))); + + add(Pair.with(Pattern.compile("l\\[\\]"), s -> "[]")); + add(Pair.with(Pattern.compile("l\\[(.*)\\]"), s -> { + final String[] items = s.split(","); + final String listItems = Stream.of(items).map(String::trim).map(x -> convertToString(x)).collect(Collectors.joining(",")); + return String.format("[%s]", listItems); + })); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.i"), s -> s)); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.l"), s -> s + "l")); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.f"), s -> s + "f")); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.d"), s -> s + "d")); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.m"), s -> String.format("new BigDecimal(%s)", s))); + + add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> g.V().has("name", s).id().next().toString())); + add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.sid"), s -> g.V().has("name", s).id().next().toString())); + add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.id"), s -> getEdgeIdString(g, s))); + add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.sid"), s -> getEdgeIdString(g, s))); + + add(Pair.with(Pattern.compile("t\\[(.*)\\]"), s -> String.format("T.%s", s))); + add(Pair.with(Pattern.compile("D\\[(.*)\\]"), s -> String.format("Direction.%s", s))); + + // the following force ignore conditions as they cannot be parsed by the grammar at this time. the grammar will + // need to be modified to handle them or perhaps these tests stay relegated to the JVM in some way for certain + // cases like the lambda item which likely won't make it to the grammar as it's raw groovy. + add(Pair.with(Pattern.compile("c\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a lambda as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("v\\[(.+)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Vertex as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("e\\[(.+)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Edge as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("p\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Path as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("s\\[\\]"), s -> { + throw new AssumptionViolatedException("This test uses a empty Set as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("s\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Set as a parameter which is not supported by gremlin-language"); + })); + }}; + + private List<Pair<Pattern, Function<String,Object>>> objectMatcherConverters = new ArrayList<Pair<Pattern, Function<String,Object>>>() {{ + // expects json so that should port to the Gremlin script form - replace curly json braces with square ones + // for Gremlin sake. + add(Pair.with(Pattern.compile("m\\[(.*)\\]"), s -> { + try { + // read tree from JSON - can't parse right to Map as each m[] level needs to be managed individually + return convertToObject(mapper.readTree(s)); + } catch (Exception ex) { + throw new IllegalStateException(String.format("Can't parse JSON to map for %s", s), ex); + } + })); + + add(Pair.with(Pattern.compile("l\\[\\]"), s -> Collections.emptyList())); + add(Pair.with(Pattern.compile("l\\[(.*)\\]"), s -> { + final String[] items = s.split(","); + return Stream.of(items).map(String::trim).map(x -> convertToObject(x)).collect(Collectors.toList()); + })); + + add(Pair.with(Pattern.compile("p\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Path as a parameter which is not supported by gremlin-language"); + })); + + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.i"), Integer::parseInt)); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.l"), Long::parseLong)); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.f"), Float::parseFloat)); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.d"), Double::parseDouble)); + add(Pair.with(Pattern.compile("d\\[(.*)\\]\\.m"), BigDecimal::new)); + + add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> g.V().has("name", s).id().next())); + add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.sid"), s -> g.V().has("name", s).id().next().toString())); + add(Pair.with(Pattern.compile("v\\[(.+)\\]"), s -> g.V().has("name", s).next())); + add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.id"), s -> getEdgeId(g, s))); + add(Pair.with(Pattern.compile("e\\[(.+)\\]\\.sid"), s -> getEdgeIdString(g, s))); + add(Pair.with(Pattern.compile("e\\[(.+)\\]"), s -> getEdge(g, s))); + + add(Pair.with(Pattern.compile("t\\[(.*)\\]"), T::valueOf)); + add(Pair.with(Pattern.compile("D\\[(.*)\\]"), Direction::valueOf)); + + add(Pair.with(Pattern.compile("c\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a lambda as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("s\\[\\]"), s -> { + throw new AssumptionViolatedException("This test uses a empty Set as a parameter which is not supported by gremlin-language"); + })); + add(Pair.with(Pattern.compile("s\\[(.*)\\]"), s -> { + throw new AssumptionViolatedException("This test uses a Set as a parameter which is not supported by gremlin-language"); + })); + + add(Pair.with(Pattern.compile("(null)"), s -> null)); + }}; + + @Inject + public StepDefinition(final World world) { + this.world = Objects.requireNonNull(world, "world must not be null"); + } + + @Before + public void beforeEachScenario(final Scenario scenario) throws Exception { + world.beforeEachScenario(scenario); + stringParameters.clear(); + if (traversal != null) { + traversal.close(); + traversal = null; + } + + if (result != null) result = null; + } + + @After + public void afterEachScenario() throws Exception { + world.afterEachScenario(); + } + + @Given("the {word} graph") + public void givenTheXGraph(final String graphName) { + if (graphName.equals("empty")) + this.g = world.getGraphTraversalSource(null); + else + this.g = world.getGraphTraversalSource(GraphData.valueOf(graphName.toUpperCase())); + } + + @Given("the graph initializer of") + public void theGraphInitializerOf(final String gremlin) { + parseGremlin(gremlin).iterate(); + } + + @Given("using the parameter {word} defined as {string}") + public void usingTheParameterXDefinedAsX(final String key, final String value) { + stringParameters.put(key, convertToString(value)); + } + + @Given("using the parameter {word} of P.{word}\\({string})") + public void usingTheParameterXOfPX(final String key, final String pval, final String string) { + stringParameters.put(key, String.format("P.%s(%s)", pval, convertToString(string))); + } + + @Given("the traversal of") + public void theTraversalOf(final String docString) { + traversal = parseGremlin(applyParameters(docString)); + } + + @When("iterated to list") + public void iteratedToList() { + result = traversal.toList(); + } + + @When("iterated next") + public void iteratedNext() { + result = traversal.next(); + } + + @Then("the result should be unordered") + public void theResultShouldBeUnordered(final DataTable dataTable) { + final List<Object> actual = translateResultsToActual(); + + // account for header in dataTable size + assertEquals(dataTable.height() - 1, actual.size()); + + // skip the header in the dataTable + final Object[] expected = dataTable.asList().stream().skip(1).map(this::convertToObject).toArray(); + assertThat(actual, containsInAnyOrder(expected)); + } + + @Then("the result should be ordered") + public void theResultShouldBeOrdered(final DataTable dataTable) { + final List<Object> actual = translateResultsToActual(); + + // account for header in dataTable size + assertEquals(dataTable.height() - 1, actual.size()); + + // skip the header in the dataTable + final Object[] expected = dataTable.asList().stream().skip(1).map(this::convertToObject).toArray(); + assertThat(actual, contains(expected)); + } + + @Then("the result should be of") + public void theResultShouldBeOf(final DataTable dataTable) { + final List<Object> actual = translateResultsToActual(); + + // skip the header in the dataTable + final Object[] expected = dataTable.asList().stream().skip(1).map(this::convertToObject).toArray(); + assertThat(actual, everyItem(in(expected))); + } + + @Then("the result should have a count of {int}") + public void theResultShouldHaveACountOf(final Integer val) { + if (result instanceof Iterable) + assertEquals(val.intValue(), IteratorUtils.count((Iterable) result)); + else if (result instanceof Map) + assertEquals(val.intValue(), ((Map) result).size()); + else + fail(String.format("Missing an assert for this type", result.getClass())); + } + + @Then("the graph should return {int} for count of {string}") + public void theGraphShouldReturnForCountOf(final Integer count, final String gremlin) { + assertEquals(count.longValue(), ((GraphTraversal) parseGremlin(applyParameters(gremlin))).count().next()); + } + + @Then("the result should be empty") + public void theResultShouldBeEmpty() { + assertThat(result, instanceOf(Collection.class)); + assertEquals(0, IteratorUtils.count((Collection) result)); + } + + ////////////////////////////////////////////// + + @Given("an unsupported test") + public void anUnsupportedTest() { + // placeholder text - no operation needed because it should be followed by nothingShouldHappenBecause() + } + + @Then("nothing should happen because") + public void nothingShouldHappenBecause(final String message) { + throw new AssumptionViolatedException(String.format("This test is not supported by Gherkin because: %s", message)); + } + + ////////////////////////////////////////////// + + private Traversal parseGremlin(final String script) { + if (script.contains(".withComputer(")) + throw new AssumptionViolatedException("withComputer() syntax is not supported by gremlin-language at this time"); + + // TODO: fix io() data pathing stuff to bind it better to the graph + if (script.startsWith("g.io(\"")) { + throw new AssumptionViolatedException("io() syntax"); + } + + final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script)); + final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); + final GremlinParser.QueryContext ctx = parser.query(); + return (Traversal) new GremlinAntlrToJava(g.getGraph()).visitQuery(ctx); + } + + private List<Object> translateResultsToActual() { + final List<Object> r = result instanceof List ? (List<Object>) result : IteratorUtils.asList(result); + + // gotta convert Map.Entry to individual Map coz that how we assert those for GLVs - dah + final List<Object> actual = r.stream().map(o -> { + if (o instanceof Map.Entry) { + return new HashMap() {{ + put(((Entry<?, ?>) o).getKey(), ((Entry<?, ?>) o).getValue()); + }}; + } else { + return o; + } + }).collect(Collectors.toList()); + return actual; + } + + private String convertToString(final String pvalue) { + return convertToString(null, pvalue); + } + + private String convertToString(final String pkey, final String pvalue) { + for (Pair<Pattern,Function<String,String>> matcherConverter : stringMatcherConverters) { + final Pattern pattern = matcherConverter.getValue0(); + final Matcher matcher = pattern.matcher(pvalue); + if (matcher.find()) { + final Function<String,String> converter = matcherConverter.getValue1(); + + // when there are no groups there is a direct match + return converter.apply(matcher.groupCount() == 0 ? "" : matcher.group(1)); + } + } + + // this should be a raw string if it didn't match anything - suppose it could be a syntax error in the + // test too, but i guess the test would fail so perhaps ok to just assume it's raw string value that + // didn't need a transform by default + return String.format("\"%s\"", pvalue); + } + + private Object convertToObject(final Object pvalue) { + final Object v; + // we may get some json stuff if it's a m[] + if (pvalue instanceof JsonNode) { + final JsonNode n = (JsonNode) pvalue; + if (n.isArray()) { + v = IteratorUtils.stream(n.elements()).map(this::convertToObject).collect(Collectors.toList()); + } else if (n.isObject()) { + final Map<Object,Object> m = new HashMap<>(n.size()); + n.fields().forEachRemaining(e -> m.put(convertToObject(e.getKey()), convertToObject(e.getValue()))); + v = m; + } else if (n.isNumber()) { + v = n.numberValue(); + } else if (n.isBoolean()) { + v = n.booleanValue(); + } else { + v = n.textValue(); + } + } else { + v = pvalue; + } + + // if the object is already of a type then no need to push it through the matchers. + if (!(v instanceof String)) return v; + + for (Pair<Pattern,Function<String,Object>> matcherConverter : objectMatcherConverters) { + final Pattern pattern = matcherConverter.getValue0(); + final Matcher matcher = pattern.matcher((String) v); + if (matcher.find()) { + final Function<String,Object> converter = matcherConverter.getValue1(); + return converter.apply(matcher.group(1)); + } + } + + // this should be a raw string if it didn't match anything - suppose it could be a syntax error in the + // test too, but i guess the test would fail so perhaps ok to just assume it's raw string value that + // didn't need a transform by default + return String.format("%s", v); + } + + private static Triplet<String,String,String> getEdgeTriplet(final String e) { + final Matcher m = edgeTriplet.matcher(e); + if (m.matches()) { + return Triplet.with(m.group(1), m.group(2), m.group(3)); + } + + throw new IllegalStateException(String.format("Invalid edge identifier: %s", e)); + } + + private static Edge getEdge(final GraphTraversalSource g, final String e) { + final Triplet<String,String,String> t = getEdgeTriplet(e); + return g.V().has("name", t.getValue0()).outE(t.getValue1()).where(inV().has("name", t.getValue2())).next(); + } + + private static Object getEdgeId(final GraphTraversalSource g, final String e) { + return getEdge(g, e).id(); + } + + private static String getEdgeIdString(final GraphTraversalSource g, final String e) { + return getEdgeId(g, e).toString(); + } + + private String applyParameters(final String docString) { + String replaced = docString; + for (Map.Entry<String, String> kv : stringParameters.entrySet()) { + replaced = replaced.replace(kv.getKey(), kv.getValue()); + } + return replaced; + } +} diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java new file mode 100644 index 0000000..99fac71 --- /dev/null +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java @@ -0,0 +1,57 @@ +/* + * 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.features; + +import io.cucumber.java.Scenario; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; + +import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData; + +/** + * This interface provides the context the test suite needs in order to execute the Gherkin tests. It is implemented + * by graph providers who wish to test their graph systems against the TinkerPop test suite. It is paired with a + * test that uses the Cucumber test runner (i.e. {@code @RunWith(Cucumber.class)}) and requires a dependency injection + * package (e.g. {@code guice}) to push an instance into the Cucumber execution. + */ +public interface World { + + /** + * Gets a {@link GraphTraversalSource} that is backed by the specified {@link GraphData}. For {@code null}, the + * returned source should be an empty graph with no data in it. Tests do not mutate the standard graphs. Only tests + * that use an empty graph will change its state. + */ + public GraphTraversalSource getGraphTraversalSource(final GraphData graphData); + + /** + * Called before each individual test is executed which provides an opportunity to do some setup. For example, + * if there is a specific test that can't be supported it can be ignored by checking for the name with + * {@code scenario.getName()} and then throwing an {@code AssumptionViolationException}. + * @param scenario + */ + public default void beforeEachScenario(final Scenario scenario) { + // do nothing + } + + /** + * Called after each individual test is executed allowing for cleanup of any open resources. + */ + public default void afterEachScenario() { + // do nothing + } +} diff --git a/pom.xml b/pom.xml index efe897d..8460bf9 100644 --- a/pom.xml +++ b/pom.xml @@ -405,6 +405,7 @@ limitations under the License. <exclude>**/goal.txt</exclude> <exclude>**/src/main/resources/META-INF/services/**</exclude> <exclude>**/src/test/resources/META-INF/services/**</exclude> + <exclude>**/src/test/resources/cucumber.properties</exclude> <exclude>**/src/test/resources/incorrect-traversals.txt</exclude> <exclude>**/src/test/resources/org/apache/tinkerpop/gremlin/console/groovy/plugin/script-customizer-*.groovy</exclude> <exclude>**/src/test/resources/org/apache/tinkerpop/gremlin/jsr223/script-customizer-*.groovy</exclude> diff --git a/tinkergraph-gremlin/pom.xml b/tinkergraph-gremlin/pom.xml index 3f31389..ea943b3 100644 --- a/tinkergraph-gremlin/pom.xml +++ b/tinkergraph-gremlin/pom.xml @@ -36,6 +36,12 @@ limitations under the License. <artifactId>commons-lang3</artifactId> </dependency> <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <version>4.2.3</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <scope>test</scope> diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java new file mode 100644 index 0000000..51896ea --- /dev/null +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerGraphFeatureTest.java @@ -0,0 +1,89 @@ +/* + * 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.tinkergraph; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Stage; +import io.cucumber.guice.CucumberModules; +import io.cucumber.guice.GuiceFactory; +import io.cucumber.guice.InjectorSource; +import io.cucumber.java.Scenario; +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.apache.tinkerpop.gremlin.features.World; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph; +import org.junit.AssumptionViolatedException; +import org.junit.runner.RunWith; + +import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData; + +@RunWith(Cucumber.class) +@CucumberOptions( + tags = "not @RemoteOnly", + glue = { "org.apache.tinkerpop.gremlin.features" }, + objectFactory = GuiceFactory.class, + features = { "../gremlin-test/features" }, + plugin = {"pretty", "junit:target/cucumber.xml"}) +public class TinkerGraphFeatureTest { + + public static final class ServiceModule extends AbstractModule { + @Override + protected void configure() { + bind(World.class).to(TinkerGraphWorld.class); + } + } + + public static class TinkerGraphWorld implements World { + private static final TinkerGraph modern = TinkerFactory.createModern(); + private static final TinkerGraph classic = TinkerFactory.createClassic(); + private static final TinkerGraph crew = TinkerFactory.createTheCrew(); + private static final TinkerGraph sink = TinkerFactory.createKitchenSink(); + private static final TinkerGraph grateful = TinkerFactory.createGratefulDead(); + + @Override + public GraphTraversalSource getGraphTraversalSource(final GraphData graphData) { + if (null == graphData) + return TinkerGraph.open().traversal(); + else if (graphData == GraphData.CLASSIC) + return classic.traversal(); + else if (graphData == GraphData.CREW) + return crew.traversal(); + else if (graphData == GraphData.MODERN) + return modern.traversal(); + else if (graphData == GraphData.SINK) + return sink.traversal(); + else if (graphData == GraphData.GRATEFUL) + return grateful.traversal(); + else + throw new UnsupportedOperationException("GraphData not supported: " + graphData.name()); + } + } + + public static final class WorldInjectorSource implements InjectorSource { + @Override + public Injector getInjector() { + return Guice.createInjector(Stage.PRODUCTION, CucumberModules.createScenarioModule(), new ServiceModule()); + } + } + +} diff --git a/tinkergraph-gremlin/src/test/resources/cucumber.properties b/tinkergraph-gremlin/src/test/resources/cucumber.properties new file mode 100644 index 0000000..159b8c8 --- /dev/null +++ b/tinkergraph-gremlin/src/test/resources/cucumber.properties @@ -0,0 +1 @@ +guice.injector-source=org.apache.tinkerpop.gremlin.tinkergraph.TinkerGraphFeatureTest$WorldInjectorSource \ No newline at end of file
