diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b358b600..248b0f993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo ## [Unreleased] ### Added - Print thread backtraces on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) +- Added `Suggestion` messages that will show all the snippets for all message based formatters ([#1870](https://github.com/cucumber/cucumber-ruby/pull/1870)) [luke-hill](https://github.com/luke-hill) ### Changed - Heavy refactor to the internals for message building (Used in formatters - should be no noticeable change) ([#1853](https://github.com/cucumber/cucumber-ruby/pull/1853) [luke-hill](https://github.com/luke-hill)) - Altered the concept of how `BeforeAll` and `AfterAll` hooks would run. They now attempt to all run before continuing test execution ([#1857](https://github.com/cucumber/cucumber-ruby/pull/1857) [brasmusson](https://github.com/brasmusson)) - Internal refactor to `MessageBuilder` class to send envelopes through event bus (Should be no noticeable change) +- Updated `cucumber-compatibility-kit` to v24 ## [11.0.0] - 2026-04-14 ### Added @@ -36,7 +38,6 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo > The `rerun` formatter was chosen as the first formatter to migrate to this new structure as it is one of the simpler > formatters and will allow us to test the new structure in a real-world scenario. - Updated `cucumber-compatibility-kit` to v22 -- Security: Switched out `IO.read` for more secure `File.read` in a few areas of the codebase - Implemented the new cucumber-query structure in all message based formatters (Currently HTML / Rerun and Message) ([#1844](https://github.com/cucumber/cucumber-ruby/pull/1844) [luke-hill](https://github.com/luke-hill)) @@ -49,6 +50,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo - Fixed an issue where NoMethodError could be raised when declaring a parameter-type that used bound methods ([#1789](https://github.com/cucumber/cucumber-ruby/pull/1789)) +### Security +- Switched out `IO.read` for more secure `File.read` in a few areas of the codebase + ## [10.2.0] - 2025-12-10 ### Changed - Permit the latest version of the `cucumber-html-formatter` (v22.0.0+) diff --git a/compatibility/cck_spec.rb b/compatibility/cck_spec.rb index 3dfa999c0..e4a1f620e 100644 --- a/compatibility/cck_spec.rb +++ b/compatibility/cck_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'support/shared_examples' -require_relative 'support/compatibility_kit' +require_relative 'support/cucumber/compatibility_kit' require 'cucumber/compatibility_kit' @@ -10,16 +10,18 @@ # # All step definition and required supporting logic is contained here, the CCK gem proper contains the source of truth # of the "golden" NDJSON files and attachments / miscellaneous files -RSpec.describe CCK, :cck do +RSpec.describe 'CCK', :cck do let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } - # CCK v22 conformance - # OVERALL: 93 examples, 0 failures, 93 passed - # SANITIZED: 84 examples, 0 failures, 84 passed + # CCK v24 conformance + # OVERALL: 111 examples, 2 failures, 109 passed + # SANITIZED: 108 examples, 0 failures, 108 passed items_to_fix = - %w[] - _failing, passing = CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } + %w[ + test-run-exception + ] + _failing, passing = Cucumber::CompatibilityKit.gherkin.partition { |name| items_to_fix.include?(name) } passing.each do |example_name| describe "'#{example_name}' example" do @@ -32,7 +34,7 @@ '' end end - let(:support_code_path) { CompatibilityKit.supporting_code_for(example) } + let(:support_code_path) { Cucumber::CompatibilityKit.supporting_code_for(example) } let(:messages) { `#{cucumber_command} --require #{support_code_path} #{cck_path} #{extra_args}` } end end diff --git a/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb b/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb new file mode 100644 index 000000000..9b0bcd5a6 --- /dev/null +++ b/compatibility/features/examples-tables-undefined/examples-tables-undefined_steps.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +Given('there are {int} cucumbers') do |initial_count| + @count = initial_count +end + +When('I eat {int} cucumbers') do |eat_count| + @count -= eat_count +end + +Then('I should have {int} cucumbers') do |expected_count| + expect(@count).to eq(expected_count) +end diff --git a/compatibility/features/hooks-undefined/hooks-undefined_steps.rb b/compatibility/features/hooks-undefined/hooks-undefined_steps.rb new file mode 100644 index 000000000..208c7c96a --- /dev/null +++ b/compatibility/features/hooks-undefined/hooks-undefined_steps.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Before do + # no-op +end + +After do + # no-op +end diff --git a/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb new file mode 100644 index 000000000..34a35fe2c --- /dev/null +++ b/compatibility/features/retry-ambiguous/retry-ambiguous_steps.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Given('an ambiguous step') do + # first one +end + +Given('an ambiguous step') do + # second one +end diff --git a/compatibility/features/retry-pending/retry-pending_steps.rb b/compatibility/features/retry-pending/retry-pending_steps.rb new file mode 100644 index 000000000..61259124a --- /dev/null +++ b/compatibility/features/retry-pending/retry-pending_steps.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Given('a pending step') do + pending('') +end diff --git a/compatibility/features/retry-undefined/retry-undefined_steps.rb b/compatibility/features/retry-undefined/retry-undefined_steps.rb new file mode 100644 index 000000000..9ed675349 --- /dev/null +++ b/compatibility/features/retry-undefined/retry-undefined_steps.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +# There are intentionally no steps defined for this sample diff --git a/compatibility/features/retry/retry_steps.rb b/compatibility/features/retry/retry_steps.rb index d042e9304..c31a0aa63 100644 --- a/compatibility/features/retry/retry_steps.rb +++ b/compatibility/features/retry/retry_steps.rb @@ -19,15 +19,3 @@ Given('a step that always fails') do raise 'Exception in step' end - -Given('an ambiguous step') do - # first one -end - -Given('an ambiguous step') do - # second one -end - -Given('a pending step') do - pending('') -end diff --git a/compatibility/features/test-run-exception/test-run-exception_steps.rb b/compatibility/features/test-run-exception/test-run-exception_steps.rb new file mode 100644 index 000000000..fd015a0fa --- /dev/null +++ b/compatibility/features/test-run-exception/test-run-exception_steps.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Given('a step') do + # no-op +end diff --git a/compatibility/spec/cck/keys_checker_spec.rb b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb similarity index 95% rename from compatibility/spec/cck/keys_checker_spec.rb rename to compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb index 3af9a1699..b57007fb7 100644 --- a/compatibility/spec/cck/keys_checker_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/keys_checker_spec.rb @@ -2,9 +2,10 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cck/keys_checker' -RSpec.describe CCK::KeysChecker do +require_relative '../../../support/cucumber/compatibility_kit' + +RSpec.describe Cucumber::CompatibilityKit::KeysChecker do describe '#compare' do let(:expected_kvps) { Cucumber::Messages::Attachment.new(url: 'https://foo.com', file_name: 'file.extension', test_step_id: 123_456) } let(:missing_kvps) { Cucumber::Messages::Attachment.new(url: 'https://foo.com') } diff --git a/compatibility/spec/cck/messages_comparator_spec.rb b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb similarity index 88% rename from compatibility/spec/cck/messages_comparator_spec.rb rename to compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb index 4de4bed12..b4ce9333f 100644 --- a/compatibility/spec/cck/messages_comparator_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit/messages_comparator_spec.rb @@ -2,9 +2,10 @@ require 'rspec' require 'cucumber/messages' -require_relative '../../support/cck/messages_comparator' -RSpec.describe CCK::MessagesComparator do +require_relative '../../../support/cucumber/compatibility_kit' + +RSpec.describe Cucumber::CompatibilityKit::MessagesComparator do describe '#errors' do context 'when executed as part of a CI' do before { allow(ENV).to receive(:[]).with('CI').and_return(true) } diff --git a/compatibility/spec/compatibility_kit_spec.rb b/compatibility/spec/cucumber/compatibility_kit_spec.rb similarity index 82% rename from compatibility/spec/compatibility_kit_spec.rb rename to compatibility/spec/cucumber/compatibility_kit_spec.rb index 56541c958..e57d359e1 100644 --- a/compatibility/spec/compatibility_kit_spec.rb +++ b/compatibility/spec/cucumber/compatibility_kit_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative '../support/compatibility_kit' +require_relative '../../support/cucumber/compatibility_kit' -RSpec.describe CompatibilityKit do - let(:features_path) { File.expand_path("#{File.dirname(__FILE__)}/../features") } +RSpec.describe Cucumber::CompatibilityKit do + let(:features_path) { File.expand_path("#{File.dirname(__FILE__)}/../../features") } describe '.supporting_code_for' do context 'with an example that exists' do diff --git a/compatibility/support/cck/helpers.rb b/compatibility/support/cck/helpers.rb deleted file mode 100644 index 066141813..000000000 --- a/compatibility/support/cck/helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module CCK - module Helpers - def message_type(message) - message.to_h.each do |key, value| - return key unless value.nil? - end - end - - def parse_ndjson_file(path) - parse_ndjson(File.read(path)) - end - - def parse_ndjson(ndjson) - Cucumber::Messages::Helpers::NdjsonToMessageEnumerator.new(ndjson) - end - end -end diff --git a/compatibility/support/cck/keys_checker.rb b/compatibility/support/cck/keys_checker.rb deleted file mode 100644 index 17416d6ad..000000000 --- a/compatibility/support/cck/keys_checker.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module CCK - class KeysChecker - def self.compare(detected, expected) - new(detected, expected).compare - end - - attr_reader :detected, :expected - - def initialize(detected, expected) - @detected = detected - @expected = expected - end - - def compare - return if identical_keys? - return "Detected extra keys in message #{message_name}: #{extra_keys}" if extra_keys.any? - - # TODO: Remove this override when the CCK is being checked at v29+ - return if missing_keys == [:children] - - "Missing keys in message #{message_name}: #{missing_keys}" if missing_keys.any? - rescue StandardError => e - ["Unexpected error: #{e.message}"] - end - - private - - def identical_keys? - detected_keys == expected_keys - end - - def detected_keys - @detected_keys ||= ordered_uniq_hash_keys(detected) - end - - def expected_keys - @expected_keys ||= ordered_uniq_hash_keys(expected) - end - - def ordered_uniq_hash_keys(object) - object.to_h(reject_nil_values: true).keys.sort - end - - def extra_keys - (detected_keys - expected_keys).reject { |key| meta_message? && key == :ci } - end - - def missing_keys - (expected_keys - detected_keys).reject { |key| meta_message? && key == :ci } - end - - def meta_message? - detected.instance_of?(Cucumber::Messages::Meta) - end - - def message_name - detected.class.name - end - end -end diff --git a/compatibility/support/cck/messages_comparator.rb b/compatibility/support/cck/messages_comparator.rb deleted file mode 100644 index 971af63ad..000000000 --- a/compatibility/support/cck/messages_comparator.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require_relative 'keys_checker' -require_relative 'helpers' - -module CCK - class MessagesComparator - include Helpers - - def initialize(detected, expected) - compare(detected, expected) - end - - def errors - all_errors.compact - end - - private - - def compare(detected, expected) - detected_by_type = messages_by_type(detected) - expected_by_type = messages_by_type(expected) - - detected_by_type.each_key do |type| - compare_list(detected_by_type[type], expected_by_type[type]) - rescue StandardError => e - # TODO: Remove this override when the CCK is being checked at v29+ - if e.message.include?('each_with_index') - :ignore_until_cck_v29 - else - all_errors << "Error while comparing #{type}: #{e.message}" - end - end - end - - def messages_by_type(messages) - by_type = Hash.new { |h, k| h[k] = [] } - messages.each do |msg| - by_type[message_type(msg)] << remove_envelope(msg) - end - by_type - end - - def remove_envelope(message) - message.send(message_type(message)) - end - - def compare_list(detected, expected) - detected.each_with_index do |message, index| - compare_message(message, expected[index]) - end - end - - def compare_message(detected, expected) - return if not_message?(detected) - return if ignorable?(detected) - return if incomparable?(detected) - - all_errors << CCK::KeysChecker.compare(detected, expected) - compare_sub_messages(detected, expected) - end - - def not_message?(detected) - !detected.is_a?(Cucumber::Messages::Message) - end - - # These messages need to be ignored because they are too large, or they feature timestamps which will be different - def ignorable?(detected) - too_large_message?(detected) || time_message?(detected) - end - - def too_large_message?(detected) - detected.is_a?(Cucumber::Messages::GherkinDocument) || detected.is_a?(Cucumber::Messages::Pickle) - end - - def time_message?(detected) - detected.is_a?(Cucumber::Messages::Timestamp) || detected.is_a?(Cucumber::Messages::Duration) - end - - # These messages need to be ignored because they are often not of identical shape - def incomparable?(detected) - detected.is_a?(Cucumber::Messages::Ci) || detected.is_a?(Cucumber::Messages::Git) - end - - def compare_sub_messages(detected, expected) - return unless expected.respond_to? :to_h - - expected.to_h.each_key do |key| - value = expected.send(key) - if value.is_a?(Array) - compare_list(detected.send(key), value) - else - compare_message(detected.send(key), value) - end - end - end - - def all_errors - @all_errors ||= [] - end - end -end diff --git a/compatibility/support/compatibility_kit.rb b/compatibility/support/compatibility_kit.rb deleted file mode 100644 index 70b0b8c63..000000000 --- a/compatibility/support/compatibility_kit.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class CompatibilityKit - class << self - def supporting_code_for(example_name) - path = File.join(local_features_folder_location, example_name) - - return path if File.directory?(path) - - raise ArgumentError, "No supporting code directory found locally for CCK example: #{example_name}" - end - - private - - def local_features_folder_location - File.expand_path("#{File.dirname(__FILE__)}/../features/") - end - end -end diff --git a/compatibility/support/cucumber/compatibility_kit.rb b/compatibility/support/cucumber/compatibility_kit.rb new file mode 100644 index 000000000..a3004d445 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative 'compatibility_kit/helpers' + +require_relative 'compatibility_kit/keys_checker' +require_relative 'compatibility_kit/messages_comparator' + +module Cucumber + class CompatibilityKit + class << self + def supporting_code_for(example_name) + path = File.join(local_features_folder_location, example_name) + + return path if File.directory?(path) + + raise ArgumentError, "No supporting code directory found locally for CCK example: #{example_name}" + end + + private + + def local_features_folder_location + File.expand_path("#{File.dirname(__FILE__)}/../../features/") + end + end + end +end diff --git a/compatibility/support/cucumber/compatibility_kit/helpers.rb b/compatibility/support/cucumber/compatibility_kit/helpers.rb new file mode 100644 index 000000000..558917422 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/helpers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Cucumber + class CompatibilityKit + module Helpers + def message_type(message) + message.to_h.each do |key, value| + return key unless value.nil? + end + end + + def parse_ndjson_file(path) + parse_ndjson(File.read(path)) + end + + def parse_ndjson(ndjson) + Cucumber::Messages::Helpers::NdjsonToMessageEnumerator.new(ndjson) + end + end + end +end diff --git a/compatibility/support/cucumber/compatibility_kit/keys_checker.rb b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb new file mode 100644 index 000000000..db821e3e6 --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/keys_checker.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Cucumber + class CompatibilityKit + class KeysChecker + def self.compare(detected, expected) + new(detected, expected).compare + end + + attr_reader :detected, :expected + + def initialize(detected, expected) + @detected = detected + @expected = expected + end + + def compare + return if identical_keys? + return "Detected extra keys in message #{message_name}: #{extra_keys}" if extra_keys.any? + + # TODO: Remove this override when the CCK is being checked at v29+ + return if missing_keys == [:children] + + "Missing keys in message #{message_name}: #{missing_keys}" if missing_keys.any? + rescue StandardError => e + ["Unexpected error: #{e.message}"] + end + + private + + def identical_keys? + detected_keys == expected_keys + end + + def detected_keys + @detected_keys ||= ordered_uniq_hash_keys(detected) + end + + def expected_keys + @expected_keys ||= ordered_uniq_hash_keys(expected) + end + + def ordered_uniq_hash_keys(object) + object.to_h(reject_nil_values: true).keys.sort + end + + def extra_keys + (detected_keys - expected_keys).reject { |key| meta_message? && key == :ci } + end + + def missing_keys + (expected_keys - detected_keys).reject { |key| meta_message? && key == :ci } + end + + def meta_message? + detected.instance_of?(Cucumber::Messages::Meta) + end + + def message_name + detected.class.name + end + end + end +end diff --git a/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb new file mode 100644 index 000000000..a47c1e68b --- /dev/null +++ b/compatibility/support/cucumber/compatibility_kit/messages_comparator.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative 'keys_checker' +require_relative 'helpers' + +module Cucumber + class CompatibilityKit + class MessagesComparator + include Helpers + + def initialize(detected, expected) + compare(detected, expected) + end + + def errors + all_errors.compact + end + + private + + def compare(detected, expected) + detected_by_type = messages_by_type(detected) + expected_by_type = messages_by_type(expected) + + detected_by_type.each_key do |type| + compare_list(detected_by_type[type], expected_by_type[type]) + rescue StandardError => e + # TODO: Remove this override when the CCK is being checked at v29+ + if e.message.include?('each_with_index') + :ignore_until_cck_v29 + else + all_errors << "Error while comparing #{type}: #{e.message}" + end + end + end + + def messages_by_type(messages) + by_type = Hash.new { |h, k| h[k] = [] } + messages.each do |msg| + by_type[message_type(msg)] << remove_envelope(msg) + end + by_type + end + + def remove_envelope(message) + message.send(message_type(message)) + end + + def compare_list(detected, expected) + detected.each_with_index do |message, index| + compare_message(message, expected[index]) + end + end + + def compare_message(detected, expected) + return if not_message?(detected) + return if ignorable?(detected) + return if incomparable?(detected) + + all_errors << Cucumber::CompatibilityKit::KeysChecker.compare(detected, expected) + compare_sub_messages(detected, expected) + end + + def not_message?(detected) + !detected.is_a?(Cucumber::Messages::Message) + end + + # These messages need to be ignored because they are too large, or they feature timestamps which will be different + def ignorable?(detected) + too_large_message?(detected) || time_message?(detected) + end + + def too_large_message?(detected) + detected.is_a?(Cucumber::Messages::GherkinDocument) || detected.is_a?(Cucumber::Messages::Pickle) + end + + def time_message?(detected) + detected.is_a?(Cucumber::Messages::Timestamp) || detected.is_a?(Cucumber::Messages::Duration) + end + + # These messages need to be ignored because they are often not of identical shape + def incomparable?(detected) + detected.is_a?(Cucumber::Messages::Ci) || detected.is_a?(Cucumber::Messages::Git) + end + + def compare_sub_messages(detected, expected) + return unless expected.respond_to? :to_h + + expected.to_h.each_key do |key| + value = expected.send(key) + if value.is_a?(Array) + compare_list(detected.send(key), value) + else + compare_message(detected.send(key), value) + end + end + end + + def all_errors + @all_errors ||= [] + end + end + end +end diff --git a/compatibility/support/shared_examples.rb b/compatibility/support/shared_examples.rb index 6f2674dbe..bf9526ba7 100644 --- a/compatibility/support/shared_examples.rb +++ b/compatibility/support/shared_examples.rb @@ -4,13 +4,13 @@ require 'rspec' require 'cucumber/messages' -require_relative 'cck/helpers' -require_relative 'cck/messages_comparator' +require_relative 'cucumber/compatibility_kit/helpers' +require_relative 'cucumber/compatibility_kit/messages_comparator' RSpec.shared_examples 'cucumber compatibility kit' do - include CCK::Helpers + include Cucumber::CompatibilityKit::Helpers - let(:cck_path) { CompatibilityKit.feature_code_for(example) } + let(:cck_path) { Cucumber::CompatibilityKit.feature_code_for(example) } let(:parsed_original) { parse_ndjson_file("#{cck_path}/#{example}.ndjson") } let(:parsed_generated) { parse_ndjson(messages) } @@ -23,7 +23,7 @@ end it 'generates valid message structure' do - comparator = CCK::MessagesComparator.new(parsed_generated, parsed_original) + comparator = Cucumber::CompatibilityKit::MessagesComparator.new(parsed_generated, parsed_original) expect(comparator.errors).to be_empty, "There were comparison errors: #{comparator.errors}" end diff --git a/cucumber.gemspec b/cucumber.gemspec index 5e944c2c2..43d2cf846 100644 --- a/cucumber.gemspec +++ b/cucumber.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| s.add_dependency 'multi_test', '~> 1.1' s.add_dependency 'sys-uname', '~> 1.5' - s.add_development_dependency 'cucumber-compatibility-kit', '~> 22.0' + s.add_development_dependency 'cucumber-compatibility-kit', '~> 24.0' # Only needed whilst we are testing the formatters. Can be removed once we remove tests for those s.add_development_dependency 'nokogiri', '~> 1.15' s.add_development_dependency 'rake', '~> 13.2' diff --git a/features/docs/formatters/message.feature b/features/docs/formatters/message.feature index 30bfba16b..b9a8f5411 100644 --- a/features/docs/formatters/message.feature +++ b/features/docs/formatters/message.feature @@ -18,6 +18,11 @@ Feature: Message output formatter | passed | | failed | """ + And a file named "features/steps.rb" with: + """ + Given('a passed step') {} + Given('a failed step') { raise 'oops' } + """ Scenario: it produces NDJSON messages When I run `cucumber features/my_feature.feature --format message` @@ -25,6 +30,8 @@ Feature: Message output formatter And messages types should be: """ meta + stepDefinition + stepDefinition source gherkinDocument pickle @@ -44,11 +51,6 @@ Feature: Message output formatter """ Scenario: it sets "testRunFinished"."success" to false if something failed - Given a file named "features/steps.rb" with: - """ - Given('a passed step') {} - Given('a failed step') { fail } - """ When I run `cucumber features/my_feature.feature --format message` Then output should be valid NDJSON And the output should contain NDJSON with key "testRunFinished" @@ -62,10 +64,6 @@ Feature: Message output formatter Scenario Outline: a scenario Given a passed step """ - And a file named "features/steps.rb" with: - """ - Given('a passed step') {} - """ When I run `cucumber features/my_feature.feature --format message` Then output should be valid NDJSON And the output should contain NDJSON with key "testRunFinished" diff --git a/lib/cucumber/formatter/console.rb b/lib/cucumber/formatter/console.rb index dfe7688ae..573bd099e 100644 --- a/lib/cucumber/formatter/console.rb +++ b/lib/cucumber/formatter/console.rb @@ -116,11 +116,11 @@ def linebreaks(msg, max) def collect_snippet_data(test_step, ast_lookup) # collect snippet data for undefined steps keyword = ast_lookup.snippet_step_keyword(test_step) - @snippets_input << Console::SnippetData.new(keyword, test_step) + snippets_input << Console::SnippetData.new(keyword, test_step) end def collect_undefined_parameter_type_names(undefined_parameter_type) - @undefined_parameter_types << undefined_parameter_type.type_name + undefined_parameter_types << undefined_parameter_type.type_name end def print_snippets(options) @@ -129,9 +129,9 @@ def print_snippets(options) snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| snippet_text(step_keyword, step_name, multiline_arg) end - do_print_snippets(snippet_text_proc) unless @snippets_input.empty? + do_print_snippets(snippet_text_proc) unless snippets_input.empty? - @undefined_parameter_types.map do |type_name| + undefined_parameter_types.map do |type_name| do_print_undefined_parameter_type_snippet(type_name) end end @@ -249,11 +249,19 @@ def element_messages(elements, status) def snippet_text(step_keyword, step_name, multiline_arg) keyword = Cucumber::Gherkin::I18n.code_keyword_for(step_keyword).strip - config.snippet_generators.map do |generator| - generator.call(keyword, step_name, multiline_arg, config.snippet_type) + @config.snippet_generators.map do |generator| + generator.call(keyword, step_name, multiline_arg, @config.snippet_type) end.join("\n") end + def snippets_input + @snippets_input ||= [] + end + + def undefined_parameter_types + @undefined_parameter_types ||= [] + end + class SnippetData attr_reader :actual_keyword, :step diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb index 65babbfcc..ffc48d57e 100644 --- a/lib/cucumber/formatter/message_builder.rb +++ b/lib/cucumber/formatter/message_builder.rb @@ -8,9 +8,11 @@ module Formatter class MessageBuilder include Cucumber::Messages::Helpers::TimeConversion include Io + include Console def initialize(config) @config = config + @ast_lookup = AstLookup.new(config) @repository = Cucumber::Repository.new @test_run_started_id = config.id_generator.new_id @@ -282,7 +284,6 @@ def on_test_step_finished(event) .max_by(&:attempt) result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter) - result_message = result.to_message if result.failed? || result.pending? message_element = result.failed? ? result.exception : result @@ -295,6 +296,8 @@ def on_test_step_finished(event) ) end + output_snippet_envelope(event) + message = Cucumber::Messages::Envelope.new( test_step_finished: Cucumber::Messages::TestStepFinished.new( test_step_id: event.test_step.id, @@ -307,6 +310,34 @@ def on_test_step_finished(event) @config.event_bus.envelope(message) end + def output_snippet_envelope(event) + return unless event.result.undefined? + + collect_snippet_data(event.test_step, @ast_lookup) + snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg| + snippet_text(step_keyword, step_name, multiline_arg) + end + + message = generate_snippet_envelope(snippet_text_proc, event) + @config.event_bus.envelope(message) + # To ensure we don't redistribute the "same" snippets over and over again + snippets_input.clear + end + + def generate_snippet_envelope(snippet_text_proc, event) + snippets_array = snippets_input.map do |data| + snippet_text_proc.call(data.actual_keyword, data.step.text, data.step.multiline_arg) + end.uniq + + Cucumber::Messages::Envelope.new( + suggestion: Cucumber::Messages::Suggestion.new( + id: @config.id_generator.new_id, + pickle_step_id: @repository.test_step_by_id[event.test_step.id].pickle_step_id, + snippets: snippets_array.map { |code_snippet| Cucumber::Messages::Snippet.new(language: 'ruby', code: code_snippet) } + ) + ) + end + def on_undefined_parameter_type(event) message = Cucumber::Messages::Envelope.new( undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new( diff --git a/lib/cucumber/formatter/pretty.rb b/lib/cucumber/formatter/pretty.rb index 97ba8d796..33615e23e 100644 --- a/lib/cucumber/formatter/pretty.rb +++ b/lib/cucumber/formatter/pretty.rb @@ -33,8 +33,6 @@ def initialize(config) @io = ensure_io(config.out_stream, config.error_stream) @config = config @options = config.to_hash - @snippets_input = [] - @undefined_parameter_types = [] @total_duration = 0 @exceptions = [] @gherkin_sources = {} diff --git a/lib/cucumber/formatter/progress.rb b/lib/cucumber/formatter/progress.rb index 88b2b0b2c..0526b749a 100644 --- a/lib/cucumber/formatter/progress.rb +++ b/lib/cucumber/formatter/progress.rb @@ -20,8 +20,6 @@ class Progress def initialize(config) @config = config @io = ensure_io(config.out_stream, config.error_stream) - @snippets_input = [] - @undefined_parameter_types = [] @total_duration = 0 @matches = {} @pending_step_matches = [] diff --git a/spec/cucumber/query_spec.rb b/spec/cucumber/query_spec.rb index ab2aeba08..e1ecf5d12 100644 --- a/spec/cucumber/query_spec.rb +++ b/spec/cucumber/query_spec.rb @@ -40,10 +40,10 @@ def list_of_tests end end -require_relative '../../compatibility/support/cck/helpers' +require_relative '../../compatibility/support/cucumber/compatibility_kit/helpers' RSpec.describe Cucumber::Query do - include CCK::Helpers + include Cucumber::CompatibilityKit::Helpers subject(:query) { described_class.new(repository) }