Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dbd3e04
bump cck to 20 and refresh resources
davidjgoss Aug 12, 2025
8ce7f7e
remove .feature part from ndjson paths
davidjgoss Aug 12, 2025
7067a27
assume multiple features
davidjgoss Aug 12, 2025
fcf1527
convert path to uri before using
davidjgoss Aug 12, 2025
f7a9857
Simplify
mpkorstanje Aug 13, 2025
df2ace3
Reduce diff
mpkorstanje Aug 13, 2025
582a3bd
Reduce diff
mpkorstanje Aug 13, 2025
d2d92b9
Throw on unexpected errors in the compatibility test
mpkorstanje Aug 13, 2025
045d3e0
Merge remote-tracking branch 'origin/main' into cck-20
mpkorstanje Sep 11, 2025
2a8a86d
Update
mpkorstanje Sep 11, 2025
d486c73
Spotless
mpkorstanje Sep 11, 2025
9c3b0eb
Use the java artifact
mpkorstanje Oct 1, 2025
6108bb1
Merge remote-tracking branch 'origin/main' into cck-20
mpkorstanje Oct 1, 2025
ea44b2d
Update
mpkorstanje Oct 1, 2025
ccc0ea5
Cleanup
mpkorstanje Oct 1, 2025
e03ed36
Add OS version to Meta message
mpkorstanje Nov 13, 2025
2b02a5c
Update CHANGELOG
mpkorstanje Nov 19, 2025
e915a69
Refactor exceptions to compatibility test
mpkorstanje Nov 20, 2025
e52c9f0
Merge branch 'add-os-version-to-meta' into cck-20
mpkorstanje Nov 20, 2025
d810e14
Update CCK samples
mpkorstanje Nov 21, 2025
77aab3d
Update CCK samples
mpkorstanje Nov 21, 2025
f1820da
Merge remote-tracking branch 'origin/main' into cck-20
mpkorstanje Nov 21, 2025
dc76d1d
Upgrade cck
mpkorstanje Nov 21, 2025
c9b7b00
Allow all hooks to signal skipping
mpkorstanje Nov 21, 2025
a698426
Polishing
mpkorstanje Nov 21, 2025
d35a3fd
Revert changes to runner
mpkorstanje Nov 21, 2025
634b7eb
Revert changes to runner
mpkorstanje Nov 21, 2025
916b465
Remove unused
mpkorstanje Nov 21, 2025
3be380e
Restructure
mpkorstanje Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions .github/workflows/test-testdata.yml

This file was deleted.

5 changes: 5 additions & 0 deletions compatibility/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>compatibility-kit</artifactId>
<version>25.0.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ protected boolean matchesSafely(JsonNode item, Description mismatchDescription)
.appendText(pointer.toString()).appendText(" ")
.appendText(actual.toString()).appendText(" ");
// Copy and paste needed to suppress this finding.
// System.out.printf("%s.put(Pattern.compile(\"%s\"),
// isA(%s.class));%n", messageType, key,
// actual.getClass().getSimpleName());
System.out.printf("%s.put(Pattern.compile(\"%s\"), isA(%s.class));%n", messageType, pointer,
actual.getClass().getSimpleName());
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,31 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.cucumber.core.options.RuntimeOptionsBuilder;
import io.cucumber.core.order.PickleOrder;
import io.cucumber.core.order.StandardPickleOrders;
import io.cucumber.core.plugin.MessageFormatter;
import io.cucumber.core.runtime.Runtime;
import org.hamcrest.Matcher;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.io.ResourceFilter;
import org.junit.platform.commons.support.ResourceSupport;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
Expand All @@ -28,9 +37,12 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newOutputStream;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Comparator.comparing;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
Expand All @@ -40,14 +52,37 @@

