From 45190e7c94ece3d020c65535f74068678466821d Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Tue, 26 May 2026 15:32:33 +0100 Subject: [PATCH 1/8] V11 - Event Migration to Envelopes -> GherkinSourceRead (#1877) * Add envelope generation at source for gherkin_source_read event * Do not double fire events --- lib/cucumber/formatter/message_builder.rb | 14 ++++---------- lib/cucumber/runtime.rb | 13 +++++++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 86a0e9204..9259ea907 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -87,16 +87,10 @@ def on_gherkin_source_parsed(_event) # TODO: Handle GherkinSourceParsed end - def on_gherkin_source_read(event) - message = Cucumber::Messages::Envelope.new( - source: Cucumber::Messages::Source.new( - uri: event.path, - data: event.body, - media_type: 'text/x.cucumber.gherkin+plain' - ) - ) - - output_envelope(message) + def on_gherkin_source_read(_event) + :no_op + # This is now emitted at the required source (Single event listener - cucumber/runtime.rb#features) + # It does not need to be emitted here as well end def on_hook_test_step_created(event) diff --git a/lib/cucumber/runtime.rb b/lib/cucumber/runtime.rb index 3315fd171..36164aeec 100644 --- a/lib/cucumber/runtime.rb +++ b/lib/cucumber/runtime.rb @@ -136,6 +136,19 @@ def features @features ||= feature_files.map do |path| source = NormalisedEncodingFile.read(path) @configuration.notify :gherkin_source_read, path, source + + # TODO: Move this into `Cucumber::Core::Gherkin::Document#to_envelope` + to_envelope = + Cucumber::Messages::Envelope.new( + source: Cucumber::Messages::Source.new( + uri: path, + data: source, + media_type: 'text/x.cucumber.gherkin+plain' + ) + ) + + @configuration.notify :envelope, to_envelope + Cucumber::Core::Gherkin::Document.new(path, source) end end From 28916723a51a6141f2baa23653c0c6e6b5e2122c Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Tue, 26 May 2026 16:15:41 +0100 Subject: [PATCH 2/8] V11 - Event Migration to Envelopes -> TestRunHookStarted & TestRunHookFinished (#1878) * Add in logic for both run hook events to be passed as messages * Store test_run_started_id cache on config class instance * Pass in the test_run_started_id on hook_run_started message generation and remove faux update from envelope emittence --- lib/cucumber/configuration.rb | 4 ++ lib/cucumber/formatter/message_builder.rb | 45 ++++------------ lib/cucumber/glue/registry_and_more.rb | 63 +++++++++++++++++++++++ 3 files changed, 76 insertions(+), 36 deletions(-) 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 9259ea907..99371168f 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 = {} @@ -196,43 +196,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..036c78975 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: @configuration.test_run_started_id, + 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 db8ea5134122452d8dd1a99ac0298c4b533ab752 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 18:19:10 +0100 Subject: [PATCH 3/8] Add note for v17 switch --- lib/cucumber/runtime.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cucumber/runtime.rb b/lib/cucumber/runtime.rb index 36164aeec..4260f650a 100644 --- a/lib/cucumber/runtime.rb +++ b/lib/cucumber/runtime.rb @@ -137,7 +137,10 @@ def features source = NormalisedEncodingFile.read(path) @configuration.notify :gherkin_source_read, path, source - # TODO: Move this into `Cucumber::Core::Gherkin::Document#to_envelope` + # TODO: When core is v17+ switch the below code out to the following + # Cucumber::Core::Gherkin::Document.new(path, source).tap do |document| + # @configuration.notify(:envelope, document.to_envelope) + # end to_envelope = Cucumber::Messages::Envelope.new( source: Cucumber::Messages::Source.new( @@ -147,7 +150,7 @@ def features ) ) - @configuration.notify :envelope, to_envelope + @configuration.notify(:envelope, to_envelope) Cucumber::Core::Gherkin::Document.new(path, source) end From 2cba0eb27814fec588895b4b144da44fad5e1eaa Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 18:20:06 +0100 Subject: [PATCH 4/8] Remove no-op handlers in favour of less code --- lib/cucumber/formatter/message_builder.rb | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 99371168f..354883271 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -30,7 +30,6 @@ def initialize(config) config.on_event :envelope, &method(:on_envelope) config.on_event :gherkin_source_parsed, &method(:on_gherkin_source_parsed) - config.on_event :gherkin_source_read, &method(:on_gherkin_source_read) config.on_event :hook_test_step_created, &method(:on_hook_test_step_created) @@ -45,9 +44,6 @@ def initialize(config) config.on_event :test_run_started, &method(:on_test_run_started) config.on_event :test_run_finished, &method(:on_test_run_finished) - config.on_event :test_run_hook_started, &method(:on_test_run_hook_started) - config.on_event :test_run_hook_finished, &method(:on_test_run_hook_finished) - config.on_event :test_step_created, &method(:on_test_step_created) config.on_event :test_step_started, &method(:on_test_step_started) config.on_event :test_step_finished, &method(:on_test_step_finished) @@ -87,12 +83,6 @@ def on_gherkin_source_parsed(_event) # TODO: Handle GherkinSourceParsed end - def on_gherkin_source_read(_event) - :no_op - # This is now emitted at the required source (Single event listener - cucumber/runtime.rb#features) - # It does not need to be emitted here as well - end - def on_hook_test_step_created(event) @hook_id_by_test_step_id[event.test_step.id] = event.hook.id end @@ -196,18 +186,6 @@ def on_test_run_finished(event) output_envelope(message) end - 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) - :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) @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id @step_definition_ids_by_test_step_id[event.test_step.id] = [] From 72f824017aca4b8aa97243a0caa9bc388d0c2ed6 Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Wed, 27 May 2026 08:56:05 +0100 Subject: [PATCH 5/8] Define undefinedparametertype event as an envelope at source (#1880) --- lib/cucumber/formatter/message_builder.rb | 13 ------------- lib/cucumber/glue/registry_and_more.rb | 10 ++++------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 4a1c51d31..805eda2d3 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -42,8 +42,6 @@ def initialize(config) config.on_event :test_step_created, &method(:on_test_step_created) config.on_event :test_step_started, &method(:on_test_step_started) config.on_event :test_step_finished, &method(:on_test_step_finished) - - config.on_event :undefined_parameter_type, &method(:on_undefined_parameter_type) end def attach(src, media_type, filename) @@ -252,17 +250,6 @@ def on_test_step_finished(event) @config.event_bus.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 - ) - ) - - @config.event_bus.envelope(message) - end - def test_step_to_message(step) return hook_step_to_message(step) if step.hook? diff --git a/lib/cucumber/glue/registry_and_more.rb b/lib/cucumber/glue/registry_and_more.rb index cf788b1b0..65cd1fc32 100644 --- a/lib/cucumber/glue/registry_and_more.rb +++ b/lib/cucumber/glue/registry_and_more.rb @@ -102,14 +102,12 @@ def define_parameter_type(parameter_type) def register_rb_step_definition(string_or_regexp, proc_or_sym, options) step_definition = StepDefinition.new(@configuration.id_generator.new_id, self, string_or_regexp, proc_or_sym, options) @step_definitions << step_definition - @configuration.notify :step_definition_registered, step_definition - @configuration.notify :envelope, step_definition.to_envelope + @configuration.notify(:step_definition_registered, step_definition) + @configuration.notify(:envelope, step_definition.to_envelope) step_definition rescue Cucumber::CucumberExpressions::UndefinedParameterTypeError => e - # TODO: add a way to extract the parameter type directly from the error. - type_name = e.message.match(/^Undefined parameter type ['|{](.*)['|}].?$/)[1] - - @configuration.notify :undefined_parameter_type, type_name, string_or_regexp + @configuration.notify(:undefined_parameter_type, e.undefined_parameter_type_name, string_or_regexp) + @configuration.notify(:envelope, e.to_envelope(string_or_regexp)) end def build_rb_world_factory(world_modules, namespaced_world_modules, proc) From 0f8b5ccffba4c7a4331dd5dff1cffd0ad6e61632 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Wed, 27 May 2026 09:04:48 +0100 Subject: [PATCH 6/8] For now the porting hasnt been done for cucumber expression error envelope --- lib/cucumber/formatter/message_builder.rb | 2 +- lib/cucumber/glue/registry_and_more.rb | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index a7e587aea..45cbfcfc6 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -252,7 +252,7 @@ def on_test_step_finished(event) @config.event_bus.envelope(message) end - + def output_snippet_envelope(event) return unless event.result.undefined? diff --git a/lib/cucumber/glue/registry_and_more.rb b/lib/cucumber/glue/registry_and_more.rb index 65cd1fc32..c47665905 100644 --- a/lib/cucumber/glue/registry_and_more.rb +++ b/lib/cucumber/glue/registry_and_more.rb @@ -107,7 +107,15 @@ def register_rb_step_definition(string_or_regexp, proc_or_sym, options) step_definition rescue Cucumber::CucumberExpressions::UndefinedParameterTypeError => e @configuration.notify(:undefined_parameter_type, e.undefined_parameter_type_name, string_or_regexp) - @configuration.notify(:envelope, e.to_envelope(string_or_regexp)) + # Move the below code into cucumber-expressions. Once done. Switch the line for + # @configuration.notify(:envelope, e.to_envelope(string_or_regexp)) + to_envelope = Cucumber::Messages::Envelope.new( + undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new( + name: e.undefined_parameter_type_name, + expression: string_or_regexp + ) + ) + @configuration.notify(:envelope, to_envelope) end def build_rb_world_factory(world_modules, namespaced_world_modules, proc) From e43afaaff73999612506b40bbbe01cf61829d3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rasmusson?= Date: Sun, 31 May 2026 14:50:20 +0200 Subject: [PATCH 7/8] Store the id of the current test run hook in MessageBuilder * Until the attchment messages are generated at the source, the MessageBuilder need to keep track of the id of the current test run hook to be able to generate the attachment messages correctly. --- lib/cucumber/formatter/message_builder.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 45cbfcfc6..c5c5c2087 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -40,6 +40,7 @@ def initialize(config) config.on_event :test_run_started, &method(:on_test_run_started) config.on_event :test_run_finished, &method(:on_test_run_finished) + config.on_event :test_run_hook_started, &method(:on_test_run_hook_started) config.on_event :test_step_created, &method(:on_test_step_created) config.on_event :test_step_started, &method(:on_test_step_started) @@ -185,6 +186,10 @@ def on_test_run_finished(event) @config.event_bus.envelope(message) end + def on_test_run_hook_started(event) + @current_test_run_hook_started_id = event.test_step.id + end + def on_test_step_created(event) @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id @step_definition_ids_by_test_step_id[event.test_step.id] = [] From d1e510695c4d1d6eef875b0cecfa75aa7a57a471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rasmusson?= Date: Sun, 31 May 2026 18:02:55 +0200 Subject: [PATCH 8/8] Let MessageBuilder listen to envelopes for the current run hook id --- lib/cucumber/formatter/message_builder.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index c5c5c2087..2afad896b 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -40,11 +40,16 @@ def initialize(config) config.on_event :test_run_started, &method(:on_test_run_started) config.on_event :test_run_finished, &method(:on_test_run_finished) - config.on_event :test_run_hook_started, &method(:on_test_run_hook_started) config.on_event :test_step_created, &method(:on_test_step_created) config.on_event :test_step_started, &method(:on_test_step_started) config.on_event :test_step_finished, &method(:on_test_step_finished) + + config.on_event :envelope, &method(:on_envelope) + end + + def on_envelope(event) + @current_test_run_hook_started_id = event.envelope.test_run_hook_started.id if event.envelope.test_run_hook_started end def attach(src, media_type, filename) @@ -186,10 +191,6 @@ def on_test_run_finished(event) @config.event_bus.envelope(message) end - def on_test_run_hook_started(event) - @current_test_run_hook_started_id = event.test_step.id - end - def on_test_step_created(event) @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id @step_definition_ids_by_test_step_id[event.test_step.id] = []