From 57f3aafcc9c4685b70385e48b9861347dd28e77e Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Thu, 4 Jun 2026 11:45:36 +0100 Subject: [PATCH 01/11] Fix HTML formatter not running output_envelope correctly --- lib/cucumber/formatter/html.rb | 13 +++++++++---- lib/cucumber/formatter/rerun.rb | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index 300009a10..70cf48668 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -1,17 +1,22 @@ # frozen_string_literal: true -require 'cucumber/formatter/io' +require 'cucumber/query' require 'cucumber/html_formatter' -require 'cucumber/formatter/message_builder' + +require_relative 'io' module Cucumber module Formatter - class HTML < MessageBuilder + class HTML + include Io + def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) + @repository = Cucumber::Repository.new + @query = Cucumber::Query.new(@repository) @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io) @html_formatter.write_pre_message - super(config) + config.on_event :envelope, &method(:output_envelope) end def output_envelope(envelope) diff --git a/lib/cucumber/formatter/rerun.rb b/lib/cucumber/formatter/rerun.rb index ae6436628..b44ebe701 100644 --- a/lib/cucumber/formatter/rerun.rb +++ b/lib/cucumber/formatter/rerun.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'cucumber/formatter/io' require 'cucumber/query' +require_relative 'io' + module Cucumber module Formatter class Rerun From d0cd3d651bbe7049fa915ffed9443db638ba1f52 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Thu, 4 Jun 2026 11:55:31 +0100 Subject: [PATCH 02/11] Unwrap event envelope into true envelope --- lib/cucumber/formatter/html.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index 70cf48668..f9dbf36b3 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -19,7 +19,8 @@ def initialize(config) config.on_event :envelope, &method(:output_envelope) end - def output_envelope(envelope) + def output_envelope(event) + envelope = event.envelope @repository.update(envelope) @html_formatter.write_message(envelope) @html_formatter.write_post_message if envelope.test_run_finished From 0badcff7c142d1c569676da23c600c72f233764e Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Thu, 4 Jun 2026 16:20:20 +0100 Subject: [PATCH 03/11] Tidy up formatters that dont need query / repository --- lib/cucumber/formatter/html.rb | 5 +---- lib/cucumber/formatter/message.rb | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index f9dbf36b3..4e619cda5 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'cucumber/query' require 'cucumber/html_formatter' require_relative 'io' @@ -12,8 +11,6 @@ class HTML def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) - @repository = Cucumber::Repository.new - @query = Cucumber::Query.new(@repository) @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io) @html_formatter.write_pre_message config.on_event :envelope, &method(:output_envelope) @@ -21,8 +18,8 @@ def initialize(config) def output_envelope(event) envelope = event.envelope - @repository.update(envelope) @html_formatter.write_message(envelope) + # TODO: Move this conditional logic into the HTML formatter proper @html_formatter.write_post_message if envelope.test_run_finished end end diff --git a/lib/cucumber/formatter/message.rb b/lib/cucumber/formatter/message.rb index a4abcb805..2c91ce883 100644 --- a/lib/cucumber/formatter/message.rb +++ b/lib/cucumber/formatter/message.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require 'cucumber/formatter/io' -require 'cucumber/query' +require_relative 'io' module Cucumber module Formatter @@ -11,14 +10,11 @@ class Message def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) - @repository = Cucumber::Repository.new - @query = Cucumber::Query.new(@repository) config.on_event :envelope, &method(:output_envelope) end def output_envelope(event) envelope = event.envelope - @repository.update(envelope) @io.write(envelope.to_json) @io.write("\n") end From 1acbf8f802e291339c814598f16ac2a2f5870d9b Mon Sep 17 00:00:00 2001 From: Luke Hill <20105237+luke-hill@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:41:19 +0100 Subject: [PATCH 04/11] Bugfix/language examples (#1887) * Fix up arabic translation example Fix step defs to use native terms Fix capture to ensure scenario will work * Permit arabic to run again in CI * Switch ukranian to use anglicised step def code to permit example to be ran in CI - Still validly testing the parser and the localised terms in gherkin * Switch uzbek to use anglicised step def code to permit example to be ran in CI - Still validly testing the parser and the localised terms in gherkin * Switch russian to use anglicised step def code to permit example to be ran in CI - Still validly testing the parser and the localised terms in gherkin * Enhance test suite to run all new examples * Add note * Add changelog --- CHANGELOG.md | 4 ++- docs/jruby-limitations.md | 4 ++- examples/i18n/Rakefile | 11 +++----- examples/i18n/ar/features/addition.feature | 2 +- .../step_definitions/calculator_steps.rb | 6 ++--- examples/i18n/ru/features/addition.feature | 2 +- .../features/consecutive_calculations.feature | 2 +- examples/i18n/ru/features/division.feature | 2 +- .../step_definitions/calculator_steps.rb | 26 +++++++++---------- .../step_definitions/calculator_steps.rb | 18 ++++++------- examples/i18n/uz/features/division.feature | 1 - .../step_definitions/calculator_steps.rb | 26 +++++++++---------- 12 files changed, 51 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7a1915d..fc5dc1464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo ## [Unreleased] ### Fixed -- Show failed step error details in the summary formatter output. +- Show failed step error details in the summary formatter output +- Fixed up JRuby examples which weren't running due to anglicisation issues (Pivoted to use English step definitions to help JRuby testing) +- Fixed up Arabic example which had some incorrect logic for step definition matching (Due to RTL nature of the language) ## [11.1.0] - 2026-06-02 ### Added diff --git a/docs/jruby-limitations.md b/docs/jruby-limitations.md index daad6321a..e8f239ef9 100644 --- a/docs/jruby-limitations.md +++ b/docs/jruby-limitations.md @@ -16,7 +16,7 @@ That means, for example, that you can not write the following code: end ``` -Instead, you have to write: +Instead, you have to write (**NB:** both the gherkin term and the block variable(s) are in English): ```ruby Given('я ввожу число {int}') do |number| calc.push(number) @@ -39,3 +39,5 @@ feature file can be executed on JRuby: Если я нажимаю "+" То результатом должно быть число 120 ``` + +In our CI system these languages are tested using the anglicised step definitions diff --git a/examples/i18n/Rakefile b/examples/i18n/Rakefile index f88821c32..8988803eb 100644 --- a/examples/i18n/Rakefile +++ b/examples/i18n/Rakefile @@ -23,19 +23,14 @@ end task default: :cucumber def examples_disabled?(lang) - return make_warning("SKIPPING #{lang} (The examples are out of date - please help update them)") unless examples_working?(lang) - make_warning("SKIPPING #{lang}: examples have been disabled for JRuby.") if jruby_disabled_examples?(lang) end -def jruby_disabled_examples?(lang) +def jruby_disabled_examples?(_lang) return unless RUBY_PLATFORM == 'java' - %w[ru uk uz].include?(lang) -end - -def examples_working?(lang) - !%w[ar].index(lang) + # No longer any disabled examples for JRuby as of cucumber 11.1.0 + nil end def make_warning(msg) diff --git a/examples/i18n/ar/features/addition.feature b/examples/i18n/ar/features/addition.feature index e2dea4454..2b890b4f2 100644 --- a/examples/i18n/ar/features/addition.feature +++ b/examples/i18n/ar/features/addition.feature @@ -14,4 +14,4 @@ | input_1 | input_2 | button | output | | 20 | 30 | جمع | 50 | | 2 | 5 | جمع | 7 | - | 0 | 40 | جمع | 40 | \ No newline at end of file + | 0 | 40 | جمع | 40 | diff --git a/examples/i18n/ar/features/step_definitions/calculator_steps.rb b/examples/i18n/ar/features/step_definitions/calculator_steps.rb index dbf50856b..1974dae66 100644 --- a/examples/i18n/ar/features/step_definitions/calculator_steps.rb +++ b/examples/i18n/ar/features/step_definitions/calculator_steps.rb @@ -10,14 +10,14 @@ After do end -Given 'كتابة $n في الآلة الحاسبة' do |n| +بفرض(/كتابة (.+) في الآلة الحاسبة/) do |n| @calc.push n.to_i end -When(/يتم الضغط على (.+)/) do |op| +متى(/يتم الضغط على (.+)/) do |op| @result = @calc.send op end -Then(/يظهر (.*) على الشاشة/) do |result| +اذاً(/يظهر (\d+) على الشاشة/) do |result| expect(@result).to eq(result) end diff --git a/examples/i18n/ru/features/addition.feature b/examples/i18n/ru/features/addition.feature index eda57222a..3b72a1afc 100644 --- a/examples/i18n/ru/features/addition.feature +++ b/examples/i18n/ru/features/addition.feature @@ -8,4 +8,4 @@ Допустим я ввожу число 50 И затем ввожу число 70 Если я нажимаю "+" - То результатом должно быть число 120 \ No newline at end of file + То результатом должно быть число 120 diff --git a/examples/i18n/ru/features/consecutive_calculations.feature b/examples/i18n/ru/features/consecutive_calculations.feature index 87cc7f2ee..bc5cda9d6 100644 --- a/examples/i18n/ru/features/consecutive_calculations.feature +++ b/examples/i18n/ru/features/consecutive_calculations.feature @@ -14,4 +14,4 @@ Сценарий: деление результата последней операции Если я ввожу число 2 И нажимаю "/" - То результатом должно быть число 4 \ No newline at end of file + То результатом должно быть число 4 diff --git a/examples/i18n/ru/features/division.feature b/examples/i18n/ru/features/division.feature index 736d029a1..1406c2360 100644 --- a/examples/i18n/ru/features/division.feature +++ b/examples/i18n/ru/features/division.feature @@ -13,4 +13,4 @@ | делимое | делитель | частное | | 100 | 2 | 50 | | 28 | 7 | 4 | - | 0 | 5 | 0 | \ No newline at end of file + | 0 | 5 | 0 | diff --git a/examples/i18n/ru/features/step_definitions/calculator_steps.rb b/examples/i18n/ru/features/step_definitions/calculator_steps.rb index a0d59638c..b6e0deb3f 100644 --- a/examples/i18n/ru/features/step_definitions/calculator_steps.rb +++ b/examples/i18n/ru/features/step_definitions/calculator_steps.rb @@ -1,27 +1,27 @@ # frozen_string_literal: true -Допустим('я ввожу число {int}') do |число| - calc.push число +Given('я ввожу число {int}') do |number| + calc.push number end -Допустим('затем ввожу число {int}') do |число| - calc.push число +Given('затем ввожу число {int}') do |number| + calc.push number end -Если('нажимаю {string}') do |операция| - calc.send операция +When('нажимаю {string}') do |operation| + calc.send operation end -Если('я нажимаю {string}') do |операция| - calc.send операция +When('я нажимаю {string}') do |operation| + calc.send operation end -То('результатом должно быть число {float}') do |результат| - expect(calc.result).to eq(результат) +Then('результатом должно быть число {float}') do |answer| + expect(calc.result).to eq(answer) end -Допустим('я сложил {int} и {int}') do |слагаемое1, слагаемое2| - calc.push слагаемое1 - calc.push слагаемое2 +When('я сложил {int} и {int}') do |number1, number2| + calc.push number1 + calc.push number2 calc.send '+' end diff --git a/examples/i18n/uk/features/step_definitions/calculator_steps.rb b/examples/i18n/uk/features/step_definitions/calculator_steps.rb index 907fc8323..cd48f6723 100644 --- a/examples/i18n/uk/features/step_definitions/calculator_steps.rb +++ b/examples/i18n/uk/features/step_definitions/calculator_steps.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true -Припустимо('потім/я ввожу число {int}') do |число| - calc.push число +Given('потім/я ввожу число {int}') do |number| + calc.push number end -Якщо('я натискаю {string}') do |операція| - calc.send операція +Given('я натискаю {string}') do |number| + calc.send number end -То('результатом повинно бути число {float}') do |результат| - expect(calc.result).to eq(результат) +Given('результатом повинно бути число {float}') do |number| + expect(calc.result).to eq(number) end -Припустимо('я додав {int} і {int}') do |число1, число2| - calc.push число1 - calc.push число2 +Given('я додав {int} і {int}') do |number1, number2| + calc.push number1 + calc.push number2 calc.send '+' end diff --git a/examples/i18n/uz/features/division.feature b/examples/i18n/uz/features/division.feature index e6550f277..efb62c6dd 100644 --- a/examples/i18n/uz/features/division.feature +++ b/examples/i18n/uz/features/division.feature @@ -14,4 +14,3 @@ | 100 | 2 | 50 | | 28 | 7 | 4 | | 0 | 5 | 0 | - diff --git a/examples/i18n/uz/features/step_definitions/calculator_steps.rb b/examples/i18n/uz/features/step_definitions/calculator_steps.rb index 4ff8a3636..08ddcff05 100644 --- a/examples/i18n/uz/features/step_definitions/calculator_steps.rb +++ b/examples/i18n/uz/features/step_definitions/calculator_steps.rb @@ -1,27 +1,27 @@ # frozen_string_literal: true -Агар('{int} сонини киритсам') do |сон| - calc.push сон +Given('{int} сонини киритсам') do |number| + calc.push number end -Агар('ундан сунг {int} сонини киритсам') do |сон| - calc.push сон +Given('ундан сунг {int} сонини киритсам') do |number| + calc.push number end -Агар('ман {int} сонини киритсам') do |сон| - calc.push сон +Given('ман {int} сонини киритсам') do |number| + calc.push number end -Агар('{word} боссам') do |операция| - calc.send операция +When('{word} боссам') do |operation| + calc.send operation end -Агар('{int} ва {int} сонини кушсам') do |сон1, сон2| - calc.push сон1.to_i - calc.push сон2.to_i +When('{int} ва {int} сонини кушсам') do |number1, number2| + calc.push number1 + calc.push number2 calc.send '+' end -Унда('жавоб {int} сони булиши керак') do |жавоб| - expect(calc.result).to eq(жавоб) +Then('жавоб {int} сони булиши керак') do |answer| + expect(calc.result).to eq(answer) end From 6dcb63dd25897402e5881466f483220c00109ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Rasmusson?= Date: Thu, 4 Jun 2026 18:49:27 +0200 Subject: [PATCH 05/11] Use events also when step definitions use "attach" and "log" (#1881) * Remove the ancient procedure call interface from formatters completely * Until now the data from "attach" and "log" calls from step definitions has been passed to the formatter using the ancient procedure call interface used in Cucumber-Ruby v1 and v2. From Cucumber-Ruby v3 all other data to formatters has been passed using events. * Introduce an event AttachCalled to pass the data from "attach" and "log" calls from step definitions to event listeners like formatters. * Update Changelog.md --- CHANGELOG.md | 3 ++ .../lib/step_definitions/cucumber_steps.rb | 5 ++-- lib/cucumber/events.rb | 1 + lib/cucumber/events/attach_called.rb | 22 +++++++++++++++ lib/cucumber/formatter/console.rb | 10 +++---- lib/cucumber/formatter/duration_extractor.rb | 2 -- lib/cucumber/formatter/fanout.rb | 28 ------------------- lib/cucumber/formatter/json.rb | 18 ++++++------ lib/cucumber/formatter/message_builder.rb | 17 +++++------ lib/cucumber/formatter/pretty.rb | 11 ++++---- lib/cucumber/formatter/progress.rb | 1 + lib/cucumber/runtime.rb | 23 ++++++++------- lib/cucumber/runtime/user_interface.rb | 6 ++-- spec/cucumber/formatter/spec_helper.rb | 3 +- 14 files changed, 79 insertions(+), 71 deletions(-) create mode 100644 lib/cucumber/events/attach_called.rb delete mode 100644 lib/cucumber/formatter/fanout.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fc5dc1464..57b1fe6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo - Fixed up JRuby examples which weren't running due to anglicisation issues (Pivoted to use English step definitions to help JRuby testing) - Fixed up Arabic example which had some incorrect logic for step definition matching (Due to RTL nature of the language) +### Changed +- Change to use events to pass the data from "log" and "attach" calls from the step definitions to the formatters. With this the last part of the ancient (pre event) formatter inteface has been removed. ([#1881](https://github.com/cucumber/cucumber-ruby/pull/1881) [brasmusson](https://github.com/brasmusson)) + ## [11.1.0] - 2026-06-02 ### Added - Print thread backtraces on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) diff --git a/features/lib/step_definitions/cucumber_steps.rb b/features/lib/step_definitions/cucumber_steps.rb index 9b33e62a3..97b41fdc9 100644 --- a/features/lib/step_definitions/cucumber_steps.rb +++ b/features/lib/step_definitions/cucumber_steps.rb @@ -13,10 +13,11 @@ '', ' def initialize(config)', ' @io = config.out_stream', + ' config.on_event :attach_called, &method(:on_attach_called)', ' end', '', - ' def attach(src, media_type, _filename, _streamed_file)', - ' @io.puts(src)', + ' def on_attach_called(event)', + ' @io.puts(event.src)', ' end', 'end' ].join("\n")) diff --git a/lib/cucumber/events.rb b/lib/cucumber/events.rb index f5214479c..f240de5b2 100644 --- a/lib/cucumber/events.rb +++ b/lib/cucumber/events.rb @@ -24,6 +24,7 @@ def self.make_event_bus def self.registry Core::Events.build_registry( + AttachCalled, GherkinSourceParsed, GherkinSourceRead, HookTestStepCreated, diff --git a/lib/cucumber/events/attach_called.rb b/lib/cucumber/events/attach_called.rb new file mode 100644 index 000000000..ec8c95752 --- /dev/null +++ b/lib/cucumber/events/attach_called.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'cucumber/core/events' + +module Cucumber + module Events + # Fired when attach is called in a step definition + class AttachCalled < Core::Event.new(:src, :media_type, :filename, :streamed_file) + # The attachment body + attr_reader :src + + # The content media type + attr_reader :media_type + + # An optional filename + attr_reader :filename + + # Whether the file is streamed or not + attr_reader :streamed_file + end + end +end diff --git a/lib/cucumber/formatter/console.rb b/lib/cucumber/formatter/console.rb index 1c6f5fb53..391b9853b 100644 --- a/lib/cucumber/formatter/console.rb +++ b/lib/cucumber/formatter/console.rb @@ -169,15 +169,15 @@ def do_print_passing_wip(passed_messages) end end - def attach(src, media_type, filename, _streamed_file) - return unless media_type == 'text/x.cucumber.log+plain' + def on_attach_called(event) + return unless event.media_type == 'text/x.cucumber.log+plain' return unless @io @io.puts - if filename - @io.puts("#{filename}: #{format_string(src, :tag)}") + if event.filename + @io.puts("#{event.filename}: #{format_string(event.src, :tag)}") else - @io.puts(format_string(src, :tag)) + @io.puts(format_string(event.src, :tag)) end @io.flush diff --git a/lib/cucumber/formatter/duration_extractor.rb b/lib/cucumber/formatter/duration_extractor.rb index d38a921c5..5bde28c91 100644 --- a/lib/cucumber/formatter/duration_extractor.rb +++ b/lib/cucumber/formatter/duration_extractor.rb @@ -27,8 +27,6 @@ def exception(*) end def duration(duration, *) duration.tap { |dur| @result_duration = dur.nanoseconds / 10**9.0 } end - - def attach(*) end end end end diff --git a/lib/cucumber/formatter/fanout.rb b/lib/cucumber/formatter/fanout.rb deleted file mode 100644 index 3c907ca5d..000000000 --- a/lib/cucumber/formatter/fanout.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Cucumber - module Formatter - # Forwards any messages sent to this object to all recipients - # that respond to that message. - class Fanout < BasicObject - attr_reader :recipients - private :recipients - - def initialize(recipients) - @recipients = recipients - end - - def method_missing(message, *args) - super unless recipients - - recipients.each do |recipient| - recipient.send(message, *args) if recipient.respond_to?(message) - end - end - - def respond_to_missing?(name, include_private = false) - recipients.any? { |recipient| recipient.respond_to?(name, include_private) } || super(name, include_private) - end - end - end -end diff --git a/lib/cucumber/formatter/json.rb b/lib/cucumber/formatter/json.rb index 61fdcd010..4942dbc89 100644 --- a/lib/cucumber/formatter/json.rb +++ b/lib/cucumber/formatter/json.rb @@ -22,6 +22,7 @@ def initialize(config) 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 :test_run_finished, &method(:on_test_run_finished) + config.on_event :attach_called, &method(:on_attach_called) end def on_test_case_started(event) @@ -87,18 +88,19 @@ def on_test_run_finished(_event) @io.write(JSON.pretty_generate(@feature_hashes)) end - def attach(src, mime_type, _filename, _streamed_file) - if mime_type == 'text/x.cucumber.log+plain' - test_step_output << src + def on_attach_called(event) + if event.media_type == 'text/x.cucumber.log+plain' + test_step_output << event.src return end - if mime_type =~ /;base64$/ - mime_type = mime_type[0..-8] - data = src + if event.media_type =~ /;base64$/ + media_type = event.media_type[0..-8] + data = event.src else - data = encode64(src) + data = encode64(event.src) + media_type = event.media_type end - test_step_embeddings << { mime_type: mime_type, data: data } + test_step_embeddings << { mime_type: media_type, data: data } end private diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 8aca52aef..c2a9e1f95 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -48,6 +48,7 @@ def initialize(config) 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 :attach_called, &method(:on_attach_called) config.on_event :envelope, &method(:on_envelope) end @@ -55,31 +56,31 @@ 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, streamed_file) + def on_attach_called(event) attachment_data = if @current_test_run_hook_started_id.nil? { test_step_id: @current_test_step_id, test_case_started_id: @current_test_case_started_id, - media_type: media_type, - file_name: filename, + media_type: event.media_type, + file_name: event.filename, timestamp: time_to_timestamp(Time.now) } else { test_run_hook_started_id: @current_test_run_hook_started_id, - media_type: media_type, - file_name: filename, + media_type: event.media_type, + file_name: event.filename, timestamp: time_to_timestamp(Time.now) } end - if streamed_file + if event.streamed_file attachment_data[:content_encoding] = Cucumber::Messages::AttachmentContentEncoding::BASE64 - attachment_data[:body] = Base64.strict_encode64(src) + attachment_data[:body] = Base64.strict_encode64(event.src) else attachment_data[:content_encoding] = Cucumber::Messages::AttachmentContentEncoding::IDENTITY - attachment_data[:body] = src.is_a?(Hash) ? src.to_json : src + attachment_data[:body] = event.src.is_a?(Hash) ? event.src.to_json : event.src end message = Cucumber::Messages::Envelope.new(attachment: Cucumber::Messages::Attachment.new(**attachment_data)) diff --git a/lib/cucumber/formatter/pretty.rb b/lib/cucumber/formatter/pretty.rb index 8feb6b89d..ada962bc0 100644 --- a/lib/cucumber/formatter/pretty.rb +++ b/lib/cucumber/formatter/pretty.rb @@ -64,6 +64,7 @@ def bind_events(config) config.on_event :test_case_finished, &method(:on_test_case_finished) config.on_event :test_run_finished, &method(:on_test_run_finished) config.on_event :undefined_parameter_type, &method(:collect_undefined_parameter_type_names) + config.on_event :attach_called, &method(:on_attach_called) end def on_gherkin_source_read(event) @@ -137,13 +138,13 @@ def on_test_run_finished(_event) print_summary end - def attach(src, media_type, filename, _streamed_file) - return unless media_type == 'text/x.cucumber.log+plain' + def on_attach_called(event) + return unless event.media_type == 'text/x.cucumber.log+plain' - if filename - @test_step_output.push("#{filename}: #{src}") + if event.filename + @test_step_output.push("#{event.filename}: #{event.src}") else - @test_step_output.push(src) + @test_step_output.push(event.src) end end diff --git a/lib/cucumber/formatter/progress.rb b/lib/cucumber/formatter/progress.rb index 0526b749a..23f25d55f 100644 --- a/lib/cucumber/formatter/progress.rb +++ b/lib/cucumber/formatter/progress.rb @@ -36,6 +36,7 @@ def initialize(config) config.on_event :test_case_finished, &method(:on_test_case_finished) config.on_event :test_run_finished, &method(:on_test_run_finished) config.on_event :undefined_parameter_type, &method(:collect_undefined_parameter_type_names) + config.on_event :attach_called, &method(:on_attach_called) end def on_step_activated(event) diff --git a/lib/cucumber/runtime.rb b/lib/cucumber/runtime.rb index 70a7a68f8..3a7f28244 100644 --- a/lib/cucumber/runtime.rb +++ b/lib/cucumber/runtime.rb @@ -6,7 +6,6 @@ require 'cucumber/formatter/duration' require 'cucumber/file_specs' require 'cucumber/filters' -require 'cucumber/formatter/fanout' require 'cucumber/gherkin/i18n' require 'cucumber/glue/registry_wrapper' require 'cucumber/step_match_search' @@ -72,8 +71,7 @@ def run! load_step_definitions fire_install_plugin_hook - # TODO: can we remove this state? - self.visitor = report + create_formatters receiver = Test::Runner.new(@configuration.event_bus) compile features, receiver, filters, @configuration.event_bus @@ -198,13 +196,18 @@ def set_encoding require 'cucumber/formatter/publish_banner_printer' require 'cucumber/core/report/summary' - def report - return @report if @report - - reports = [message_builder, summary_report, global_hooks_summary_report] + formatters - reports << fail_fast_report if @configuration.fail_fast? - reports << publish_banner_printer unless @configuration.publish_quiet? - @report ||= Formatter::Fanout.new(reports) + def create_formatters + # Until all messages are generated at the source the message_builder + # is necessary + message_builder + # summary_report and global_hooks_summary_report is use to determine + # the exit code + summary_report + global_hooks_summary_report + # the formatters defined by the cli options + formatters + fail_fast_report if @configuration.fail_fast? + publish_banner_printer unless @configuration.publish_quiet? end def message_builder diff --git a/lib/cucumber/runtime/user_interface.rb b/lib/cucumber/runtime/user_interface.rb index 1621d87f8..5d7da8893 100644 --- a/lib/cucumber/runtime/user_interface.rb +++ b/lib/cucumber/runtime/user_interface.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require 'timeout' +require 'cucumber/events/attach_called' module Cucumber class Runtime module UserInterface - attr_writer :visitor + attr_writer :configuration # Suspends execution and prompts +question+ to the console (STDOUT). # An operator (manual tester) can then enter a line of text and hit @@ -42,7 +43,8 @@ def ask(question, timeout_seconds) # The embedded data may or may not be ignored, depending on what kind of formatter(s) are active. # def attach(src, media_type, filename, streamed_file) - @visitor.attach(src, media_type, filename, streamed_file) + @configuration.notify(:attach_called, src, media_type, filename, streamed_file) + self end private diff --git a/spec/cucumber/formatter/spec_helper.rb b/spec/cucumber/formatter/spec_helper.rb index ba9d14ee1..7be8752d8 100644 --- a/spec/cucumber/formatter/spec_helper.rb +++ b/spec/cucumber/formatter/spec_helper.rb @@ -21,7 +21,8 @@ module SpecHelper def run_defined_feature define_steps - actual_runtime.visitor = Fanout.new([actual_runtime.send(:message_builder), @formatter]) + # Make sure that the MessageBuilder has been created + actual_runtime.send(:message_builder) receiver = Test::Runner.new(event_bus) event_bus.gherkin_source_read(gherkin_doc.uri, gherkin_doc.body) From c686d02a621a1e1eb43697b28b4d28911386185d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 8 Jun 2026 14:23:32 +0100 Subject: [PATCH 06/11] Remove redundant param now we have a better check --- lib/cucumber/events/attach_called.rb | 5 +---- lib/cucumber/glue/proto_world.rb | 9 ++++----- lib/cucumber/runtime/user_interface.rb | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/cucumber/events/attach_called.rb b/lib/cucumber/events/attach_called.rb index ec8c95752..f53cf55c7 100644 --- a/lib/cucumber/events/attach_called.rb +++ b/lib/cucumber/events/attach_called.rb @@ -5,7 +5,7 @@ module Cucumber module Events # Fired when attach is called in a step definition - class AttachCalled < Core::Event.new(:src, :media_type, :filename, :streamed_file) + class AttachCalled < Core::Event.new(:src, :media_type, :filename) # The attachment body attr_reader :src @@ -14,9 +14,6 @@ class AttachCalled < Core::Event.new(:src, :media_type, :filename, :streamed_fil # An optional filename attr_reader :filename - - # Whether the file is streamed or not - attr_reader :streamed_file end end end diff --git a/lib/cucumber/glue/proto_world.rb b/lib/cucumber/glue/proto_world.rb index a797d34b8..6a0204abf 100644 --- a/lib/cucumber/glue/proto_world.rb +++ b/lib/cucumber/glue/proto_world.rb @@ -93,12 +93,11 @@ def attach(file, media_type = nil, filename = nil) if File.file?(file) media_type = MiniMime.lookup_by_filename(file)&.content_type if media_type.nil? file = File.read(file, mode: 'rb') - streamed_file = true end # We pass in the concept of whether the file is streamed to ensure that the envelope encoding is correct - super(file, media_type, filename, streamed_file) + super(file, media_type, filename) rescue StandardError - super(file, media_type, filename, streamed_file) + super(file, media_type, filename) end # Mark the matched step as pending. @@ -155,8 +154,8 @@ def add_modules!(world_modules, namespaced_world_modules) runtime.ask(question, timeout_seconds) end - define_method(:attach) do |file, media_type, filename, streamed_file| - runtime.attach(file, media_type, filename, streamed_file) + define_method(:attach) do |file, media_type, filename| + runtime.attach(file, media_type, filename) end # Prints the list of modules that are included in the World diff --git a/lib/cucumber/runtime/user_interface.rb b/lib/cucumber/runtime/user_interface.rb index 5d7da8893..51471dddf 100644 --- a/lib/cucumber/runtime/user_interface.rb +++ b/lib/cucumber/runtime/user_interface.rb @@ -42,8 +42,8 @@ def ask(question, timeout_seconds) # be a path to a file, or if it's an image it may also be a Base64 encoded image. # The embedded data may or may not be ignored, depending on what kind of formatter(s) are active. # - def attach(src, media_type, filename, streamed_file) - @configuration.notify(:attach_called, src, media_type, filename, streamed_file) + def attach(src, media_type, filename) + @configuration.notify(:attach_called, src, media_type, filename) self end From d24ea5fcb201a838fe255335410107f991e7cfa9 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 8 Jun 2026 14:29:18 +0100 Subject: [PATCH 07/11] Use common inheritance for new thin-layer module to share commonalities --- lib/cucumber/formatter/html.rb | 2 ++ lib/cucumber/formatter/message_builder.rb | 5 ++++- lib/cucumber/formatter/message_handlers.rb | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 lib/cucumber/formatter/message_handlers.rb diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index 4e619cda5..94da608a7 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -8,6 +8,7 @@ module Cucumber module Formatter class HTML include Io + include MessageHandlers def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) @@ -17,6 +18,7 @@ def initialize(config) end def output_envelope(event) + store_current_test_run_hook_started_id(event) envelope = event.envelope @html_formatter.write_message(envelope) # TODO: Move this conditional logic into the HTML formatter proper diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index c2a9e1f95..684da96d3 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -6,11 +6,14 @@ require 'cucumber/formatter/backtrace_filter' require 'cucumber/query' +require_relative 'message_handlers' + module Cucumber module Formatter class MessageBuilder include Cucumber::Messages::Helpers::TimeConversion include Io + include MessageHandlers include Console def initialize(config) @@ -53,7 +56,7 @@ def initialize(config) 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 + store_current_test_run_hook_started_id(event) end def on_attach_called(event) diff --git a/lib/cucumber/formatter/message_handlers.rb b/lib/cucumber/formatter/message_handlers.rb new file mode 100644 index 000000000..4379fac27 --- /dev/null +++ b/lib/cucumber/formatter/message_handlers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Cucumber + module Formatter + # Common Message Handlers to be used across all message-based formatters + # Designed to work solely with events of type `Envelope` + module MessageHandlers + def store_current_test_run_hook_started_id(event) + @current_test_run_hook_started_id = event.envelope.test_run_hook_started.id if event.envelope.test_run_hook_started + end + end + end +end From 5ddebf58ee4f1a0f6851c47cce8b4c26a54933f2 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 8 Jun 2026 14:30:03 +0100 Subject: [PATCH 08/11] Fix missing require --- lib/cucumber/formatter/html.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index 94da608a7..8f694fd60 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -3,6 +3,7 @@ require 'cucumber/html_formatter' require_relative 'io' +require_relative 'message_handlers' module Cucumber module Formatter From 00923a8f998663857a021e9ed62fb8f842a934d8 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 8 Jun 2026 14:40:22 +0100 Subject: [PATCH 09/11] Fix bad merge --- lib/cucumber/formatter/message_builder.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 684da96d3..e9aa13004 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -78,7 +78,9 @@ def on_attach_called(event) } end - if event.streamed_file + streamed_file = event.src.encoding == Encoding::BINARY + + if streamed_file attachment_data[:content_encoding] = Cucumber::Messages::AttachmentContentEncoding::BASE64 attachment_data[:body] = Base64.strict_encode64(event.src) else From 1f597f2399ac3930ba76da5b1d2a1bddeea209dc Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Mon, 8 Jun 2026 14:46:19 +0100 Subject: [PATCH 10/11] Use prior inheritance from equally bad merge --- lib/cucumber/formatter/html.rb | 14 +++++--------- spec/cucumber/glue/step_definition_spec.rb | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/cucumber/formatter/html.rb b/lib/cucumber/formatter/html.rb index 8f694fd60..67637518d 100644 --- a/lib/cucumber/formatter/html.rb +++ b/lib/cucumber/formatter/html.rb @@ -2,24 +2,20 @@ require 'cucumber/html_formatter' -require_relative 'io' -require_relative 'message_handlers' +require_relative 'message_builder' module Cucumber module Formatter - class HTML - include Io - include MessageHandlers - + class HTML < MessageBuilder def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io) @html_formatter.write_pre_message - config.on_event :envelope, &method(:output_envelope) + super(config) end - def output_envelope(event) - store_current_test_run_hook_started_id(event) + def on_envelope(event) + super(event) envelope = event.envelope @html_formatter.write_message(envelope) # TODO: Move this conditional logic into the HTML formatter proper diff --git a/spec/cucumber/glue/step_definition_spec.rb b/spec/cucumber/glue/step_definition_spec.rb index 89514268c..05f1d4220 100644 --- a/spec/cucumber/glue/step_definition_spec.rb +++ b/spec/cucumber/glue/step_definition_spec.rb @@ -191,14 +191,14 @@ def step_match(text) describe '#log' do it 'calls "attach" with the correct media type' do - expect(user_interface).to receive(:attach).with('wasup', 'text/x.cucumber.log+plain', nil, nil) + expect(user_interface).to receive(:attach).with('wasup', 'text/x.cucumber.log+plain', nil) dsl.Given('Loud') { log 'wasup' } run_step 'Loud' end it 'calls `to_s` if the message is not a String' do - expect(user_interface).to receive(:attach).with('["Not", 1, "string"]', 'text/x.cucumber.log+plain', nil, nil) + expect(user_interface).to receive(:attach).with('["Not", 1, "string"]', 'text/x.cucumber.log+plain', nil) dsl.Given('Loud') { log ['Not', 1, 'string'] } run_step 'Loud' From 4f427cbdf63ae0b93909b17f1e7ee9f975a34d8c Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Fri, 19 Jun 2026 13:20:58 +0100 Subject: [PATCH 11/11] Add changelog; --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b1fe6fd..cd47d4a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo ## [Unreleased] ### Fixed +- Fixed issue with `html-formatter` where attachments and envelopes were causing the entire message pool to be blank ([#1891](https://github.com/cucumber/cucumber-ruby/pull/1891)) [luke-hill](https://github.com/luke-hill) - Show failed step error details in the summary formatter output - Fixed up JRuby examples which weren't running due to anglicisation issues (Pivoted to use English step definitions to help JRuby testing) - Fixed up Arabic example which had some incorrect logic for step definition matching (Due to RTL nature of the language)