44import com .fasterxml .jackson .databind .DeserializationFeature ;
55import com .fasterxml .jackson .databind .JsonNode ;
66import com .fasterxml .jackson .databind .ObjectMapper ;
7+ import com .fasterxml .jackson .databind .node .ArrayNode ;
78import com .fasterxml .jackson .databind .node .IntNode ;
89import com .fasterxml .jackson .databind .node .MissingNode ;
910import com .fasterxml .jackson .databind .node .NumericNode ;
1011import com .fasterxml .jackson .databind .node .TextNode ;
1112import io .cucumber .core .options .RuntimeOptionsBuilder ;
13+ import io .cucumber .core .order .PickleOrder ;
14+ import io .cucumber .core .order .StandardPickleOrders ;
1215import io .cucumber .core .plugin .MessageFormatter ;
1316import io .cucumber .core .runtime .Runtime ;
1417import org .hamcrest .Matcher ;
1518import org .junit .jupiter .params .ParameterizedTest ;
1619import org .junit .jupiter .params .provider .MethodSource ;
20+ import org .junit .platform .commons .io .ResourceFilter ;
21+ import org .junit .platform .commons .support .ResourceSupport ;
1722
23+ import java .io .BufferedReader ;
1824import java .io .IOException ;
25+ import java .io .InputStream ;
26+ import java .io .InputStreamReader ;
1927import java .nio .file .Files ;
2028import java .nio .file .Path ;
2129import java .nio .file .Paths ;
2230import java .util .ArrayList ;
31+ import java .util .Arrays ;
2332import java .util .Comparator ;
2433import java .util .Iterator ;
2534import java .util .LinkedHashMap ;
2837import java .util .regex .Pattern ;
2938import java .util .stream .Collectors ;
3039
40+ import static java .nio .charset .StandardCharsets .UTF_8 ;
3141import static java .nio .file .Files .newOutputStream ;
42+ import static java .nio .file .StandardCopyOption .REPLACE_EXISTING ;
3243import static java .util .Collections .emptyList ;
3344import static java .util .Collections .emptyMap ;
45+ import static java .util .Comparator .comparing ;
3446import static org .hamcrest .CoreMatchers .anyOf ;
3547import static org .hamcrest .CoreMatchers .is ;
3648import static org .hamcrest .MatcherAssert .assertThat ;
4052
4153public class CompatibilityTest {
4254
43- private static final Map <String , Map <Pattern , Matcher <?>>> exceptions = createExceptions ();
44-
45- private static Map <String , Map <Pattern , Matcher <?>>> createExceptions () {
55+ private static final List <String > unsupportedTestCases = Arrays .asList (
56+ // exception: not applicable
57+ "test-run-exception" ,
58+ // exception: Cucumber JVM does not support named hooks
59+ "hooks-named" ,
60+ // exception: Cucumber executes all hooks,
61+ // but skipped hooks can skip a scenario
62+ "hooks-skipped" ,
63+ // exception: Cucumber JVM does not support markdown features
64+ "markdown" ,
65+ // exception: Cucumber JVM does not support retrying features
66+ "retry" ,
67+ "retry-ambiguous" ,
68+ "retry-pending" ,
69+ // exception: Cucumber JVM does not support messages for global hooks
70+ "global-hooks" ,
71+ "global-hooks-afterall-error" ,
72+ "global-hooks-attachments" ,
73+ "global-hooks-beforeall-error" );
74+
75+ private static final Map <String , Map <Pattern , Matcher <?>>> divergingExpectations = createDivergingExpectations ();
76+
77+ private static Map <String , Map <Pattern , Matcher <?>>> createDivergingExpectations () {
4678 Map <String , Map <Pattern , Matcher <?>>> exceptions = new LinkedHashMap <>();
4779
4880 Map <Pattern , Matcher <?>> attachment = new LinkedHashMap <>();
4981 attachment .put (Pattern .compile ("/testCaseStartedId" ), isA (TextNode .class ));
5082 attachment .put (Pattern .compile ("/testStepId" ), isA (TextNode .class ));
83+ // exception: timestamps and durations are not predictable
84+ attachment .put (Pattern .compile ("/timestamp/seconds" ), isA (NumericNode .class ));
85+ attachment .put (Pattern .compile ("/timestamp/nanos" ), isA (NumericNode .class ));
5186 exceptions .put ("attachment" , attachment );
5287
5388 Map <Pattern , Matcher <?>> meta = new LinkedHashMap <>();
@@ -76,15 +111,17 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
76111 exceptions .put ("source" , source );
77112
78113 Map <Pattern , Matcher <?>> gherkinDocument = new LinkedHashMap <>();
114+ // exception: ids are not predictable
79115 gherkinDocument .put (Pattern .compile ("/feature/children/.*/scenario/id" ), isA (TextNode .class ));
80116 gherkinDocument .put (Pattern .compile ("/feature/children/.*/scenario/steps/.*/id" ), isA (TextNode .class ));
81117 gherkinDocument .put (Pattern .compile ("/feature/children/.*/scenario/examples/.*/id" ), isA (TextNode .class ));
82118 gherkinDocument .put (Pattern .compile ("/feature/children/.*/rule/id" ), isA (TextNode .class ));
83119 gherkinDocument .put (Pattern .compile ("/feature/children/.*/rule/tags/.*/id" ), isA (TextNode .class ));
84120 gherkinDocument .put (Pattern .compile ("/feature/children/.*/scenario/tags/.*/id" ), isA (TextNode .class ));
85-
121+ gherkinDocument .put (Pattern .compile ("/feature/children/.*/background/id" ), isA (TextNode .class ));
122+ gherkinDocument .put (Pattern .compile ("/feature/children/.*/background/steps/.*/id" ), isA (TextNode .class ));
123+ // exception: the CCK uses relative paths as uris
86124 gherkinDocument .put (Pattern .compile ("/uri" ), isA (TextNode .class ));
87-
88125 exceptions .put ("gherkinDocument" , gherkinDocument );
89126
90127 Map <Pattern , Matcher <?>> pickle = new LinkedHashMap <>();
@@ -94,7 +131,6 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
94131 pickle .put (Pattern .compile ("/astNodeIds/.*" ), isA (TextNode .class ));
95132 pickle .put (Pattern .compile ("/steps/.*/id" ), isA (TextNode .class ));
96133 pickle .put (Pattern .compile ("/steps/.*/astNodeIds/.*" ), isA (TextNode .class ));
97-
98134 pickle .put (Pattern .compile ("/tags/.*/astNodeId" ), isA (TextNode .class ));
99135 pickle .put (Pattern .compile ("/name" ), isA (TextNode .class ));
100136 exceptions .put ("pickle" , pickle );
@@ -196,50 +232,37 @@ private static Map<String, Map<Pattern, Matcher<?>>> createExceptions() {
196232 parameterType .put (Pattern .compile ("/sourceReference/location/line" ), isA (MissingNode .class ));
197233 exceptions .put ("parameterType" , parameterType );
198234
199- return exceptions ;
200- }
201-
202- @ ParameterizedTest
203- @ MethodSource ("io.cucumber.compatibility.TestCase#testCases" )
204- void produces_expected_output_for (TestCase testCase ) throws IOException {
205- Path parentDir = Files .createDirectories (Paths .get ("target" , "messages" ,
206- testCase .getId ()));
207- Path outputNdjson = parentDir .resolve ("out.ndjson" );
235+ Map <Pattern , Matcher <?>> suggestion = new LinkedHashMap <>();
236+ // exception: ids are not predictable
237+ suggestion .put (Pattern .compile ("/id" ), isA (TextNode .class ));
238+ suggestion .put (Pattern .compile ("/pickleStepId" ), isA (TextNode .class ));
239+ // exception: language is implementation specific
240+ suggestion .put (Pattern .compile ("/snippets/.*/language" ), isA (TextNode .class ));
241+ // exception: code is implementation specific
242+ suggestion .put (Pattern .compile ("/snippets/.*/code" ), isA (TextNode .class ));
208243
209- try {
210- Runtime .builder ()
211- .withRuntimeOptions (new RuntimeOptionsBuilder ()
212- .addGlue (testCase .getGlue ())
213- .addFeature (testCase .getFeatures ()).build ())
214- .withAdditionalPlugins (
215- new MessageFormatter (newOutputStream (outputNdjson )))
216- .build ()
217- .run ();
218- } catch (Exception e ) {
219- // exception: Scenario with unknown parameter types fails by
220- // throwing an exceptions
221- if (!"unknown-parameter-type" .equals (testCase .getId ())) {
222- throw e ;
223- }
224- }
244+ exceptions .put ("suggestion" , suggestion );
225245
226- // exception: Cucumber JVM does not support named hooks
227- if ("hooks-named" .equals (testCase .getId ())) {
228- return ;
229- }
246+ return exceptions ;
247+ }
230248
231- // exception: Cucumber JVM does not support markdown features
232- if ("markdown" .equals (testCase .getId ())) {
233- return ;
234- }
249+ static List <TestCase > acceptance () {
250+ ResourceFilter ndjson = ResourceFilter .of (resource -> resource .getName ().endsWith (".ndjson" ));
251+ return ResourceSupport .findAllResourcesInPackage (TestCase .TEST_CASES_PACKAGE , ndjson )
252+ .stream ()
253+ .map (TestCase ::new )
254+ .filter (testCase -> !unsupportedTestCases .contains (testCase .getId ()))
255+ .sorted (comparing (TestCase ::getId ))
256+ .collect (Collectors .toList ());
257+ }
235258
236- // exception: Cucumber JVM does not support retrying features
237- if ( "retry" . equals ( testCase . getId ())) {
238- return ;
239- }
259+ @ ParameterizedTest
260+ @ MethodSource ( "acceptance" )
261+ void test ( TestCase testCase ) throws IOException {
262+ Path actualNdjson = writeNdjsonReport ( testCase );
240263
241264 List <JsonNode > expected = readAllMessages (testCase .getExpectedFile ());
242- List <JsonNode > actual = readAllMessages (outputNdjson );
265+ List <JsonNode > actual = readAllMessages (Files . newInputStream ( actualNdjson ) );
243266
244267 Map <String , List <JsonNode >> expectedEnvelopes = openEnvelopes (expected );
245268 Map <String , List <JsonNode >> actualEnvelopes = openEnvelopes (actual );
@@ -268,6 +291,17 @@ void produces_expected_output_for(TestCase testCase) throws IOException {
268291 expectedEnvelopes .remove ("testStepStarted" );
269292 expectedEnvelopes .remove ("testStepFinished" );
270293 expectedEnvelopes .remove ("testCaseFinished" );
294+ expectedEnvelopes .remove ("suggestion" );
295+ }
296+
297+ if ("undefined" .equals (testCase .getId ())) {
298+ // bug: Cucumber JVM doesn't produce a suggestion that matches float
299+ ((ArrayNode ) expectedEnvelopes .get ("suggestion" ).get (3 ).get ("snippets" )).remove (1 );
300+ }
301+ if ("ambiguous" .equals (testCase .getId ())) {
302+ // bug: Cucumber JVM doesn't include the ambiguous step definitions
303+ // https://github.com/cucumber/cucumber-jvm/issues/3006
304+ expectedEnvelopes .remove ("testCase" );
271305 }
272306
273307 expectedEnvelopes .forEach ((messageType , expectedMessages ) -> assertThat (
@@ -276,14 +310,44 @@ void produces_expected_output_for(TestCase testCase) throws IOException {
276310 containsInRelativeOrder (aComparableMessage (messageType , expectedMessages )))));
277311 }
278312
279- private static List <JsonNode > readAllMessages (Path output ) throws IOException {
313+ private static Path writeNdjsonReport (TestCase testCase ) throws IOException {
314+ Path parentDir = Files .createDirectories (Paths .get ("target" , "messages" , testCase .getId ()));
315+ Path actualNdjson = parentDir .resolve ("actual.ndjson" );
316+ Path expectedNdjson = parentDir .resolve ("expected.ndjson" );
317+ Files .copy (testCase .getExpectedFile (), expectedNdjson , REPLACE_EXISTING );
318+
319+ try {
320+ PickleOrder pickleOrder = StandardPickleOrders .lexicalUriOrder ();
321+ if ("multiple-features-reversed" .equals (testCase .getId ())) {
322+ pickleOrder = StandardPickleOrders .reverseLexicalUriOrder ();
323+ }
324+ Runtime .builder ()
325+ .withRuntimeOptions (new RuntimeOptionsBuilder ()
326+ .addGlue (testCase .getGlue ())
327+ .setPickleOrder (pickleOrder )
328+ .addFeature (testCase .getFeatures ()).build ())
329+ .withAdditionalPlugins (
330+ new MessageFormatter (newOutputStream (actualNdjson )))
331+ .build ()
332+ .run ();
333+ } catch (Exception e ) {
334+ // exception: Scenario with unknown parameter types fails by
335+ // throwing an exceptions
336+ if (!"unknown-parameter-type" .equals (testCase .getId ())) {
337+ throw e ;
338+ }
339+ }
340+ return actualNdjson ;
341+ }
342+
343+ private static List <JsonNode > readAllMessages (InputStream output ) throws IOException {
280344 List <JsonNode > expectedEnvelopes = new ArrayList <>();
281345
282346 ObjectMapper mapper = new ObjectMapper ()
283347 .enable (DeserializationFeature .READ_ENUMS_USING_TO_STRING )
284348 .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
285349
286- Files . readAllLines (output ).forEach (s -> {
350+ readAllLines (output ).forEach (s -> {
287351 try {
288352 expectedEnvelopes .add (mapper .readTree (s ));
289353 } catch (JsonProcessingException e ) {
@@ -294,6 +358,17 @@ private static List<JsonNode> readAllMessages(Path output) throws IOException {
294358 return expectedEnvelopes ;
295359 }
296360
361+ public static List <String > readAllLines (InputStream is ) throws IOException {
362+ try (BufferedReader reader = new BufferedReader (new InputStreamReader (is , UTF_8 ))) {
363+ List <String > lines = new ArrayList <>();
364+ String line ;
365+ while ((line = reader .readLine ()) != null ) {
366+ lines .add (line );
367+ }
368+ return lines ;
369+ }
370+ }
371+
297372 @ SuppressWarnings ("unchecked" )
298373 private static <T > Map <String , List <T >> openEnvelopes (List <JsonNode > actual ) {
299374 Map <String , List <T >> map = new LinkedHashMap <>();
@@ -329,7 +404,7 @@ private void sortStepDefinitionsAndHooks(Map<String, List<JsonNode>> envelopes)
329404 private static List <Matcher <? super JsonNode >> aComparableMessage (String messageType , List <JsonNode > messages ) {
330405 return messages .stream ()
331406 .map (jsonNode -> new AComparableMessage (messageType , jsonNode ,
332- exceptions .getOrDefault (messageType , emptyMap ())))
407+ divergingExpectations .getOrDefault (messageType , emptyMap ())))
333408 .collect (Collectors .toList ());
334409 }
335410
0 commit comments