From 2cde0dc30f0cd7770ecf7252a0bed83fd0cb1bdb Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:16:51 +0100 Subject: [PATCH 1/8] Add readers for fake query objects that are being isolated (#1852) --- lib/cucumber/formatter/query/hook_by_test_step.rb | 2 ++ lib/cucumber/formatter/query/pickle_by_test.rb | 2 ++ lib/cucumber/formatter/query/step_definitions_by_test_step.rb | 2 ++ lib/cucumber/formatter/query/test_case_started_by_test_case.rb | 2 ++ 4 files changed, 8 insertions(+) diff --git a/lib/cucumber/formatter/query/hook_by_test_step.rb b/lib/cucumber/formatter/query/hook_by_test_step.rb index dd1be34c2..0aebc7640 100644 --- a/lib/cucumber/formatter/query/hook_by_test_step.rb +++ b/lib/cucumber/formatter/query/hook_by_test_step.rb @@ -6,6 +6,8 @@ module Cucumber module Formatter module Query class HookByTestStep + attr_reader :hook_id_by_test_step_id + def initialize(config) @hook_id_by_test_step_id = {} diff --git a/lib/cucumber/formatter/query/pickle_by_test.rb b/lib/cucumber/formatter/query/pickle_by_test.rb index 304fb4939..3cee6e5f6 100644 --- a/lib/cucumber/formatter/query/pickle_by_test.rb +++ b/lib/cucumber/formatter/query/pickle_by_test.rb @@ -6,6 +6,8 @@ module Cucumber module Formatter module Query class PickleByTest + attr_reader :pickle_id_by_test_case_id + def initialize(config) @pickle_id_by_test_case_id = {} config.on_event :test_case_created, &method(:on_test_case_created) diff --git a/lib/cucumber/formatter/query/step_definitions_by_test_step.rb b/lib/cucumber/formatter/query/step_definitions_by_test_step.rb index 833b78620..4da3cbd5c 100644 --- a/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +++ b/lib/cucumber/formatter/query/step_definitions_by_test_step.rb @@ -6,6 +6,8 @@ module Cucumber module Formatter module Query class StepDefinitionsByTestStep + attr_reader :step_definition_ids_by_test_step_id, :step_match_arguments_by_test_step_id + def initialize(config) @step_definition_ids_by_test_step_id = {} @step_match_arguments_by_test_step_id = {} diff --git a/lib/cucumber/formatter/query/test_case_started_by_test_case.rb b/lib/cucumber/formatter/query/test_case_started_by_test_case.rb index f3fcdbde4..b657121ba 100644 --- a/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +++ b/lib/cucumber/formatter/query/test_case_started_by_test_case.rb @@ -6,6 +6,8 @@ module Cucumber module Formatter module Query class TestCaseStartedByTestCase + attr_reader :attempts_by_test_case_id, :test_case_started_id_by_test_case_id + def initialize(config) @config = config config.on_event :test_case_created, &method(:on_test_case_created) From 28e168063cd25d565919c67b90c712fdedaf00db Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:29:03 +0100 Subject: [PATCH 2/8] Removed a couple more of the fake queries (#1856) --- lib/cucumber/formatter/message_builder.rb | 35 +++++-- .../formatter/query/hook_by_test_step.rb | 36 ------- .../formatter/query/pickle_by_test.rb | 30 ------ .../formatter/query/hook_by_test_step_spec.rb | 93 ------------------- .../formatter/query/pickle_by_test_spec.rb | 63 ------------- 5 files changed, 26 insertions(+), 231 deletions(-) delete mode 100644 lib/cucumber/formatter/query/hook_by_test_step.rb delete mode 100644 lib/cucumber/formatter/query/pickle_by_test.rb delete mode 100644 spec/cucumber/formatter/query/hook_by_test_step_spec.rb delete mode 100644 spec/cucumber/formatter/query/pickle_by_test_spec.rb diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 3eaa5b7a6..635f45f0a 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -2,8 +2,6 @@ require 'base64' require 'cucumber/formatter/backtrace_filter' -require 'cucumber/formatter/query/hook_by_test_step' -require 'cucumber/formatter/query/pickle_by_test' require 'cucumber/formatter/query/step_definitions_by_test_step' require 'cucumber/formatter/query/test_case_started_by_test_case' @@ -18,8 +16,6 @@ class MessageBuilder def initialize(config) @config = config - @hook_by_test_step = Query::HookByTestStep.new(config) - @pickle_by_test = Query::PickleByTest.new(config) @step_definitions_by_test_step = Query::StepDefinitionsByTestStep.new(config) @test_case_started_by_test_case = Query::TestCaseStartedByTestCase.new(config) @@ -27,8 +23,12 @@ def initialize(config) @query = Cucumber::Query.new(@repository) @test_run_started_id = config.id_generator.new_id + + # Fake Query objects @test_case_by_step_id = {} + @pickle_id_by_test_case_id = {} @pickle_id_step_by_test_step_id = {} + @hook_id_by_test_step_id = {} # Ensure all handlers for events occur after all ivars are instantiated @@ -42,7 +42,7 @@ def initialize(config) config.on_event :step_activated, &method(:on_step_activated) config.on_event :step_definition_registered, &method(:on_step_definition_registered) - # TODO: Handle TestCaseCreated + config.on_event :test_case_created, &method(:on_test_case_created) config.on_event :test_case_ready, &method(:on_test_case_ready) config.on_event :test_case_started, &method(:on_test_case_started) config.on_event :test_case_finished, &method(:on_test_case_finished) @@ -104,15 +104,19 @@ def on_gherkin_source_read(event) output_envelope(message) end - def on_hook_test_step_created(_event) - # TODO: Handle HookTestStepCreated + def on_hook_test_step_created(event) + @hook_id_by_test_step_id[event.test_step.id] = event.hook.id + end + + def on_test_case_created(event) + @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id end def on_test_case_ready(event) message = Cucumber::Messages::Envelope.new( test_case: Cucumber::Messages::TestCase.new( id: event.test_case.id, - pickle_id: @pickle_by_test.pickle_id(event.test_case), + pickle_id: fake_query_pickle_id(event.test_case), test_steps: event.test_case.test_steps.map { |step| test_step_to_message(step) }, test_run_started_id: @test_run_started_id ) @@ -146,7 +150,7 @@ def test_step_to_message(step) def hook_step_to_message(step) Cucumber::Messages::TestStep.new( id: step.id, - hook_id: @hook_by_test_step.hook_id(step) + hook_id: fake_query_hook_id(step) ) end @@ -209,6 +213,7 @@ def on_test_case_started(event) def on_test_step_created(event) @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id test_step_to_message(event.test_step) + @hook_id_by_test_step_id[event.test_step.id] = nil # TODO: We need to determine what message to output here (Unsure - Placeholder added) # message = Cucumber::Messages::Envelope.new( # pickle: { @@ -378,6 +383,18 @@ def on_undefined_parameter_type(event) def test_case_started_id(test_case) @test_case_started_by_test_case.test_case_started_id_by_test_case(test_case) end + + def fake_query_hook_id(test_step) + return @hook_id_by_test_step_id[test_step.id] if @hook_id_by_test_step_id.key?(test_step.id) + + raise TestStepUnknownError, "No hook found for #{test_step.id} }. Known: #{@hook_id_by_test_step_id.keys}" + end + + def fake_query_pickle_id(test_case) + return @pickle_id_by_test_case_id[test_case.id] if @pickle_id_by_test_case_id.key?(test_case.id) + + raise TestCaseUnknownError, "No pickle found for #{test_case.id} }. Known: #{@pickle_id_by_test_case_id.keys}" + end end end end diff --git a/lib/cucumber/formatter/query/hook_by_test_step.rb b/lib/cucumber/formatter/query/hook_by_test_step.rb deleted file mode 100644 index 0aebc7640..000000000 --- a/lib/cucumber/formatter/query/hook_by_test_step.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/errors' - -module Cucumber - module Formatter - module Query - class HookByTestStep - attr_reader :hook_id_by_test_step_id - - def initialize(config) - @hook_id_by_test_step_id = {} - - config.on_event :test_step_created, &method(:on_test_step_created) - config.on_event :hook_test_step_created, &method(:on_hook_test_step_created) - end - - def hook_id(test_step) - return @hook_id_by_test_step_id[test_step.id] if @hook_id_by_test_step_id.key?(test_step.id) - - raise TestStepUnknownError, "No hook found for #{test_step.id} }. Known: #{@hook_id_by_test_step_id.keys}" - end - - private - - def on_test_step_created(event) - @hook_id_by_test_step_id[event.test_step.id] = nil - end - - def on_hook_test_step_created(event) - @hook_id_by_test_step_id[event.test_step.id] = event.hook.id - end - end - end - end -end diff --git a/lib/cucumber/formatter/query/pickle_by_test.rb b/lib/cucumber/formatter/query/pickle_by_test.rb deleted file mode 100644 index 3cee6e5f6..000000000 --- a/lib/cucumber/formatter/query/pickle_by_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/errors' - -module Cucumber - module Formatter - module Query - class PickleByTest - attr_reader :pickle_id_by_test_case_id - - def initialize(config) - @pickle_id_by_test_case_id = {} - config.on_event :test_case_created, &method(:on_test_case_created) - end - - def pickle_id(test_case) - return @pickle_id_by_test_case_id[test_case.id] if @pickle_id_by_test_case_id.key?(test_case.id) - - raise TestCaseUnknownError, "No pickle found for #{test_case.id} }. Known: #{@pickle_id_by_test_case_id.keys}" - end - - private - - def on_test_case_created(event) - @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id - end - end - end - end -end diff --git a/spec/cucumber/formatter/query/hook_by_test_step_spec.rb b/spec/cucumber/formatter/query/hook_by_test_step_spec.rb deleted file mode 100644 index 840c134aa..000000000 --- a/spec/cucumber/formatter/query/hook_by_test_step_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/spec_helper' -require 'cucumber/formatter/query/hook_by_test_step' - -RSpec.describe Cucumber::Formatter::Query::HookByTestStep do - extend Cucumber::Formatter::SpecHelperDsl - include Cucumber::Formatter::SpecHelper - - before(:each) do - Cucumber::Term::ANSIColor.coloring = false - @test_cases = [] - - @out = StringIO.new - @config = actual_runtime.configuration.with_options(out_stream: @out) - @formatter = described_class.new(@config) - - @config.on_event :test_case_started do |event| - @test_cases << event.test_case - end - - @hook_ids = [] - @config.on_event :envelope do |event| - next unless event.envelope.hook - - @hook_ids << event.envelope.hook.id - end - end - - context 'given a single feature' do - before(:each) do - run_defined_feature - end - - context 'with a scenario' do - describe '#pickle_step_id' do - define_feature <<-FEATURE - Feature: Banana party - - Scenario: Monkey eats banana - Given there are bananas - FEATURE - - define_steps do - Before() {} - After() {} - end - - it 'provides the ID of the Before Hook used to generate the Test::Step' do - test_case = @test_cases.first - expect(@formatter.hook_id(test_case.test_steps.first)).to eq(@hook_ids.first) - end - - it 'provides the ID of the After Hook used to generate the Test::Step' do - test_case = @test_cases.first - expect(@formatter.hook_id(test_case.test_steps.last)).to eq(@hook_ids.last) - end - - it 'returns nil if the step was not generated from a hook' do - test_case = @test_cases.first - expect(@formatter.hook_id(test_case.test_steps[1])).to be_nil - end - - it 'raises an exception when the test_step is unknown' do - test_step = double - allow(test_step).to receive(:id).and_return('whatever-id') - - expect { @formatter.hook_id(test_step) }.to raise_error(Cucumber::Formatter::TestStepUnknownError) - end - end - end - - context 'with AfterStep hooks' do - describe '#pickle_step_id' do - define_feature <<-FEATURE - Feature: Banana party - - Scenario: Monkey eats banana - Given there are bananas - FEATURE - - define_steps do - AfterStep() {} - end - - it 'provides the ID of the AfterStepHook used to generate the Test::Step' do - test_case = @test_cases.first - expect(@formatter.hook_id(test_case.test_steps.last)).to eq(@hook_ids.first) - end - end - end - end -end diff --git a/spec/cucumber/formatter/query/pickle_by_test_spec.rb b/spec/cucumber/formatter/query/pickle_by_test_spec.rb deleted file mode 100644 index 2cdf95b9e..000000000 --- a/spec/cucumber/formatter/query/pickle_by_test_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/spec_helper' -require 'cucumber/formatter/query/pickle_by_test' - -module Cucumber - module Formatter - module Query - RSpec.describe PickleByTest do - extend SpecHelperDsl - include SpecHelper - - before(:each) do - Cucumber::Term::ANSIColor.coloring = false - @test_cases = [] - - @out = StringIO.new - @config = actual_runtime.configuration.with_options(out_stream: @out) - @formatter = described_class.new(@config) - - @config.on_event :test_case_created do |event| - @test_cases << event.test_case - end - - @pickle_ids = [] - @config.on_event :envelope do |event| - next unless event.envelope.pickle - - @pickle_ids << event.envelope.pickle.id - end - end - - describe 'given a single feature' do - before(:each) do - run_defined_feature - end - - describe 'with a scenario' do - describe '#pickle_id' do - define_feature <<-FEATURE - Feature: Banana party - - Scenario: Monkey eats banana - Given there are bananas - FEATURE - - it 'provides the ID of the pickle used to generate the Test::Case' do - expect(@formatter.pickle_id(@test_cases.first)).to eq(@pickle_ids.first) - end - - it 'raises an error when the Test::Case is unknown' do - test_case = double - allow(test_case).to receive(:id).and_return('whatever-id') - - expect { @formatter.pickle_id(test_case) }.to raise_error(Cucumber::Formatter::TestCaseUnknownError) - end - end - end - end - end - end - end -end From 7305926a7ad086717953e9236b92a69698dae06d Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:39:39 +0100 Subject: [PATCH 3/8] V11/refactor out test steps fake (#1855) * Replace the 2 instances of `@test_case_by_step_id` * Add note for iVar removal * Avoid fully the use of the fake query private method by relying entirely on the repository data; * Add redudancy warnings; * Remove redundancies * Tidy up code * Revert minor change to make tests pass * Fix up naming conventions * Add notes / trace for where to look next to try triage * WIP always select the test case started message with the largest attempt counter when multiple exist * Remove all placeholders --- lib/cucumber/formatter/message_builder.rb | 34 ++++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 635f45f0a..033a5421f 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -125,12 +125,6 @@ def on_test_case_ready(event) # TODO: This may be a redundant update. But for now we're leaving this in whilst we're in the transitory phase @repository.update(message) - # TODO: Switch this over to using the Repo Query object -> `test_step_by_id` - # TODO: The finder in query is `find_test_step_by` (Using +TestStepStarted+ message) - event.test_case.test_steps.each do |step| - @test_case_by_step_id[step.id] = event.test_case - end - output_envelope(message) end @@ -233,12 +227,21 @@ def on_test_step_created(event) def on_test_step_started(event) @current_test_step_id = event.test_step.id - test_case = @test_case_by_step_id[event.test_step.id] + find_test_case_by_step_id = + @repository.test_case_by_id + .values + .detect { |test_case_message| test_case_message.test_steps.any? { |step_message| step_message.id == event.test_step.id } } + + find_test_case_started_by_test_case = + @repository.test_case_started_by_id + .values + .select { |test_case_started_message| test_case_started_message.test_case_id == find_test_case_by_step_id.id } + .max_by(&:attempt) message = Cucumber::Messages::Envelope.new( test_step_started: Cucumber::Messages::TestStepStarted.new( test_step_id: event.test_step.id, - test_case_started_id: test_case_started_id(test_case), + test_case_started_id: find_test_case_started_by_test_case.id, timestamp: time_to_timestamp(Time.now) ) ) @@ -247,7 +250,17 @@ def on_test_step_started(event) end def on_test_step_finished(event) - test_case = @test_case_by_step_id[event.test_step.id] + find_test_case_by_step_id = + @repository.test_case_by_id + .values + .detect { |test_case_message| test_case_message.test_steps.any? { |step_message| step_message.id == event.test_step.id } } + + find_test_case_started_by_test_case = + @repository.test_case_started_by_id + .values + .select { |test_case_started_message| test_case_started_message.test_case_id == find_test_case_by_step_id.id } + .max_by(&:attempt) + result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter) result_message = result.to_message @@ -265,7 +278,7 @@ def on_test_step_finished(event) message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( test_step_id: event.test_step.id, - test_case_started_id: test_case_started_id(test_case), + test_case_started_id: find_test_case_started_by_test_case.id, test_step_result: result_message, timestamp: time_to_timestamp(Time.now) ) @@ -380,6 +393,7 @@ def on_undefined_parameter_type(event) output_envelope(message) end + # TODO: This is used in 3 locations def test_case_started_id(test_case) @test_case_started_by_test_case.test_case_started_id_by_test_case(test_case) end From e9cc1c888f0b276a03d123ee18335f85fe5b279e Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:08:20 +0100 Subject: [PATCH 4/8] v11/Remove Fake Query -> `Query::TestCaseStartedByTestCase` (#1854) * Add fake query constant to generate new way of calculating attempts * Switch attempts and initial test case started over to new system, commenting out old code * Cleanup of boilerplate placeholder code * Remove fake query and spec for test_case_started_by_test_case * WIP * WIP 2 * Remove duplicate method call that should be :no_op * Refactor and remove fake query step that can be a :no_op * Switch out the test_case_started_id method to use the refactored one from previous PR * For test case started events, we always need to ensure the current test case started id is newly generated and not fetched * Add note for new id generation --- lib/cucumber/formatter/message_builder.rb | 30 +++++-- .../query/test_case_started_by_test_case.rb | 47 ---------- .../test_case_started_by_test_case_spec.rb | 88 ------------------- spec/support/fake_objects.rb | 2 +- 4 files changed, 22 insertions(+), 145 deletions(-) delete mode 100644 lib/cucumber/formatter/query/test_case_started_by_test_case.rb delete mode 100644 spec/cucumber/formatter/query/test_case_started_by_test_case_spec.rb diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 033a5421f..c9664c774 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -3,7 +3,6 @@ require 'base64' require 'cucumber/formatter/backtrace_filter' require 'cucumber/formatter/query/step_definitions_by_test_step' -require 'cucumber/formatter/query/test_case_started_by_test_case' require 'cucumber/query' @@ -17,7 +16,6 @@ def initialize(config) @config = config @step_definitions_by_test_step = Query::StepDefinitionsByTestStep.new(config) - @test_case_started_by_test_case = Query::TestCaseStartedByTestCase.new(config) @repository = Cucumber::Repository.new @query = Cucumber::Query.new(@repository) @@ -190,14 +188,24 @@ def on_test_run_started(*) end def on_test_case_started(event) - @current_test_case_started_id = test_case_started_id(event.test_case) + # For any new test_case_started events, we must ALWAYS generate a new id for a new run + @current_test_case_started_id = @config.id_generator.new_id + + # Query missing: `#find_all_test_case_started_by_test_case_id` + find_all_test_case_started_by_test_case_id = + @repository.test_case_started_by_id + .values + .select { |test_case_started| test_case_started.test_case_id == event.test_case.id } + + # If no TestCaseStarted messages exist. We must be on attempt 1 (Hence the .to_i casting for a `nil` value) + attempts_previously_made = find_all_test_case_started_by_test_case_id.map(&:attempt).max.to_i message = Cucumber::Messages::Envelope.new( test_case_started: Cucumber::Messages::TestCaseStarted.new( - id: test_case_started_id(event.test_case), + id: @current_test_case_started_id, test_case_id: event.test_case.id, timestamp: time_to_timestamp(Time.now), - attempt: @test_case_started_by_test_case.attempt_by_test_case(event.test_case) + attempt: attempts_previously_made + 1 ) ) @@ -207,7 +215,7 @@ def on_test_case_started(event) def on_test_step_created(event) @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id test_step_to_message(event.test_step) - @hook_id_by_test_step_id[event.test_step.id] = nil + :no_op # TODO: We need to determine what message to output here (Unsure - Placeholder added) # message = Cucumber::Messages::Envelope.new( # pickle: { @@ -308,7 +316,6 @@ def on_test_case_finished(event) test_case_started_id = test_case_started_id(event.test_case) test_case_started_message = @repository.test_case_started_by_id[test_case_started_id] max_attempts = @config.retry_attempts - # See "fake query" for reason this is index shifted retries_attempted = test_case_started_message.attempt - 1 will_be_retried = event.result.failed? && (retries_attempted < max_attempts) @@ -393,9 +400,14 @@ def on_undefined_parameter_type(event) output_envelope(message) end - # TODO: This is used in 3 locations def test_case_started_id(test_case) - @test_case_started_by_test_case.test_case_started_id_by_test_case(test_case) + test_case_started = + @repository.test_case_started_by_id + .values + .select { |test_case_started_message| test_case_started_message.test_case_id == test_case.id } + .max_by(&:attempt) + + test_case_started&.id || @config.id_generator.new_id end def fake_query_hook_id(test_step) diff --git a/lib/cucumber/formatter/query/test_case_started_by_test_case.rb b/lib/cucumber/formatter/query/test_case_started_by_test_case.rb deleted file mode 100644 index b657121ba..000000000 --- a/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/errors' - -module Cucumber - module Formatter - module Query - class TestCaseStartedByTestCase - attr_reader :attempts_by_test_case_id, :test_case_started_id_by_test_case_id - - def initialize(config) - @config = config - config.on_event :test_case_created, &method(:on_test_case_created) - config.on_event :test_case_started, &method(:on_test_case_started) - - @attempts_by_test_case_id = {} - @test_case_started_id_by_test_case_id = {} - end - - def attempt_by_test_case(test_case) - raise TestCaseUnknownError, "No test case found for #{test_case.id} }. Known: #{@attempts_by_test_case_id.keys}" unless @attempts_by_test_case_id.key?(test_case.id) - - @attempts_by_test_case_id[test_case.id] - end - - def test_case_started_id_by_test_case(test_case) - raise TestCaseUnknownError, "No test case found for #{test_case.id} }. Known: #{@test_case_started_id_by_test_case_id.keys}" unless @test_case_started_id_by_test_case_id.key?(test_case.id) - - @test_case_started_id_by_test_case_id[test_case.id] - end - - private - - def on_test_case_created(event) - @attempts_by_test_case_id[event.test_case.id] = 0 - @test_case_started_id_by_test_case_id[event.test_case.id] = nil - end - - def on_test_case_started(event) - # TODO: LH - Apr '26 -> Clarify if this should start with attempt 1 or attempt 0 - @attempts_by_test_case_id[event.test_case.id] += 1 - @test_case_started_id_by_test_case_id[event.test_case.id] = @config.id_generator.new_id - end - end - end - end -end diff --git a/spec/cucumber/formatter/query/test_case_started_by_test_case_spec.rb b/spec/cucumber/formatter/query/test_case_started_by_test_case_spec.rb deleted file mode 100644 index 00b697d77..000000000 --- a/spec/cucumber/formatter/query/test_case_started_by_test_case_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/formatter/spec_helper' -require 'cucumber/formatter/query/test_case_started_by_test_case' - -module Cucumber - module Formatter - module Query - RSpec.describe TestCaseStartedByTestCase do - extend SpecHelperDsl - include SpecHelper - - before(:each) do - Cucumber::Term::ANSIColor.coloring = false - - @out = StringIO.new - @config = actual_runtime.configuration.with_options(out_stream: @out) - @formatter = described_class.new(@config) - end - - let(:unknown_test_case) do - test_case = double - allow(test_case).to receive(:id).and_return('whatever-id') - test_case - end - - describe '#attempt_by_test_case' do - it 'raises an exception when the TestCase is unknown' do - expect { @formatter.attempt_by_test_case(unknown_test_case) }.to raise_exception(TestCaseUnknownError) - end - - context 'when the test case has been declared' do - before do - @test_case = double - allow(@test_case).to receive(:id).and_return('some-valid-id') - - @config.notify :test_case_created, @test_case, nil - end - - it 'returns 0 if no test_case_started event has been fired' do - expect(@formatter.attempt_by_test_case(@test_case)).to eq(0) - end - - it 'increments the attemp on every test_case_started event' do - @config.notify :test_case_started, @test_case - expect(@formatter.attempt_by_test_case(@test_case)).to eq(1) - - @config.notify :test_case_started, @test_case - expect(@formatter.attempt_by_test_case(@test_case)).to eq(2) - end - end - end - - describe '#test_case_started_id_by_test_case' do - it 'raises an exception when the TestCase is unknown' do - expect { @formatter.test_case_started_id_by_test_case(unknown_test_case) }.to raise_exception(TestCaseUnknownError) - end - - context 'when the test case has been declared' do - before do - @test_case = double - allow(@test_case).to receive(:id).and_return('some-valid-id') - - @config.notify :test_case_created, @test_case, nil - end - - it 'returns nil if no test_case_started event has been fired' do - expect(@formatter.test_case_started_id_by_test_case(@test_case)).to be_nil - end - - it 'gives a new id when a test_case_started event is fired' do - @config.notify :test_case_started, @test_case - - first_attempt_id = @formatter.test_case_started_id_by_test_case(@test_case) - expect(first_attempt_id).not_to be_nil - - @config.notify :test_case_started, @test_case - second_attempt_id = @formatter.test_case_started_id_by_test_case(@test_case) - expect(second_attempt_id).not_to be_nil - - expect(second_attempt_id).not_to eq(first_attempt_id) - end - end - end - end - end - end -end diff --git a/spec/support/fake_objects.rb b/spec/support/fake_objects.rb index 3baa49583..543cc8a35 100644 --- a/spec/support/fake_objects.rb +++ b/spec/support/fake_objects.rb @@ -41,7 +41,7 @@ def initialize(name) class FlakyStepActions < ::Cucumber::Core::Filter.new def test_case(test_case) failing_test_steps = test_case.test_steps.map do |step| - step.with_action { raise Failure } + step.with_action { raise 'Failure' } end passing_test_steps = test_case.test_steps.map do |step| step.with_action {} From f17be5f1ea5728e700dbe00de2389abc97083c67 Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:10:01 +0100 Subject: [PATCH 5/8] Refactor/v11 remove final event driven test case started id call (#1860) * WIP derive the id for final missing item - test_case_started_id when calling on_test_case_finished handler * Remove `_new` suffix * Remove private method now its no longer used * Re-order methods --- lib/cucumber/formatter/message_builder.rb | 279 +++++++++++----------- 1 file changed, 137 insertions(+), 142 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index c9664c774..dd10fab8a 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -106,6 +106,14 @@ def on_hook_test_step_created(event) @hook_id_by_test_step_id[event.test_step.id] = event.hook.id end + def on_step_activated(event) + # TODO: Handle StepActivated + end + + def on_step_definition_registered(event) + output_envelope(event.step_definition.to_envelope) + end + def on_test_case_created(event) @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id end @@ -126,54 +134,52 @@ def on_test_case_ready(event) output_envelope(message) end - def test_step_to_message(step) - return hook_step_to_message(step) if step.hook? + def on_test_case_started(event) + # For any new test_case_started events, we must ALWAYS generate a new id for a new run + @current_test_case_started_id = @config.id_generator.new_id - Cucumber::Messages::TestStep.new( - id: step.id, - # TODO: This "fake query" is only used once. It can likely be replace by `find_pickle_step_by` which - # takes a +TestStep+ message from the repo directly. - pickle_step_id: @pickle_id_step_by_test_step_id[step.id], - step_definition_ids: @step_definitions_by_test_step.step_definition_ids(step), - step_match_arguments_lists: step_match_arguments_lists(step) - ) - end + # Query missing: `#find_all_test_case_started_by_test_case_id` + find_all_test_case_started_by_test_case_id = + @repository.test_case_started_by_id + .values + .select { |test_case_started| test_case_started.test_case_id == event.test_case.id } - def hook_step_to_message(step) - Cucumber::Messages::TestStep.new( - id: step.id, - hook_id: fake_query_hook_id(step) + # If no TestCaseStarted messages exist. We must be on attempt 1 (Hence the .to_i casting for a `nil` value) + attempts_previously_made = find_all_test_case_started_by_test_case_id.map(&:attempt).max.to_i + + message = Cucumber::Messages::Envelope.new( + test_case_started: Cucumber::Messages::TestCaseStarted.new( + id: @current_test_case_started_id, + test_case_id: event.test_case.id, + timestamp: time_to_timestamp(Time.now), + attempt: attempts_previously_made + 1 + ) ) - end - def step_match_arguments_lists(step) - match_arguments = step_match_arguments(step) - [Cucumber::Messages::StepMatchArgumentsList.new( - step_match_arguments: match_arguments - )] - rescue Cucumber::Formatter::TestStepUnknownError - [] + output_envelope(message) end - def step_match_arguments(step) - @step_definitions_by_test_step.step_match_arguments(step).map do |argument| - Cucumber::Messages::StepMatchArgument.new( - group: argument_group_to_message(argument.group), - parameter_type_name: parameter_type_name(argument) - ) - end - end + def on_test_case_finished(event) + test_case_started_id = + @repository.test_case_started_by_id + .values + .detect { |test_case_started_message| test_case_started_message.test_case_id == event.test_case.id } + .id - def argument_group_to_message(group) - Cucumber::Messages::Group.new( - start: group.start, - value: group.value, - children: group.children&.map { |child| argument_group_to_message(child) } + test_case_started_message = @repository.test_case_started_by_id[test_case_started_id] + max_attempts = @config.retry_attempts + retries_attempted = test_case_started_message.attempt - 1 + will_be_retried = event.result.failed? && (retries_attempted < max_attempts) + + message = Cucumber::Messages::Envelope.new( + test_case_finished: Cucumber::Messages::TestCaseFinished.new( + test_case_started_id: test_case_started_id, + timestamp: time_to_timestamp(Time.now), + will_be_retried: will_be_retried + ) ) - end - def parameter_type_name(step_match_argument) - step_match_argument.parameter_type&.name if step_match_argument.respond_to?(:parameter_type) + output_envelope(message) end def on_test_run_started(*) @@ -187,25 +193,51 @@ def on_test_run_started(*) output_envelope(message) end - def on_test_case_started(event) - # For any new test_case_started events, we must ALWAYS generate a new id for a new run - @current_test_case_started_id = @config.id_generator.new_id + def on_test_run_finished(event) + message = Cucumber::Messages::Envelope.new( + test_run_finished: Cucumber::Messages::TestRunFinished.new( + timestamp: time_to_timestamp(Time.now), + success: event.success, + test_run_started_id: @test_run_started_id + ) + ) - # Query missing: `#find_all_test_case_started_by_test_case_id` - find_all_test_case_started_by_test_case_id = - @repository.test_case_started_by_id - .values - .select { |test_case_started| test_case_started.test_case_id == event.test_case.id } + output_envelope(message) + end - # If no TestCaseStarted messages exist. We must be on attempt 1 (Hence the .to_i casting for a `nil` value) - attempts_previously_made = find_all_test_case_started_by_test_case_id.map(&:attempt).max.to_i + def on_test_run_hook_started(event) + @current_test_run_hook_started_id = @config.id_generator.new_id message = Cucumber::Messages::Envelope.new( - test_case_started: Cucumber::Messages::TestCaseStarted.new( - id: @current_test_case_started_id, - test_case_id: event.test_case.id, + test_run_hook_started: Cucumber::Messages::TestRunHookStarted.new( + id: @current_test_run_hook_started_id, + hook_id: event.hook.id, + test_run_started_id: @test_run_started_id, + timestamp: time_to_timestamp(Time.now) + ) + ) + + output_envelope(message) + end + + def on_test_run_hook_finished(event) + result = event.test_result + result_message = result.to_message + + if result.failed? + result_message = Cucumber::Messages::TestStepResult.new( + status: result_message.status, + duration: result_message.duration, + message: create_error_message(result.exception), + exception: create_exception_object(result, result.exception) + ) + end + + message = Cucumber::Messages::Envelope.new( + test_run_hook_finished: Cucumber::Messages::TestRunHookFinished.new( + test_run_hook_started_id: @current_test_run_hook_started_id, timestamp: time_to_timestamp(Time.now), - attempt: attempts_previously_made + 1 + result: result_message ) ) @@ -295,119 +327,82 @@ def on_test_step_finished(event) output_envelope(message) end - def create_error_message(message_element) - <<~ERROR_MESSAGE - #{message_element.message} (#{message_element.class}) - #{message_element.backtrace} - ERROR_MESSAGE - end - - def create_exception_object(result, message_element) - return unless result.failed? - - Cucumber::Messages::Exception.new( - type: message_element.class, - message: message_element.message, - stack_trace: message_element.backtrace.join("\n") - ) - end - - def on_test_case_finished(event) - test_case_started_id = test_case_started_id(event.test_case) - test_case_started_message = @repository.test_case_started_by_id[test_case_started_id] - max_attempts = @config.retry_attempts - retries_attempted = test_case_started_message.attempt - 1 - will_be_retried = event.result.failed? && (retries_attempted < max_attempts) - - message = Cucumber::Messages::Envelope.new( - test_case_finished: Cucumber::Messages::TestCaseFinished.new( - test_case_started_id: test_case_started_id, - timestamp: time_to_timestamp(Time.now), - will_be_retried: will_be_retried - ) - ) - - output_envelope(message) - end - - def on_test_run_finished(event) + def on_undefined_parameter_type(event) message = Cucumber::Messages::Envelope.new( - test_run_finished: Cucumber::Messages::TestRunFinished.new( - timestamp: time_to_timestamp(Time.now), - success: event.success, - test_run_started_id: @test_run_started_id + undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new( + name: event.type_name, + expression: event.expression ) ) output_envelope(message) end - def on_step_activated(event) - # TODO: Handle StepActivated - end + def test_step_to_message(step) + return hook_step_to_message(step) if step.hook? - def on_step_definition_registered(event) - output_envelope(event.step_definition.to_envelope) + Cucumber::Messages::TestStep.new( + id: step.id, + # TODO: This "fake query" is only used once. It can likely be replace by `find_pickle_step_by` which + # takes a +TestStep+ message from the repo directly. + pickle_step_id: @pickle_id_step_by_test_step_id[step.id], + step_definition_ids: @step_definitions_by_test_step.step_definition_ids(step), + step_match_arguments_lists: step_match_arguments_lists(step) + ) end - def on_test_run_hook_started(event) - @current_test_run_hook_started_id = @config.id_generator.new_id - - message = Cucumber::Messages::Envelope.new( - test_run_hook_started: Cucumber::Messages::TestRunHookStarted.new( - id: @current_test_run_hook_started_id, - hook_id: event.hook.id, - test_run_started_id: @test_run_started_id, - timestamp: time_to_timestamp(Time.now) - ) + def hook_step_to_message(step) + Cucumber::Messages::TestStep.new( + id: step.id, + hook_id: fake_query_hook_id(step) ) - - output_envelope(message) end - def on_test_run_hook_finished(event) - result = event.test_result - result_message = result.to_message + def step_match_arguments_lists(step) + match_arguments = step_match_arguments(step) + [Cucumber::Messages::StepMatchArgumentsList.new( + step_match_arguments: match_arguments + )] + rescue Cucumber::Formatter::TestStepUnknownError + [] + end - if result.failed? - result_message = Cucumber::Messages::TestStepResult.new( - status: result_message.status, - duration: result_message.duration, - message: create_error_message(result.exception), - exception: create_exception_object(result, result.exception) + def step_match_arguments(step) + @step_definitions_by_test_step.step_match_arguments(step).map do |argument| + Cucumber::Messages::StepMatchArgument.new( + group: argument_group_to_message(argument.group), + parameter_type_name: parameter_type_name(argument) ) end + end - message = Cucumber::Messages::Envelope.new( - test_run_hook_finished: Cucumber::Messages::TestRunHookFinished.new( - test_run_hook_started_id: @current_test_run_hook_started_id, - timestamp: time_to_timestamp(Time.now), - result: result_message - ) + def argument_group_to_message(group) + Cucumber::Messages::Group.new( + start: group.start, + value: group.value, + children: group.children&.map { |child| argument_group_to_message(child) } ) - - output_envelope(message) end - def on_undefined_parameter_type(event) - message = Cucumber::Messages::Envelope.new( - undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new( - name: event.type_name, - expression: event.expression - ) - ) + def parameter_type_name(step_match_argument) + step_match_argument.parameter_type&.name if step_match_argument.respond_to?(:parameter_type) + end - output_envelope(message) + def create_error_message(message_element) + <<~ERROR_MESSAGE + #{message_element.message} (#{message_element.class}) + #{message_element.backtrace} + ERROR_MESSAGE end - def test_case_started_id(test_case) - test_case_started = - @repository.test_case_started_by_id - .values - .select { |test_case_started_message| test_case_started_message.test_case_id == test_case.id } - .max_by(&:attempt) + def create_exception_object(result, message_element) + return unless result.failed? - test_case_started&.id || @config.id_generator.new_id + Cucumber::Messages::Exception.new( + type: message_element.class, + message: message_element.message, + stack_trace: message_element.backtrace.join("\n") + ) end def fake_query_hook_id(test_step) From 5d8b7da437f5c74a4797fb83f789882408da13ed Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 24 Apr 2026 01:29:31 +0100 Subject: [PATCH 6/8] Move all relevant info to the earlier handler --- lib/cucumber/formatter/message_builder.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index dd10fab8a..088bb5c33 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -116,9 +116,7 @@ def on_step_definition_registered(event) def on_test_case_created(event) @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id - end - def on_test_case_ready(event) message = Cucumber::Messages::Envelope.new( test_case: Cucumber::Messages::TestCase.new( id: event.test_case.id, @@ -134,6 +132,11 @@ def on_test_case_ready(event) output_envelope(message) end + def on_test_case_ready(_event) + # We now publish all the relevant information and data in the `on_test_case_created` handler + :no_op + end + def on_test_case_started(event) # For any new test_case_started events, we must ALWAYS generate a new id for a new run @current_test_case_started_id = @config.id_generator.new_id From 6e9c4410a65bc9017968e30d5a23d132d9c301f1 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 24 Apr 2026 01:32:28 +0100 Subject: [PATCH 7/8] By moving one of the handler messages earlier in the chain, we can remove one fake query --- lib/cucumber/formatter/message_builder.rb | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 088bb5c33..6710e13a8 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -24,7 +24,6 @@ def initialize(config) # Fake Query objects @test_case_by_step_id = {} - @pickle_id_by_test_case_id = {} @pickle_id_step_by_test_step_id = {} @hook_id_by_test_step_id = {} @@ -115,12 +114,10 @@ def on_step_definition_registered(event) end def on_test_case_created(event) - @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id - message = Cucumber::Messages::Envelope.new( test_case: Cucumber::Messages::TestCase.new( id: event.test_case.id, - pickle_id: fake_query_pickle_id(event.test_case), + pickle_id: event.pickle.id, test_steps: event.test_case.test_steps.map { |step| test_step_to_message(step) }, test_run_started_id: @test_run_started_id ) @@ -413,12 +410,6 @@ def fake_query_hook_id(test_step) raise TestStepUnknownError, "No hook found for #{test_step.id} }. Known: #{@hook_id_by_test_step_id.keys}" end - - def fake_query_pickle_id(test_case) - return @pickle_id_by_test_case_id[test_case.id] if @pickle_id_by_test_case_id.key?(test_case.id) - - raise TestCaseUnknownError, "No pickle found for #{test_case.id} }. Known: #{@pickle_id_by_test_case_id.keys}" - end end end end From bff8a5e65a2c1f575159c44912dcff4fdd271fd6 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 24 Apr 2026 01:33:45 +0100 Subject: [PATCH 8/8] Remove redundant duplicate repository update call now we are happy with the handler calling update each and every time --- lib/cucumber/formatter/message_builder.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 6710e13a8..9f5843332 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -123,9 +123,6 @@ def on_test_case_created(event) ) ) - # TODO: This may be a redundant update. But for now we're leaving this in whilst we're in the transitory phase - @repository.update(message) - output_envelope(message) end