From bf6d71be9f95d0b685f16feff5418a86159b67df Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 22 May 2026 23:20:23 +0100 Subject: [PATCH 1/3] Add in logic for both run hook events to be passed as messages --- lib/cucumber/formatter/message_builder.rb | 49 +++++------------- lib/cucumber/glue/registry_and_more.rb | 63 +++++++++++++++++++++++ 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 86a0e9204..4cf26984c 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -80,6 +80,12 @@ def attach(src, media_type, filename) private def on_envelope(event) + # We do not have the requisite id for TestRunHookStarted#test_run_started_id at this point in time, so we + # will update the message live before updating the repository + + event.envelope.test_run_hook_started&.tap do |message| + message.instance_variable_set(:@test_run_started_id, @test_run_started_id) + end output_envelope(event.envelope) end @@ -202,43 +208,16 @@ def on_test_run_finished(event) output_envelope(message) 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) - ) - ) - - output_envelope(message) + def on_test_run_hook_started(_event) + :no_op + # This is now emitted at the required source (Single event listener - cucumber/glue/registry_and_more.rb#invoke_run_hook) + # It does not need to be emitted here as well 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), - result: result_message - ) - ) - - output_envelope(message) + def on_test_run_hook_finished(_event) + :no_op + # This is now emitted at the required source (Single event listener - cucumber/glue/registry_and_more.rb#invoke_run_hook) + # It does not need to be emitted here as well end def on_test_step_created(event) diff --git a/lib/cucumber/glue/registry_and_more.rb b/lib/cucumber/glue/registry_and_more.rb index f042c027a..59898100c 100644 --- a/lib/cucumber/glue/registry_and_more.rb +++ b/lib/cucumber/glue/registry_and_more.rb @@ -4,6 +4,8 @@ require 'cucumber/cucumber_expressions/cucumber_expression' require 'cucumber/cucumber_expressions/regular_expression' require 'cucumber/cucumber_expressions/cucumber_expression_generator' +require 'cucumber/messages/helpers/time_conversion' + require 'cucumber/glue/dsl' require 'cucumber/glue/snippet' require 'cucumber/glue/hook' @@ -48,6 +50,8 @@ def initialize(first_proc, second_proc) class RegistryAndMore attr_reader :current_world, :step_definitions + include Cucumber::Messages::Helpers::TimeConversion + all_keywords = ::Gherkin::DIALECTS.keys.map do |dialect_name| dialect = ::Gherkin::Dialect.for(dialect_name) dialect.given_keywords + dialect.when_keywords + dialect.then_keywords + dialect.and_keywords + dialect.but_keywords @@ -182,16 +186,75 @@ def create_expression(string_or_regexp) def invoke_run_hook(hook, pseudo_method) @configuration.notify(:test_run_hook_started, hook) + + current_test_run_hook_started_id = @configuration.id_generator.new_id + started_envelope = test_run_hook_started_envelope(hook, current_test_run_hook_started_id) + @configuration.notify(:envelope, started_envelope) + timer = Core::Test::Timer.new.start begin hook.invoke(pseudo_method, []) @configuration.notify(:test_run_hook_finished, hook, Core::Test::Result::Passed.new(timer.duration)) + finished_envelope = test_run_hook_finished_envelope(hook, Core::Test::Result::Passed.new(timer.duration), current_test_run_hook_started_id) + @configuration.notify(:envelope, finished_envelope) rescue StandardError => e @configuration.notify(:test_run_hook_finished, hook, Core::Test::Result::Failed.new(timer.duration, e)) + finished_envelope = test_run_hook_finished_envelope(hook, Core::Test::Result::Failed.new(timer.duration, e), current_test_run_hook_started_id) + @configuration.notify(:envelope, finished_envelope) raise end end + def test_run_hook_started_envelope(hook, id) + Cucumber::Messages::Envelope.new( + test_run_hook_started: Cucumber::Messages::TestRunHookStarted.new( + id: id, + hook_id: hook.id, + test_run_started_id: 'unknown - to be set in message builder', + timestamp: time_to_timestamp(Time.now) + ) + ) + end + + def test_run_hook_finished_envelope(_hook, test_result, test_run_hook_started_id) + result = 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 + + Cucumber::Messages::Envelope.new( + test_run_hook_finished: Cucumber::Messages::TestRunHookFinished.new( + test_run_hook_started_id: test_run_hook_started_id, + timestamp: time_to_timestamp(Time.now), + result: result_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 parameter_type_envelope(parameter_type) # TODO: should this be moved to Cucumber::Expression::ParameterType#to_envelope ?? # Note: that would mean that cucumber-expression would depend on cucumber-messages From 45a5429becf9f189b83620dd15be5dbc92b49e85 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 22 May 2026 23:20:40 +0100 Subject: [PATCH 2/3] Store test_run_started_id cache on config class instance --- lib/cucumber/configuration.rb | 4 ++++ lib/cucumber/formatter/message_builder.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/cucumber/configuration.rb b/lib/cucumber/configuration.rb index f460ee872..2efd24a18 100644 --- a/lib/cucumber/configuration.rb +++ b/lib/cucumber/configuration.rb @@ -285,6 +285,10 @@ def id_generator @id_generator ||= Cucumber::Messages::Helpers::IdGenerator::UUID.new end + def test_run_started_id + @test_run_started_id ||= id_generator.new_id + end + private def default_features_paths diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 4cf26984c..6a389fa45 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -16,7 +16,7 @@ def initialize(config) @repository = Cucumber::Repository.new @query = Cucumber::Query.new(@repository) - @test_run_started_id = config.id_generator.new_id + @test_run_started_id = config.test_run_started_id # Fake Query objects @test_case_by_step_id = {} From 00ae42d67e9e1f401981fa1a80edf592e6e0d941 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 22 May 2026 23:22:49 +0100 Subject: [PATCH 3/3] Pass in the test_run_started_id on hook_run_started message generation and remove faux update from envelope emittence --- lib/cucumber/formatter/message_builder.rb | 6 ------ lib/cucumber/glue/registry_and_more.rb | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 6a389fa45..d40e29b0e 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -80,12 +80,6 @@ def attach(src, media_type, filename) private def on_envelope(event) - # We do not have the requisite id for TestRunHookStarted#test_run_started_id at this point in time, so we - # will update the message live before updating the repository - - event.envelope.test_run_hook_started&.tap do |message| - message.instance_variable_set(:@test_run_started_id, @test_run_started_id) - end output_envelope(event.envelope) end diff --git a/lib/cucumber/glue/registry_and_more.rb b/lib/cucumber/glue/registry_and_more.rb index 59898100c..036c78975 100644 --- a/lib/cucumber/glue/registry_and_more.rb +++ b/lib/cucumber/glue/registry_and_more.rb @@ -210,7 +210,7 @@ def test_run_hook_started_envelope(hook, id) test_run_hook_started: Cucumber::Messages::TestRunHookStarted.new( id: id, hook_id: hook.id, - test_run_started_id: 'unknown - to be set in message builder', + test_run_started_id: @configuration.test_run_started_id, timestamp: time_to_timestamp(Time.now) ) )