From 938a54a45d2263d86344bf399e3dbe12c2b77def Mon Sep 17 00:00:00 2001 From: Matt Wynne Date: Sat, 30 May 2026 20:26:33 -0700 Subject: [PATCH 1/3] Use local cucumber-query gem --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index ae3253d75..087537686 100644 --- a/Gemfile +++ b/Gemfile @@ -10,4 +10,5 @@ gemspec # gem 'cucumber-gherkin', path: '../gherkin/ruby' # gem 'cucumber-html-formatter', path: '../html-formatter/ruby' # gem 'cucumber-messages', path: '../messages/ruby' +gem 'cucumber-query', path: '../query/ruby' # gem 'cucumber-tag-expressions', path: '../tag-expressions/ruby' From 394d53fb4b8e795a2c1590e98561f5c83836820c Mon Sep 17 00:00:00 2001 From: Matt Wynne Date: Sat, 30 May 2026 20:29:31 -0700 Subject: [PATCH 2/3] Load query from cucumber-query --- cucumber.gemspec | 1 + lib/cucumber/query.rb | 273 ------------------------------------- lib/cucumber/repository.rb | 136 ------------------ 3 files changed, 1 insertion(+), 409 deletions(-) delete mode 100644 lib/cucumber/query.rb delete mode 100644 lib/cucumber/repository.rb diff --git a/cucumber.gemspec b/cucumber.gemspec index 5e944c2c2..d58f2176c 100644 --- a/cucumber.gemspec +++ b/cucumber.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |s| s.add_dependency 'cucumber-core', '>= 16.2.0', '< 17' s.add_dependency 'cucumber-cucumber-expressions', '> 17', '< 20' s.add_dependency 'cucumber-html-formatter', '> 21', '< 24' + s.add_dependency 'cucumber-query', '~> 21.0' s.add_dependency 'diff-lcs', '~> 1.5' s.add_dependency 'logger', '~> 1.6' s.add_dependency 'mini_mime', '~> 1.1' diff --git a/lib/cucumber/query.rb b/lib/cucumber/query.rb deleted file mode 100644 index 735735197..000000000 --- a/lib/cucumber/query.rb +++ /dev/null @@ -1,273 +0,0 @@ -# frozen_string_literal: true - -require 'cucumber/repository' -require 'cucumber/messages' - -# Given one Cucumber Message, find another. -# -# Queries can be made while the test run is incomplete - and this will naturally return incomplete results -# see Cucumber Messages - Message Overview -# -module Cucumber - class Query - attr_reader :repository - private :repository - - include Cucumber::Messages::Helpers::TimeConversion - include Cucumber::Messages::Helpers::TestStepResultComparator - - def initialize(repository) - @repository = repository - end - - # TODO: count methods (1/2) Complete - # Missing: countMostSevereTestStepResultStatus - - # TODO: findAll methods (11/12) Complete - # Missing: findAllUndefinedParameterTypes - - # TODO: find****By methods (16/25) Complete - # Missing: findSuggestionsBy (2 variants) - # Missing: findUnambiguousStepDefinitionBy (1 variant) - # Missing: findTestStepFinishedAndTestStepBy (1 variant) - # Missing: findAttachmentsBy (2 variants) - # Missing: findTestCaseDurationBy (2 variant) - # REDUNDANT: findLineageBy (9 variants!) - # REDUNDANT: findLocationOf (1 variant) - This strictly speaking isn't a findBy but is located within them - # To Review: findMostSevereTestStepResultBy (2 variants) - # To Review: findTestRunDuration (1 variant) - This strictly speaking isn't a findBy but is located within them - - def count_test_cases_started - find_all_test_case_started.length - end - - def find_all_pickles - repository.pickle_by_id.values - end - - def find_all_pickle_steps - repository.pickle_step_by_id.values - end - - def find_all_step_definitions - repository.step_definition_by_id.values - end - - # This finds all test cases from the following conditions (UNION) - # -> Test cases that have started, but not yet finished - # -> Test cases that have started, finished, but that will NOT be retried - def find_all_test_case_started - repository.test_case_started_by_id.values.select do |test_case_started| - test_case_finished = find_test_case_finished_by(test_case_started) - test_case_finished.nil? || !test_case_finished.will_be_retried - end - end - - # This finds all test cases that have finished AND will not be retried - def find_all_test_case_finished - repository.test_case_finished_by_test_case_started_id.values.reject(&:will_be_retried) - end - - def find_all_test_cases - repository.test_case_by_id.values - end - - def find_all_test_run_hook_started - repository.test_run_hook_started_by_id.values - end - - def find_all_test_run_hook_finished - repository.test_run_hook_finished_by_test_run_hook_started_id.values - end - - def find_all_test_step_started - repository.test_steps_started_by_test_case_started_id.values.flatten - end - - def find_all_test_step_finished - repository.test_steps_finished_by_test_case_started_id.values.flatten - end - - def find_all_test_steps - repository.test_step_by_id.values - end - - # This method will be called with 1 of these 3 messages - # [TestStep || TestRunHookStarted || TestRunHookFinished] - def find_hook_by(message) - ensure_only_message_types!(message, %i[test_step test_run_hook_started test_run_hook_finished], '#find_hook_by') - - if message.is_a?(Cucumber::Messages::TestRunHookFinished) - test_run_hook_started_message = find_test_run_hook_started_by(message) - test_run_hook_started_message ? find_hook_by(test_run_hook_started_message) : nil - else - repository.hook_by_id[message.hook_id] - end - end - - def find_meta - repository.meta - end - - # This method will be called with 1 of these 2 messages - # [TestCaseStarted || TestCaseFinished] - def find_most_severe_test_step_result_by(message) - ensure_only_message_types!(message, %i[test_case_started test_case_finished], '#find_most_severe_test_step_result_by') - - if message.is_a?(Cucumber::Messages::TestCaseStarted) - find_test_steps_finished_by(message) - .map(&:test_step_result) - .max_by { |test_step_result| test_step_result_rankings[test_step_result.status] } - # Java code: "PREVIOUS".max(comparing(TestStepResult::getStatus, new TestStepResultStatusComparator())); - else - test_case_started_message = find_test_case_started_by(message) - test_case_started_message && find_most_severe_test_step_result_by(test_case_started_message) - # Java code: return findTestCaseStartedBy(testCaseFinished).flatMap(this::findMostSevereTestStepResultBy); - end - end - - # This method will be called with 1 of these 5 messages - # [TestCase || TestCaseStarted || TestCaseFinished || TestStepStarted || TestStepFinished] - def find_pickle_by(message) - ensure_only_message_types!(message, %i[test_case test_case_started test_case_finished test_step_started test_step_finished], '#find_pickle_by') - - test_case_message = message.is_a?(Cucumber::Messages::TestCase) ? message : find_test_case_by(message) - repository.pickle_by_id[test_case_message.pickle_id] - end - - # This method will be called with only 1 message - # [TestStep] - def find_pickle_step_by(test_step) - ensure_only_message_types!(test_step, %i[test_step], '#find_pickle_step_by') - - repository.pickle_step_by_id[test_step.pickle_step_id] - end - - # This method will be called with only 1 message - # [PickleStep] - def find_step_by(pickle_step) - ensure_only_message_types!(pickle_step, %i[pickle_step], '#find_step_by') - - repository.step_by_id[pickle_step.ast_node_ids.first] - end - - # This method will be called with only 1 message - # [TestStep] - def find_step_definitions_by(test_step) - ensure_only_message_types!(test_step, %i[test_step], '#find_step_definitions_by') - - ids = test_step.step_definition_ids.nil? ? [] : test_step.step_definition_ids - ids.map { |id| repository.step_definition_by_id[id] }.compact - end - - # This method will be called with 1 of these 4 messages - # [TestCaseStarted || TestCaseFinished || TestStepStarted || TestStepFinished] - def find_test_case_by(message) - ensure_only_message_types!(message, %i[test_case_started test_case_finished test_step_started test_step_finished], '#find_test_case_by') - - test_case_started_message = message.is_a?(Cucumber::Messages::TestCaseStarted) ? message : find_test_case_started_by(message) - repository.test_case_by_id[test_case_started_message.test_case_id] - end - - # This method will be called with 1 of these 3 messages - # [TestCaseFinished || TestStepStarted || TestStepFinished] - def find_test_case_started_by(message) - ensure_only_message_types!(message, %i[test_case_finished test_step_started test_step_finished], '#find_test_case_started_by') - - repository.test_case_started_by_id[message.test_case_started_id] - end - - # This method will be called with only 1 message - # [TestCaseStarted] - def find_test_case_finished_by(test_case_started) - ensure_only_message_types!(test_case_started, %i[test_case_started], '#find_test_case_finished_by') - - repository.test_case_finished_by_test_case_started_id[test_case_started.id] - end - - def find_test_run_duration - if repository.test_run_started.nil? || repository.test_run_finished.nil? - nil - else - timestamp_to_time(repository.test_run_finished.timestamp) - timestamp_to_time(repository.test_run_started.timestamp) - end - end - - # This method will be called with only 1 message - # [TestRunHookFinished] - def find_test_run_hook_started_by(test_run_hook_finished) - ensure_only_message_types!(test_run_hook_finished, %i[test_run_hook_finished], '#find_test_run_hook_started_by') - - repository.test_run_hook_started_by_id[test_run_hook_finished.test_run_hook_started_id] - end - - # This method will be called with only 1 message - # [TestRunHookStarted] - def find_test_run_hook_finished_by(test_run_hook_started) - ensure_only_message_types!(test_run_hook_started, %i[test_run_hook_started], '#find_test_run_hook_finished_by') - - repository.test_run_hook_finished_by_test_run_hook_started_id[test_run_hook_started.id] - end - - def find_test_run_started - repository.test_run_started - end - - def find_test_run_finished - repository.test_run_finished - end - - # This method will be called with 1 of these 2 messages - # [TestStepStarted || TestStepFinished] - def find_test_step_by(message) - ensure_only_message_types!(message, %i[test_case_started test_case_finished], '#find_test_step_by') - - repository.test_step_by_id[message.test_step_id] - end - - # This method will be called with 1 of these 2 messages - # [TestCaseStarted || TestCaseFinished] - def find_test_steps_started_by(message) - ensure_only_message_types!(message, %i[test_case_started test_case_finished], '#find_test_steps_started_by') - - key = message.is_a?(Cucumber::Messages::TestCaseStarted) ? message.id : message.test_case_started_id - repository.test_steps_started_by_test_case_started_id.fetch(key, []) - end - - # This method will be called with 1 of these 2 messages - # [TestCaseStarted || TestCaseFinished] - def find_test_steps_finished_by(message) - ensure_only_message_types!(message, %i[test_case_started test_case_finished], '#find_test_steps_finished_by') - - if message.is_a?(Cucumber::Messages::TestCaseStarted) - repository.test_steps_finished_by_test_case_started_id.fetch(message.id, []) - else - test_case_started_message = find_test_case_started_by(message) - test_case_started_message.nil? ? [] : find_test_steps_finished_by(test_case_started_message) - end - end - - private - - def ensure_only_message_types!(supplied_message, permissible_message_types, method_name) - raise ArgumentError, "Supplied argument is not a Cucumber Message. Argument: #{supplied_message.class}" unless supplied_message.is_a?(Cucumber::Messages::Message) - - permitted_klasses = permissible_message_types.map { |message| message_types[message] } - raise ArgumentError, "Supplied message type '#{supplied_message.class}' is not permitted to be used when calling #{method_name}" unless permitted_klasses.include?(supplied_message.class) - end - - def message_types - { - pickle_step: Cucumber::Messages::PickleStep, - test_case: Cucumber::Messages::TestCase, - test_case_started: Cucumber::Messages::TestCaseStarted, - test_case_finished: Cucumber::Messages::TestCaseFinished, - test_run_hook_started: Cucumber::Messages::TestRunHookStarted, - test_run_hook_finished: Cucumber::Messages::TestRunHookFinished, - test_step: Cucumber::Messages::TestStep, - test_step_started: Cucumber::Messages::TestStepStarted, - test_step_finished: Cucumber::Messages::TestStepFinished - } - end - end -end diff --git a/lib/cucumber/repository.rb b/lib/cucumber/repository.rb deleted file mode 100644 index 9c61dfbb7..000000000 --- a/lib/cucumber/repository.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -module Cucumber - # In memory repository i.e. a thread based link to cucumber-query - class Repository - attr_accessor :meta, :test_run_started, :test_run_finished - attr_reader :attachments_by_test_case_started_id, :attachments_by_test_run_hook_started_id, - :hook_by_id, - :pickle_by_id, :pickle_step_by_id, - :step_by_id, :step_definition_by_id, - :test_case_by_id, :test_case_started_by_id, :test_case_finished_by_test_case_started_id, - :test_run_hook_started_by_id, :test_run_hook_finished_by_test_run_hook_started_id, - :test_step_by_id, :test_steps_started_by_test_case_started_id, :test_steps_finished_by_test_case_started_id - - # TODO: Missing structs (2) - # final Map> suggestionsByPickleStepId = new LinkedHashMap<>(); - # final List undefinedParameterTypes = new ArrayList<>(); - - # TODO: Missing handlers - # Source - # - - def initialize - @attachments_by_test_case_started_id = Hash.new { |hash, key| hash[key] = [] } - @attachments_by_test_run_hook_started_id = Hash.new { |hash, key| hash[key] = [] } - @hook_by_id = {} - @pickle_by_id = {} - @pickle_step_by_id = {} - @step_by_id = {} - @step_definition_by_id = {} - @test_case_by_id = {} - @test_case_started_by_id = {} - @test_case_finished_by_test_case_started_id = {} - @test_run_hook_started_by_id = {} - @test_run_hook_finished_by_test_run_hook_started_id = {} - @test_step_by_id = {} - @test_steps_started_by_test_case_started_id = Hash.new { |hash, key| hash[key] = [] } - @test_steps_finished_by_test_case_started_id = Hash.new { |hash, key| hash[key] = [] } - end - - def update(envelope) - return self.meta = envelope.meta if envelope.meta - return self.test_run_started = envelope.test_run_started if envelope.test_run_started - return self.test_run_finished = envelope.test_run_finished if envelope.test_run_finished - return update_attachment(envelope.attachment) if envelope.attachment - return update_gherkin_document(envelope.gherkin_document) if envelope.gherkin_document - return update_hook(envelope.hook) if envelope.hook - return update_pickle(envelope.pickle) if envelope.pickle - return update_step_definition(envelope.step_definition) if envelope.step_definition - return update_test_run_hook_started(envelope.test_run_hook_started) if envelope.test_run_hook_started - return update_test_run_hook_finished(envelope.test_run_hook_finished) if envelope.test_run_hook_finished - return update_test_case_started(envelope.test_case_started) if envelope.test_case_started - return update_test_case_finished(envelope.test_case_finished) if envelope.test_case_finished - return update_test_step_started(envelope.test_step_started) if envelope.test_step_started - return update_test_step_finished(envelope.test_step_finished) if envelope.test_step_finished - return update_test_case(envelope.test_case) if envelope.test_case - - nil - end - - private - - def update_attachment(attachment) - attachments_by_test_case_started_id[attachment.test_case_started_id] << attachment if attachment.test_case_started_id - attachments_by_test_run_hook_started_id[attachment.test_run_hook_started_id] << attachment if attachment.test_run_hook_started_id - end - - def update_feature(feature) - feature.children.each do |feature_child| - update_steps(feature_child.background.steps) if feature_child.background - update_scenario(feature_child.scenario) if feature_child.scenario - next unless feature_child.rule - - feature_child.rule.children.each do |rule_child| - update_steps(rule_child.background.steps) if rule_child.background - update_scenario(rule_child.scenario) if rule_child.scenario - end - end - end - - def update_gherkin_document(gherkin_document) - # TODO: Update lineage at a later date. Java Impl -> lineageById.putAll(Lineages.of(document)); - update_feature(gherkin_document.feature) if gherkin_document.feature - end - - def update_hook(hook) - hook_by_id[hook.id] = hook - end - - def update_pickle(pickle) - pickle_by_id[pickle.id] = pickle - pickle.steps.each { |pickle_step| pickle_step_by_id[pickle_step.id] = pickle_step } - end - - def update_scenario(scenario) - update_steps(scenario.steps) - end - - def update_steps(steps) - steps.each { |step| step_by_id[step.id] = step } - end - - def update_step_definition(step_definition) - step_definition_by_id[step_definition.id] = step_definition - end - - def update_test_case(test_case) - test_case_by_id[test_case.id] = test_case - test_case.test_steps.each { |test_step| test_step_by_id[test_step.id] = test_step } - end - - def update_test_case_started(test_case_started) - test_case_started_by_id[test_case_started.id] = test_case_started - end - - def update_test_case_finished(test_case_finished) - test_case_finished_by_test_case_started_id[test_case_finished.test_case_started_id] = test_case_finished - end - - def update_test_run_hook_started(test_run_hook_started) - test_run_hook_started_by_id[test_run_hook_started.id] = test_run_hook_started - end - - def update_test_run_hook_finished(test_run_hook_finished) - test_run_hook_finished_by_test_run_hook_started_id[test_run_hook_finished.test_run_hook_started_id] = test_run_hook_finished - end - - def update_test_step_started(test_step_started) - test_steps_started_by_test_case_started_id[test_step_started.test_case_started_id] << test_step_started - end - - def update_test_step_finished(test_step_finished) - test_steps_finished_by_test_case_started_id[test_step_finished.test_case_started_id] << test_step_finished - end - end -end From 65b91a834aecab142b651d91e479ac2058ca1024 Mon Sep 17 00:00:00 2001 From: Matt Wynne Date: Wed, 3 Jun 2026 21:26:28 -0700 Subject: [PATCH 3/3] Fetch cucumber-query from extraction branch --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 087537686..dae74060b 100644 --- a/Gemfile +++ b/Gemfile @@ -10,5 +10,5 @@ gemspec # gem 'cucumber-gherkin', path: '../gherkin/ruby' # gem 'cucumber-html-formatter', path: '../html-formatter/ruby' # gem 'cucumber-messages', path: '../messages/ruby' -gem 'cucumber-query', path: '../query/ruby' +gem 'cucumber-query', git: 'https://github.com/cucumber/query.git', branch: 'mw-query-extraction-preview', glob: 'ruby/cucumber-query.gemspec' # gem 'cucumber-tag-expressions', path: '../tag-expressions/ruby'