You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2021/09/07 16:06:14 UTC
[tinkerpop] 01/08: TINKERPOP-2601 Basics in place for Gherkin test
to run on JVM
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 <st...@amazon.com>
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