From dd1ab8364b446bb5f7768a4d894946cb9b09e5da Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:20:41 +0100 Subject: [PATCH 01/26] Update CCK to v24 --- cucumber.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cucumber.gemspec b/cucumber.gemspec index 5e944c2c2..43d2cf846 100644 --- a/cucumber.gemspec +++ b/cucumber.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| s.add_dependency 'multi_test', '~> 1.1' s.add_dependency 'sys-uname', '~> 1.5' - s.add_development_dependency 'cucumber-compatibility-kit', '~> 22.0' + s.add_development_dependency 'cucumber-compatibility-kit', '~> 24.0' # Only needed whilst we are testing the formatters. Can be removed once we remove tests for those s.add_development_dependency 'nokogiri', '~> 1.15' s.add_development_dependency 'rake', '~> 13.2' From 5a7478e0cad6b3b60ff4264e07b93bb22e9cdcd4 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:36:27 +0100 Subject: [PATCH 02/26] Fix up breaking namespace changes, perform first run and document findings --- compatibility/cck_spec.rb | 8 ++++++-- compatibility/support/compatibility_kit.rb | 22 ++++++++++++---------- compatibility/support/shared_examples.rb | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 0156a0a84..457d8537c 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -13,6 +13,10 @@ RSpec.describe CCK, :cck do let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } + # CCK v22 conformance + # OVERALL: N/A (yet) + # SANITIZED: 102 examples, 22 failures, 80 passed + # CCK v22 conformance # OVERALL: 93 examples, 5 failures, 88 passed # SANITIZED: 84 examples, 0 failures, 84 passed @@ -32,7 +36,7 @@ global-hooks-attachments global-hooks-beforeall-error ] - _failing, passing = CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } + _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } passing.each do |example_name| describe "'#{example_name}' example" do @@ -45,7 +49,7 @@ '' end end - let(:support_code_path) { CompatibilityKit.supporting_code_for(example) } + let(:support_code_path) { Cucumber::CompatibilityKit.supporting_code_for(example) } let(:messages) { `#{cucumber_command} --require #{support_code_path} #{cck_path} #{extra_args}` } end end diff --git a/compatibility/support/compatibility_kit.rb b/compatibility/support/compatibility_kit.rb index 70b0b8c63..1cca7addf 100644 --- a/compatibility/support/compatibility_kit.rb +++ b/compatibility/support/compatibility_kit.rb @@ -1,19 +1,21 @@ # frozen_string_literal: true -class CompatibilityKit - class << self - def supporting_code_for(example_name) - path = File.join(local_features_folder_location, example_name) +module Cucumber + class CompatibilityKit + class << self + def supporting_code_for(example_name) + path = File.join(local_features_folder_location, example_name) - return path if File.directory?(path) + return path if File.directory?(path) - raise ArgumentError, "No supporting code directory found locally for CCK example: #{example_name}" - end + raise ArgumentError, "No supporting code directory found locally for CCK example: #{example_name}" + end - private + private - def local_features_folder_location - File.expand_path("#{File.dirname(__FILE__)}/../features/") + def local_features_folder_location + File.expand_path("#{File.dirname(__FILE__)}/../features/") + end end end end diff --git a/compatibility/support/shared_examples.rb b/compatibility/support/shared_examples.rb index 6f2674dbe..eac666387 100644 --- a/compatibility/support/shared_examples.rb +++ b/compatibility/support/shared_examples.rb @@ -10,7 +10,7 @@ RSpec.shared_examples 'cucumber compatibility kit' do include CCK::Helpers - let(:cck_path) { CompatibilityKit.feature_code_for(example) } + let(:cck_path) { Cucumber::CompatibilityKit.feature_code_for(example) } let(:parsed_original) { parse_ndjson_file("#{cck_path}/#{example}.ndjson") } let(:parsed_generated) { parse_ndjson(messages) } From 39a50cec5224ad1116b31341f914af80aa433595 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:38:56 +0100 Subject: [PATCH 03/26] Remove superfluous steps no longer needed in retryv24 --- compatibility/features/retry/retry_steps.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/compatibility/features/retry/retry_steps.rb b/compatibility/features/retry/retry_steps.rb index d042e9304..c31a0aa63 100644 --- a/compatibility/features/retry/retry_steps.rb +++ b/compatibility/features/retry/retry_steps.rb @@ -19,15 +19,3 @@ Given('a step that always fails') do raise 'Exception in step' end - -Given('an ambiguous step') do - # first one -end - -Given('an ambiguous step') do - # second one -end - -Given('a pending step') do - pending('') -end From 244a6fa42c534ea64101fa77ab16d8184ffe6b55 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:42:10 +0100 Subject: [PATCH 04/26] Add in granularity of retry sub-cck features --- .../features/retry-ambiguous/retry-ambiguous_steps.rb | 5 +++++ compatibility/features/retry-pending/retry-pending_steps.rb | 5 +++++ .../features/retry-undefined/retry-undefined_steps.rb | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb create mode 100644 compatibility/features/retry-pending/retry-pending_steps.rb create mode 100644 compatibility/features/retry-undefined/retry-undefined_steps.rb diff --git a/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb new file mode 100644 index 000000000..47e83264d --- /dev/null +++ b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Given('an ambiguous step') do + # second one +end diff --git a/compatibility/features/retry-pending/retry-pending_steps.rb b/compatibility/features/retry-pending/retry-pending_steps.rb new file mode 100644 index 000000000..61259124a --- /dev/null +++ b/compatibility/features/retry-pending/retry-pending_steps.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Given('a pending step') do + pending('') +end diff --git a/compatibility/features/retry-undefined/retry-undefined_steps.rb b/compatibility/features/retry-undefined/retry-undefined_steps.rb new file mode 100644 index 000000000..9ed675349 --- /dev/null +++ b/compatibility/features/retry-undefined/retry-undefined_steps.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +# There are intentionally no steps defined for this sample From 010fd0db1abab680b597bbf68600db8a81235a13 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:44:55 +0100 Subject: [PATCH 05/26] Add 2 more sets of steps for new cck features --- .../examples-tables-undefined_steps.rb | 13 +++++++++++++ .../hooks-undefined/hooks-undefined_steps.rb | 9 +++++++++ 2 files changed, 22 insertions(+) create mode 100644 compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb create mode 100644 compatibility/features/hooks-undefined/hooks-undefined_steps.rb diff --git a/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb b/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb new file mode 100644 index 000000000..9b0bcd5a6 --- /dev/null +++ b/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +Given('there are {int} cucumbers') do |initial_count| + @count = initial_count +end + +When('I eat {int} cucumbers') do |eat_count| + @count -= eat_count +end + +Then('I should have {int} cucumbers') do |expected_count| + expect(@count).to eq(expected_count) +end diff --git a/compatibility/features/hooks-undefined/hooks-undefined_steps.rb b/compatibility/features/hooks-undefined/hooks-undefined_steps.rb new file mode 100644 index 000000000..208c7c96a --- /dev/null +++ b/compatibility/features/hooks-undefined/hooks-undefined_steps.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Before do + # no-op +end + +After do + # no-op +end From b7e76c407a352b9ed2f33181eb2267b2aa8f3c00 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 15 May 2026 08:49:06 +0100 Subject: [PATCH 06/26] Add in final missing definition for missing feature --- compatibility/cck_spec.rb | 2 +- .../features/test-run-exception/test-run-exception_steps.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 compatibility/features/test-run-exception/test-run-exception_steps.rb diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 457d8537c..bd8e651ec 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -15,7 +15,7 @@ # CCK v22 conformance # OVERALL: N/A (yet) - # SANITIZED: 102 examples, 22 failures, 80 passed + # SANITIZED: 102 examples, 9 failures, 93 passed # CCK v22 conformance # OVERALL: 93 examples, 5 failures, 88 passed diff --git a/compatibility/features/test-run-exception/test-run-exception_steps.rb b/compatibility/features/test-run-exception/test-run-exception_steps.rb new file mode 100644 index 000000000..fd015a0fa --- /dev/null +++ b/compatibility/features/test-run-exception/test-run-exception_steps.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Given('a step') do + # no-op +end From aa607dbe3be0a700ad6333abb879a8660fa327ed Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 25 May 2026 12:49:39 +0100 Subject: [PATCH 07/26] Updated run-set --- compatibility/cck_spec.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 84aa5537e..87cc3712c 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -13,16 +13,20 @@ RSpec.describe CCK, :cck do let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } - # CCK v22 conformance - # OVERALL: N/A (yet) - # SANITIZED: 102 examples, 9 failures, 93 passed - - # CCK v22 conformance - # OVERALL: 93 examples, 5 failures, 88 passed - # SANITIZED: 84 examples, 0 failures, 84 passed + # CCK v24 conformance + # OVERALL: 111 examples, 8 failures, 103 passed + # SANITIZED: 90 examples, 0 failures, 90 passed items_to_fix = - %w[] + %w[ + undefined + examples-tables-undefined + retry-undefined + unknown-parameter-type + hooks-undefined + retry-ambiguous + test-run-exception + ] _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } passing.each do |example_name| From a5d615bd3ceaef497402b3ca02d33389c707825e Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 25 May 2026 12:51:08 +0100 Subject: [PATCH 08/26] Add note for missing items whilst triaging --- compatibility/cck_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 87cc3712c..f16c92d4d 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -17,6 +17,7 @@ # OVERALL: 111 examples, 8 failures, 103 passed # SANITIZED: 90 examples, 0 failures, 90 passed + # Items to fix - "Suggestion" message * 6, Invalid option * 1, Step Definition * 1 items_to_fix = %w[ undefined @@ -29,7 +30,7 @@ ] _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } - passing.each do |example_name| + _failing.each do |example_name| describe "'#{example_name}' example" do include_examples 'cucumber compatibility kit' do let(:example) { example_name } From d477018e6b4f310141fd48d0a7b4ad3b2cbe5fd2 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 25 May 2026 12:57:33 +0100 Subject: [PATCH 09/26] Fix up missing duplicate step in cck definition for steps --- compatibility/cck_spec.rb | 5 ++--- .../features/retry-ambiguous/retry-ambiguous_steps.rb | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index f16c92d4d..bc249e59e 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -14,10 +14,10 @@ let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } # CCK v24 conformance - # OVERALL: 111 examples, 8 failures, 103 passed + # OVERALL: 111 examples, 7 failures, 104 passed # SANITIZED: 90 examples, 0 failures, 90 passed - # Items to fix - "Suggestion" message * 6, Invalid option * 1, Step Definition * 1 + # Items to fix - "Suggestion" message * 5, Invalid option * 2 items_to_fix = %w[ undefined @@ -25,7 +25,6 @@ retry-undefined unknown-parameter-type hooks-undefined - retry-ambiguous test-run-exception ] _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } diff --git a/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb index 47e83264d..34a35fe2c 100644 --- a/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb +++ b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +Given('an ambiguous step') do + # first one +end + Given('an ambiguous step') do # second one end From e0ac149eccca0a50c8ffff34d4dca3dc15a68f3e Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 25 May 2026 13:48:17 +0100 Subject: [PATCH 10/26] WIP: Generate snippets and suggestion with message output to event bus - Currently generating too many messages --- compatibility/cck_spec.rb | 2 +- lib/cucumber/formatter/console.rb | 20 +++++++++---- lib/cucumber/formatter/message_builder.rb | 35 ++++++++++++++++++++++- lib/cucumber/formatter/pretty.rb | 2 -- lib/cucumber/formatter/progress.rb | 2 -- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index bc249e59e..5db0fc9b4 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -29,7 +29,7 @@ ] _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } - _failing.each do |example_name| + ['examples-tables-undefined'].each do |example_name| describe "'#{example_name}' example" do include_examples 'cucumber compatibility kit' do let(:example) { example_name } diff --git a/lib/cucumber/formatter/console.rb b/lib/cucumber/formatter/console.rb index dfe7688ae..573bd099e 100644 --- a/lib/cucumber/formatter/console.rb +++ b/lib/cucumber/formatter/console.rb @@ -116,11 +116,11 @@ def linebreaks(msg, max) def collect_snippet_data(test_step, ast_lookup) # collect snippet data for undefined steps keyword = ast_lookup.snippet_step_keyword(test_step) - @snippets_input << Console::SnippetData.new(keyword, test_step) + snippets_input << Console::SnippetData.new(keyword, test_step) end def collect_undefined_parameter_type_names(undefined_parameter_type) - @undefined_parameter_types << undefined_parameter_type.type_name + undefined_parameter_types << undefined_parameter_type.type_name end def print_snippets(options) @@ -129,9 +129,9 @@ def print_snippets(options) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end - do_print_snippets(snippet_text_proc) unless @snippets_input.empty? + do_print_snippets(snippet_text_proc) unless snippets_input.empty? - @undefined_parameter_types.map do |type_name| + undefined_parameter_types.map do |type_name| do_print_undefined_parameter_type_snippet(type_name) end end @@ -249,11 +249,19 @@ def element_messages(elements, status) def snippet_text(step_keyword, step_name, multiline_arg) keyword = Cucumber::Gherkin::I18n.code_keyword_for(step_keyword).strip - config.snippet_generators.map do |generator| - generator.call(keyword, step_name, multiline_arg, config.snippet_type) + @config.snippet_generators.map do |generator| + generator.call(keyword, step_name, multiline_arg, @config.snippet_type) end.join("\n") end + def snippets_input + @snippets_input ||= [] + end + + def undefined_parameter_types + @undefined_parameter_types ||= [] + end + class SnippetData attr_reader :actual_keyword, :step diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 5256a5c71..ae6c0852e 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -10,9 +10,11 @@ module Formatter class MessageBuilder include Cucumber::Messages::Helpers::TimeConversion include Io + include Console def initialize(config) @config = config + @ast_lookup = AstLookup.new(config) @repository = Cucumber::Repository.new @query = Cucumber::Query.new(@repository) @@ -281,6 +283,19 @@ def on_test_step_started(event) output_envelope(message) end + # def print_snippets(options) + # return unless options[:snippets] + # + # snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| + # snippet_text(step_keyword, step_name, multiline_arg) + # end + # do_print_snippets(snippet_text_proc) unless snippets_input.empty? + # + # undefined_parameter_types.map do |type_name| + # do_print_undefined_parameter_type_snippet(type_name) + # end + # end + def on_test_step_finished(event) find_test_case_by_step_id = @repository.test_case_by_id @@ -293,8 +308,8 @@ def on_test_step_finished(event) .select { |test_case_started_message| test_case_started_message.test_case_id == find_test_case_by_step_id.id } .max_by(&:attempt) + collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter) - result_message = result.to_message if result.failed? || result.pending? message_element = result.failed? ? result.exception : result @@ -307,6 +322,8 @@ def on_test_step_finished(event) ) end + print_snippets(@config.to_hash) + message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( test_step_id: event.test_step.id, @@ -319,6 +336,22 @@ def on_test_step_finished(event) output_envelope(message) end + def do_print_snippets(snippet_text_proc) + code_text = @snippets_input.map do |data| + snippet_text_proc.call(data.actual_keyword, data.step.text, data.step.multiline_arg) + end.uniq + + message = Cucumber::Messages::Envelope.new( + suggestion: Cucumber::Messages::Suggestion.new( + id: @config.id_generator.new_id, + pickle_step_id: 'TBC', + snippets: [Cucumber::Messages::Snippet.new(language: 'ruby', code: code_text)] + ) + ) + + output_envelope(message) + end + def on_undefined_parameter_type(event) message = Cucumber::Messages::Envelope.new( undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new( diff --git a/lib/cucumber/formatter/pretty.rb b/lib/cucumber/formatter/pretty.rb index 97ba8d796..33615e23e 100644 --- a/lib/cucumber/formatter/pretty.rb +++ b/lib/cucumber/formatter/pretty.rb @@ -33,8 +33,6 @@ def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) @config = config @options = config.to_hash - @snippets_input = [] - @undefined_parameter_types = [] @total_duration = 0 @exceptions = [] @gherkin_sources = {} diff --git a/lib/cucumber/formatter/progress.rb b/lib/cucumber/formatter/progress.rb index 88b2b0b2c..0526b749a 100644 --- a/lib/cucumber/formatter/progress.rb +++ b/lib/cucumber/formatter/progress.rb @@ -20,8 +20,6 @@ class Progress def initialize(config) @config = config @io = ensure_io(config.out_stream, config.error_stream) - @snippets_input = [] - @undefined_parameter_types = [] @total_duration = 0 @matches = {} @pending_step_matches = [] From afb247a303393e3090102b67c001650b5d97c849 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 10:56:20 +0100 Subject: [PATCH 11/26] Always generate suggestion messages in background --- lib/cucumber/formatter/console.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/cucumber/formatter/console.rb b/lib/cucumber/formatter/console.rb index 573bd099e..7a1cb39fb 100644 --- a/lib/cucumber/formatter/console.rb +++ b/lib/cucumber/formatter/console.rb @@ -123,9 +123,7 @@ def collect_undefined_parameter_type_names(undefined_parameter_type) undefined_parameter_types << undefined_parameter_type.type_name end - def print_snippets(options) - return unless options[:snippets] - + def print_snippets(_options) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end From c13ee402e5941d0dfa44c07b47e31cf5a4174dee Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 10:59:27 +0100 Subject: [PATCH 12/26] Fix snippet generation logic --- lib/cucumber/formatter/console.rb | 4 +++- lib/cucumber/formatter/message_builder.rb | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/cucumber/formatter/console.rb b/lib/cucumber/formatter/console.rb index 7a1cb39fb..573bd099e 100644 --- a/lib/cucumber/formatter/console.rb +++ b/lib/cucumber/formatter/console.rb @@ -123,7 +123,9 @@ def collect_undefined_parameter_type_names(undefined_parameter_type) undefined_parameter_types << undefined_parameter_type.type_name end - def print_snippets(_options) + def print_snippets(options) + return unless options[:snippets] + snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index ae6c0852e..948bc9328 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -322,7 +322,8 @@ def on_test_step_finished(event) ) end - print_snippets(@config.to_hash) + # We always want to build snippet messages in the `MessageBuilder` formatter - irrespective of config options + print_snippets({ snippets: true }) message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( @@ -337,7 +338,7 @@ def on_test_step_finished(event) end def do_print_snippets(snippet_text_proc) - code_text = @snippets_input.map do |data| + code_text = snippets_input.map do |data| snippet_text_proc.call(data.actual_keyword, data.step.text, data.step.multiline_arg) end.uniq From 09920e33aaedc1b4d5f024a8f5c946b3cd49585c Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:15:48 +0100 Subject: [PATCH 13/26] Remove lots of cruft from snippet generation --- lib/cucumber/formatter/message_builder.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 948bc9328..57febcd8d 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -308,7 +308,6 @@ def on_test_step_finished(event) .select { |test_case_started_message| test_case_started_message.test_case_id == find_test_case_by_step_id.id } .max_by(&:attempt) - collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter) result_message = result.to_message if result.failed? || result.pending? @@ -321,9 +320,12 @@ def on_test_step_finished(event) exception: create_exception_object(result, message_element) ) end - + collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? # We always want to build snippet messages in the `MessageBuilder` formatter - irrespective of config options - print_snippets({ snippets: true }) + snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| + snippet_text(step_keyword, step_name, multiline_arg) + end + do_print_snippets(snippet_text_proc, event) unless snippets_input.empty? message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( @@ -337,8 +339,8 @@ def on_test_step_finished(event) output_envelope(message) end - def do_print_snippets(snippet_text_proc) - code_text = snippets_input.map do |data| + def do_print_snippets(snippet_text_proc, event) + snippets_array = snippets_input.map do |data| snippet_text_proc.call(data.actual_keyword, data.step.text, data.step.multiline_arg) end.uniq @@ -346,11 +348,13 @@ def do_print_snippets(snippet_text_proc) suggestion: Cucumber::Messages::Suggestion.new( id: @config.id_generator.new_id, pickle_step_id: 'TBC', - snippets: [Cucumber::Messages::Snippet.new(language: 'ruby', code: code_text)] + snippets: snippets_array.map { |code_snippet| Cucumber::Messages::Snippet.new(language: 'ruby', code: code_snippet) } ) ) output_envelope(message) + # To ensure we don't redistribute the "same" snippets over and over again + snippets_input.clear end def on_undefined_parameter_type(event) From 8e279bcbb51199385324b7d0aefba0178dc42318 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:18:48 +0100 Subject: [PATCH 14/26] Add pickle step id into payload --- lib/cucumber/formatter/message_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 57febcd8d..8ded1d8ba 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -347,7 +347,7 @@ def do_print_snippets(snippet_text_proc, event) message = Cucumber::Messages::Envelope.new( suggestion: Cucumber::Messages::Suggestion.new( id: @config.id_generator.new_id, - pickle_step_id: 'TBC', + pickle_step_id: @repository.test_step_by_id[event.test_step.id].pickle_step_id, snippets: snippets_array.map { |code_snippet| Cucumber::Messages::Snippet.new(language: 'ruby', code: code_snippet) } ) ) From 031a74ff60631d448aacb52fb74ffd838031931d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:23:11 +0100 Subject: [PATCH 15/26] Tidy up snippet generation into more bitesize methods --- lib/cucumber/formatter/message_builder.rb | 41 +++++++++-------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 8ded1d8ba..5da2c50f5 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -283,19 +283,6 @@ def on_test_step_started(event) output_envelope(message) end - # def print_snippets(options) - # return unless options[:snippets] - # - # snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| - # snippet_text(step_keyword, step_name, multiline_arg) - # end - # do_print_snippets(snippet_text_proc) unless snippets_input.empty? - # - # undefined_parameter_types.map do |type_name| - # do_print_undefined_parameter_type_snippet(type_name) - # end - # end - def on_test_step_finished(event) find_test_case_by_step_id = @repository.test_case_by_id @@ -320,12 +307,8 @@ def on_test_step_finished(event) exception: create_exception_object(result, message_element) ) end - collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? - # We always want to build snippet messages in the `MessageBuilder` formatter - irrespective of config options - snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| - snippet_text(step_keyword, step_name, multiline_arg) - end - do_print_snippets(snippet_text_proc, event) unless snippets_input.empty? + + output_snippet_envelope(event) message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( @@ -339,22 +322,30 @@ def on_test_step_finished(event) output_envelope(message) end - def do_print_snippets(snippet_text_proc, event) + def output_snippet_envelope(event) + collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? + snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| + snippet_text(step_keyword, step_name, multiline_arg) + end + + message = generate_snippet_envelope(snippet_text_proc, event) unless snippets_input.empty? + output_envelope(message) + # To ensure we don't redistribute the "same" snippets over and over again + snippets_input.clear + end + + def generate_snippet_envelope(snippet_text_proc, event) snippets_array = snippets_input.map do |data| snippet_text_proc.call(data.actual_keyword, data.step.text, data.step.multiline_arg) end.uniq - message = Cucumber::Messages::Envelope.new( + Cucumber::Messages::Envelope.new( suggestion: Cucumber::Messages::Suggestion.new( id: @config.id_generator.new_id, pickle_step_id: @repository.test_step_by_id[event.test_step.id].pickle_step_id, snippets: snippets_array.map { |code_snippet| Cucumber::Messages::Snippet.new(language: 'ruby', code: code_snippet) } ) ) - - output_envelope(message) - # To ensure we don't redistribute the "same" snippets over and over again - snippets_input.clear end def on_undefined_parameter_type(event) From 372398957a7c1907e48c3a7364f22cae347c0bdd Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:27:19 +0100 Subject: [PATCH 16/26] Improve dual guard clause Only initially try run for undefined messages Only run when there are snippets --- lib/cucumber/formatter/message_builder.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 5da2c50f5..f5c4c9ec5 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -323,12 +323,16 @@ def on_test_step_finished(event) end def output_snippet_envelope(event) - collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined? + return unless event.result.undefined? + + collect_snippet_data(event.test_step, @ast_lookup) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end - message = generate_snippet_envelope(snippet_text_proc, event) unless snippets_input.empty? + return if snippets_input.empty? + + message = generate_snippet_envelope(snippet_text_proc, event) output_envelope(message) # To ensure we don't redistribute the "same" snippets over and over again snippets_input.clear From 60765e437246011734669af7d318981dd21d6861 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:28:00 +0100 Subject: [PATCH 17/26] Remove redundant guard clause. Snippets should never be empty if we have just generated at least 1 --- lib/cucumber/formatter/message_builder.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index f5c4c9ec5..f9423d4b7 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -329,9 +329,7 @@ def output_snippet_envelope(event) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end - - return if snippets_input.empty? - + message = generate_snippet_envelope(snippet_text_proc, event) output_envelope(message) # To ensure we don't redistribute the "same" snippets over and over again From f4474f1c989a25f2c9919dc21857045e8d6acea1 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 11:39:19 +0100 Subject: [PATCH 18/26] Update running paradigms for CCKv24 conformance --- compatibility/cck_spec.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 5db0fc9b4..5e90137f9 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -14,22 +14,16 @@ let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } # CCK v24 conformance - # OVERALL: 111 examples, 7 failures, 104 passed - # SANITIZED: 90 examples, 0 failures, 90 passed + # OVERALL: 111 examples, 2 failures, 109 passed + # SANITIZED: 108 examples, 0 failures, 108 passed - # Items to fix - "Suggestion" message * 5, Invalid option * 2 items_to_fix = %w[ - undefined - examples-tables-undefined - retry-undefined - unknown-parameter-type - hooks-undefined test-run-exception ] _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } - ['examples-tables-undefined'].each do |example_name| + passing.each do |example_name| describe "'#{example_name}' example" do include_examples 'cucumber compatibility kit' do let(:example) { example_name } From b7467a72742dd3ed8f604618c4b42c31a98648d9 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 12:01:04 +0100 Subject: [PATCH 19/26] Fix up message formatter feature --- features/docs/formatters/message.feature | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/features/docs/formatters/message.feature b/features/docs/formatters/message.feature index 30bfba16b..b9a8f5411 100644 --- a/features/docs/formatters/message.feature +++ b/features/docs/formatters/message.feature @@ -18,6 +18,11 @@ Feature: Message output formatter | passed | | failed | """ + And a file named "features/steps.rb" with: + """ + Given('a passed step') {} + Given('a failed step') { raise 'oops' } + """ Scenario: it produces NDJSON messages When I run `cucumber features/my_feature.feature --format message` @@ -25,6 +30,8 @@ Feature: Message output formatter And messages types should be: """ meta + stepDefinition + stepDefinition source gherkinDocument pickle @@ -44,11 +51,6 @@ Feature: Message output formatter """ Scenario: it sets "testRunFinished"."success" to false if something failed - Given a file named "features/steps.rb" with: - """ - Given('a passed step') {} - Given('a failed step') { fail } - """ When I run `cucumber features/my_feature.feature --format message` Then output should be valid NDJSON And the output should contain NDJSON with key "testRunFinished" @@ -62,10 +64,6 @@ Feature: Message output formatter Scenario Outline: a scenario Given a passed step """ - And a file named "features/steps.rb" with: - """ - Given('a passed step') {} - """ When I run `cucumber features/my_feature.feature --format message` Then output should be valid NDJSON And the output should contain NDJSON with key "testRunFinished" From cf61e5195cb1b0329819aaf388df89fbb0156ea0 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 17:44:25 +0100 Subject: [PATCH 20/26] Memoize platform constants to avoid spam warnings --- lib/cucumber/platform.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cucumber/platform.rb b/lib/cucumber/platform.rb index 8cbb73f62..e48256776 100644 --- a/lib/cucumber/platform.rb +++ b/lib/cucumber/platform.rb @@ -3,11 +3,11 @@ require 'rbconfig' module Cucumber - VERSION = File.read(File.expand_path('../../VERSION', __dir__)).strip - BINARY = File.expand_path("#{File.dirname(__FILE__)}/../../bin/cucumber") - RUBY_BINARY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) - JRUBY = defined?(JRUBY_VERSION) - WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + VERSION ||= File.read(File.expand_path('../../VERSION', __dir__)).strip + BINARY ||= File.expand_path("#{File.dirname(__FILE__)}/../../bin/cucumber") + RUBY_BINARY ||= File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) + JRUBY ||= defined?(JRUBY_VERSION) + WINDOWS ||= RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ class << self attr_writer :use_full_backtrace From 8c00fb0ca8aa67411f7a78ae4734819a70ce9f7a Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 17:47:58 +0100 Subject: [PATCH 21/26] Fix up legacy ref to output_envelope --- lib/cucumber/formatter/message_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index e863f10ec..51f101412 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -319,7 +319,7 @@ def output_snippet_envelope(event) end message = generate_snippet_envelope(snippet_text_proc, event) - output_envelope(message) + @config.event_bus.envelope(message) # To ensure we don't redistribute the "same" snippets over and over again snippets_input.clear end From f83424a58a3121adeb648949c148b110fa0952f9 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 18:06:13 +0100 Subject: [PATCH 22/26] Fix CCK namespace and paths --- compatibility/cck_spec.rb | 2 +- .../compatibility_kit}/keys_checker_spec.rb | 4 +- .../messages_comparator_spec.rb | 4 +- .../{ => cucumber}/compatibility_kit_spec.rb | 4 +- compatibility/support/cck/helpers.rb | 19 ---- compatibility/support/cck/keys_checker.rb | 62 ----------- .../support/cck/messages_comparator.rb | 102 ----------------- .../{ => cucumber}/compatibility_kit.rb | 0 .../cucumber/compatibility_kit/helpers.rb | 21 ++++ .../compatibility_kit/keys_checker.rb | 64 +++++++++++ .../compatibility_kit/messages_comparator.rb | 104 ++++++++++++++++++ compatibility/support/shared_examples.rb | 8 +- spec/cucumber/query_spec.rb | 4 +- 13 files changed, 202 insertions(+), 196 deletions(-) rename compatibility/spec/{cck => cucumber/compatibility_kit}/keys_checker_spec.rb (95%) rename compatibility/spec/{cck => cucumber/compatibility_kit}/messages_comparator_spec.rb (87%) rename compatibility/spec/{ => cucumber}/compatibility_kit_spec.rb (85%) delete mode 100644 compatibility/support/cck/helpers.rb delete mode 100644 compatibility/support/cck/keys_checker.rb delete mode 100644 compatibility/support/cck/messages_comparator.rb rename compatibility/support/{ => cucumber}/compatibility_kit.rb (100%) create mode 100644 compatibility/support/cucumber/compatibility_kit/helpers.rb create mode 100644 compatibility/support/cucumber/compatibility_kit/keys_checker.rb create mode 100644 compatibility/support/cucumber/compatibility_kit/messages_comparator.rb diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 5e90137f9..1580893ea 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'support/shared_examples' -require_relative 'support/compatibility_kit' +require_relative 'support/cucumber/compatibility_kit' require 'cucumber/compatibility_kit' diff --git a/compatibility/spec/cck/keys_checker_spec.rb b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb similarity index 95% rename from compatibility/spec/cck/keys_checker_spec.rb rename to compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb index 3af9a1699..ad22878f3 100644 --- a/compatibility/spec/cck/keys_checker_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb @@ -2,9 +2,9 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cck/keys_checker' +require_relative '../../support/cucumber/compatibility_kit/it/keys_checker' -RSpec.describe CCK::KeysChecker do +RSpec.describe Cucumber::CompatibilityKit::KeysChecker do describe '#compare' do let(:expected_kvps) { Cucumber::Messages::Attachment.new(url: 'https://foo.com', file_name: 'file.extension', test_step_id: 123_456) } let(:missing_kvps) { Cucumber::Messages::Attachment.new(url: 'https://foo.com') } diff --git a/compatibility/spec/cck/messages_comparator_spec.rb b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb similarity index 87% rename from compatibility/spec/cck/messages_comparator_spec.rb rename to compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb index 4de4bed12..0f8578629 100644 --- a/compatibility/spec/cck/messages_comparator_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb @@ -2,9 +2,9 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cck/messages_comparator' +require_relative '../../support/cucumber/compatibility_kit/messages_comparator' -RSpec.describe CCK::MessagesComparator do +RSpec.describe Cucumber::CompatibilityKit::MessagesComparator do describe '#errors' do context 'when executed as part of a CI' do before { allow(ENV).to receive(:[]).with('CI').and_return(true) } diff --git a/compatibility/spec/compatibility_kit_spec.rb b/compatibility/spec/cucumber/compatibility_kit_spec.rb similarity index 85% rename from compatibility/spec/compatibility_kit_spec.rb rename to compatibility/spec/cucumber/compatibility_kit_spec.rb index 56541c958..58899a8f0 100644 --- a/compatibility/spec/compatibility_kit_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative '../support/compatibility_kit' +require_relative '../../support/cucumber/compatibility_kit' -RSpec.describe CompatibilityKit do +RSpec.describe Cucumber::CompatibilityKit do let(:features_path) { File.expand_path("#{File.dirname(__FILE__)}/../features") } describe '.supporting_code_for' do diff --git a/compatibility/support/cck/helpers.rb b/compatibility/support/cck/helpers.rb deleted file mode 100644 index 066141813..000000000 --- a/compatibility/support/cck/helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module CCK - module Helpers - def message_type(message) - message.to_h.each do |key, value| - return key unless value.nil? - end - end - - def parse_ndjson_file(path) - parse_ndjson(File.read(path)) - end - - def parse_ndjson(ndjson) - Cucumber::Messages::Helpers::NdjsonToMessageEnumerator.new(ndjson) - end - end -end diff --git a/compatibility/support/cck/keys_checker.rb b/compatibility/support/cck/keys_checker.rb deleted file mode 100644 index 17416d6ad..000000000 --- a/compatibility/support/cck/keys_checker.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module CCK - class KeysChecker - def self.compare(detected, expected) - new(detected, expected).compare - end - - attr_reader :detected, :expected - - def initialize(detected, expected) - @detected = detected - @expected = expected - end - - def compare - return if identical_keys? - return "Detected extra keys in message #{message_name}: #{extra_keys}" if extra_keys.any? - - # TODO: Remove this override when the CCK is being checked at v29+ - return if missing_keys == [:children] - - "Missing keys in message #{message_name}: #{missing_keys}" if missing_keys.any? - rescue StandardError => e - ["Unexpected error: #{e.message}"] - end - - private - - def identical_keys? - detected_keys == expected_keys - end - - def detected_keys - @detected_keys ||= ordered_uniq_hash_keys(detected) - end - - def expected_keys - @expected_keys ||= ordered_uniq_hash_keys(expected) - end - - def ordered_uniq_hash_keys(object) - object.to_h(reject_nil_values: true).keys.sort - end - - def extra_keys - (detected_keys - expected_keys).reject { |key| meta_message? && key == :ci } - end - - def missing_keys - (expected_keys - detected_keys).reject { |key| meta_message? && key == :ci } - end - - def meta_message? - detected.instance_of?(Cucumber::Messages::Meta) - end - - def message_name - detected.class.name - end - end -end diff --git a/compatibility/support/cck/messages_comparator.rb b/compatibility/support/cck/messages_comparator.rb deleted file mode 100644 index 971af63ad..000000000 --- a/compatibility/support/cck/messages_comparator.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require_relative 'keys_checker' -require_relative 'helpers' - -module CCK - class MessagesComparator - include Helpers - - def initialize(detected, expected) - compare(detected, expected) - end - - def errors - all_errors.compact - end - - private - - def compare(detected, expected) - detected_by_type = messages_by_type(detected) - expected_by_type = messages_by_type(expected) - - detected_by_type.each_key do |type| - compare_list(detected_by_type[type], expected_by_type[type]) - rescue StandardError => e - # TODO: Remove this override when the CCK is being checked at v29+ - if e.message.include?('each_with_index') - :ignore_until_cck_v29 - else - all_errors << "Error while comparing #{type}: #{e.message}" - end - end - end - - def messages_by_type(messages) - by_type = Hash.new { |h, k| h[k] = [] } - messages.each do |msg| - by_type[message_type(msg)] << remove_envelope(msg) - end - by_type - end - - def remove_envelope(message) - message.send(message_type(message)) - end - - def compare_list(detected, expected) - detected.each_with_index do |message, index| - compare_message(message, expected[index]) - end - end - - def compare_message(detected, expected) - return if not_message?(detected) - return if ignorable?(detected) - return if incomparable?(detected) - - all_errors << CCK::KeysChecker.compare(detected, expected) - compare_sub_messages(detected, expected) - end - - def not_message?(detected) - !detected.is_a?(Cucumber::Messages::Message) - end - - # These messages need to be ignored because they are too large, or they feature timestamps which will be different - def ignorable?(detected) - too_large_message?(detected) || time_message?(detected) - end - - def too_large_message?(detected) - detected.is_a?(Cucumber::Messages::GherkinDocument) || detected.is_a?(Cucumber::Messages::Pickle) - end - - def time_message?(detected) - detected.is_a?(Cucumber::Messages::Timestamp) || detected.is_a?(Cucumber::Messages::Duration) - end - - # These messages need to be ignored because they are often not of identical shape - def incomparable?(detected) - detected.is_a?(Cucumber::Messages::Ci) || detected.is_a?(Cucumber::Messages::Git) - end - - def compare_sub_messages(detected, expected) - return unless expected.respond_to? :to_h - - expected.to_h.each_key do |key| - value = expected.send(key) - if value.is_a?(Array) - compare_list(detected.send(key), value) - else - compare_message(detected.send(key), value) - end - end - end - - def all_errors - @all_errors ||= [] - end - end -end diff --git a/compatibility/support/compatibility_kit.rb b/compatibility/support/cucumber/compatibility_kit.rb similarity index 100% rename from compatibility/support/compatibility_kit.rb rename to compatibility/support/cucumber/compatibility_kit.rb diff --git a/compatibility/support/cucumber/compatibility_kit/helpers.rb b/compatibility/support/cucumber/compatibility_kit/helpers.rb new file mode 100644 index 000000000..f7d9a8d98 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/helpers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Cucumber + module CompatibilityKit + module Helpers + def message_type(message) + message.to_h.each do |key, value| + return key unless value.nil? + end + end + + def parse_ndjson_file(path) + parse_ndjson(File.read(path)) + end + + def parse_ndjson(ndjson) + Cucumber::Messages::Helpers::NdjsonToMessageEnumerator.new(ndjson) + end + end + end +end diff --git a/compatibility/support/cucumber/compatibility_kit/keys_checker.rb b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb new file mode 100644 index 000000000..17a96a4c6 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Cucumber + module CompatibilityKit + class KeysChecker + def self.compare(detected, expected) + new(detected, expected).compare + end + + attr_reader :detected, :expected + + def initialize(detected, expected) + @detected = detected + @expected = expected + end + + def compare + return if identical_keys? + return "Detected extra keys in message #{message_name}: #{extra_keys}" if extra_keys.any? + + # TODO: Remove this override when the CCK is being checked at v29+ + return if missing_keys == [:children] + + "Missing keys in message #{message_name}: #{missing_keys}" if missing_keys.any? + rescue StandardError => e + ["Unexpected error: #{e.message}"] + end + + private + + def identical_keys? + detected_keys == expected_keys + end + + def detected_keys + @detected_keys ||= ordered_uniq_hash_keys(detected) + end + + def expected_keys + @expected_keys ||= ordered_uniq_hash_keys(expected) + end + + def ordered_uniq_hash_keys(object) + object.to_h(reject_nil_values: true).keys.sort + end + + def extra_keys + (detected_keys - expected_keys).reject { |key| meta_message? && key == :ci } + end + + def missing_keys + (expected_keys - detected_keys).reject { |key| meta_message? && key == :ci } + end + + def meta_message? + detected.instance_of?(Cucumber::Messages::Meta) + end + + def message_name + detected.class.name + end + end + end +end diff --git a/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb new file mode 100644 index 000000000..7a0baeda5 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative 'keys_checker' +require_relative 'helpers' + +module Cucumber + module CompatibilityKit + class MessagesComparator + include Helpers + + def initialize(detected, expected) + compare(detected, expected) + end + + def errors + all_errors.compact + end + + private + + def compare(detected, expected) + detected_by_type = messages_by_type(detected) + expected_by_type = messages_by_type(expected) + + detected_by_type.each_key do |type| + compare_list(detected_by_type[type], expected_by_type[type]) + rescue StandardError => e + # TODO: Remove this override when the CCK is being checked at v29+ + if e.message.include?('each_with_index') + :ignore_until_cck_v29 + else + all_errors << "Error while comparing #{type}: #{e.message}" + end + end + end + + def messages_by_type(messages) + by_type = Hash.new { |h, k| h[k] = [] } + messages.each do |msg| + by_type[message_type(msg)] << remove_envelope(msg) + end + by_type + end + + def remove_envelope(message) + message.send(message_type(message)) + end + + def compare_list(detected, expected) + detected.each_with_index do |message, index| + compare_message(message, expected[index]) + end + end + + def compare_message(detected, expected) + return if not_message?(detected) + return if ignorable?(detected) + return if incomparable?(detected) + + all_errors << CCK::KeysChecker.compare(detected, expected) + compare_sub_messages(detected, expected) + end + + def not_message?(detected) + !detected.is_a?(Cucumber::Messages::Message) + end + + # These messages need to be ignored because they are too large, or they feature timestamps which will be different + def ignorable?(detected) + too_large_message?(detected) || time_message?(detected) + end + + def too_large_message?(detected) + detected.is_a?(Cucumber::Messages::GherkinDocument) || detected.is_a?(Cucumber::Messages::Pickle) + end + + def time_message?(detected) + detected.is_a?(Cucumber::Messages::Timestamp) || detected.is_a?(Cucumber::Messages::Duration) + end + + # These messages need to be ignored because they are often not of identical shape + def incomparable?(detected) + detected.is_a?(Cucumber::Messages::Ci) || detected.is_a?(Cucumber::Messages::Git) + end + + def compare_sub_messages(detected, expected) + return unless expected.respond_to? :to_h + + expected.to_h.each_key do |key| + value = expected.send(key) + if value.is_a?(Array) + compare_list(detected.send(key), value) + else + compare_message(detected.send(key), value) + end + end + end + + def all_errors + @all_errors ||= [] + end + end + end +end diff --git a/compatibility/support/shared_examples.rb b/compatibility/support/shared_examples.rb index eac666387..bf9526ba7 100644 --- a/compatibility/support/shared_examples.rb +++ b/compatibility/support/shared_examples.rb @@ -4,11 +4,11 @@ require 'rspec' require 'cucumber/messages' -require_relative 'cck/helpers' -require_relative 'cck/messages_comparator' +require_relative 'cucumber/compatibility_kit/helpers' +require_relative 'cucumber/compatibility_kit/messages_comparator' RSpec.shared_examples 'cucumber compatibility kit' do - include CCK::Helpers + include Cucumber::CompatibilityKit::Helpers let(:cck_path) { Cucumber::CompatibilityKit.feature_code_for(example) } @@ -23,7 +23,7 @@ end it 'generates valid message structure' do - comparator = CCK::MessagesComparator.new(parsed_generated, parsed_original) + comparator = Cucumber::CompatibilityKit::MessagesComparator.new(parsed_generated, parsed_original) expect(comparator.errors).to be_empty, "There were comparison errors: #{comparator.errors}" end diff --git a/spec/cucumber/query_spec.rb b/spec/cucumber/query_spec.rb index ab2aeba08..e1ecf5d12 100644 --- a/spec/cucumber/query_spec.rb +++ b/spec/cucumber/query_spec.rb @@ -40,10 +40,10 @@ def list_of_tests end end -require_relative '../../compatibility/support/cck/helpers' +require_relative '../../compatibility/support/cucumber/compatibility_kit/helpers' RSpec.describe Cucumber::Query do - include CCK::Helpers + include Cucumber::CompatibilityKit::Helpers subject(:query) { described_class.new(repository) } From 1f814107e9ae9ae30e3ec949c05e2c8e965c4930 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 18:14:49 +0100 Subject: [PATCH 23/26] Remove all erroneous namespaces out; --- compatibility/cck_spec.rb | 2 +- .../spec/cucumber/compatibility_kit/keys_checker_spec.rb | 3 ++- .../cucumber/compatibility_kit/messages_comparator_spec.rb | 3 ++- compatibility/spec/cucumber/compatibility_kit_spec.rb | 2 +- compatibility/support/cucumber/compatibility_kit.rb | 7 ++++++- .../support/cucumber/compatibility_kit/helpers.rb | 2 +- .../support/cucumber/compatibility_kit/keys_checker.rb | 2 +- .../cucumber/compatibility_kit/messages_comparator.rb | 4 ++-- 8 files changed, 16 insertions(+), 9 deletions(-) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 1580893ea..e4a1f620e 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -10,7 +10,7 @@ # # All step definition and required supporting logic is contained here, the CCK gem proper contains the source of truth # of the "golden" NDJSON files and attachments / miscellaneous files -RSpec.describe CCK, :cck do +RSpec.describe 'CCK', :cck do let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } # CCK v24 conformance diff --git a/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb index ad22878f3..b57007fb7 100644 --- a/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb @@ -2,7 +2,8 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cucumber/compatibility_kit/it/keys_checker' + +require_relative '../../../support/cucumber/compatibility_kit' RSpec.describe Cucumber::CompatibilityKit::KeysChecker do describe '#compare' do diff --git a/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb index 0f8578629..b4ce9333f 100644 --- a/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb @@ -2,7 +2,8 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cucumber/compatibility_kit/messages_comparator' + +require_relative '../../../support/cucumber/compatibility_kit' RSpec.describe Cucumber::CompatibilityKit::MessagesComparator do describe '#errors' do diff --git a/compatibility/spec/cucumber/compatibility_kit_spec.rb b/compatibility/spec/cucumber/compatibility_kit_spec.rb index 58899a8f0..e57d359e1 100644 --- a/compatibility/spec/cucumber/compatibility_kit_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit_spec.rb @@ -3,7 +3,7 @@ require_relative '../../support/cucumber/compatibility_kit' RSpec.describe Cucumber::CompatibilityKit do - let(:features_path) { File.expand_path("#{File.dirname(__FILE__)}/../features") } + let(:features_path) { File.expand_path("#{File.dirname(__FILE__)}/../../features") } describe '.supporting_code_for' do context 'with an example that exists' do diff --git a/compatibility/support/cucumber/compatibility_kit.rb b/compatibility/support/cucumber/compatibility_kit.rb index 1cca7addf..a3004d445 100644 --- a/compatibility/support/cucumber/compatibility_kit.rb +++ b/compatibility/support/cucumber/compatibility_kit.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +require_relative 'compatibility_kit/helpers' + +require_relative 'compatibility_kit/keys_checker' +require_relative 'compatibility_kit/messages_comparator' + module Cucumber class CompatibilityKit class << self @@ -14,7 +19,7 @@ def supporting_code_for(example_name) private def local_features_folder_location - File.expand_path("#{File.dirname(__FILE__)}/../features/") + File.expand_path("#{File.dirname(__FILE__)}/../../features/") end end end diff --git a/compatibility/support/cucumber/compatibility_kit/helpers.rb b/compatibility/support/cucumber/compatibility_kit/helpers.rb index f7d9a8d98..558917422 100644 --- a/compatibility/support/cucumber/compatibility_kit/helpers.rb +++ b/compatibility/support/cucumber/compatibility_kit/helpers.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Cucumber - module CompatibilityKit + class CompatibilityKit module Helpers def message_type(message) message.to_h.each do |key, value| diff --git a/compatibility/support/cucumber/compatibility_kit/keys_checker.rb b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb index 17a96a4c6..db821e3e6 100644 --- a/compatibility/support/cucumber/compatibility_kit/keys_checker.rb +++ b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Cucumber - module CompatibilityKit + class CompatibilityKit class KeysChecker def self.compare(detected, expected) new(detected, expected).compare diff --git a/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb index 7a0baeda5..a47c1e68b 100644 --- a/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb +++ b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb @@ -4,7 +4,7 @@ require_relative 'helpers' module Cucumber - module CompatibilityKit + class CompatibilityKit class MessagesComparator include Helpers @@ -57,7 +57,7 @@ def compare_message(detected, expected) return if ignorable?(detected) return if incomparable?(detected) - all_errors << CCK::KeysChecker.compare(detected, expected) + all_errors << Cucumber::CompatibilityKit::KeysChecker.compare(detected, expected) compare_sub_messages(detected, expected) end From 952ed13064ccb49ad53eea6813457b86e4f42fb1 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 18:16:12 +0100 Subject: [PATCH 24/26] Fix up rubocop --- lib/cucumber/formatter/message_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 51f101412..ffc48d57e 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -317,7 +317,7 @@ def output_snippet_envelope(event) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end - + message = generate_snippet_envelope(snippet_text_proc, event) @config.event_bus.envelope(message) # To ensure we don't redistribute the "same" snippets over and over again From 0609cdee2edaad54b084efa9ef76e12b464669fe Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Wed, 27 May 2026 08:35:51 +0100 Subject: [PATCH 25/26] Undo memoization guard --- lib/cucumber/platform.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cucumber/platform.rb b/lib/cucumber/platform.rb index e48256776..8cbb73f62 100644 --- a/lib/cucumber/platform.rb +++ b/lib/cucumber/platform.rb @@ -3,11 +3,11 @@ require 'rbconfig' module Cucumber - VERSION ||= File.read(File.expand_path('../../VERSION', __dir__)).strip - BINARY ||= File.expand_path("#{File.dirname(__FILE__)}/../../bin/cucumber") - RUBY_BINARY ||= File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) - JRUBY ||= defined?(JRUBY_VERSION) - WINDOWS ||= RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + VERSION = File.read(File.expand_path('../../VERSION', __dir__)).strip + BINARY = File.expand_path("#{File.dirname(__FILE__)}/../../bin/cucumber") + RUBY_BINARY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) + JRUBY = defined?(JRUBY_VERSION) + WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ class << self attr_writer :use_full_backtrace From 5b469c7f300572f7cf5ebc37ff6b06cf1c7e1ae9 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Wed, 27 May 2026 08:39:08 +0100 Subject: [PATCH 26/26] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b358b600..248b0f993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo ## [Unreleased] ### Added - Print thread backtraces on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) +- Added `Suggestion` messages that will show all the snippets for all message based formatters ([#1870](https://github.com/cucumber/cucumber-ruby/pull/1870)) [luke-hill](https://github.com/luke-hill) ### Changed - Heavy refactor to the internals for message building (Used in formatters - should be no noticeable change) ([#1853](https://github.com/cucumber/cucumber-ruby/pull/1853) [luke-hill](https://github.com/luke-hill)) - Altered the concept of how `BeforeAll` and `AfterAll` hooks would run. They now attempt to all run before continuing test execution ([#1857](https://github.com/cucumber/cucumber-ruby/pull/1857) [brasmusson](https://github.com/brasmusson)) - Internal refactor to `MessageBuilder` class to send envelopes through event bus (Should be no noticeable change) +- Updated `cucumber-compatibility-kit` to v24 ## [11.0.0] - 2026-04-14 ### Added @@ -36,7 +38,6 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo > The `rerun` formatter was chosen as the first formatter to migrate to this new structure as it is one of the simpler > formatters and will allow us to test the new structure in a real-world scenario. - Updated `cucumber-compatibility-kit` to v22 -- Security: Switched out `IO.read` for more secure `File.read` in a few areas of the codebase - Implemented the new cucumber-query structure in all message based formatters (Currently HTML / Rerun and Message) ([#1844](https://github.com/cucumber/cucumber-ruby/pull/1844) [luke-hill](https://github.com/luke-hill)) @@ -49,6 +50,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo - Fixed an issue where NoMethodError could be raised when declaring a parameter-type that used bound methods ([#1789](https://github.com/cucumber/cucumber-ruby/pull/1789)) +### Security +- Switched out `IO.read` for more secure `File.read` in a few areas of the codebase + ## [10.2.0] - 2025-12-10 ### Changed - Permit the latest version of the `cucumber-html-formatter` (v22.0.0+)