From db3fe15bc97ae7a12ec3c6c572ed82ef000b1d42 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 16:25:36 +0100 Subject: [PATCH 1/6] Remove all traces of strict --- lib/cucumber/core/report/summary.rb | 4 +- lib/cucumber/core/test/result.rb | 72 ++-------- spec/cucumber/core/report/summary_spec.rb | 12 +- spec/cucumber/core/test/result_spec.rb | 154 ++-------------------- 4 files changed, 28 insertions(+), 214 deletions(-) diff --git a/lib/cucumber/core/report/summary.rb b/lib/cucumber/core/report/summary.rb index 16bd431a..f7ce6539 100644 --- a/lib/cucumber/core/report/summary.rb +++ b/lib/cucumber/core/report/summary.rb @@ -13,8 +13,8 @@ def initialize(event_bus) subscribe_to(event_bus) end - def ok?(strict: Test::Result::StrictConfiguration.new) - test_cases.ok?(strict: strict) + def ok? + test_cases.ok? end private diff --git a/lib/cucumber/core/test/result.rb b/lib/cucumber/core/test/result.rb index d8e20a00..cd406d0e 100644 --- a/lib/cucumber/core/test/result.rb +++ b/lib/cucumber/core/test/result.rb @@ -8,11 +8,10 @@ module Core module Test module Result TYPES = %i[failed ambiguous flaky skipped undefined pending passed unknown].freeze - STRICT_AFFECTED_TYPES = %i[flaky undefined pending].freeze - def self.ok?(type, strict: StrictConfiguration.new) + def self.ok?(type) class_name = type.to_s.slice(0, 1).capitalize + type.to_s.slice(1..-1) - const_get(class_name).ok?(strict: strict.strict?(type)) + const_get(class_name).ok? end # Defines to_sym on a result class for the given result type @@ -161,8 +160,8 @@ def with_filtered_backtrace(filter) # reporting result type for test cases that fails and the passes on # retry, therefore only the class method self.ok? is needed. class Flaky - def self.ok?(strict: false) - !strict + def self.ok? + false end end @@ -199,8 +198,8 @@ def with_filtered_backtrace(filter) filter.new(dup).exception end - def ok?(strict: StrictConfiguration.new) - self.class.ok?(strict: strict.strict?(to_sym)) + def ok? + self.class.ok? end end @@ -232,8 +231,8 @@ def to_message class Undefined < Raisable include Result.query_methods :undefined - def self.ok?(strict: false) - !strict + def self.ok? + false end def describe_to(visitor, *) @@ -282,8 +281,8 @@ def to_message class Pending < Raisable include Result.query_methods :pending - def self.ok?(strict: false) - !strict + def self.ok? + false end def describe_to(visitor, *) @@ -304,53 +303,6 @@ def to_message end end - # Handles the strict settings for the result types that are - # affected by the strict options (that is the STRICT_AFFECTED_TYPES). - class StrictConfiguration - attr_accessor :settings - private :settings - - def initialize(strict_types = []) - @settings = STRICT_AFFECTED_TYPES.to_h { |t| [t, :default] } - strict_types.each do |type| - set_strict(true, type) - end - end - - def strict?(type = nil) - if type.nil? - settings.each_value do |value| - return true if value == true - end - false - else - return false unless settings.key?(type) - return false unless set?(type) - - settings[type] - end - end - - def set_strict(setting, type = nil) - if type.nil? - STRICT_AFFECTED_TYPES.each { |type| set_strict(setting, type) } - else - settings[type] = setting - end - end - - def merge!(other) - settings.each_key do |type| - set_strict(other.strict?(type), type) if other.set?(type) - end - self - end - - def set?(type) - settings[type] != :default - end - end - # # An object that responds to the description protocol from the results and collects summary information. # @@ -381,9 +333,9 @@ def respond_to_missing?(*) true end - def ok?(strict: StrictConfiguration.new) + def ok? TYPES.each do |type| - return false if get_total(type).positive? && !Result.ok?(type, strict: strict) + return false if get_total(type).positive? && !Result.ok?(type) end true end diff --git a/spec/cucumber/core/report/summary_spec.rb b/spec/cucumber/core/report/summary_spec.rb index 0adef527..f6882244 100644 --- a/spec/cucumber/core/report/summary_spec.rb +++ b/spec/cucumber/core/report/summary_spec.rb @@ -150,20 +150,16 @@ expect(summary.ok?).to be false end - it 'pending test cases are not ok if strict is configured for pending tests' do + it 'pending test cases are not ok' do event_bus.send(:test_case_finished, test_case, pending_result) - expect(summary.ok?).to be true - strict = Cucumber::Core::Test::Result::StrictConfiguration.new([:pending]) - expect(summary.ok?(strict: strict)).to be false + expect(summary.ok?).to be false end - it 'undefined test cases are not ok if strict is configured for undefined tests' do + it 'undefined test cases are not ok' do event_bus.send(:test_case_finished, test_case, undefined_result) - expect(summary.ok?).to be true - strict = Cucumber::Core::Test::Result::StrictConfiguration.new([:undefined]) - expect(summary.ok?(strict: strict)).to be false + expect(summary.ok?).to be false end end end diff --git a/spec/cucumber/core/test/result_spec.rb b/spec/cucumber/core/test/result_spec.rb index ccd4093c..da42a9eb 100644 --- a/spec/cucumber/core/test/result_spec.rb +++ b/spec/cucumber/core/test/result_spec.rb @@ -225,7 +225,6 @@ module Test describe Result::Undefined do subject(:result) { described_class.new } - let(:undefined_strictness) { Result::StrictConfiguration.new([:undefined]) } it 'describes itself to a visitor' do expect(visitor).to receive(:undefined).with(args) @@ -246,9 +245,8 @@ module Test it { expect(result).not_to be_unknown } it { expect(result).not_to be_skipped } it { expect(result).not_to be_flaky } - it { expect(result).to be_ok } - it { expect(result).not_to be_ok(strict: undefined_strictness) } + it { expect(result).not_to be_ok } end describe Result::Skipped do @@ -278,7 +276,6 @@ module Test describe Result::Pending do subject(:result) { described_class.new } - let(:strict_configuration) { Result::StrictConfiguration.new([:pending]) } it 'describes itself to a visitor' do expect(visitor).to receive(:pending).with(result, args) @@ -299,138 +296,12 @@ module Test it { expect(result).not_to be_unknown } it { expect(result).not_to be_skipped } it { expect(result).not_to be_flaky } - it { expect(result).to be_ok } - it { expect(result).not_to be_ok(strict: strict_configuration) } + it { expect(result).not_to be_ok } end describe Result::Flaky do - it { expect(described_class).to be_ok(strict: false) } - it { expect(described_class).not_to be_ok(strict: true) } - end - - describe Result::StrictConfiguration do - subject(:strict_configuration) { described_class.new } - - describe '#set_strict' do - context 'without a type argument' do - it 'can set undefined to be strict' do - strict_configuration.set_strict(true) - - expect(strict_configuration).to be_strict(:undefined) - end - - it 'can set pending to be strict' do - strict_configuration.set_strict(true) - - expect(strict_configuration).to be_strict(:pending) - end - - it 'can set flaky to be strict' do - strict_configuration.set_strict(true) - - expect(strict_configuration).to be_strict(:flaky) - end - - it 'can set undefined not to be strict' do - strict_configuration.set_strict(false) - - expect(strict_configuration).not_to be_strict(:undefined) - end - - it 'can set pending not to be strict' do - strict_configuration.set_strict(false) - - expect(strict_configuration).not_to be_strict(:pending) - end - - it 'can set flaky not to be strict' do - strict_configuration.set_strict(false) - - expect(strict_configuration).not_to be_strict(:flaky) - end - end - - context 'with a type argument' do - it 'can set undefined only to be strict' do - strict_configuration.set_strict(true, :undefined) - - expect(strict_configuration).to be_strict(:undefined) - end - - it 'can set pending only to be strict' do - strict_configuration.set_strict(true, :pending) - - expect(strict_configuration).to be_strict(:pending) - end - - it 'can set flaky only to be strict' do - strict_configuration.set_strict(true, :flaky) - - expect(strict_configuration).to be_strict(:flaky) - end - - it 'can set undefined only not to be strict' do - strict_configuration.set_strict(false, :undefined) - - expect(strict_configuration).not_to be_strict(:undefined) - end - - it 'can set pending only not to be strict' do - strict_configuration.set_strict(false, :pending) - - expect(strict_configuration).not_to be_strict(:pending) - end - - it 'can set flaky only not to be strict' do - strict_configuration.set_strict(false, :flaky) - - expect(strict_configuration).not_to be_strict(:flaky) - end - end - end - - describe '#strict?' do - context 'without a type argument' do - it 'returns true if any result type is set to strict' do - strict_configuration.set_strict(false, :pending) - expect(strict_configuration).not_to be_strict - - strict_configuration.set_strict(true, :flaky) - expect(strict_configuration).to be_strict - end - end - - context 'with a type argument' do - it 'returns true if the specified result type is set to strict' do - strict_configuration.set_strict(false, :pending) - strict_configuration.set_strict(true, :flaky) - - expect(strict_configuration).not_to be_strict(:undefined) - expect(strict_configuration).not_to be_strict(:pending) - expect(strict_configuration).to be_strict(:flaky) - end - end - end - - describe '#merge!' do - let(:merged_configuration) { described_class.new } - - before do - strict_configuration.set_strict(false, :undefined) - strict_configuration.set_strict(false, :pending) - strict_configuration.set_strict(true, :flaky) - merged_configuration.set_strict(true, :pending) - merged_configuration.set_strict(false, :flaky) - strict_configuration.merge!(merged_configuration) - end - - it 'sets the not default values from the argument accordingly' do - expect(strict_configuration).not_to be_strict(:undefined) - expect(strict_configuration).to be_strict(:pending) - expect(strict_configuration).not_to be_strict(:flaky) - end - end + it { expect(described_class).not_to be_ok } end describe Result::Summary do @@ -544,27 +415,22 @@ def describe_to(visitor, *args) expect(summary.ok?).to be false end - it 'pending result is ok if not strict' do + it 'pending result is not ok' do pending.describe_to(summary) - expect(summary.ok?).to be true - strict = Result::StrictConfiguration.new([:pending]) - expect(summary.ok?(strict: strict)).to be false + expect(summary.ok?).to be false end - it 'undefined result is ok if not strict' do + it 'undefined result is not ok' do undefined.describe_to(summary) - expect(summary.ok?).to be true - strict = Result::StrictConfiguration.new([:undefined]) - expect(summary.ok?(strict: strict)).to be false + expect(summary.ok?).to be false end - it 'flaky result is ok if not strict' do + it 'flaky result is not ok' do summary.flaky - expect(summary.ok?).to be true - strict = Result::StrictConfiguration.new([:flaky]) - expect(summary.ok?(strict: strict)).to be false + + expect(summary.ok?).to be false end end end From 3b1e36a171021e41d81d0a67f7f65440f20fac87 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 16:32:38 +0100 Subject: [PATCH 2/6] Split out a bunch of the results into their own requisite class files --- lib/cucumber/core/test/result.rb | 283 +-------------------- lib/cucumber/core/test/result/ambiguous.rb | 37 +++ lib/cucumber/core/test/result/failed.rb | 72 ++++++ lib/cucumber/core/test/result/flaky.rb | 21 ++ lib/cucumber/core/test/result/passed.rb | 57 +++++ lib/cucumber/core/test/result/pending.rb | 37 +++ lib/cucumber/core/test/result/raisable.rb | 50 ++++ lib/cucumber/core/test/result/skipped.rb | 37 +++ lib/cucumber/core/test/result/undefined.rb | 37 +++ lib/cucumber/core/test/result/unknown.rb | 32 +++ 10 files changed, 391 insertions(+), 272 deletions(-) create mode 100644 lib/cucumber/core/test/result/ambiguous.rb create mode 100644 lib/cucumber/core/test/result/failed.rb create mode 100644 lib/cucumber/core/test/result/flaky.rb create mode 100644 lib/cucumber/core/test/result/passed.rb create mode 100644 lib/cucumber/core/test/result/pending.rb create mode 100644 lib/cucumber/core/test/result/raisable.rb create mode 100644 lib/cucumber/core/test/result/skipped.rb create mode 100644 lib/cucumber/core/test/result/undefined.rb create mode 100644 lib/cucumber/core/test/result/unknown.rb diff --git a/lib/cucumber/core/test/result.rb b/lib/cucumber/core/test/result.rb index cd406d0e..e6044443 100644 --- a/lib/cucumber/core/test/result.rb +++ b/lib/cucumber/core/test/result.rb @@ -3,6 +3,17 @@ require 'cucumber/messages' require 'cucumber/messages/helpers/time_conversion' +require_relative 'result/raisable' + +require_relative 'result/ambiguous' +require_relative 'result/failed' +require_relative 'result/flaky' +require_relative 'result/passed' +require_relative 'result/pending' +require_relative 'result/skipped' +require_relative 'result/undefined' +require_relative 'result/unknown' + module Cucumber module Core module Test @@ -31,278 +42,6 @@ def self.query_methods(result_type) end end - # Null object for results. Represents the state where we haven't run anything yet - class Unknown - include Result.query_methods :unknown - - def describe_to(_visitor, *_args) - self - end - - def with_filtered_backtrace(_filter) - self - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::UNKNOWN, - duration: UnknownDuration.new.to_message_duration - ) - end - end - - class Passed - include Result.query_methods :passed - - attr_accessor :duration - - def self.ok?(*) - true - end - - def initialize(duration) - raise ArgumentError unless duration - - @duration = duration - end - - def describe_to(visitor, *) - visitor.passed(*) - visitor.duration(duration, *) - self - end - - def to_s - '✓' - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::PASSED, - duration: duration.to_message_duration - ) - end - - def ok?(*) - self.class.ok? - end - - def with_appended_backtrace(_step) - self - end - - def with_filtered_backtrace(_filter) - self - end - end - - class Failed - include Result.query_methods :failed - - attr_reader :duration, :exception - - def self.ok?(*) - false - end - - def initialize(duration, exception) - raise ArgumentError unless duration - raise ArgumentError unless exception - - @duration = duration - @exception = exception - end - - def describe_to(visitor, *) - visitor.failed(*) - visitor.duration(duration, *) - visitor.exception(exception, *) if exception - self - end - - def to_s - '✗' - end - - def to_message - begin - message = exception.backtrace.join("\n") - rescue NoMethodError - message = '' - end - - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::FAILED, - duration: duration.to_message_duration, - message: message - ) - end - - def ok?(*) - self.class.ok? - end - - def with_duration(new_duration) - self.class.new(new_duration, exception) - end - - def with_appended_backtrace(step) - exception.backtrace << step.backtrace_line if step.respond_to?(:backtrace_line) - self - end - - def with_filtered_backtrace(filter) - self.class.new(duration, filter.new(exception.dup).exception) - end - end - - # Flaky is not used directly as an execution result, but is used as a - # reporting result type for test cases that fails and the passes on - # retry, therefore only the class method self.ok? is needed. - class Flaky - def self.ok? - false - end - end - - # Base class for exceptions that can be raised in a step definition causing the step to have that result. - class Raisable < StandardError - attr_reader :message, :duration - - def initialize(message = '', duration = UnknownDuration.new, backtrace = nil) - @message = message - @duration = duration - super(message) - set_backtrace(backtrace) if backtrace - end - - def with_message(new_message) - self.class.new(new_message, duration, backtrace) - end - - def with_duration(new_duration) - self.class.new(message, new_duration, backtrace) - end - - def with_appended_backtrace(step) - return self unless step.respond_to?(:backtrace_line) - - set_backtrace([]) unless backtrace - backtrace << step.backtrace_line - self - end - - def with_filtered_backtrace(filter) - return self unless backtrace - - filter.new(dup).exception - end - - def ok? - self.class.ok? - end - end - - class Ambiguous < Raisable - include Result.query_methods :ambiguous - - def self.ok?(*) - false - end - - def describe_to(visitor, *) - visitor.ambiguous(*) - visitor.duration(duration, *) - self - end - - def to_s - 'A' - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::AMBIGUOUS, - duration: duration.to_message_duration - ) - end - end - - class Undefined < Raisable - include Result.query_methods :undefined - - def self.ok? - false - end - - def describe_to(visitor, *) - visitor.undefined(*) - visitor.duration(duration, *) - self - end - - def to_s - '?' - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::UNDEFINED, - duration: duration.to_message_duration - ) - end - end - - class Skipped < Raisable - include Result.query_methods :skipped - - def self.ok?(*) - true - end - - def describe_to(visitor, *) - visitor.skipped(*) - visitor.duration(duration, *) - self - end - - def to_s - '-' - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::SKIPPED, - duration: duration.to_message_duration - ) - end - end - - class Pending < Raisable - include Result.query_methods :pending - - def self.ok? - false - end - - def describe_to(visitor, *) - visitor.pending(self, *) - visitor.duration(duration, *) - self - end - - def to_s - 'P' - end - - def to_message - Cucumber::Messages::TestStepResult.new( - status: Cucumber::Messages::TestStepResultStatus::PENDING, - duration: duration.to_message_duration - ) - end - end - # # An object that responds to the description protocol from the results and collects summary information. # diff --git a/lib/cucumber/core/test/result/ambiguous.rb b/lib/cucumber/core/test/result/ambiguous.rb new file mode 100644 index 00000000..c0777e78 --- /dev/null +++ b/lib/cucumber/core/test/result/ambiguous.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Ambiguous < Raisable + include Result.query_methods :ambiguous + + def self.ok?(*) + false + end + + def describe_to(visitor, *) + visitor.ambiguous(*) + visitor.duration(duration, *) + self + end + + def to_s + 'A' + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::AMBIGUOUS, + duration: duration.to_message_duration + ) + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/failed.rb b/lib/cucumber/core/test/result/failed.rb new file mode 100644 index 00000000..f9c27927 --- /dev/null +++ b/lib/cucumber/core/test/result/failed.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Failed + include Result.query_methods :failed + + attr_reader :duration, :exception + + def self.ok?(*) + false + end + + def initialize(duration, exception) + raise ArgumentError unless duration + raise ArgumentError unless exception + + @duration = duration + @exception = exception + end + + def describe_to(visitor, *) + visitor.failed(*) + visitor.duration(duration, *) + visitor.exception(exception, *) if exception + self + end + + def to_s + '✗' + end + + def to_message + begin + message = exception.backtrace.join("\n") + rescue NoMethodError + message = '' + end + + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::FAILED, + duration: duration.to_message_duration, + message: message + ) + end + + def ok?(*) + self.class.ok? + end + + def with_duration(new_duration) + self.class.new(new_duration, exception) + end + + def with_appended_backtrace(step) + exception.backtrace << step.backtrace_line if step.respond_to?(:backtrace_line) + self + end + + def with_filtered_backtrace(filter) + self.class.new(duration, filter.new(exception.dup).exception) + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/flaky.rb b/lib/cucumber/core/test/result/flaky.rb new file mode 100644 index 00000000..ee350488 --- /dev/null +++ b/lib/cucumber/core/test/result/flaky.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + # Flaky is not used directly as an execution result, but is used as a + # reporting result type for test cases that fails and the passes on + # retry, therefore only the class method self.ok? is needed. + class Flaky + def self.ok? + false + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/passed.rb b/lib/cucumber/core/test/result/passed.rb new file mode 100644 index 00000000..d481ae49 --- /dev/null +++ b/lib/cucumber/core/test/result/passed.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Passed + include Result.query_methods :passed + + attr_accessor :duration + + def self.ok?(*) + true + end + + def initialize(duration) + raise ArgumentError unless duration + + @duration = duration + end + + def describe_to(visitor, *) + visitor.passed(*) + visitor.duration(duration, *) + self + end + + def to_s + '✓' + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::PASSED, + duration: duration.to_message_duration + ) + end + + def ok?(*) + self.class.ok? + end + + def with_appended_backtrace(_step) + self + end + + def with_filtered_backtrace(_filter) + self + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/pending.rb b/lib/cucumber/core/test/result/pending.rb new file mode 100644 index 00000000..7d519138 --- /dev/null +++ b/lib/cucumber/core/test/result/pending.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Pending < Raisable + include Result.query_methods :pending + + def self.ok? + false + end + + def describe_to(visitor, *) + visitor.pending(self, *) + visitor.duration(duration, *) + self + end + + def to_s + 'P' + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::PENDING, + duration: duration.to_message_duration + ) + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/raisable.rb b/lib/cucumber/core/test/result/raisable.rb new file mode 100644 index 00000000..cee85941 --- /dev/null +++ b/lib/cucumber/core/test/result/raisable.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + # Base class for exceptions that can be raised in a step definition causing the step to have that result. + class Raisable < StandardError + attr_reader :message, :duration + + def initialize(message = '', duration = UnknownDuration.new, backtrace = nil) + @message = message + @duration = duration + super(message) + set_backtrace(backtrace) if backtrace + end + + def with_message(new_message) + self.class.new(new_message, duration, backtrace) + end + + def with_duration(new_duration) + self.class.new(message, new_duration, backtrace) + end + + def with_appended_backtrace(step) + return self unless step.respond_to?(:backtrace_line) + + set_backtrace([]) unless backtrace + backtrace << step.backtrace_line + self + end + + def with_filtered_backtrace(filter) + return self unless backtrace + + filter.new(dup).exception + end + + def ok? + self.class.ok? + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/skipped.rb b/lib/cucumber/core/test/result/skipped.rb new file mode 100644 index 00000000..36fc185b --- /dev/null +++ b/lib/cucumber/core/test/result/skipped.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Skipped < Raisable + include Result.query_methods :skipped + + def self.ok?(*) + true + end + + def describe_to(visitor, *) + visitor.skipped(*) + visitor.duration(duration, *) + self + end + + def to_s + '-' + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::SKIPPED, + duration: duration.to_message_duration + ) + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/undefined.rb b/lib/cucumber/core/test/result/undefined.rb new file mode 100644 index 00000000..1cfe23e9 --- /dev/null +++ b/lib/cucumber/core/test/result/undefined.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Undefined < Raisable + include Result.query_methods :undefined + + def self.ok? + false + end + + def describe_to(visitor, *) + visitor.undefined(*) + visitor.duration(duration, *) + self + end + + def to_s + '?' + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::UNDEFINED, + duration: duration.to_message_duration + ) + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/unknown.rb b/lib/cucumber/core/test/result/unknown.rb new file mode 100644 index 00000000..a6c10c23 --- /dev/null +++ b/lib/cucumber/core/test/result/unknown.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + # Null object for results. Represents the state where we haven't run anything yet + class Unknown + include Result.query_methods :unknown + + def describe_to(_visitor, *_args) + self + end + + def with_filtered_backtrace(_filter) + self + end + + def to_message + Cucumber::Messages::TestStepResult.new( + status: Cucumber::Messages::TestStepResultStatus::UNKNOWN, + duration: UnknownDuration.new.to_message_duration + ) + end + end + end + end + end +end From 478b9d9c5f8b231d139f8792c928d4aec195be51 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 16:47:44 +0100 Subject: [PATCH 3/6] Removal of complexity regarding raisable types and query methods - instead favouring overt definition of to_sym casting --- lib/cucumber/core/test/result.rb | 20 ++-------------- lib/cucumber/core/test/result/ambiguous.rb | 6 ++++- .../core/test/result/boolean_methods.rb | 23 +++++++++++++++++++ lib/cucumber/core/test/result/failed.rb | 6 ++++- lib/cucumber/core/test/result/passed.rb | 6 ++++- lib/cucumber/core/test/result/pending.rb | 6 ++++- lib/cucumber/core/test/result/skipped.rb | 6 ++++- lib/cucumber/core/test/result/undefined.rb | 6 ++++- lib/cucumber/core/test/result/unknown.rb | 6 ++++- 9 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 lib/cucumber/core/test/result/boolean_methods.rb diff --git a/lib/cucumber/core/test/result.rb b/lib/cucumber/core/test/result.rb index e6044443..dffe97fb 100644 --- a/lib/cucumber/core/test/result.rb +++ b/lib/cucumber/core/test/result.rb @@ -3,6 +3,8 @@ require 'cucumber/messages' require 'cucumber/messages/helpers/time_conversion' +require_relative 'result/boolean_methods' + require_relative 'result/raisable' require_relative 'result/ambiguous' @@ -25,24 +27,6 @@ def self.ok?(type) const_get(class_name).ok? end - # Defines to_sym on a result class for the given result type - # - # Defines predicate methods on a result class with only the given one returning true - def self.query_methods(result_type) - Module.new do - define_method :to_sym do - result_type - end - - TYPES.each do |possible_result_type| - define_method("#{possible_result_type}?") do - possible_result_type == to_sym - end - end - end - end - - # # An object that responds to the description protocol from the results and collects summary information. # # e.g. diff --git a/lib/cucumber/core/test/result/ambiguous.rb b/lib/cucumber/core/test/result/ambiguous.rb index c0777e78..25e08c2f 100644 --- a/lib/cucumber/core/test/result/ambiguous.rb +++ b/lib/cucumber/core/test/result/ambiguous.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Ambiguous < Raisable - include Result.query_methods :ambiguous + include BooleanMethods def self.ok?(*) false @@ -24,6 +24,10 @@ def to_s 'A' end + def to_sym + :ambiguous + end + def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::AMBIGUOUS, diff --git a/lib/cucumber/core/test/result/boolean_methods.rb b/lib/cucumber/core/test/result/boolean_methods.rb new file mode 100644 index 00000000..be4143e4 --- /dev/null +++ b/lib/cucumber/core/test/result/boolean_methods.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + # Simple module that when included generates all the boolean methods for each category of result + module BooleanMethods + TYPES = %i[failed ambiguous flaky skipped undefined pending passed unknown].freeze + + TYPES.each do |result| + define_method("#{result}?") do + result == to_sym + end + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/failed.rb b/lib/cucumber/core/test/result/failed.rb index f9c27927..a834d720 100644 --- a/lib/cucumber/core/test/result/failed.rb +++ b/lib/cucumber/core/test/result/failed.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Failed - include Result.query_methods :failed + include BooleanMethods attr_reader :duration, :exception @@ -35,6 +35,10 @@ def to_s '✗' end + def to_sym + :failed + end + def to_message begin message = exception.backtrace.join("\n") diff --git a/lib/cucumber/core/test/result/passed.rb b/lib/cucumber/core/test/result/passed.rb index d481ae49..77546ff2 100644 --- a/lib/cucumber/core/test/result/passed.rb +++ b/lib/cucumber/core/test/result/passed.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Passed - include Result.query_methods :passed + include BooleanMethods attr_accessor :duration @@ -32,6 +32,10 @@ def to_s '✓' end + def to_sym + :passed + end + def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::PASSED, diff --git a/lib/cucumber/core/test/result/pending.rb b/lib/cucumber/core/test/result/pending.rb index 7d519138..03b156d0 100644 --- a/lib/cucumber/core/test/result/pending.rb +++ b/lib/cucumber/core/test/result/pending.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Pending < Raisable - include Result.query_methods :pending + include BooleanMethods def self.ok? false @@ -24,6 +24,10 @@ def to_s 'P' end + def to_sym + :pending + end + def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::PENDING, diff --git a/lib/cucumber/core/test/result/skipped.rb b/lib/cucumber/core/test/result/skipped.rb index 36fc185b..afcd1308 100644 --- a/lib/cucumber/core/test/result/skipped.rb +++ b/lib/cucumber/core/test/result/skipped.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Skipped < Raisable - include Result.query_methods :skipped + include BooleanMethods def self.ok?(*) true @@ -24,6 +24,10 @@ def to_s '-' end + def to_sym + :skipped + end + def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::SKIPPED, diff --git a/lib/cucumber/core/test/result/undefined.rb b/lib/cucumber/core/test/result/undefined.rb index 1cfe23e9..1b901aa3 100644 --- a/lib/cucumber/core/test/result/undefined.rb +++ b/lib/cucumber/core/test/result/undefined.rb @@ -8,7 +8,7 @@ module Core module Test module Result class Undefined < Raisable - include Result.query_methods :undefined + include BooleanMethods def self.ok? false @@ -24,6 +24,10 @@ def to_s '?' end + def to_sym + :undefined + end + def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::UNDEFINED, diff --git a/lib/cucumber/core/test/result/unknown.rb b/lib/cucumber/core/test/result/unknown.rb index a6c10c23..aab112a2 100644 --- a/lib/cucumber/core/test/result/unknown.rb +++ b/lib/cucumber/core/test/result/unknown.rb @@ -9,7 +9,7 @@ module Test module Result # Null object for results. Represents the state where we haven't run anything yet class Unknown - include Result.query_methods :unknown + include BooleanMethods def describe_to(_visitor, *_args) self @@ -25,6 +25,10 @@ def to_message duration: UnknownDuration.new.to_message_duration ) end + + def to_sym + :unknown + end end end end From 0fa4e78a446c78e925e155276757d4ee41c873fd Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 16:50:44 +0100 Subject: [PATCH 4/6] More tidyup --- lib/cucumber/core/test/result/ambiguous.rb | 2 +- lib/cucumber/core/test/result/boolean_methods.rb | 5 +++++ lib/cucumber/core/test/result/failed.rb | 6 +----- lib/cucumber/core/test/result/passed.rb | 6 +----- lib/cucumber/core/test/result/raisable.rb | 4 ---- lib/cucumber/core/test/result/skipped.rb | 2 +- lib/cucumber/core/test/result/unknown.rb | 4 ++++ 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/cucumber/core/test/result/ambiguous.rb b/lib/cucumber/core/test/result/ambiguous.rb index 25e08c2f..1195b91a 100644 --- a/lib/cucumber/core/test/result/ambiguous.rb +++ b/lib/cucumber/core/test/result/ambiguous.rb @@ -10,7 +10,7 @@ module Result class Ambiguous < Raisable include BooleanMethods - def self.ok?(*) + def self.ok? false end diff --git a/lib/cucumber/core/test/result/boolean_methods.rb b/lib/cucumber/core/test/result/boolean_methods.rb index be4143e4..78c9c865 100644 --- a/lib/cucumber/core/test/result/boolean_methods.rb +++ b/lib/cucumber/core/test/result/boolean_methods.rb @@ -8,6 +8,7 @@ module Core module Test module Result # Simple module that when included generates all the boolean methods for each category of result + # The single exception to this is the class method `self.ok?` which is defined for each result individually module BooleanMethods TYPES = %i[failed ambiguous flaky skipped undefined pending passed unknown].freeze @@ -16,6 +17,10 @@ module BooleanMethods result == to_sym end end + + def ok? + self.class.ok? + end end end end diff --git a/lib/cucumber/core/test/result/failed.rb b/lib/cucumber/core/test/result/failed.rb index a834d720..208ff76f 100644 --- a/lib/cucumber/core/test/result/failed.rb +++ b/lib/cucumber/core/test/result/failed.rb @@ -12,7 +12,7 @@ class Failed attr_reader :duration, :exception - def self.ok?(*) + def self.ok? false end @@ -53,10 +53,6 @@ def to_message ) end - def ok?(*) - self.class.ok? - end - def with_duration(new_duration) self.class.new(new_duration, exception) end diff --git a/lib/cucumber/core/test/result/passed.rb b/lib/cucumber/core/test/result/passed.rb index 77546ff2..7b6ee119 100644 --- a/lib/cucumber/core/test/result/passed.rb +++ b/lib/cucumber/core/test/result/passed.rb @@ -12,7 +12,7 @@ class Passed attr_accessor :duration - def self.ok?(*) + def self.ok? true end @@ -43,10 +43,6 @@ def to_message ) end - def ok?(*) - self.class.ok? - end - def with_appended_backtrace(_step) self end diff --git a/lib/cucumber/core/test/result/raisable.rb b/lib/cucumber/core/test/result/raisable.rb index cee85941..8d33d63c 100644 --- a/lib/cucumber/core/test/result/raisable.rb +++ b/lib/cucumber/core/test/result/raisable.rb @@ -39,10 +39,6 @@ def with_filtered_backtrace(filter) filter.new(dup).exception end - - def ok? - self.class.ok? - end end end end diff --git a/lib/cucumber/core/test/result/skipped.rb b/lib/cucumber/core/test/result/skipped.rb index afcd1308..c69edb64 100644 --- a/lib/cucumber/core/test/result/skipped.rb +++ b/lib/cucumber/core/test/result/skipped.rb @@ -10,7 +10,7 @@ module Result class Skipped < Raisable include BooleanMethods - def self.ok?(*) + def self.ok? true end diff --git a/lib/cucumber/core/test/result/unknown.rb b/lib/cucumber/core/test/result/unknown.rb index aab112a2..95bb5713 100644 --- a/lib/cucumber/core/test/result/unknown.rb +++ b/lib/cucumber/core/test/result/unknown.rb @@ -11,6 +11,10 @@ module Result class Unknown include BooleanMethods + def self.ok? + false + end + def describe_to(_visitor, *_args) self end From 00c6311aa494a12515966c4646b3c41570bc9e3d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 17:04:16 +0100 Subject: [PATCH 5/6] Refactor out final pieces and use `_with` pragma to keep consistency --- lib/cucumber/core/test/result.rb | 124 +----------------- lib/cucumber/core/test/result/duration.rb | 33 +++++ lib/cucumber/core/test/result/summary.rb | 89 +++++++++++++ .../core/test/result/unknown_duration.rb | 26 ++++ 4 files changed, 151 insertions(+), 121 deletions(-) create mode 100644 lib/cucumber/core/test/result/duration.rb create mode 100644 lib/cucumber/core/test/result/summary.rb create mode 100644 lib/cucumber/core/test/result/unknown_duration.rb diff --git a/lib/cucumber/core/test/result.rb b/lib/cucumber/core/test/result.rb index dffe97fb..61cdf367 100644 --- a/lib/cucumber/core/test/result.rb +++ b/lib/cucumber/core/test/result.rb @@ -8,131 +8,13 @@ require_relative 'result/raisable' require_relative 'result/ambiguous' +require_relative 'result/duration' require_relative 'result/failed' require_relative 'result/flaky' require_relative 'result/passed' require_relative 'result/pending' +require_relative 'result/summary' require_relative 'result/skipped' require_relative 'result/undefined' require_relative 'result/unknown' - -module Cucumber - module Core - module Test - module Result - TYPES = %i[failed ambiguous flaky skipped undefined pending passed unknown].freeze - - def self.ok?(type) - class_name = type.to_s.slice(0, 1).capitalize + type.to_s.slice(1..-1) - const_get(class_name).ok? - end - - # An object that responds to the description protocol from the results and collects summary information. - # - # e.g. - # summary = Result::Summary.new - # Result::Passed.new(0).describe_to(summary) - # puts summary.total_passed - # => 1 - # - class Summary - attr_reader :exceptions, :durations - - def initialize - @totals = Hash.new { 0 } - @exceptions = [] - @durations = [] - end - - def method_missing(name, *_args) - if name =~ /^total_/ - get_total(name) - else - increment_total(name) - end - end - - def respond_to_missing?(*) - true - end - - def ok? - TYPES.each do |type| - return false if get_total(type).positive? && !Result.ok?(type) - end - true - end - - def exception(exception) - @exceptions << exception - self - end - - def duration(duration) - @durations << duration - self - end - - def total(for_status = nil) - if for_status - @totals.fetch(for_status, 0) - else - @totals.values.reduce(0) { |total, count| total + count } - end - end - - def decrement_failed - @totals[:failed] -= 1 - end - - private - - def get_total(method_name) - status = method_name.to_s.gsub('total_', '').to_sym - @totals.fetch(status, 0) - end - - def increment_total(status) - @totals[status] += 1 - self - end - end - - class Duration - include Cucumber::Messages::Helpers::TimeConversion - - attr_reader :nanoseconds - - def initialize(nanoseconds) - @nanoseconds = nanoseconds - end - - def to_message_duration - duration_hash = seconds_to_duration(nanoseconds.to_f / NANOSECONDS_PER_SECOND) - Cucumber::Messages::Duration.new(seconds: duration_hash[:seconds], nanos: duration_hash[:nanos]) - end - - def seconds_to_duration(seconds_float) - seconds, second_modulus = seconds_float.divmod(1) - nanos = second_modulus * NANOSECONDS_PER_SECOND - { seconds: seconds, nanos: nanos.to_i } - end - end - - class UnknownDuration - def tap - self - end - - def nanoseconds - raise '#nanoseconds only allowed to be used in #tap block' - end - - def to_message_duration - Cucumber::Messages::Duration.new(seconds: 0, nanos: 0) - end - end - end - end - end -end +require_relative 'result/unknown_duration' diff --git a/lib/cucumber/core/test/result/duration.rb b/lib/cucumber/core/test/result/duration.rb new file mode 100644 index 00000000..88e424b9 --- /dev/null +++ b/lib/cucumber/core/test/result/duration.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class Duration + include Cucumber::Messages::Helpers::TimeConversion + + attr_reader :nanoseconds + + def initialize(nanoseconds) + @nanoseconds = nanoseconds + end + + def to_message_duration + duration_hash = seconds_to_duration(nanoseconds.to_f / NANOSECONDS_PER_SECOND) + Cucumber::Messages::Duration.new(seconds: duration_hash[:seconds], nanos: duration_hash[:nanos]) + end + + def seconds_to_duration(seconds_float) + seconds, second_modulus = seconds_float.divmod(1) + nanos = second_modulus * NANOSECONDS_PER_SECOND + { seconds: seconds, nanos: nanos.to_i } + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/summary.rb b/lib/cucumber/core/test/result/summary.rb new file mode 100644 index 00000000..80116191 --- /dev/null +++ b/lib/cucumber/core/test/result/summary.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + # An object that responds to the description protocol from the results and collects summary information. + # + # e.g. + # summary = Result::Summary.new + # Result::Passed.new(0).describe_to(summary) + # puts summary.total_passed + # => 1 + # + class Summary + TYPES = %i[failed ambiguous flaky skipped undefined pending passed unknown].freeze + + attr_reader :exceptions, :durations + + def initialize + @totals = Hash.new { 0 } + @exceptions = [] + @durations = [] + end + + def method_missing(name, *_args) + if name =~ /^total_/ + get_total(name) + else + increment_total(name) + end + end + + def respond_to_missing?(*) + true + end + + def ok? + TYPES.each do |type| + return false if get_total(type).positive? && !with_type(type).ok? + end + true + end + + def exception(exception) + @exceptions << exception + self + end + + def duration(duration) + @durations << duration + self + end + + def total(for_status = nil) + if for_status + @totals.fetch(for_status, 0) + else + @totals.values.reduce(0) { |total, count| total + count } + end + end + + def with_type(type) + Object.const_get("Cucumber::Core::Test::Result::#{type.capitalize}") + end + + def decrement_failed + @totals[:failed] -= 1 + end + + private + + def get_total(method_name) + status = method_name.to_s.gsub('total_', '').to_sym + @totals.fetch(status, 0) + end + + def increment_total(status) + @totals[status] += 1 + self + end + end + end + end + end +end diff --git a/lib/cucumber/core/test/result/unknown_duration.rb b/lib/cucumber/core/test/result/unknown_duration.rb new file mode 100644 index 00000000..9b6c935c --- /dev/null +++ b/lib/cucumber/core/test/result/unknown_duration.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'cucumber/messages' +require 'cucumber/messages/helpers/time_conversion' + +module Cucumber + module Core + module Test + module Result + class UnknownDuration + def tap + self + end + + def nanoseconds + raise '#nanoseconds only allowed to be used in #tap block' + end + + def to_message_duration + Cucumber::Messages::Duration.new(seconds: 0, nanos: 0) + end + end + end + end + end +end From 32181b4a9e6e6cdb57e56cd62fa34499769a911d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 26 May 2026 17:16:56 +0100 Subject: [PATCH 6/6] Add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80796d89..1db18b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) for more info on how to contribute to Cucumber. ## [Unreleased] +### Removed +- Strict configuration has been removed from all `Result` classes + ### Changed - Refactored the internal base `Event` class to reduce complexity and make it more flexible for future use (No user facing changes) +- Refactored the `Result` classes to be more simplified and one class per file ## [16.2.0] - 2026-02-06 ### Changed