From d3ced2e449ea7de4647bfb52578b4f50261d471d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 18:54:05 +0100 Subject: [PATCH 01/11] Split gherkin writer helpers into a series of unique modules --- .../core/gherkin/writer/accepts_comments.rb | 23 +++ .../core/gherkin/writer/has_description.rb | 21 ++ .../core/gherkin/writer/has_elements.rb | 47 +++++ .../gherkin/writer/has_options_initializer.rb | 58 ++++++ lib/cucumber/core/gherkin/writer/has_rows.rb | 43 +++++ lib/cucumber/core/gherkin/writer/helpers.rb | 180 +----------------- .../core/gherkin/writer/indentation.rb | 30 +++ 7 files changed, 228 insertions(+), 174 deletions(-) create mode 100644 lib/cucumber/core/gherkin/writer/accepts_comments.rb create mode 100644 lib/cucumber/core/gherkin/writer/has_description.rb create mode 100644 lib/cucumber/core/gherkin/writer/has_elements.rb create mode 100644 lib/cucumber/core/gherkin/writer/has_options_initializer.rb create mode 100644 lib/cucumber/core/gherkin/writer/has_rows.rb create mode 100644 lib/cucumber/core/gherkin/writer/indentation.rb diff --git a/lib/cucumber/core/gherkin/writer/accepts_comments.rb b/lib/cucumber/core/gherkin/writer/accepts_comments.rb new file mode 100644 index 00000000..8956bf56 --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/accepts_comments.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module AcceptsComments + def comment(line) + comment_lines << "# #{line}" + end + + def comment_lines + @comment_lines ||= [] + end + + def slurp_comments + comment_lines.tap { @comment_lines = nil } + end + end + end + end + end +end diff --git a/lib/cucumber/core/gherkin/writer/has_description.rb b/lib/cucumber/core/gherkin/writer/has_description.rb new file mode 100644 index 00000000..f82d93c6 --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/has_description.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module HasDescription + private + + def description + options.fetch(:description, '').split("\n").map(&:strip) + end + + def description_statement + description.map { |s| indent(s, 2) } unless description.empty? + end + end + end + end + end +end diff --git a/lib/cucumber/core/gherkin/writer/has_elements.rb b/lib/cucumber/core/gherkin/writer/has_elements.rb new file mode 100644 index 00000000..6edc3a53 --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/has_elements.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module HasElements + include AcceptsComments + + def self.included(base) + base.extend HasElementBuilders + end + + def build(source = []) + elements.inject(source + statements) { |acc, el| el.build(acc) } + end + + private + + def elements + @elements ||= [] + end + + module HasElementBuilders + def elements(*names) + names.each { |name| element(name) } + end + + private + + def element(name) + define_method(name) do |*args, &source| + factory_name = String(name).split('_').map(&:capitalize).join + factory = Writer.const_get(factory_name) + factory.new(slurp_comments, *args).tap do |builder| + builder.instance_exec(&source) if source + elements << builder + end + self + end + end + end + end + end + end + end +end diff --git a/lib/cucumber/core/gherkin/writer/has_options_initializer.rb b/lib/cucumber/core/gherkin/writer/has_options_initializer.rb new file mode 100644 index 00000000..3c8b757e --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/has_options_initializer.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module HasOptionsInitializer + def self.included(base) + base.extend HasDefaultKeyword + end + + attr_reader :name, :options + private :name, :options + + def initialize(*args) + @comments = args.shift if args.first.is_a?(Array) + @comments ||= [] + @options = args.pop if args.last.is_a?(Hash) + @options ||= {} + @name = args.first + end + + private + + def comments_statement + @comments + end + + def keyword + options.fetch(:keyword) { self.class.keyword } + end + + def name_statement + "#{keyword}: #{name}".strip + end + + def tag_statement + tags + end + + def tags + options[:tags] + end + + module HasDefaultKeyword + def default_keyword(keyword) + @keyword = keyword + end + + def keyword + @keyword + end + end + end + end + end + end +end diff --git a/lib/cucumber/core/gherkin/writer/has_rows.rb b/lib/cucumber/core/gherkin/writer/has_rows.rb new file mode 100644 index 00000000..5421577c --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/has_rows.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module HasRows + def row(*cells) + rows << cells + end + + def rows + @rows ||= [] + end + + private + + def row_statements(indent = nil) + rows.map { |row| indent(table_row(row), indent) } + end + + def table_row(row) + padded = pad(row) + "| #{padded.join(' | ')} |" + end + + def pad(row) + row.map.with_index { |text, position| justify_cell(text, position) } + end + + def column_length(column) + lengths = rows.transpose.map { |r| r.map(&:length).max } + lengths[column] + end + + def justify_cell(cell, position) + cell.ljust(column_length(position)) + end + end + end + end + end +end diff --git a/lib/cucumber/core/gherkin/writer/helpers.rb b/lib/cucumber/core/gherkin/writer/helpers.rb index 37e3bb00..bdde2a18 100644 --- a/lib/cucumber/core/gherkin/writer/helpers.rb +++ b/lib/cucumber/core/gherkin/writer/helpers.rb @@ -1,177 +1,9 @@ # frozen_string_literal: true -module Cucumber - module Core - module Gherkin - module Writer - module HasOptionsInitializer - def self.included(base) - base.extend HasDefaultKeyword - end +require_relative 'accepts_comments' - attr_reader :name, :options - private :name, :options - - def initialize(*args) - @comments = args.shift if args.first.is_a?(Array) - @comments ||= [] - @options = args.pop if args.last.is_a?(Hash) - @options ||= {} - @name = args.first - end - - private - - def comments_statement - @comments - end - - def keyword - options.fetch(:keyword) { self.class.keyword } - end - - def name_statement - "#{keyword}: #{name}".strip - end - - def tag_statement - tags - end - - def tags - options[:tags] - end - - module HasDefaultKeyword - def default_keyword(keyword) - @keyword = keyword - end - - def keyword - @keyword - end - end - end - - module AcceptsComments - def comment(line) - comment_lines << "# #{line}" - end - - def comment_lines - @comment_lines ||= [] - end - - def slurp_comments - comment_lines.tap { @comment_lines = nil } - end - end - - module HasElements - include AcceptsComments - - def self.included(base) - base.extend HasElementBuilders - end - - def build(source = []) - elements.inject(source + statements) { |acc, el| el.build(acc) } - end - - private - - def elements - @elements ||= [] - end - - module HasElementBuilders - def elements(*names) - names.each { |name| element(name) } - end - - private - - def element(name) - define_method(name) do |*args, &source| - factory_name = String(name).split('_').map(&:capitalize).join - factory = Writer.const_get(factory_name) - factory.new(slurp_comments, *args).tap do |builder| - builder.instance_exec(&source) if source - elements << builder - end - self - end - end - end - end - - module Indentation - def self.level(number) - Module.new do - define_method(:indent) do |string, amount = nil| - return string if string.nil? || string.empty? - - amount ||= number - "#{' ' * amount}#{string}" - end - - define_method(:indent_level) do - number - end - - define_method(:prepare_statements) do |*statements| - statements.flatten.compact.map { |s| indent(s) } - end - end - end - end - - module HasDescription - private - - def description - options.fetch(:description, '').split("\n").map(&:strip) - end - - def description_statement - description.map { |s| indent(s, 2) } unless description.empty? - end - end - - module HasRows - def row(*cells) - rows << cells - end - - def rows - @rows ||= [] - end - - private - - def row_statements(indent = nil) - rows.map { |row| indent(table_row(row), indent) } - end - - def table_row(row) - padded = pad(row) - "| #{padded.join(' | ')} |" - end - - def pad(row) - row.map.with_index { |text, position| justify_cell(text, position) } - end - - def column_length(column) - lengths = rows.transpose.map { |r| r.map(&:length).max } - lengths[column] - end - - def justify_cell(cell, position) - cell.ljust(column_length(position)) - end - end - end - end - end -end +require_relative 'has_description' +require_relative 'has_elements' +require_relative 'has_options_initializer' +require_relative 'has_rows' +require_relative 'indentation' diff --git a/lib/cucumber/core/gherkin/writer/indentation.rb b/lib/cucumber/core/gherkin/writer/indentation.rb new file mode 100644 index 00000000..acd0fabd --- /dev/null +++ b/lib/cucumber/core/gherkin/writer/indentation.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Cucumber + module Core + module Gherkin + module Writer + module Indentation + def self.level(number) + Module.new do + define_method(:indent) do |string, amount = nil| + return string if string.nil? || string.empty? + + amount ||= number + "#{' ' * amount}#{string}" + end + + define_method(:indent_level) do + number + end + + define_method(:prepare_statements) do |*statements| + statements.flatten.compact.map { |s| indent(s) } + end + end + end + end + end + end + end +end From a8e647b70cc365023906e63973235828ece3ab0f Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:04:59 +0100 Subject: [PATCH 02/11] Fix tech debt and rubocop issue regarding indentation level; --- .rubocop_todo.yml | 8 ++--- .../core/gherkin/writer/background.rb | 4 ++- .../core/gherkin/writer/doc_string.rb | 3 +- lib/cucumber/core/gherkin/writer/example.rb | 3 +- lib/cucumber/core/gherkin/writer/examples.rb | 4 ++- lib/cucumber/core/gherkin/writer/feature.rb | 3 +- .../core/gherkin/writer/indentation.rb | 36 ++++++++++++------- lib/cucumber/core/gherkin/writer/rule.rb | 4 ++- lib/cucumber/core/gherkin/writer/scenario.rb | 4 ++- .../core/gherkin/writer/scenario_outline.rb | 4 ++- lib/cucumber/core/gherkin/writer/step.rb | 4 ++- lib/cucumber/core/gherkin/writer/table.rb | 4 ++- 12 files changed, 54 insertions(+), 27 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b227c2b8..90460966 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -23,10 +23,10 @@ Lint/RedundantRequireStatement: Metrics/AbcSize: Max: 21 -# Offense count: 3 -# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. -Metrics/MethodLength: - Max: 28 +## Offense count: 3 +## Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +#Metrics/MethodLength: +# Max: 28 # Offense count: 4 # Configuration parameters: CountComments, CountAsOne. diff --git a/lib/cucumber/core/gherkin/writer/background.rb b/lib/cucumber/core/gherkin/writer/background.rb index c1f54a3d..db2287c1 100644 --- a/lib/cucumber/core/gherkin/writer/background.rb +++ b/lib/cucumber/core/gherkin/writer/background.rb @@ -10,7 +10,9 @@ class Background include HasElements include HasOptionsInitializer include HasDescription - include Indentation.level 2 + extend Indentation + + indentation_level 2 default_keyword 'Background' diff --git a/lib/cucumber/core/gherkin/writer/doc_string.rb b/lib/cucumber/core/gherkin/writer/doc_string.rb index 41020327..fd3c1676 100644 --- a/lib/cucumber/core/gherkin/writer/doc_string.rb +++ b/lib/cucumber/core/gherkin/writer/doc_string.rb @@ -7,7 +7,8 @@ module Core module Gherkin module Writer class DocString - include Indentation.level(6) + extend Indentation + indentation_level 6 attr_reader :strings, :content_type private :strings, :content_type diff --git a/lib/cucumber/core/gherkin/writer/example.rb b/lib/cucumber/core/gherkin/writer/example.rb index 6e59e01d..279ee51e 100644 --- a/lib/cucumber/core/gherkin/writer/example.rb +++ b/lib/cucumber/core/gherkin/writer/example.rb @@ -8,7 +8,8 @@ module Core module Gherkin module Writer class Example < Scenario - include Indentation.level 4 + extend Indentation + indentation_level 4 default_keyword 'Example' end diff --git a/lib/cucumber/core/gherkin/writer/examples.rb b/lib/cucumber/core/gherkin/writer/examples.rb index 88bb93b2..56a49297 100644 --- a/lib/cucumber/core/gherkin/writer/examples.rb +++ b/lib/cucumber/core/gherkin/writer/examples.rb @@ -12,7 +12,9 @@ class Examples include HasOptionsInitializer include HasRows include HasDescription - include Indentation.level(4) + extend Indentation + + indentation_level 4 default_keyword 'Examples' diff --git a/lib/cucumber/core/gherkin/writer/feature.rb b/lib/cucumber/core/gherkin/writer/feature.rb index 4eac76e2..d920ebf0 100644 --- a/lib/cucumber/core/gherkin/writer/feature.rb +++ b/lib/cucumber/core/gherkin/writer/feature.rb @@ -12,7 +12,8 @@ class Feature include HasElements include HasOptionsInitializer include HasDescription - include Indentation.level(0) + extend Indentation + indentation_level 0 default_keyword 'Feature' diff --git a/lib/cucumber/core/gherkin/writer/indentation.rb b/lib/cucumber/core/gherkin/writer/indentation.rb index acd0fabd..5c8c1a87 100644 --- a/lib/cucumber/core/gherkin/writer/indentation.rb +++ b/lib/cucumber/core/gherkin/writer/indentation.rb @@ -5,22 +5,32 @@ module Core module Gherkin module Writer module Indentation - def self.level(number) - Module.new do - define_method(:indent) do |string, amount = nil| - return string if string.nil? || string.empty? + def indentation_level(number) + create_indent(number) + create_indent_level(number) + create_prepare_statements + end + + private - amount ||= number - "#{' ' * amount}#{string}" - end + def create_indent(number) + define_method(:indent) do |string, amount = nil| + return string if string.nil? || string.empty? - define_method(:indent_level) do - number - end + amount ||= number + "#{' ' * amount}#{string}" + end + end + + def create_indent_level(number) + define_method(:indent_level) do + number + end + end - define_method(:prepare_statements) do |*statements| - statements.flatten.compact.map { |s| indent(s) } - end + def create_prepare_statements + define_method(:prepare_statements) do |*statements| + statements.flatten.compact.map { |s| indent(s) } end end end diff --git a/lib/cucumber/core/gherkin/writer/rule.rb b/lib/cucumber/core/gherkin/writer/rule.rb index 2859f766..f2882009 100644 --- a/lib/cucumber/core/gherkin/writer/rule.rb +++ b/lib/cucumber/core/gherkin/writer/rule.rb @@ -12,7 +12,9 @@ class Rule include HasElements include HasOptionsInitializer include HasDescription - include Indentation.level 2 + extend Indentation + + indentation_level 2 default_keyword 'Rule' diff --git a/lib/cucumber/core/gherkin/writer/scenario.rb b/lib/cucumber/core/gherkin/writer/scenario.rb index 1482478c..6ad9899a 100644 --- a/lib/cucumber/core/gherkin/writer/scenario.rb +++ b/lib/cucumber/core/gherkin/writer/scenario.rb @@ -10,7 +10,9 @@ class Scenario include HasElements include HasOptionsInitializer include HasDescription - include Indentation.level 2 + extend Indentation + + indentation_level 2 default_keyword 'Scenario' diff --git a/lib/cucumber/core/gherkin/writer/scenario_outline.rb b/lib/cucumber/core/gherkin/writer/scenario_outline.rb index 5c8af36d..f20db3ba 100644 --- a/lib/cucumber/core/gherkin/writer/scenario_outline.rb +++ b/lib/cucumber/core/gherkin/writer/scenario_outline.rb @@ -10,7 +10,9 @@ class ScenarioOutline include HasElements include HasOptionsInitializer include HasDescription - include Indentation.level 2 + extend Indentation + + indentation_level 2 default_keyword 'Scenario Outline' diff --git a/lib/cucumber/core/gherkin/writer/step.rb b/lib/cucumber/core/gherkin/writer/step.rb index 959f7e40..815a7086 100644 --- a/lib/cucumber/core/gherkin/writer/step.rb +++ b/lib/cucumber/core/gherkin/writer/step.rb @@ -11,7 +11,9 @@ module Writer class Step include HasElements include HasOptionsInitializer - include Indentation.level 4 + extend Indentation + + indentation_level 4 default_keyword 'Given' diff --git a/lib/cucumber/core/gherkin/writer/table.rb b/lib/cucumber/core/gherkin/writer/table.rb index 8c03a670..f4990f98 100644 --- a/lib/cucumber/core/gherkin/writer/table.rb +++ b/lib/cucumber/core/gherkin/writer/table.rb @@ -7,7 +7,9 @@ module Core module Gherkin module Writer class Table - include Indentation.level(6) + extend IndentationDSLNew + indentation_level 6 + include HasRows def initialize(*); end From e9ddd06766049be859e45fb29236663165e1a2e2 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:05:50 +0100 Subject: [PATCH 03/11] Fix rubocop --- lib/cucumber/core/gherkin/writer/doc_string.rb | 1 + lib/cucumber/core/gherkin/writer/example.rb | 1 + lib/cucumber/core/gherkin/writer/feature.rb | 1 + lib/cucumber/core/gherkin/writer/table.rb | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/cucumber/core/gherkin/writer/doc_string.rb b/lib/cucumber/core/gherkin/writer/doc_string.rb index fd3c1676..c9c7e46a 100644 --- a/lib/cucumber/core/gherkin/writer/doc_string.rb +++ b/lib/cucumber/core/gherkin/writer/doc_string.rb @@ -8,6 +8,7 @@ module Gherkin module Writer class DocString extend Indentation + indentation_level 6 attr_reader :strings, :content_type diff --git a/lib/cucumber/core/gherkin/writer/example.rb b/lib/cucumber/core/gherkin/writer/example.rb index 279ee51e..0bd088f4 100644 --- a/lib/cucumber/core/gherkin/writer/example.rb +++ b/lib/cucumber/core/gherkin/writer/example.rb @@ -9,6 +9,7 @@ module Gherkin module Writer class Example < Scenario extend Indentation + indentation_level 4 default_keyword 'Example' diff --git a/lib/cucumber/core/gherkin/writer/feature.rb b/lib/cucumber/core/gherkin/writer/feature.rb index d920ebf0..eda8c37f 100644 --- a/lib/cucumber/core/gherkin/writer/feature.rb +++ b/lib/cucumber/core/gherkin/writer/feature.rb @@ -13,6 +13,7 @@ class Feature include HasOptionsInitializer include HasDescription extend Indentation + indentation_level 0 default_keyword 'Feature' diff --git a/lib/cucumber/core/gherkin/writer/table.rb b/lib/cucumber/core/gherkin/writer/table.rb index f4990f98..061ae1cc 100644 --- a/lib/cucumber/core/gherkin/writer/table.rb +++ b/lib/cucumber/core/gherkin/writer/table.rb @@ -7,7 +7,8 @@ module Core module Gherkin module Writer class Table - extend IndentationDSLNew + extend Indentation + indentation_level 6 include HasRows From 725310cc7eb63728c22c0a84870ccf098861e138 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:14:20 +0100 Subject: [PATCH 04/11] Fix up tech-debt regarding linelength for compile sub-method in compiler spec --- spec/cucumber/core/compiler_spec.rb | 63 ++++++++++++----------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/spec/cucumber/core/compiler_spec.rb b/spec/cucumber/core/compiler_spec.rb index 0cc05cb6..e114dffb 100644 --- a/spec/cucumber/core/compiler_spec.rb +++ b/spec/cucumber/core/compiler_spec.rb @@ -47,6 +47,16 @@ end end end + let(:event_bus_class) do + Class.new(Cucumber::Core::EventBus) do + def gherkin_source_parsed(*); end + def test_case_created(*); end + def test_step_created(*); end + def envelope(*); end + end + end + + let(:event_bus) { event_bus_class.new } it 'compiles a feature with a single scenario' do compile([single_step_gherkin_document]) do |visitor| @@ -56,37 +66,24 @@ end end - context 'when the event_bus is provided' do - let(:event_bus_class) do - Class.new(Cucumber::Core::EventBus) do - def gherkin_source_parsed(*); end - def test_case_created(*); end - def test_step_created(*); end - def envelope(*); end - end - end - - let(:event_bus) { event_bus_class.new } - - it 'emits a TestCaseCreated event with the created Test::Case and Pickle' do - compile([single_step_gherkin_document], event_bus) do |visitor| - allow(visitor).to receive(:test_case) - allow(visitor).to receive(:test_step) - allow(visitor).to receive(:done) + it 'emits a TestCaseCreated event with the created Test::Case and Pickle' do + compile([single_step_gherkin_document]) do |visitor| + allow(visitor).to receive(:test_case) + allow(visitor).to receive(:test_step) + allow(visitor).to receive(:done) - expect(event_bus).to receive(:test_case_created).once - end + expect(event_bus).to receive(:test_case_created).once end + end - it 'emits a TestStepCreated event with the created Test::Step and PickleStep' do - compile([double_step_gherkin_document], event_bus) do |visitor| - allow(visitor).to receive(:test_case) - allow(visitor).to receive(:test_step) - allow(visitor).to receive(:done) - allow(event_bus).to receive(:envelope) + it 'emits a TestStepCreated event with the created Test::Step and PickleStep' do + compile([double_step_gherkin_document]) do |visitor| + allow(visitor).to receive(:test_case) + allow(visitor).to receive(:test_step) + allow(visitor).to receive(:done) + allow(event_bus).to receive(:envelope) - expect(event_bus).to receive(:test_step_created).twice - end + expect(event_bus).to receive(:test_step_created).twice end end @@ -222,21 +219,11 @@ def envelope(*); end end end - def compile(gherkin_documents, event_bus = nil) + def compile(gherkin_documents) visitor = double allow(visitor).to receive(:test_suite).and_yield(visitor) allow(visitor).to receive(:test_case).and_yield(visitor) - if event_bus.nil? - event_bus = double - allow(event_bus).to receive_messages( - envelope: nil, - gherkin_source_parsed: nil, - test_case_created: nil, - test_step_created: nil - ) - end - yield visitor super(gherkin_documents, visitor, [], event_bus) end From 93f17248864a2dd430173fe101b194e9753744d2 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:28:16 +0100 Subject: [PATCH 05/11] Tidy up example length cops for writer spec --- spec/cucumber/core/compiler_spec.rb | 32 ++-------- spec/cucumber/core/gherkin/writer_spec.rb | 74 ++++++++++++++--------- 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/spec/cucumber/core/compiler_spec.rb b/spec/cucumber/core/compiler_spec.rb index e114dffb..e74d9672 100644 --- a/spec/cucumber/core/compiler_spec.rb +++ b/spec/cucumber/core/compiler_spec.rb @@ -68,9 +68,7 @@ def envelope(*); end it 'emits a TestCaseCreated event with the created Test::Case and Pickle' do compile([single_step_gherkin_document]) do |visitor| - allow(visitor).to receive(:test_case) - allow(visitor).to receive(:test_step) - allow(visitor).to receive(:done) + allow(visitor).to receive_messages(test_case: nil, test_step: nil, done: nil) expect(event_bus).to receive(:test_case_created).once end @@ -78,9 +76,7 @@ def envelope(*); end it 'emits a TestStepCreated event with the created Test::Step and PickleStep' do compile([double_step_gherkin_document]) do |visitor| - allow(visitor).to receive(:test_case) - allow(visitor).to receive(:test_step) - allow(visitor).to receive(:done) + allow(visitor).to receive_messages(test_case: nil, test_step: nil, done: nil) allow(event_bus).to receive(:envelope) expect(event_bus).to receive(:test_step_created).twice @@ -88,20 +84,7 @@ def envelope(*); end end it 'compiles a feature with a background' do - gherkin_documents = [ - gherkin do - feature do - background do - step 'passing' - end - - scenario do - step 'passing' - end - end - end - ] - compile(gherkin_documents) do |visitor| + compile([background_step_gherkin_document]) do |visitor| expect(visitor).to receive(:test_case).once.ordered.and_yield(visitor) expect(visitor).to receive(:test_step).twice.ordered expect(visitor).to receive(:done).once.ordered @@ -166,9 +149,8 @@ def envelope(*); end it 'produces test cases' do compile(gherkin_documents_with_examples) do |visitor| + allow(visitor).to receive_messages(test_step: nil, done: nil) expect(visitor).to receive(:test_case).exactly(3).times.and_yield(visitor) - allow(visitor).to receive(:test_step) - allow(visitor).to receive(:done) end end @@ -205,16 +187,14 @@ def envelope(*); end it 'creates a single test case' do compile([empty_gherkin_document]) do |visitor| allow(visitor).to receive(:done) - - expect(visitor).to receive(:test_case).once.ordered + expect(visitor).to receive(:test_case).once end end it 'finishes the compilation once' do compile([empty_gherkin_document]) do |visitor| allow(visitor).to receive(:test_case) - - expect(visitor).to receive(:done).once.ordered + expect(visitor).to receive(:done).once end end end diff --git a/spec/cucumber/core/gherkin/writer_spec.rb b/spec/cucumber/core/gherkin/writer_spec.rb index 177cb126..4ba8b7f8 100644 --- a/spec/cucumber/core/gherkin/writer_spec.rb +++ b/spec/cucumber/core/gherkin/writer_spec.rb @@ -67,36 +67,42 @@ end context 'when a language is supplied' do - it 'inserts a language statement' do - source = gherkin do - feature language: 'ru' + let(:source) do + gherkin do + feature language: 'ja' end + end - expect(source).to eq("# language: ru\nFeature:\n") + it 'inserts a language statement' do + expect(source).to eq("# language: ja\nFeature:\n") end end context 'when a comment is supplied' do - it 'inserts a comment' do - source = gherkin do + let(:source) do + gherkin do comment 'wow' comment 'great' feature end + end + it 'inserts a comment' do expect(source.to_s).to eq("# wow\n# great\nFeature:\n") end end context 'with a scenario' do - it 'includes the scenario statement' do - source = gherkin do + let(:source) do + gherkin do feature 'A Feature' do scenario end end + end - expect(source.to_s).to match(/Scenario:/) + it 'includes the scenario statement' do + expect(source.to_s).to include('Scenario:') end context 'when a comment is provided' do @@ -108,15 +114,16 @@ Scenario: FEATURE end - - it 'includes the comment in the scenario statement' do - source = gherkin do + let(:source) do + gherkin do feature do comment 'wow' scenario end end + end + it 'includes the comment in the scenario statement' do expect(source.to_s).to eq(expected) end end @@ -132,9 +139,8 @@ multiple lines. FEATURE end - - it 'includes the description in the scenario statement' do - source = gherkin do + let(:source) do + gherkin do feature do scenario description: <<~SCENARIO This is the description @@ -143,22 +149,26 @@ SCENARIO end end + end + it 'includes the description in the scenario statement' do expect(source).to eq(expected) end end context 'with a step' do - it 'includes the step statement' do - source = gherkin do + let(:source) do + gherkin do feature 'A Feature' do scenario do step 'passing' end end end + end - expect(source.to_s).to match(/Given passing\Z/m) + it 'includes the step statement' do + expect(source.to_s).to include('Given passing') end context 'when a docstring is provided' do @@ -173,9 +183,8 @@ """ FEATURE end - - it 'includes the content type when provided' do - source = gherkin do + let(:source) do + gherkin do feature do scenario do step 'failing' do @@ -184,7 +193,9 @@ end end end + end + it 'includes the content type when provided' do expect(source).to eq(expected) end end @@ -201,14 +212,15 @@ and two.. FEATURE end - - it 'can have a description' do - source = gherkin do + let(:source) do + gherkin do feature do background description: "One line,\nand two.." end end + end + it 'can have a description' do expect(source).to eq(expected) end end @@ -222,14 +234,15 @@ Doesn't need to be multi-line. FEATURE end - - it 'can have a description' do - source = gherkin do + let(:source) do + gherkin do feature do scenario_outline description: "Doesn't need to be multi-line." end end + end + it 'can have a description' do expect(source).to eq(expected) end @@ -244,16 +257,17 @@ Doesn't need to be multi-line. FEATURE end - - it 'can have a description' do - source = gherkin do + let(:source) do + gherkin do feature do scenario_outline do examples description: "Doesn't need to be multi-line." end end end + end + it 'can have a description' do expect(source).to eq(expected) end end From cc93cf76381e4af52a2b55a4a604f56ce8fb0c66 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:35:16 +0100 Subject: [PATCH 06/11] Remove large nesting from tests --- spec/cucumber/core/test/result_spec.rb | 768 ++++++++++++------------- 1 file changed, 384 insertions(+), 384 deletions(-) diff --git a/spec/cucumber/core/test/result_spec.rb b/spec/cucumber/core/test/result_spec.rb index da42a9eb..3fc79352 100644 --- a/spec/cucumber/core/test/result_spec.rb +++ b/spec/cucumber/core/test/result_spec.rb @@ -3,459 +3,459 @@ require 'cucumber/core/test/result' require 'support/duration_matcher' -module Cucumber - module Core - module Test - describe Result do - let(:visitor) { double } - let(:args) { double } - - describe Result::Passed do - subject(:result) { described_class.new(duration) } - let(:duration) { Result::Duration.new(1 * 1000 * 1000) } - - before do - allow(visitor).to receive(:duration) - allow(visitor).to receive(:passed) - end - - it 'is described as a passing test' do - expect(visitor).to receive(:passed).with(args) - - result.describe_to(visitor, args) - end - - it 'converts to a string' do - expect(result.to_s).to eq('✓') - end - - it 'converts to a `Cucumber::Message::TestResult`' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::PASSED) - end - - it 'has a duration' do - expect(result.duration).to eq(duration) - end - - it 'does nothing when appending the backtrace' do - expect(result.with_appended_backtrace(double)).to eq(result) - end - - it 'does nothing when filtering the backtrace' do - expect(result.with_filtered_backtrace(double)).to eq(result) - end - - it { expect(result.to_sym).to eq(:passed) } - it { expect(result).to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).not_to be_undefined } - 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 } - end +describe Cucumber::Core::Test::Result do + let(:visitor) { double } + let(:args) { double } - describe Result::Failed do - subject(:result) { described_class.new(duration, exception) } - - let(:duration) { Result::Duration.new(1 * 1000 * 1000) } - let(:exception) { StandardError.new('error message') } - - before do - allow(visitor).to receive(:failed) - allow(visitor).to receive(:duration) - allow(visitor).to receive(:exception) - end - - it 'is described as a failing test' do - expect(visitor).to receive(:failed).with(args) - - result.describe_to(visitor, args) - end - - it 'contains an exception message' do - expect(visitor).to receive(:exception).with(exception, args) - - result.describe_to(visitor, args) - end - - it 'has a duration' do - expect(result.duration).to eq(duration) - end - - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::FAILED) - end - - it 'requires both constructor arguments' do - expect { described_class.new }.to raise_error(ArgumentError) - expect { described_class.new(duration) }.to raise_error(ArgumentError) - end - - it 'does nothing if step has no backtrace line' do - result.exception.set_backtrace('exception backtrace') - step = 'does not respond_to?(:backtrace_line)' - - expect(result.with_appended_backtrace(step).exception.backtrace).to eq(['exception backtrace']) - end - - it 'appends the backtrace line of the step' do - result.exception.set_backtrace('exception backtrace') - step = double - allow(step).to receive(:backtrace_line).and_return('step_line') - - expect(result.with_appended_backtrace(step).exception.backtrace).to eq(['exception backtrace', 'step_line']) - end - - it 'applies filters to the exception' do - filter_class = double - filter = double - filtered_exception = double - allow(filter_class).to receive(:new).with(result.exception).and_return(filter) - allow(filter).to receive(:exception).and_return(filtered_exception) - - expect(result.with_filtered_backtrace(filter_class).exception).to eq(filtered_exception) - end - - it { expect(result.to_sym).to eq(:failed) } - it { expect(result).not_to be_passed } - it { expect(result).to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).not_to be_undefined } - it { expect(result).not_to be_unknown } - it { expect(result).not_to be_skipped } - it { expect(result).not_to be_flaky } - it { expect(result).not_to be_ok } - end + describe Cucumber::Core::Test::Result::Passed do + subject(:result) { described_class.new(duration) } - describe Result::Unknown do - subject(:result) { described_class.new } - - it 'defines a with_filtered_backtrace method' do - expect(result.with_filtered_backtrace(double)).to eq(result) - end - - it { expect(result.to_sym).to eq(:unknown) } - it { expect(result).not_to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).not_to be_undefined } - it { expect(result).to be_unknown } - it { expect(result).not_to be_skipped } - it { expect(result).not_to be_flaky } - - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::UNKNOWN) - end - end + let(:duration) { Cucumber::Core::Test::Result::Duration.new(1 * 1000 * 1000) } - describe Result::Raisable do - context 'with or without backtrace' do - subject(:result) { described_class.new } + before do + allow(visitor).to receive(:duration) + allow(visitor).to receive(:passed) + end - it 'does nothing if step has no backtrace line' do - step = 'does not respond_to?(:backtrace_line)' + it 'is described as a passing test' do + expect(visitor).to receive(:passed).with(args) - expect(result.with_appended_backtrace(step).backtrace).to be_nil - end - end + result.describe_to(visitor, args) + end - context 'without backtrace' do - subject(:result) { described_class.new } + it 'converts to a string' do + expect(result.to_s).to eq('✓') + end - it 'set the backtrace to the backtrace line of the step' do - step = double - allow(step).to receive(:backtrace_line).and_return('step_line') + it 'converts to a `Cucumber::Message::TestResult`' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::PASSED) + end - expect(result.with_appended_backtrace(step).backtrace).to eq(['step_line']) - end + it 'has a duration' do + expect(result.duration).to eq(duration) + end - it 'does nothing when filtering the backtrace' do - expect(result.with_filtered_backtrace(double)).to eq(result) - end - end + it 'does nothing when appending the backtrace' do + expect(result.with_appended_backtrace(double)).to eq(result) + end - context 'with backtrace' do - subject(:result) { described_class.new('message', 0, 'backtrace') } + it 'does nothing when filtering the backtrace' do + expect(result.with_filtered_backtrace(double)).to eq(result) + end - it 'appends the backtrace line of the step' do - step = double - allow(step).to receive(:backtrace_line).and_return('step_line') + it { expect(result.to_sym).to eq(:passed) } + it { expect(result).to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).not_to be_undefined } + 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 } + end - expect(result.with_appended_backtrace(step).backtrace).to eq(%w[backtrace step_line]) - end + describe Cucumber::Core::Test::Result::Failed do + subject(:result) { described_class.new(duration, exception) } - it 'apply filters to the backtrace' do - filter_class = double - filter = double - filtered_result = double - allow(filter_class).to receive(:new).with(result.exception).and_return(filter) - allow(filter).to receive(:exception).and_return(filtered_result) + let(:duration) { Cucumber::Core::Test::Result::Duration.new(1 * 1000 * 1000) } + let(:exception) { StandardError.new('error message') } - expect(result.with_filtered_backtrace(filter_class)).to eq(filtered_result) - end - end - end + before do + allow(visitor).to receive(:failed) + allow(visitor).to receive(:duration) + allow(visitor).to receive(:exception) + end - describe Result::Ambiguous do - subject(:result) { described_class.new } - - it 'describes itself to a visitor' do - expect(visitor).to receive(:ambiguous).with(args) - expect(visitor).to receive(:duration).with(an_unknown_duration, args) - - result.describe_to(visitor, args) - end - - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::AMBIGUOUS) - end - - it { expect(result.to_sym).to eq(:ambiguous) } - it { expect(result).not_to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).to be_ambiguous } - it { expect(result).not_to be_undefined } - it { expect(result).not_to be_unknown } - it { expect(result).not_to be_skipped } - it { expect(result).not_to be_flaky } - it { expect(result).not_to be_ok } - end + it 'is described as a failing test' do + expect(visitor).to receive(:failed).with(args) - describe Result::Undefined do - subject(:result) { described_class.new } + result.describe_to(visitor, args) + end - it 'describes itself to a visitor' do - expect(visitor).to receive(:undefined).with(args) - expect(visitor).to receive(:duration).with(an_unknown_duration, args) + it 'contains an exception message' do + expect(visitor).to receive(:exception).with(exception, args) - result.describe_to(visitor, args) - end + result.describe_to(visitor, args) + end - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::UNDEFINED) - end + it 'has a duration' do + expect(result.duration).to eq(duration) + end - it { expect(result.to_sym).to eq(:undefined) } - it { expect(result).not_to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).to be_undefined } - it { expect(result).not_to be_unknown } - it { expect(result).not_to be_skipped } - it { expect(result).not_to be_flaky } + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::FAILED) + end - it { expect(result).not_to be_ok } - end + it 'requires both constructor arguments' do + expect { described_class.new }.to raise_error(ArgumentError) + expect { described_class.new(duration) }.to raise_error(ArgumentError) + end - describe Result::Skipped do - subject(:result) { described_class.new } - - it 'describes itself to a visitor' do - expect(visitor).to receive(:skipped).with(args) - expect(visitor).to receive(:duration).with(an_unknown_duration, args) - - result.describe_to(visitor, args) - end - - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::SKIPPED) - end - - it { expect(result.to_sym).to eq(:skipped) } - it { expect(result).not_to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).not_to be_undefined } - it { expect(result).not_to be_unknown } - it { expect(result).to be_skipped } - it { expect(result).not_to be_flaky } - it { expect(result).to be_ok } - end + it 'does nothing if step has no backtrace line' do + result.exception.set_backtrace('exception backtrace') + step = 'does not respond_to?(:backtrace_line)' - describe Result::Pending do - subject(:result) { described_class.new } + expect(result.with_appended_backtrace(step).exception.backtrace).to eq(['exception backtrace']) + end - it 'describes itself to a visitor' do - expect(visitor).to receive(:pending).with(result, args) - expect(visitor).to receive(:duration).with(an_unknown_duration, args) + it 'appends the backtrace line of the step' do + result.exception.set_backtrace('exception backtrace') + step = double + allow(step).to receive(:backtrace_line).and_return('step_line') - result.describe_to(visitor, args) - end + expect(result.with_appended_backtrace(step).exception.backtrace).to eq(['exception backtrace', 'step_line']) + end - it 'converts to a Cucumber::Message::TestResult' do - expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::PENDING) - end + it 'applies filters to the exception' do + filter_class = double + filter = double + filtered_exception = double + permit_exception_passthrough(filter_class, filter, filtered_exception) - it { expect(result.to_sym).to eq(:pending) } - it { expect(result).not_to be_passed } - it { expect(result).not_to be_failed } - it { expect(result).not_to be_ambiguous } - it { expect(result).not_to be_undefined } - it { expect(result).not_to be_unknown } - it { expect(result).not_to be_skipped } - it { expect(result).not_to be_flaky } + expect(result.with_filtered_backtrace(filter_class).exception).to eq(filtered_exception) + end - it { expect(result).not_to be_ok } - end + it { expect(result.to_sym).to eq(:failed) } + it { expect(result).not_to be_passed } + it { expect(result).to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).not_to be_undefined } + it { expect(result).not_to be_unknown } + it { expect(result).not_to be_skipped } + it { expect(result).not_to be_flaky } + it { expect(result).not_to be_ok } + end - describe Result::Flaky do - it { expect(described_class).not_to be_ok } - end + describe Cucumber::Core::Test::Result::Unknown do + subject(:result) { described_class.new } + + it 'defines a with_filtered_backtrace method' do + expect(result.with_filtered_backtrace(double)).to eq(result) + end - describe Result::Summary do - let(:summary) { described_class.new } - let(:failed) { Result::Failed.new(Result::Duration.new(10), exception) } - let(:passed) { Result::Passed.new(Result::Duration.new(11)) } - let(:skipped) { Result::Skipped.new } - let(:unknown) { Result::Unknown.new } - let(:pending) { Result::Pending.new } - let(:undefined) { Result::Undefined.new } - let(:exception) { StandardError.new } + it { expect(result.to_sym).to eq(:unknown) } + it { expect(result).not_to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).not_to be_undefined } + it { expect(result).to be_unknown } + it { expect(result).not_to be_skipped } + it { expect(result).not_to be_flaky } + + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::UNKNOWN) + end + end - it 'counts failed results' do - failed.describe_to(summary) + describe Cucumber::Core::Test::Result::Raisable do + context 'with or without backtrace' do + subject(:result) { described_class.new } - expect(summary.total_failed).to eq(1) - expect(summary.total(:failed)).to eq(1) - expect(summary.total).to eq(1) - end + it 'does nothing if step has no backtrace line' do + step = 'does not respond_to?(:backtrace_line)' - it 'counts passed results' do - passed.describe_to(summary) + expect(result.with_appended_backtrace(step).backtrace).to be_nil + end + end + + context 'without backtrace' do + subject(:result) { described_class.new } - expect(summary.total_passed).to eq(1) - expect(summary.total(:passed)).to eq(1) - expect(summary.total).to eq(1) - end + it 'set the backtrace to the backtrace line of the step' do + step = double + allow(step).to receive(:backtrace_line).and_return('step_line') - it 'counts skipped results' do - skipped.describe_to(summary) + expect(result.with_appended_backtrace(step).backtrace).to eq(['step_line']) + end - expect(summary.total_skipped).to eq(1) - expect(summary.total(:skipped)).to eq(1) - expect(summary.total).to eq(1) - end + it 'does nothing when filtering the backtrace' do + expect(result.with_filtered_backtrace(double)).to eq(result) + end + end - it 'counts undefined results' do - undefined.describe_to(summary) + context 'with backtrace' do + subject(:result) { described_class.new('message', 0, 'backtrace') } - expect(summary.total_undefined).to eq(1) - expect(summary.total(:undefined)).to eq(1) - expect(summary.total).to eq(1) - end + it 'appends the backtrace line of the step' do + step = double + allow(step).to receive(:backtrace_line).and_return('step_line') - it 'counts arbitrary raisable results' do - flickering = Class.new(Result::Raisable) do - def describe_to(visitor, *args) - visitor.flickering(*args) - end - end + expect(result.with_appended_backtrace(step).backtrace).to eq(%w[backtrace step_line]) + end - flickering.new.describe_to(summary) + it 'apply filters to the backtrace' do + filter_class = double + filter = double + filtered_result = double + allow(filter_class).to receive(:new).with(result.exception).and_return(filter) + allow(filter).to receive(:exception).and_return(filtered_result) - expect(summary.total_flickering).to eq(1) - expect(summary.total(:flickering)).to eq(1) - expect(summary.total).to eq(1) - end + expect(result.with_filtered_backtrace(filter_class)).to eq(filtered_result) + end + end + end - it 'returns zero for a status where no messages have been received' do - expect(summary.total_passed).to eq(0) - expect(summary.total(:passed)).to eq(0) - expect(summary.total_ponies).to eq(0) - expect(summary.total(:ponies)).to eq(0) - end + describe Cucumber::Core::Test::Result::Ambiguous do + subject(:result) { described_class.new } - it "doesn't count unknown results" do - unknown.describe_to(summary) + it 'describes itself to a visitor' do + expect(visitor).to receive(:ambiguous).with(args) + expect(visitor).to receive(:duration).with(an_unknown_duration, args) - expect(summary.total).to eq(0) - end + result.describe_to(visitor, args) + end - it 'counts combinations' do - [passed, passed, failed, skipped, undefined].each { |result| result.describe_to(summary) } + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::AMBIGUOUS) + end - expect(summary.total).to eq(5) - expect(summary.total_passed).to eq(2) - expect(summary.total_failed).to eq(1) - expect(summary.total_skipped).to eq(1) - expect(summary.total_undefined).to eq(1) - end + it { expect(result.to_sym).to eq(:ambiguous) } + it { expect(result).not_to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).to be_ambiguous } + it { expect(result).not_to be_undefined } + it { expect(result).not_to be_unknown } + it { expect(result).not_to be_skipped } + it { expect(result).not_to be_flaky } + it { expect(result).not_to be_ok } + end - it 'records durations' do - [passed, failed].each { |result| result.describe_to(summary) } + describe Cucumber::Core::Test::Result::Undefined do + subject(:result) { described_class.new } - expect(summary.durations[0]).to be_duration(11) - expect(summary.durations[1]).to be_duration(10) - end + it 'describes itself to a visitor' do + expect(visitor).to receive(:undefined).with(args) + expect(visitor).to receive(:duration).with(an_unknown_duration, args) - it 'records exceptions' do - [passed, failed].each { |result| result.describe_to(summary) } + result.describe_to(visitor, args) + end - expect(summary.exceptions).to eq([exception]) - end + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::UNDEFINED) + end - describe '#ok?' do - it 'passed result is ok' do - passed.describe_to(summary) + it { expect(result.to_sym).to eq(:undefined) } + it { expect(result).not_to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).to be_undefined } + it { expect(result).not_to be_unknown } + it { expect(result).not_to be_skipped } + it { expect(result).not_to be_flaky } - expect(summary.ok?).to be true - end + it { expect(result).not_to be_ok } + end - it 'skipped result is ok' do - skipped.describe_to(summary) + describe Cucumber::Core::Test::Result::Skipped do + subject(:result) { described_class.new } - expect(summary.ok?).to be true - end + it 'describes itself to a visitor' do + expect(visitor).to receive(:skipped).with(args) + expect(visitor).to receive(:duration).with(an_unknown_duration, args) - it 'failed result is not ok' do - failed.describe_to(summary) + result.describe_to(visitor, args) + end - expect(summary.ok?).to be false - end + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::SKIPPED) + end - it 'pending result is not ok' do - pending.describe_to(summary) + it { expect(result.to_sym).to eq(:skipped) } + it { expect(result).not_to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).not_to be_undefined } + it { expect(result).not_to be_unknown } + it { expect(result).to be_skipped } + it { expect(result).not_to be_flaky } + it { expect(result).to be_ok } + end - expect(summary.ok?).to be false - end + describe Cucumber::Core::Test::Result::Pending do + subject(:result) { described_class.new } - it 'undefined result is not ok' do - undefined.describe_to(summary) + it 'describes itself to a visitor' do + expect(visitor).to receive(:pending).with(result, args) + expect(visitor).to receive(:duration).with(an_unknown_duration, args) - expect(summary.ok?).to be false - end + result.describe_to(visitor, args) + end - it 'flaky result is not ok' do - summary.flaky + it 'converts to a Cucumber::Message::TestResult' do + expect(result.to_message.status).to eq(Cucumber::Messages::TestStepResultStatus::PENDING) + end - expect(summary.ok?).to be false - end - end - end + it { expect(result.to_sym).to eq(:pending) } + it { expect(result).not_to be_passed } + it { expect(result).not_to be_failed } + it { expect(result).not_to be_ambiguous } + it { expect(result).not_to be_undefined } + it { expect(result).not_to be_unknown } + it { expect(result).not_to be_skipped } + it { expect(result).not_to be_flaky } - describe Result::Duration do - subject(:duration) { described_class.new(10) } + it { expect(result).not_to be_ok } + end - it '#nanoseconds can be accessed in #tap' do - expect(duration.tap { |duration| @duration = duration.nanoseconds }).to eq(duration) - expect(@duration).to eq(10) - end - end + describe Cucumber::Core::Test::Result::Flaky do + it { expect(described_class).not_to be_ok } + end - describe Result::UnknownDuration do - subject(:duration) { described_class.new } + describe Cucumber::Core::Test::Result::Summary do + let(:summary) { described_class.new } + let(:failed) { Cucumber::Core::Test::Result::Failed.new(Cucumber::Core::Test::Result::Duration.new(10), exception) } + let(:passed) { Cucumber::Core::Test::Result::Passed.new(Cucumber::Core::Test::Result::Duration.new(11)) } + let(:skipped) { Cucumber::Core::Test:: Result::Skipped.new } + let(:unknown) { Cucumber::Core::Test::Result::Unknown.new } + let(:pending) { Cucumber::Core::Test::Result::Pending.new } + let(:undefined) { Cucumber::Core::Test::Result::Undefined.new } + let(:exception) { StandardError.new } + + it 'counts failed results' do + failed.describe_to(summary) + + expect(summary.total_failed).to eq(1) + expect(summary.total(:failed)).to eq(1) + expect(summary.total).to eq(1) + end + + it 'counts passed results' do + passed.describe_to(summary) + + expect(summary.total_passed).to eq(1) + expect(summary.total(:passed)).to eq(1) + expect(summary.total).to eq(1) + end + + it 'counts skipped results' do + skipped.describe_to(summary) + + expect(summary.total_skipped).to eq(1) + expect(summary.total(:skipped)).to eq(1) + expect(summary.total).to eq(1) + end - it '#tap does not execute the passed block' do - expect(duration.tap { raise 'tap executed block' }).to eq duration - end + it 'counts undefined results' do + undefined.describe_to(summary) - it 'accessing #nanoseconds outside #tap block raises exception' do - expect { duration.nanoseconds }.to raise_error(RuntimeError) - end + expect(summary.total_undefined).to eq(1) + expect(summary.total(:undefined)).to eq(1) + expect(summary.total).to eq(1) + end + + it 'counts arbitrary raisable results' do + flickering = Class.new(Cucumber::Core::Test::Result::Raisable) do + def describe_to(visitor, *args) + visitor.flickering(*args) end end + + flickering.new.describe_to(summary) + + expect(summary.total_flickering).to eq(1) + expect(summary.total(:flickering)).to eq(1) + expect(summary.total).to eq(1) + end + + it 'returns zero for a status where no messages have been received' do + expect(summary.total_passed).to eq(0) + expect(summary.total(:passed)).to eq(0) + expect(summary.total_ponies).to eq(0) + expect(summary.total(:ponies)).to eq(0) + end + + it "doesn't count unknown results" do + unknown.describe_to(summary) + + expect(summary.total).to eq(0) + end + + it 'counts combinations' do + [passed, passed, failed, skipped, undefined].each { |result| result.describe_to(summary) } + + expect(summary.total).to eq(5) + expect(summary.total_passed).to eq(2) + expect(summary.total_failed).to eq(1) + expect(summary.total_skipped).to eq(1) + expect(summary.total_undefined).to eq(1) + end + + it 'records durations' do + [passed, failed].each { |result| result.describe_to(summary) } + + expect(summary.durations[0]).to be_duration(11) + expect(summary.durations[1]).to be_duration(10) + end + + it 'records exceptions' do + [passed, failed].each { |result| result.describe_to(summary) } + + expect(summary.exceptions).to eq([exception]) + end + + describe '#ok?' do + it 'passed result is ok' do + passed.describe_to(summary) + + expect(summary.ok?).to be true + end + + it 'skipped result is ok' do + skipped.describe_to(summary) + + expect(summary.ok?).to be true + end + + it 'failed result is not ok' do + failed.describe_to(summary) + + expect(summary.ok?).to be false + end + + it 'pending result is not ok' do + pending.describe_to(summary) + + expect(summary.ok?).to be false + end + + it 'undefined result is not ok' do + undefined.describe_to(summary) + + expect(summary.ok?).to be false + end + + it 'flaky result is not ok' do + summary.flaky + + expect(summary.ok?).to be false + end + end + end + + describe Cucumber::Core::Test::Result::Duration do + subject(:duration) { described_class.new(10) } + + it '#nanoseconds can be accessed in #tap' do + expect(duration.tap { |duration| @duration = duration.nanoseconds }).to eq(duration) + expect(@duration).to eq(10) + end + end + + describe Cucumber::Core::Test::Result::UnknownDuration do + subject(:duration) { described_class.new } + + it '#tap does not execute the passed block' do + expect(duration.tap { raise 'tap executed block' }).to eq duration end + + it 'accessing #nanoseconds outside #tap block raises exception' do + expect { duration.nanoseconds }.to raise_error(RuntimeError) + end + end + + # Permit an exception to be filtered and not excluded + def permit_exception_passthrough(filter_class, filter, filtered_exception) + allow(filter_class).to receive(:new).with(result.exception).and_return(filter) + allow(filter).to receive(:exception).and_return(filtered_exception) end end From 5cb89636a81607e153743659f4bd9ce608e74f84 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:39:03 +0100 Subject: [PATCH 07/11] Tidy up some of result spec line example length --- spec/cucumber/core/test/result_spec.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/cucumber/core/test/result_spec.rb b/spec/cucumber/core/test/result_spec.rb index 3fc79352..b39db198 100644 --- a/spec/cucumber/core/test/result_spec.rb +++ b/spec/cucumber/core/test/result_spec.rb @@ -183,14 +183,13 @@ expect(result.with_appended_backtrace(step).backtrace).to eq(%w[backtrace step_line]) end - it 'apply filters to the backtrace' do + it 'applies filters to the backtrace' do filter_class = double filter = double - filtered_result = double - allow(filter_class).to receive(:new).with(result.exception).and_return(filter) - allow(filter).to receive(:exception).and_return(filtered_result) + filtered_backtrace = double + permit_exception_passthrough(filter_class, filter, filtered_backtrace) - expect(result.with_filtered_backtrace(filter_class)).to eq(filtered_result) + expect(result.with_filtered_backtrace(filter_class)).to eq(filtered_backtrace) end end end @@ -454,8 +453,8 @@ def describe_to(visitor, *args) end # Permit an exception to be filtered and not excluded - def permit_exception_passthrough(filter_class, filter, filtered_exception) + def permit_exception_passthrough(filter_class, filter, filtered_value) allow(filter_class).to receive(:new).with(result.exception).and_return(filter) - allow(filter).to receive(:exception).and_return(filtered_exception) + allow(filter).to receive(:exception).and_return(filtered_value) end end From d877f6c7eef9228a10e5cef46e902b0e76e68a1b Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 19:56:01 +0100 Subject: [PATCH 08/11] Partially tidy up line length and complexity of speccs --- .../core/test/locations_filter_spec.rb | 125 +++++++++--------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/spec/cucumber/core/test/locations_filter_spec.rb b/spec/cucumber/core/test/locations_filter_spec.rb index 3bd4c150..dc68d483 100644 --- a/spec/cucumber/core/test/locations_filter_spec.rb +++ b/spec/cucumber/core/test/locations_filter_spec.rb @@ -42,30 +42,27 @@ def test_cases end end - it 'sorts by the given locations' do - locations = [ - Cucumber::Core::Test::Location.new('features/test.feature', 6), - Cucumber::Core::Test::Location.new('features/test.feature', 3) - ] + def to_location(file, line = nil) + Cucumber::Core::Test::Location.new(file, line) + end + + it 'filters by the given locations' do + locations = [to_location('features/test.feature', 6), to_location('features/test.feature', 3)] filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq(locations) end - it 'works with wildcard locations' do - locations = [Cucumber::Core::Test::Location.new('features/test.feature')] - filter = described_class.new(locations) + it 'returns all locations in a specific file when a specific line is omitted' do + filter = described_class.new([to_location('features/test.feature')]) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq( - [ - Cucumber::Core::Test::Location.new('features/test.feature', 3), - Cucumber::Core::Test::Location.new('features/test.feature', 6) - ] + [to_location('features/test.feature', 3), to_location('features/test.feature', 6)] ) end - it "filters out scenarios that don't match" do - locations = [Cucumber::Core::Test::Location.new('features/test.feature', 3)] + it "filters out scenarios that do not match" do + locations = [to_location('features/test.feature', 3)] filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq(locations) @@ -135,30 +132,30 @@ def test_case_named(name) test_cases.detect { |tc| tc.name == name } end - it 'matches the feature location to all scenarios' do - location = Cucumber::Core::Test::Location.new(file, 1) - filter = described_class.new([location]) + it 'matches the feature keyword location to all scenarios' do + locations = [to_location(file, 1)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq(test_cases.map(&:location)) end - it 'matches the feature background location to all scenarios' do - location = Cucumber::Core::Test::Location.new(file, 2) - filter = described_class.new([location]) + it 'matches the feature background keyword location to all scenarios' do + locations = [to_location(file, 2)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq(test_cases.map(&:location)) end - it 'matches a feature background step location to all scenarios' do - location = Cucumber::Core::Test::Location.new(file, 3) - filter = described_class.new([location]) + it 'matches a feature background step keyword location to all scenarios' do + locations = [to_location(file, 3)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq(test_cases.map(&:location)) end - it "matches a rule location (containing a background) to all of the rule's scenarios" do - location = Cucumber::Core::Test::Location.new(file, 29) - filter = described_class.new([location]) + it "matches a rule keyword location (containing a background), to all of the scenarios contained in the rule" do + locations = [to_location(file, 29)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq( [ @@ -168,9 +165,9 @@ def test_case_named(name) ) end - it "matches the rule background location to all of the rule's scenarios" do - location = Cucumber::Core::Test::Location.new(file, 30) - filter = described_class.new([location]) + it "matches a rule background keyword location to all of the scenarios contained in the rule" do + locations = [to_location(file, 30)] + filter = described_class.new(locations) compile [doc], receiver, [filter] expect(receiver.test_case_locations).to eq( [ @@ -180,9 +177,9 @@ def test_case_named(name) ) end - it "matches a rule background step location to all of the rule's scenarios" do - location = Cucumber::Core::Test::Location.new(file, 31) - filter = described_class.new([location]) + it "matches a rule background step keyword location to all of the scenarios contained in the rule" do + locations = [to_location(file, 31)] + filter = described_class.new(locations) compile [doc], receiver, [filter] expect(receiver.test_case_locations).to eq( [ @@ -192,9 +189,9 @@ def test_case_named(name) ) end - it "matches a rule location (without a background) to all of the rule's scenarios" do - location = Cucumber::Core::Test::Location.new(file, 39) - filter = described_class.new([location]) + it "matches a rule keyword location (without a background), to all of the scenarios contained in the rule" do + locations = [to_location(file, 39)] + filter = described_class.new(locations) compile [doc], receiver, [filter] expect(receiver.test_case_locations).to eq( [ @@ -212,10 +209,8 @@ def test_case_named(name) end it 'matches multiple locations, ignoring whitespace locations' do - scenario_location = Cucumber::Core::Test::Location.new(file, 5) - another_scenario_location = Cucumber::Core::Test::Location.new(file, 10) - whitespace_location = Cucumber::Core::Test::Location.new(file, 7) - filter = described_class.new([scenario_location, another_scenario_location, whitespace_location]) + locations = [to_location(file, 5), to_location(file, 10), to_location(file, 7)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq( [ @@ -226,53 +221,53 @@ def test_case_named(name) end it 'matches the first scenario step location to the scenario' do - location = Cucumber::Core::Test::Location.new(file, 11) - filter = described_class.new([location]) + locations = [to_location(file, 11)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('two').location]) end it 'matches the last scenario step location to the scenario' do - location = Cucumber::Core::Test::Location.new(file, 12) - filter = described_class.new([location]) + locations = [to_location(file, 12)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('two').location]) end it "matches a scenario's tag location to the scenario" do - location = Cucumber::Core::Test::Location.new(file, 9) - filter = described_class.new([location]) + locations = [to_location(file, 9)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('two').location]) end it 'does not match a whitespace location to any scenarios' do - location = Cucumber::Core::Test::Location.new(file, 13) - filter = described_class.new([location]) + locations = [to_location(file, 13)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([]) end context 'with a docstring' do it 'matches a location at the start the docstring' do - location = Cucumber::Core::Test::Location.new(file, 17) - filter = described_class.new([location]) + locations = [to_location(file, 17)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('with docstring').location]) end it 'matches a location in the middle of the docstring' do - location = Cucumber::Core::Test::Location.new(file, 18) - filter = described_class.new([location]) + locations = [to_location(file, 18)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('with docstring').location]) end it 'matches a location at the end of the docstring' do - location = Cucumber::Core::Test::Location.new(file, 19) - filter = described_class.new([location]) + locations = [to_location(file, 19)] + filter = described_class.new(locations) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('with docstring').location]) @@ -281,9 +276,9 @@ def test_case_named(name) context 'with a table' do let(:test_case) { test_cases.detect { |tc| tc.name == 'with a table' } } - let(:starting_location) { Cucumber::Core::Test::Location.new(file, 23) } - let(:midpoint_location) { Cucumber::Core::Test::Location.new(file, 24) } - let(:ending_location) { Cucumber::Core::Test::Location.new(file, 25) } + let(:starting_location) { to_location(file, 23) } + let(:midpoint_location) { to_location(file, 24) } + let(:ending_location) { to_location(file, 25) } it 'matches a location at the start of the table' do filter = described_class.new([starting_location]) @@ -308,7 +303,7 @@ def test_case_named(name) it 'matches each test case only once' do location_tc_two = test_case_named('two').location location_tc_one = test_case_named('one').location - location_last_step_tc_two = Cucumber::Core::Test::Location.new(file, 12) + location_last_step_tc_two = to_location(file, 12) filter = described_class.new([location_tc_two, location_tc_one, location_last_step_tc_two]) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([test_case_named('two').location, test_case_named('one').location]) @@ -350,11 +345,12 @@ def test_case_named(name) end let(:test_case) { test_cases.detect { |tc| tc.name == 'two b' } } - let(:feature_location) { Cucumber::Core::Test::Location.new(file, 1) } - let(:row_location) { Cucumber::Core::Test::Location.new(file, 19) } - let(:start_of_outline_location) { Cucumber::Core::Test::Location.new(file, 8) } - let(:middle_of_outline_location) { Cucumber::Core::Test::Location.new(file, 10) } - let(:outline_tags_location) { Cucumber::Core::Test::Location.new(file, 7) } + + let(:feature_location) { to_location(file, 1) } + let(:row_location) { to_location(file, 19) } + let(:start_of_outline_location) { to_location(file, 8) } + let(:middle_of_outline_location) { to_location(file, 10) } + let(:outline_tags_location) { to_location(file, 7) } it 'matches the feature line to all scenarios' do filter = described_class.new([feature_location]) @@ -386,9 +382,8 @@ def test_case_named(name) expect(receiver.test_case_locations.map(&:line)).to eq([19, 23, 24]) end - it "doesn't match the location of the examples line" do - location = Cucumber::Core::Test::Location.new(file, 17) - filter = described_class.new([location]) + it "does not match the location of the examples keyword line" do + filter = described_class.new([to_location(file, 17)]) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([]) end From 2ac7135dc696c54e4314b5763bd872fa99ec7f47 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 20:00:57 +0100 Subject: [PATCH 09/11] Final tidy for example length; --- spec/cucumber/core/test/runner_spec.rb | 24 +++++++++++++++++++++++- spec/cucumber/core/test/step_spec.rb | 7 ++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/spec/cucumber/core/test/runner_spec.rb b/spec/cucumber/core/test/runner_spec.rb index 3cbae6f5..69384ff5 100644 --- a/spec/cucumber/core/test/runner_spec.rb +++ b/spec/cucumber/core/test/runner_spec.rb @@ -457,6 +457,14 @@ test_case = Cucumber::Core::Test::Case.new(double, double, [passing_step], double, double, double, double, [around_hook]) allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result).to be_failed + end + test_case.describe_to(runner) + end + + it 'returns the exception object if the Around hook fails before the test case is run' do + around_hook = Cucumber::Core::Test::AroundHook.new { |_block| raise StandardError } + test_case = Cucumber::Core::Test::Case.new(double, double, [passing_step], double, double, double, double, [around_hook]) + allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result.exception).to be_a StandardError end test_case.describe_to(runner) @@ -466,15 +474,29 @@ test_case = Cucumber::Core::Test::Case.new(double, double, [passing_step], double, double, double, double, [failing_around_hook]) allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result).to be_failed + end + test_case.describe_to(runner) + end + + it 'returns the exception object if the Around hook fails after the test case is run' do + test_case = Cucumber::Core::Test::Case.new(double, double, [passing_step], double, double, double, double, [failing_around_hook]) + allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result.exception).to be_a(StandardError) end test_case.describe_to(runner) end - it 'fails when a step fails if the around hook works' do + it 'gets a failed result when a step fails if the around hook works' do test_case = Cucumber::Core::Test::Case.new(double, double, [failing_step], double, double, double, double, [passing_around_hook]) allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result).to be_failed + end + test_case.describe_to(runner) + end + + it 'returns an exception object when a step fails if the around hook works' do + test_case = Cucumber::Core::Test::Case.new(double, double, [failing_step], double, double, double, double, [passing_around_hook]) + allow(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| expect(result.exception).to be_a(StandardError) end test_case.describe_to(runner) diff --git a/spec/cucumber/core/test/step_spec.rb b/spec/cucumber/core/test/step_spec.rb index 8340ad0c..bd8feb82 100644 --- a/spec/cucumber/core/test/step_spec.rb +++ b/spec/cucumber/core/test/step_spec.rb @@ -28,15 +28,12 @@ end describe 'executing' do - it "passes arbitrary arguments to the action's block" do - args_spy = nil + it "passes arbitrary arguments to the action block" do expected_args = [double, double] test_step = described_class.new(id, text, location).with_action do |*actual_args| - args_spy = actual_args + expect(actual_args).to eq(expected_args) end test_step.execute(*expected_args) - - expect(args_spy).to eq expected_args end context 'when a passing action exists' do From 1d137e8450c5cf43617b5a350b3171ee24e07d5d Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 20:14:11 +0100 Subject: [PATCH 10/11] Autofix a few cops - regenerate the TODO --- .rubocop_todo.yml | 67 ++++++++----------- lib/cucumber/core/gherkin/parser.rb | 2 +- lib/cucumber/core/test/data_table.rb | 2 +- lib/cucumber/core/test/location.rb | 2 +- lib/cucumber/core/test/timer.rb | 2 +- .../core/test/locations_filter_spec.rb | 12 ++-- spec/cucumber/core/test/step_spec.rb | 2 +- spec/support/activate_steps_for_self_test.rb | 2 +- 8 files changed, 40 insertions(+), 51 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 90460966..b514ca62 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-06-01 22:49:40 UTC using RuboCop version 1.87.0. +# on 2026-06-02 19:13:42 UTC using RuboCop version 1.87.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -12,6 +12,12 @@ # TODO: [LH] v16.2 (Generic refactors / new events) -> 79 files inspected, 222 offenses detected, 10 offenses autocorrectable # TODO: [LH] v17 prep -> 92 files inspected, 212 offenses detected, 10 offenses autocorrectable +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/SpaceAroundMethodCallOperator: + Exclude: + - 'spec/cucumber/core/test/result_spec.rb' + # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/RedundantRequireStatement: @@ -23,33 +29,40 @@ Lint/RedundantRequireStatement: Metrics/AbcSize: Max: 21 -## Offense count: 3 -## Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. -#Metrics/MethodLength: -# Max: 28 +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 28 -# Offense count: 4 +# Offense count: 3 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 346 + Max: 110 # Offense count: 1 # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Max: 8 -# Offense count: 2 +# Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: - 'spec/cucumber/core/compiler_spec.rb' + - 'spec/cucumber/core/gherkin/writer_spec.rb' -# Offense count: 38 +# Offense count: 21 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 11 -# Offense count: 24 +# Offense count: 1 +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/cucumber/core/test/result_spec.rb' + +# Offense count: 23 RSpec/MissingExampleGroupArgument: Exclude: - 'spec/cucumber/core/compiler_spec.rb' @@ -60,21 +73,21 @@ RSpec/MissingExampleGroupArgument: - 'spec/cucumber/core/test/locations_filter_spec.rb' - 'spec/cucumber/core_spec.rb' -# Offense count: 69 +# Offense count: 66 RSpec/MultipleExpectations: Max: 5 -# Offense count: 48 +# Offense count: 53 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 11 -# Offense count: 8 +# Offense count: 13 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: - Max: 4 + Max: 5 -# Offense count: 23 +# Offense count: 22 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: @@ -105,27 +118,3 @@ RSpec/RepeatedExample: Style/AccessModifierDeclarations: Exclude: - 'lib/cucumber/core/filter.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowedParentClasses. -# SupportedStyles: class_keyword, class_new, class_definition -Style/EmptyClassDefinition: - Exclude: - - 'lib/cucumber/core/gherkin/parser.rb' - - 'lib/cucumber/core/test/location.rb' - - 'spec/support/activate_steps_for_self_test.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'lib/cucumber/core/test/timer.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Exclude: - - 'lib/cucumber/core/test/data_table.rb' diff --git a/lib/cucumber/core/gherkin/parser.rb b/lib/cucumber/core/gherkin/parser.rb index 3ee8eea6..2dd8f81c 100644 --- a/lib/cucumber/core/gherkin/parser.rb +++ b/lib/cucumber/core/gherkin/parser.rb @@ -5,7 +5,7 @@ module Cucumber module Core module Gherkin - ParseError = Class.new(StandardError) + class ParseError < StandardError; end class Parser attr_reader :receiver, :event_bus, :gherkin_query diff --git a/lib/cucumber/core/test/data_table.rb b/lib/cucumber/core/test/data_table.rb index e44a2e4c..ddbef1ee 100644 --- a/lib/cucumber/core/test/data_table.rb +++ b/lib/cucumber/core/test/data_table.rb @@ -57,7 +57,7 @@ def dup end def inspect - %{#<#{self.class} #{raw.inspect}>} + %(#<#{self.class} #{raw.inspect}>) end def lines_count diff --git a/lib/cucumber/core/test/location.rb b/lib/cucumber/core/test/location.rb index ca98bb07..5cbc0105 100644 --- a/lib/cucumber/core/test/location.rb +++ b/lib/cucumber/core/test/location.rb @@ -5,7 +5,7 @@ module Cucumber module Core module Test - IncompatibleLocations = Class.new(StandardError) + class IncompatibleLocations < StandardError; end module Location def self.of_caller(additional_depth = 0) diff --git a/lib/cucumber/core/test/timer.rb b/lib/cucumber/core/test/timer.rb index 906de0ae..b4bd5a7b 100644 --- a/lib/cucumber/core/test/timer.rb +++ b/lib/cucumber/core/test/timer.rb @@ -38,7 +38,7 @@ def time_in_nanoseconds end elsif (defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby') == 'jruby' def time_in_nanoseconds - java.lang.System.nanoTime() + java.lang.System.nanoTime end else def time_in_nanoseconds diff --git a/spec/cucumber/core/test/locations_filter_spec.rb b/spec/cucumber/core/test/locations_filter_spec.rb index dc68d483..a3993341 100644 --- a/spec/cucumber/core/test/locations_filter_spec.rb +++ b/spec/cucumber/core/test/locations_filter_spec.rb @@ -61,7 +61,7 @@ def to_location(file, line = nil) ) end - it "filters out scenarios that do not match" do + it 'filters out scenarios that do not match' do locations = [to_location('features/test.feature', 3)] filter = described_class.new(locations) compile([doc], receiver, [filter]) @@ -153,7 +153,7 @@ def test_case_named(name) expect(receiver.test_case_locations).to eq(test_cases.map(&:location)) end - it "matches a rule keyword location (containing a background), to all of the scenarios contained in the rule" do + it 'matches a rule keyword location (containing a background), to all of the scenarios contained in the rule' do locations = [to_location(file, 29)] filter = described_class.new(locations) compile([doc], receiver, [filter]) @@ -165,7 +165,7 @@ def test_case_named(name) ) end - it "matches a rule background keyword location to all of the scenarios contained in the rule" do + it 'matches a rule background keyword location to all of the scenarios contained in the rule' do locations = [to_location(file, 30)] filter = described_class.new(locations) compile [doc], receiver, [filter] @@ -177,7 +177,7 @@ def test_case_named(name) ) end - it "matches a rule background step keyword location to all of the scenarios contained in the rule" do + it 'matches a rule background step keyword location to all of the scenarios contained in the rule' do locations = [to_location(file, 31)] filter = described_class.new(locations) compile [doc], receiver, [filter] @@ -189,7 +189,7 @@ def test_case_named(name) ) end - it "matches a rule keyword location (without a background), to all of the scenarios contained in the rule" do + it 'matches a rule keyword location (without a background), to all of the scenarios contained in the rule' do locations = [to_location(file, 39)] filter = described_class.new(locations) compile [doc], receiver, [filter] @@ -382,7 +382,7 @@ def test_case_named(name) expect(receiver.test_case_locations.map(&:line)).to eq([19, 23, 24]) end - it "does not match the location of the examples keyword line" do + it 'does not match the location of the examples keyword line' do filter = described_class.new([to_location(file, 17)]) compile([doc], receiver, [filter]) expect(receiver.test_case_locations).to eq([]) diff --git a/spec/cucumber/core/test/step_spec.rb b/spec/cucumber/core/test/step_spec.rb index bd8feb82..43aef53f 100644 --- a/spec/cucumber/core/test/step_spec.rb +++ b/spec/cucumber/core/test/step_spec.rb @@ -28,7 +28,7 @@ end describe 'executing' do - it "passes arbitrary arguments to the action block" do + it 'passes arbitrary arguments to the action block' do expected_args = [double, double] test_step = described_class.new(id, text, location).with_action do |*actual_args| expect(actual_args).to eq(expected_args) diff --git a/spec/support/activate_steps_for_self_test.rb b/spec/support/activate_steps_for_self_test.rb index aff1b220..5af09c45 100644 --- a/spec/support/activate_steps_for_self_test.rb +++ b/spec/support/activate_steps_for_self_test.rb @@ -3,7 +3,7 @@ # This filter is used for testing Cucumber itself. It adds step definitions that # will activate steps to have passed / failed / pending results if they use expected names. class ActivateStepsForSelfTest < Cucumber::Core::Filter.new - Failure = Class.new(StandardError) + class Failure < StandardError; end def test_case(test_case) test_case.with_steps(test_steps(test_case)).describe_to(receiver) From 2cbe7e4db00198c76515561e714deb2e20ec2e93 Mon Sep 17 00:00:00 2001 From: Luke Hill Date: Tue, 2 Jun 2026 20:14:49 +0100 Subject: [PATCH 11/11] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 634962ac..c08b597d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ 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] +### Changed +- Tidied up a bunch of mostly internal test code to be more rubocop compliant ## [17.0.0] - 2026-06-01 ### Added