public class CompatibilityTest {

private static final Map<String, Map<Pattern, Matcher<?>>> exceptions = createExceptions();

private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
private static final List<String> unsupportedTestCases = Arrays.asList(
// exception: not applicable
"test-run-exception",
// exception: Cucumber JVM does not support named hooks
"hooks-named",
// exception: Cucumber executes all hooks,
// but skipped hooks can skip a scenario
"hooks-skipped",
// exception: Cucumber JVM does not support markdown features
"markdown",
// exception: Cucumber JVM does not support retrying features
"retry",
"retry-ambiguous",
"retry-pending",
// exception: Cucumber JVM does not support messages for global hooks
"global-hooks",
"global-hooks-afterall-error",
"global-hooks-attachments",
"global-hooks-beforeall-error");

private static final Map<String, Map<Pattern, Matcher<?>>> divergingExpectations = createDivergingExpectations();

private static Map<String, Map<Pattern, Matcher<?>>> createDivergingExpectations() {
Map<String, Map<Pattern, Matcher<?>>> exceptions = new LinkedHashMap<>();

Map<Pattern, Matcher<?>> attachment = new LinkedHashMap<>();
attachment.put(Pattern.compile("/testCaseStartedId"), isA(TextNode.class));
attachment.put(Pattern.compile("/testStepId"), isA(TextNode.class));
// exception: timestamps and durations are not predictable
attachment.put(Pattern.compile("/timestamp/seconds"), isA(NumericNode.class));
attachment.put(Pattern.compile("/timestamp/nanos"), isA(NumericNode.class));
exceptions.put("attachment", attachment);

Map<Pattern, Matcher<?>> meta = new LinkedHashMap<>();
Expand Down Expand Up @@ -76,15 +111,17 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
exceptions.put("source", source);

Map<Pattern, Matcher<?>> gherkinDocument = new LinkedHashMap<>();
// exception: ids are not predictable
gherkinDocument.put(Pattern.compile("/feature/children/.*/scenario/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/scenario/steps/.*/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/scenario/examples/.*/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/rule/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/rule/tags/.*/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/scenario/tags/.*/id"), isA(TextNode.class));

gherkinDocument.put(Pattern.compile("/feature/children/.*/background/id"), isA(TextNode.class));
gherkinDocument.put(Pattern.compile("/feature/children/.*/background/steps/.*/id"), isA(TextNode.class));
// exception: the CCK uses relative paths as uris
gherkinDocument.put(Pattern.compile("/uri"), isA(TextNode.class));

exceptions.put("gherkinDocument", gherkinDocument);

Map<Pattern, Matcher<?>> pickle = new LinkedHashMap<>();
Expand All @@ -94,7 +131,6 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
pickle.put(Pattern.compile("/astNodeIds/.*"), isA(TextNode.class));
pickle.put(Pattern.compile("/steps/.*/id"), isA(TextNode.class));
pickle.put(Pattern.compile("/steps/.*/astNodeIds/.*"), isA(TextNode.class));

pickle.put(Pattern.compile("/tags/.*/astNodeId"), isA(TextNode.class));
pickle.put(Pattern.compile("/name"), isA(TextNode.class));
exceptions.put("pickle", pickle);
Expand Down Expand Up @@ -196,50 +232,37 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
parameterType.put(Pattern.compile("/sourceReference/location/line"), isA(MissingNode.class));
exceptions.put("parameterType", parameterType);

return exceptions;
}

@ParameterizedTest
@MethodSource("io.cucumber.compatibility.TestCase#testCases")
void produces_expected_output_for(TestCase testCase) throws IOException {
Path parentDir = Files.createDirectories(Paths.get("target", "messages",
testCase.getId()));
Path outputNdjson = parentDir.resolve("out.ndjson");
Map<Pattern, Matcher<?>> suggestion = new LinkedHashMap<>();
// exception: ids are not predictable
suggestion.put(Pattern.compile("/id"), isA(TextNode.class));
suggestion.put(Pattern.compile("/pickleStepId"), isA(TextNode.class));
// exception: language is implementation specific
suggestion.put(Pattern.compile("/snippets/.*/language"), isA(TextNode.class));
// exception: code is implementation specific
suggestion.put(Pattern.compile("/snippets/.*/code"), isA(TextNode.class));

try {
Runtime.builder()
.withRuntimeOptions(new RuntimeOptionsBuilder()
.addGlue(testCase.getGlue())
.addFeature(testCase.getFeatures()).build())
.withAdditionalPlugins(
new MessageFormatter(newOutputStream(outputNdjson)))
.build()
.run();
} catch (Exception e) {
// exception: Scenario with unknown parameter types fails by
// throwing an exceptions
if (!"unknown-parameter-type".equals(testCase.getId())) {
throw e;
}
}
exceptions.put("suggestion", suggestion);

// exception: Cucumber JVM does not support named hooks
if ("hooks-named".equals(testCase.getId())) {
return;
}
return exceptions;
}

// exception: Cucumber JVM does not support markdown features
if ("markdown".equals(testCase.getId())) {
return;
}
static List<TestCase> acceptance() {
ResourceFilter ndjson = ResourceFilter.of(resource -> resource.getName().endsWith(".ndjson"));
return ResourceSupport.findAllResourcesInPackage(TestCase.TEST_CASES_PACKAGE, ndjson)
.stream()
.map(TestCase::new)
.filter(testCase -> !unsupportedTestCases.contains(testCase.getId()))
.sorted(comparing(TestCase::getId))
.collect(Collectors.toList());
}

// exception: Cucumber JVM does not support retrying features
if ("retry".equals(testCase.getId())) {
return;
}
@ParameterizedTest
@MethodSource("acceptance")
void test(TestCase testCase) throws IOException {
Path actualNdjson = writeNdjsonReport(testCase);

List<JsonNode> expected = readAllMessages(testCase.getExpectedFile());
List<JsonNode> actual = readAllMessages(outputNdjson);
List<JsonNode> actual = readAllMessages(Files.newInputStream(actualNdjson));

Map<String, List<JsonNode>> expectedEnvelopes = openEnvelopes(expected);
Map<String, List<JsonNode>> actualEnvelopes = openEnvelopes(actual);
Expand Down Expand Up @@ -268,6 +291,17 @@ void produces_expected_output_for(TestCase testCase) throws IOException {
expectedEnvelopes.remove("testStepStarted");
expectedEnvelopes.remove("testStepFinished");
expectedEnvelopes.remove("testCaseFinished");
expectedEnvelopes.remove("suggestion");
}

if ("undefined".equals(testCase.getId())) {
// bug: Cucumber JVM doesn't produce a suggestion that matches float
((ArrayNode) expectedEnvelopes.get("suggestion").get(3).get("snippets")).remove(1);
}
if ("ambiguous".equals(testCase.getId())) {
// bug: Cucumber JVM doesn't include the ambiguous step definitions
// https://github.com/cucumber/cucumber-jvm/issues/3006
expectedEnvelopes.remove("testCase");
}

expectedEnvelopes.forEach((messageType, expectedMessages) -> assertThat(
Expand All @@ -276,14 +310,44 @@ void produces_expected_output_for(TestCase testCase) throws IOException {
containsInRelativeOrder(aComparableMessage(messageType, expectedMessages)))));
}

private static List<JsonNode> readAllMessages(Path output) throws IOException {
private static Path writeNdjsonReport(TestCase testCase) throws IOException {
Path parentDir = Files.createDirectories(Paths.get("target", "messages", testCase.getId()));
Path actualNdjson = parentDir.resolve("actual.ndjson");
Path expectedNdjson = parentDir.resolve("expected.ndjson");
Files.copy(testCase.getExpectedFile(), expectedNdjson, REPLACE_EXISTING);

try {
PickleOrder pickleOrder = StandardPickleOrders.lexicalUriOrder();
if ("multiple-features-reversed".equals(testCase.getId())) {
pickleOrder = StandardPickleOrders.reverseLexicalUriOrder();
}
Runtime.builder()
.withRuntimeOptions(new RuntimeOptionsBuilder()
.addGlue(testCase.getGlue())
.setPickleOrder(pickleOrder)
.addFeature(testCase.getFeatures()).build())
.withAdditionalPlugins(
new MessageFormatter(newOutputStream(actualNdjson)))
.build()
.run();
} catch (Exception e) {
// exception: Scenario with unknown parameter types fails by
// throwing an exceptions
if (!"unknown-parameter-type".equals(testCase.getId())) {
throw e;
}
}
return actualNdjson;
}

private static List<JsonNode> readAllMessages(InputStream output) throws IOException {
List<JsonNode> expectedEnvelopes = new ArrayList<>();

ObjectMapper mapper = new ObjectMapper()
.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Files.readAllLines(output).forEach(s -> {
readAllLines(output).forEach(s -> {
try {
expectedEnvelopes.add(mapper.readTree(s));
} catch (JsonProcessingException e) {
Expand All @@ -294,6 +358,17 @@ private static List<JsonNode> readAllMessages(Path output) throws IOException {
return expectedEnvelopes;
}

public static List<String> readAllLines(InputStream is) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, UTF_8))) {
List<String> lines = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
return lines;
}
}

@SuppressWarnings("unchecked")
private static <T> Map<String, List<T>> openEnvelopes(List<JsonNode> actual) {
Map<String, List<T>> map = new LinkedHashMap<>();
Expand Down Expand Up @@ -329,7 +404,7 @@ private void sortStepDefinitionsAndHooks(Map<String, List<JsonNode>> envelopes)
private static List<Matcher<? super JsonNode>> aComparableMessage(String messageType, List<JsonNode> messages) {
return messages.stream()
.map(jsonNode -> new AComparableMessage(messageType, jsonNode,
exceptions.getOrDefault(messageType, emptyMap())))
divergingExpectations.getOrDefault(messageType, emptyMap())))
.collect(Collectors.toList());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.cucumber.compatibility;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public final class Resources {

private Resources() {
// utility class
}

public static byte[] read(String name) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (InputStream is = Resources.class.getResourceAsStream(name)) {
int read;
byte[] data = new byte[4096];
while ((read = is.read(data, 0, data.length)) != -1) {
bytes.write(data, 0, read);
}
}
return bytes.toByteArray();
}
}
Loading