diff --git a/lib/cucumber/formatter/message_builder.rb b/lib/cucumber/formatter/message_builder.rb
index 7285083e5..1d232688a 100644
--- a/lib/cucumber/formatter/message_builder.rb
+++ b/lib/cucumber/formatter/message_builder.rb
@@ -9,6 +9,8 @@
require 'cucumber/formatter/query/test_case_started_by_test_case'
require 'cucumber/formatter/query/test_run_started'
+require 'cucumber/query'
+
module Cucumber
module Formatter
class MessageBuilder
@@ -39,6 +41,8 @@ def initialize(config)
@current_test_run_started_id = nil
@current_test_case_started_id = nil
@current_test_step_id = nil
+
+ @query = Cucumber::Query.new
end
def output_message
diff --git a/lib/cucumber/formatter/new_rerun.rb b/lib/cucumber/formatter/new_rerun.rb
new file mode 100644
index 000000000..2faa172e8
--- /dev/null
+++ b/lib/cucumber/formatter/new_rerun.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'cucumber/formatter/io'
+require 'cucumber/query'
+require 'cucumber/formatter/message_builder'
+
+module Cucumber
+ module Formatter
+ class NewRerun
+ include Formatter::Io
+
+ def initialize(config)
+ @io = ensure_io(config.out_stream, config.error_stream)
+ @config = config
+ @failures = {}
+ config.on_event :test_case_finished, &method(:on_test_case_finished)
+ config.on_event :test_run_finished, &method(:on_test_run_finished)
+ end
+
+ def on_test_case_finished(event)
+ test_case, result = *event.attributes
+ if @config.strict.strict?(:flaky)
+ next if result.ok?(strict: @config.strict)
+
+ add_to_failures(test_case)
+ else
+ unless @latest_failed_test_case.nil?
+ if @latest_failed_test_case != test_case
+ add_to_failures(@latest_failed_test_case)
+ @latest_failed_test_case = nil
+ elsif result.ok?(strict: @config.strict)
+ @latest_failed_test_case = nil
+ end
+ end
+ @latest_failed_test_case = test_case unless result.ok?(strict: @config.strict)
+ end
+ end
+
+ def on_test_run_finished(_event)
+ add_to_failures(@latest_failed_test_case) unless @latest_failed_test_case.nil?
+ next if @failures.empty?
+
+ @io.print file_failures.join("\n")
+ end
+
+ private
+
+ def file_failures
+ @failures.map { |file, lines| [file, lines].join(':') }
+ end
+
+ def add_to_failures(test_case)
+ location = test_case.location
+ @failures[location.file] ||= []
+ @failures[location.file] << location.lines.max unless @failures[location.file].include?(location.lines.max)
+ end
+ end
+ end
+end
diff --git a/lib/cucumber/query.rb b/lib/cucumber/query.rb
new file mode 100644
index 000000000..b8fa90e60
--- /dev/null
+++ b/lib/cucumber/query.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'cucumber/repository'
+
+# 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
+
+ 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 (3/25) Complete
+ # Missing: findAttachmentsBy (2 variants)
+ # Missing: findHookBy (3 variants)
+ # Missing: findMeta (1 variant) - This strictly speaking isn't a findBy but is located within them
+ # Missing: findMostSevereTestStepResultBy (2 variants)
+ # Missing: findLocationOf (1 variant) - This strictly speaking isn't a findBy but is located within them
+ # Partially Complete (3/5): findPickleBy (5 variants)
+ # Missing: findPickleStepBy (1 variant)
+ # Missing: findSuggestionsBy (2 variants)
+ # Missing: findStepBy (1 variant)
+ # Missing: findStepDefinitionsBy (1 variant)
+ # Missing: findUnambiguousStepDefinitionBy (1 variant)
+ # Fully Complete (4/4): findTestCaseBy (4 variants)
+ # Missing: findTestCaseDurationBy (2 variant)
+ # Fully Complete (3/3): findTestCaseStartedBy (3 variants)
+ # Fully Complete (1/1): findTestCaseFinishedBy (1 variant)
+ # Missing: findTestRunHookFinishedBy (1 variant)
+ # Missing: findTestRunHookStartedBy (1 variant)
+ # Missing: findTestRunDuration (1 variant) - This strictly speaking isn't a findBy but is located within them
+ # Missing: findTestRunFinished (1 variant) - This strictly speaking isn't a findBy but is located within them
+ # Missing: findTestRunStarted (1 variant) - This strictly speaking isn't a findBy but is located within them
+ # Missing: findTestStepBy (2 variants)
+ # Missing: findTestStepsStartedBy (2 variants)
+ # Missing: findTestStepsFinishedBy (2 variants)
+ # Missing: findTestStepFinishedAndTestStepBy (1 variant)
+ # Missing: findLineageBy (9 variants!)
+
+ 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 that have started, but not yet finished
+ # AS WELL AS (AND)
+ # This finds all test cases that have started AND have 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 do |test_case_finished|
+ test_case_finished.will_be_retried
+ end
+ 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
+ # [TestCaseStarted || TestCaseFinished || TestStepStarted]
+ def find_pickle_by(element)
+ test_case = find_test_case_by(element)
+ raise 'Expected to find TestCase from TestCaseStarted' unless test_case
+
+ repository.pickle_by_id[test_case.pickle_id]
+ end
+
+ # This method will be called with 1 of these 4 messages
+ # [TestCaseStarted || TestCaseFinished || TestStepStarted || TestStepFinished]
+ def find_test_case_by(element)
+ test_case_started = element.respond_to?(:test_case_started_id) ? find_test_case_started_by(element) : element
+ raise 'Expected to find TestCaseStarted by TestStepStarted' unless test_case_started
+
+ repository.test_case_by_id[test_case_started.test_case_id]
+ end
+
+ # This method will be called with 1 of these 3 messages
+ # [TestCaseFinished || TestStepStarted || TestStepFinished]
+ def find_test_case_started_by(element)
+ repository.test_case_started_by_id[element.test_case_started_id]
+ end
+
+ # This method will be called with only 1 message
+ # [TestCaseStarted]
+ def find_test_case_finished_by(test_case_started)
+ repository.test_case_finished_by_test_case_started_id[test_case_started.id]
+ end
+ end
+end
diff --git a/lib/cucumber/repository.rb b/lib/cucumber/repository.rb
new file mode 100644
index 000000000..c80fe4523
--- /dev/null
+++ b/lib/cucumber/repository.rb
@@ -0,0 +1,127 @@
+# 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 :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
+
+ def initialize
+ @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_gherkin_document(envelope.gherkin_document) if envelope.gherkin_document
+ return update_pickle(envelope.pickle) if envelope.pickle
+ 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_feature(feature)
+ # Java impl:
+ # feature.getChildren()
+ # .forEach(featureChild -> {
+ # featureChild.getBackground().ifPresent(background -> updateSteps(background.getSteps()));
+ # featureChild.getScenario().ifPresent(this::updateScenario);
+ # featureChild.getRule().ifPresent(rule -> rule.getChildren().forEach(ruleChild -> {
+ # ruleChild.getBackground().ifPresent(background -> updateSteps(background.getSteps()));
+ # ruleChild.getScenario().ifPresent(this::updateScenario);
+ # }));
+ # });
+
+ 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
+ if 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
+ end
+
+ def update_gherkin_document(gherkin_document)
+ # Java impl:
+ # private void updateGherkinDocument(GherkinDocument document) {
+ # lineageById.putAll(Lineages.of(document));
+ # document.getFeature().ifPresent(this::updateFeature);
+ # }
+ # TODO: Update lineage??
+ update_feature(gherkin_document.feature) if gherkin_document.feature
+ 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
diff --git a/spec/cucumber/formatter/rerun_spec.rb b/spec/cucumber/formatter/rerun_spec.rb
index a4ffb226d..109084702 100644
--- a/spec/cucumber/formatter/rerun_spec.rb
+++ b/spec/cucumber/formatter/rerun_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'cucumber/formatter/rerun'
+require 'cucumber/formatter/new_rerun'
require 'cucumber/core'
require 'cucumber/core/gherkin/writer'
require 'support/standard_step_actions'
@@ -9,7 +9,7 @@
module Cucumber
module Formatter
- describe Rerun do
+ describe NewRerun do
include Cucumber::Core::Gherkin::Writer
include Cucumber::Core
@@ -34,6 +34,7 @@ module Formatter
end
end
end
+
described_class.new(config)
execute [gherkin], [StandardStepActions.new], config.event_bus
config.event_bus.test_run_finished
diff --git a/spec/cucumber/query_spec.rb b/spec/cucumber/query_spec.rb
new file mode 100644
index 000000000..e64537559
--- /dev/null
+++ b/spec/cucumber/query_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+def source_names
+ %w[attachments empty hooks minimal rules]
+end
+
+def sources
+ source_names.map { |name| "#{Dir.pwd}/spec/support/#{name}.ndjson" }
+end
+
+def queries
+ {
+ 'findAllPickles' => ->(query) { query.find_all_pickles.length },
+ 'findAllTestCases' => ->(query) { query.find_all_test_cases.length },
+ 'findAllTestSteps' => ->(query) { query.find_all_test_steps.length },
+ 'findTestCaseBy' => lambda do |query|
+ results = {}
+ results['testCaseStarted'] = query.find_all_test_case_started.map { |message| query.find_test_case_by(message).id }
+ results['testCaseFinished'] = query.find_all_test_case_finished.map { |message| query.find_test_case_by(message).id }
+ results['testStepStarted'] = query.find_all_test_step_started.map { |message| query.find_test_case_by(message).id }
+ results['testStepFinished'] = query.find_all_test_step_finished.map { |message| query.find_test_case_by(message).id }
+ results
+ end,
+ 'findPickleBy' => lambda do |query|
+ results = {}
+ results['testCaseStarted'] = query.find_all_test_case_started.map { |message| query.find_pickle_by(message).name }
+ results['testCaseFinished'] = query.find_all_test_case_finished.map { |message| query.find_pickle_by(message).name }
+ results['testStepStarted'] = query.find_all_test_step_started.map { |message| query.find_pickle_by(message).name }
+ results['testStepFinished'] = query.find_all_test_step_finished.map { |message| query.find_pickle_by(message).name }
+ results
+ end
+ }
+end
+
+def list_of_tests
+ sources.flat_map do |source|
+ queries.map do |query_name, query_proc|
+ { cck_spec: source, query_name:, query_proc: }
+ end
+ end
+end
+
+require 'cucumber/query'
+require 'cucumber/messages'
+require_relative '../../compatibility/support/cck/helpers'
+
+describe Cucumber::Query do
+ include CCK::Helpers
+
+ subject(:query) { described_class.new(repository) }
+
+ let(:repository) { Cucumber::Repository.new }
+
+ list_of_tests.each do |test|
+ describe "executes the query '#{test[:query_name]}' against the CCK definition '#{test[:cck_spec]}'" do
+ let(:cck_messages) { parse_ndjson_file(test[:cck_spec]).map.itself }
+ let(:filename_to_check) { test[:cck_spec].sub('.ndjson', ".#{test[:query_name]}.results.json") }
+
+ before { cck_messages.each { |message| repository.update(message) } }
+
+ it 'returns the expected query result' do
+ evaluated_query = test[:query_proc].call(query)
+ expected_query_result = JSON.parse(File.read(filename_to_check))
+
+ expect(evaluated_query).to eq(expected_query_result)
+ end
+ end
+ end
+end
diff --git a/spec/support/attachments.findAllPickles.results.json b/spec/support/attachments.findAllPickles.results.json
new file mode 100644
index 000000000..c7930257d
--- /dev/null
+++ b/spec/support/attachments.findAllPickles.results.json
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/spec/support/attachments.findAllTestCases.results.json b/spec/support/attachments.findAllTestCases.results.json
new file mode 100644
index 000000000..c7930257d
--- /dev/null
+++ b/spec/support/attachments.findAllTestCases.results.json
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/spec/support/attachments.findAllTestSteps.results.json b/spec/support/attachments.findAllTestSteps.results.json
new file mode 100644
index 000000000..c7930257d
--- /dev/null
+++ b/spec/support/attachments.findAllTestSteps.results.json
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/spec/support/attachments.findPickleBy.results.json b/spec/support/attachments.findPickleBy.results.json
new file mode 100644
index 000000000..af46dda98
--- /dev/null
+++ b/spec/support/attachments.findPickleBy.results.json
@@ -0,0 +1,38 @@
+{
+ "testCaseStarted" : [
+ "Strings can be attached with a media type",
+ "Log text",
+ "Log ANSI coloured text",
+ "Log JSON",
+ "Byte arrays are base64-encoded regardless of media type",
+ "Attaching PDFs with a different filename",
+ "Attaching URIs"
+ ],
+ "testCaseFinished" : [
+ "Strings can be attached with a media type",
+ "Log text",
+ "Log ANSI coloured text",
+ "Log JSON",
+ "Byte arrays are base64-encoded regardless of media type",
+ "Attaching PDFs with a different filename",
+ "Attaching URIs"
+ ],
+ "testStepStarted" : [
+ "Strings can be attached with a media type",
+ "Log text",
+ "Log ANSI coloured text",
+ "Log JSON",
+ "Byte arrays are base64-encoded regardless of media type",
+ "Attaching PDFs with a different filename",
+ "Attaching URIs"
+ ],
+ "testStepFinished" : [
+ "Strings can be attached with a media type",
+ "Log text",
+ "Log ANSI coloured text",
+ "Log JSON",
+ "Byte arrays are base64-encoded regardless of media type",
+ "Attaching PDFs with a different filename",
+ "Attaching URIs"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/attachments.findTestCaseBy.results.json b/spec/support/attachments.findTestCaseBy.results.json
new file mode 100644
index 000000000..298a33f9b
--- /dev/null
+++ b/spec/support/attachments.findTestCaseBy.results.json
@@ -0,0 +1,38 @@
+{
+ "testCaseStarted" : [
+ "36",
+ "38",
+ "40",
+ "42",
+ "44",
+ "46",
+ "48"
+ ],
+ "testCaseFinished" : [
+ "36",
+ "38",
+ "40",
+ "42",
+ "44",
+ "46",
+ "48"
+ ],
+ "testStepStarted" : [
+ "36",
+ "38",
+ "40",
+ "42",
+ "44",
+ "46",
+ "48"
+ ],
+ "testStepFinished" : [
+ "36",
+ "38",
+ "40",
+ "42",
+ "44",
+ "46",
+ "48"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/attachments.ndjson b/spec/support/attachments.ndjson
new file mode 100644
index 000000000..c16bb8778
--- /dev/null
+++ b/spec/support/attachments.ndjson
@@ -0,0 +1,61 @@
+{"meta":{"protocolVersion":"28.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}}
+{"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","uri":"samples/attachments/attachments.feature","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Attachments","description":" It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Strings can be attached with a media type","description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","steps":[{"id":"0","location":{"line":15,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is attached as \"application/octet-stream\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Log text","description":"","steps":[{"id":"2","location":{"line":18,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is logged"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":20,"column":3},"keyword":"Scenario","name":"Log ANSI coloured text","description":"","steps":[{"id":"4","location":{"line":21,"column":5},"keyword":"When ","keywordType":"Action","text":"text with ANSI escapes is logged"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":23,"column":3},"keyword":"Scenario","name":"Log JSON","description":"","steps":[{"id":"6","location":{"line":24,"column":6},"keyword":"When ","keywordType":"Action","text":"the following string is attached as \"application/json\":","docString":{"location":{"line":25,"column":8},"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```"}}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":29,"column":3},"keyword":"Scenario","name":"Byte arrays are base64-encoded regardless of media type","description":"","steps":[{"id":"8","location":{"line":30,"column":5},"keyword":"When ","keywordType":"Action","text":"an array with 10 bytes is attached as \"text/plain\""}],"examples":[]}},{"scenario":{"id":"11","tags":[],"location":{"line":32,"column":3},"keyword":"Scenario","name":"Attaching PDFs with a different filename","description":"","steps":[{"id":"10","location":{"line":33,"column":5},"keyword":"When ","keywordType":"Action","text":"a PDF document is attached and renamed"}],"examples":[]}},{"scenario":{"id":"13","tags":[],"location":{"line":35,"column":3},"keyword":"Scenario","name":"Attaching URIs","description":"","steps":[{"id":"12","location":{"line":36,"column":5},"keyword":"When ","keywordType":"Action","text":"a link to \"https://cucumber.io\" is attached"}],"examples":[]}}]},"comments":[],"uri":"samples/attachments/attachments.feature"}}
+{"pickle":{"id":"15","uri":"samples/attachments/attachments.feature","astNodeIds":["1"],"tags":[],"name":"Strings can be attached with a media type","language":"en","steps":[{"id":"14","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action","astNodeIds":["0"]}]}}
+{"pickle":{"id":"17","uri":"samples/attachments/attachments.feature","astNodeIds":["3"],"tags":[],"name":"Log text","language":"en","steps":[{"id":"16","text":"the string \"hello\" is logged","type":"Action","astNodeIds":["2"]}]}}
+{"pickle":{"id":"19","uri":"samples/attachments/attachments.feature","astNodeIds":["5"],"tags":[],"name":"Log ANSI coloured text","language":"en","steps":[{"id":"18","text":"text with ANSI escapes is logged","type":"Action","astNodeIds":["4"]}]}}
+{"pickle":{"id":"21","uri":"samples/attachments/attachments.feature","astNodeIds":["7"],"tags":[],"name":"Log JSON","language":"en","steps":[{"id":"20","text":"the following string is attached as \"application/json\":","type":"Action","argument":{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}"}},"astNodeIds":["6"]}]}}
+{"pickle":{"id":"23","uri":"samples/attachments/attachments.feature","astNodeIds":["9"],"tags":[],"name":"Byte arrays are base64-encoded regardless of media type","language":"en","steps":[{"id":"22","text":"an array with 10 bytes is attached as \"text/plain\"","type":"Action","astNodeIds":["8"]}]}}
+{"pickle":{"id":"25","uri":"samples/attachments/attachments.feature","astNodeIds":["11"],"tags":[],"name":"Attaching PDFs with a different filename","language":"en","steps":[{"id":"24","text":"a PDF document is attached and renamed","type":"Action","astNodeIds":["10"]}]}}
+{"pickle":{"id":"27","uri":"samples/attachments/attachments.feature","astNodeIds":["13"],"tags":[],"name":"Attaching URIs","language":"en","steps":[{"id":"26","text":"a link to \"https://cucumber.io\" is attached","type":"Action","astNodeIds":["12"]}]}}
+{"stepDefinition":{"id":"28","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":4}}}}
+{"stepDefinition":{"id":"29","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":8}}}}
+{"stepDefinition":{"id":"30","pattern":{"type":"CUCUMBER_EXPRESSION","source":"text with ANSI escapes is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":12}}}}
+{"stepDefinition":{"id":"31","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the following string is attached as {string}:"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":18}}}}
+{"stepDefinition":{"id":"32","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an array with {int} bytes is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":22}}}}
+{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a PDF document is attached and renamed"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":31}}}}
+{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a link to {string} is attached"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":38}}}}
+{"testRunStarted":{"id":"35","timestamp":{"seconds":0,"nanos":0}}}
+{"testCase":{"id":"36","pickleId":"15","testSteps":[{"id":"37","pickleStepId":"14","stepDefinitionIds":["28"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"},{"group":{"start":34,"value":"\"application/octet-stream\"","children":[{"start":35,"value":"application/octet-stream","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"38","pickleId":"17","testSteps":[{"id":"39","pickleStepId":"16","stepDefinitionIds":["29"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"40","pickleId":"19","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"42","pickleId":"21","testSteps":[{"id":"43","pickleStepId":"20","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":36,"value":"\"application/json\"","children":[{"start":37,"value":"application/json","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"44","pickleId":"23","testSteps":[{"id":"45","pickleStepId":"22","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"10","children":[]},"parameterTypeName":"int"},{"group":{"start":38,"value":"\"text/plain\"","children":[{"start":39,"value":"text/plain","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"46","pickleId":"25","testSteps":[{"id":"47","pickleStepId":"24","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}}
+{"testCase":{"id":"48","pickleId":"27","testSteps":[{"id":"49","pickleStepId":"26","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"\"https://cucumber.io\"","children":[{"start":11,"value":"https://cucumber.io","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}}
+{"testCaseStarted":{"id":"50","testCaseId":"36","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"50","testStepId":"37","timestamp":{"seconds":0,"nanos":2000000}}}
+{"attachment":{"testCaseStartedId":"50","testStepId":"37","body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream"}}
+{"testStepFinished":{"testCaseStartedId":"50","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}}
+{"testCaseFinished":{"testCaseStartedId":"50","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"51","testCaseId":"38","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"51","testStepId":"39","timestamp":{"seconds":0,"nanos":6000000}}}
+{"attachment":{"testCaseStartedId":"51","testStepId":"39","body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain"}}
+{"testStepFinished":{"testCaseStartedId":"51","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}}
+{"testCaseFinished":{"testCaseStartedId":"51","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"52","testCaseId":"40","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"52","testStepId":"41","timestamp":{"seconds":0,"nanos":10000000}}}
+{"attachment":{"testCaseStartedId":"52","testStepId":"41","body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain"}}
+{"testStepFinished":{"testCaseStartedId":"52","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}}
+{"testCaseFinished":{"testCaseStartedId":"52","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"53","testCaseId":"42","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"53","testStepId":"43","timestamp":{"seconds":0,"nanos":14000000}}}
+{"attachment":{"testCaseStartedId":"53","testStepId":"43","body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json"}}
+{"testStepFinished":{"testCaseStartedId":"53","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}}
+{"testCaseFinished":{"testCaseStartedId":"53","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"54","testCaseId":"44","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"54","testStepId":"45","timestamp":{"seconds":0,"nanos":18000000}}}
+{"attachment":{"testCaseStartedId":"54","testStepId":"45","body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain"}}
+{"testStepFinished":{"testCaseStartedId":"54","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}}
+{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"55","testCaseId":"46","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"seconds":0,"nanos":22000000}}}
+{"attachment":{"testCaseStartedId":"55","testStepId":"47","body":"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RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","mediaType":"application/pdf","fileName":"renamed.pdf"}}
+{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}}
+{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"56","testCaseId":"48","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":26000000}}}
+{"attachment":{"testCaseStartedId":"56","testStepId":"49","body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list"}}
+{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}}
+{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":false}}
+{"testRunFinished":{"testRunStartedId":"35","timestamp":{"seconds":0,"nanos":29000000},"success":true}}
diff --git a/spec/support/empty.findAllPickles.results.json b/spec/support/empty.findAllPickles.results.json
new file mode 100644
index 000000000..56a6051ca
--- /dev/null
+++ b/spec/support/empty.findAllPickles.results.json
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/spec/support/empty.findAllTestCases.results.json b/spec/support/empty.findAllTestCases.results.json
new file mode 100644
index 000000000..56a6051ca
--- /dev/null
+++ b/spec/support/empty.findAllTestCases.results.json
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/spec/support/empty.findAllTestSteps.results.json b/spec/support/empty.findAllTestSteps.results.json
new file mode 100644
index 000000000..c22708346
--- /dev/null
+++ b/spec/support/empty.findAllTestSteps.results.json
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/spec/support/empty.findPickleBy.results.json b/spec/support/empty.findPickleBy.results.json
new file mode 100644
index 000000000..377f01bfa
--- /dev/null
+++ b/spec/support/empty.findPickleBy.results.json
@@ -0,0 +1,10 @@
+{
+ "testCaseStarted" : [
+ "Blank Scenario"
+ ],
+ "testCaseFinished" : [
+ "Blank Scenario"
+ ],
+ "testStepStarted" : [ ],
+ "testStepFinished" : [ ]
+}
\ No newline at end of file
diff --git a/spec/support/empty.findTestCaseBy.results.json b/spec/support/empty.findTestCaseBy.results.json
new file mode 100644
index 000000000..5a7775b38
--- /dev/null
+++ b/spec/support/empty.findTestCaseBy.results.json
@@ -0,0 +1,10 @@
+{
+ "testCaseStarted" : [
+ "3"
+ ],
+ "testCaseFinished" : [
+ "3"
+ ],
+ "testStepStarted" : [ ],
+ "testStepFinished" : [ ]
+}
\ No newline at end of file
diff --git a/spec/support/empty.ndjson b/spec/support/empty.ndjson
new file mode 100644
index 000000000..9e2b6bd39
--- /dev/null
+++ b/spec/support/empty.ndjson
@@ -0,0 +1,9 @@
+{"meta":{"protocolVersion":"28.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}}
+{"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.\n\n Scenario: Blank Scenario\n","uri":"samples/empty/empty.feature","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Empty Scenarios","description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.","children":[{"scenario":{"id":"0","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"Blank Scenario","description":"","steps":[],"examples":[]}}]},"comments":[],"uri":"samples/empty/empty.feature"}}
+{"pickle":{"id":"1","uri":"samples/empty/empty.feature","astNodeIds":["0"],"tags":[],"name":"Blank Scenario","language":"en","steps":[]}}
+{"testRunStarted":{"id":"2","timestamp":{"seconds":0,"nanos":0}}}
+{"testCase":{"id":"3","pickleId":"1","testSteps":[],"testRunStartedId":"2"}}
+{"testCaseStarted":{"id":"4","testCaseId":"3","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}}
+{"testCaseFinished":{"testCaseStartedId":"4","timestamp":{"seconds":0,"nanos":2000000},"willBeRetried":false}}
+{"testRunFinished":{"testRunStartedId":"2","timestamp":{"seconds":0,"nanos":3000000},"success":true}}
diff --git a/spec/support/hooks.findAllPickles.results.json b/spec/support/hooks.findAllPickles.results.json
new file mode 100644
index 000000000..d8263ee98
--- /dev/null
+++ b/spec/support/hooks.findAllPickles.results.json
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git a/spec/support/hooks.findAllTestCases.results.json b/spec/support/hooks.findAllTestCases.results.json
new file mode 100644
index 000000000..d8263ee98
--- /dev/null
+++ b/spec/support/hooks.findAllTestCases.results.json
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git a/spec/support/hooks.findAllTestSteps.results.json b/spec/support/hooks.findAllTestSteps.results.json
new file mode 100644
index 000000000..62f945751
--- /dev/null
+++ b/spec/support/hooks.findAllTestSteps.results.json
@@ -0,0 +1 @@
+6
\ No newline at end of file
diff --git a/spec/support/hooks.findPickleBy.results.json b/spec/support/hooks.findPickleBy.results.json
new file mode 100644
index 000000000..4bad4bc70
--- /dev/null
+++ b/spec/support/hooks.findPickleBy.results.json
@@ -0,0 +1,26 @@
+{
+ "testCaseStarted" : [
+ "No tags and a passed step",
+ "No tags and a failed step"
+ ],
+ "testCaseFinished" : [
+ "No tags and a passed step",
+ "No tags and a failed step"
+ ],
+ "testStepStarted" : [
+ "No tags and a passed step",
+ "No tags and a passed step",
+ "No tags and a passed step",
+ "No tags and a failed step",
+ "No tags and a failed step",
+ "No tags and a failed step"
+ ],
+ "testStepFinished" : [
+ "No tags and a passed step",
+ "No tags and a passed step",
+ "No tags and a passed step",
+ "No tags and a failed step",
+ "No tags and a failed step",
+ "No tags and a failed step"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/hooks.findTestCaseBy.results.json b/spec/support/hooks.findTestCaseBy.results.json
new file mode 100644
index 000000000..e6dc9fa57
--- /dev/null
+++ b/spec/support/hooks.findTestCaseBy.results.json
@@ -0,0 +1,26 @@
+{
+ "testCaseStarted" : [
+ "13",
+ "17"
+ ],
+ "testCaseFinished" : [
+ "13",
+ "17"
+ ],
+ "testStepStarted" : [
+ "13",
+ "13",
+ "13",
+ "17",
+ "17",
+ "17"
+ ],
+ "testStepFinished" : [
+ "13",
+ "13",
+ "13",
+ "17",
+ "17",
+ "17"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/hooks.ndjson b/spec/support/hooks.ndjson
new file mode 100644
index 000000000..cc0fc92e5
--- /dev/null
+++ b/spec/support/hooks.ndjson
@@ -0,0 +1,29 @@
+{"meta":{"protocolVersion":"30.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}}
+{"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n","uri":"samples/hooks/hooks.feature","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks","description":" Hooks are special steps that run before or after each scenario's steps.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"No tags and a passed step","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"No tags and a failed step","description":"","steps":[{"id":"2","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step fails"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks/hooks.feature"}}
+{"pickle":{"id":"5","uri":"samples/hooks/hooks.feature","astNodeIds":["1"],"tags":[],"name":"No tags and a passed step","language":"en","steps":[{"id":"4","text":"a step passes","type":"Action","astNodeIds":["0"]}]}}
+{"pickle":{"id":"7","uri":"samples/hooks/hooks.feature","astNodeIds":["3"],"tags":[],"name":"No tags and a failed step","language":"en","steps":[{"id":"6","text":"a step fails","type":"Action","astNodeIds":["2"]}]}}
+{"stepDefinition":{"id":"9","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":7}}}}
+{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step fails"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":11}}}}
+{"hook":{"id":"8","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":3}}}}
+{"hook":{"id":"11","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":15}}}}
+{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}}
+{"testCase":{"id":"13","pickleId":"5","testSteps":[{"id":"14","hookId":"8"},{"id":"15","pickleStepId":"4","stepDefinitionIds":["9"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"16","hookId":"11"}],"testRunStartedId":"12"}}
+{"testCase":{"id":"17","pickleId":"7","testSteps":[{"id":"18","hookId":"8"},{"id":"19","pickleStepId":"6","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"20","hookId":"11"}],"testRunStartedId":"12"}}
+{"testCaseStarted":{"id":"21","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"21","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}}
+{"testStepFinished":{"testCaseStartedId":"21","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}}
+{"testStepStarted":{"testCaseStartedId":"21","testStepId":"15","timestamp":{"seconds":0,"nanos":4000000}}}
+{"testStepFinished":{"testCaseStartedId":"21","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}}
+{"testStepStarted":{"testCaseStartedId":"21","testStepId":"16","timestamp":{"seconds":0,"nanos":6000000}}}
+{"testStepFinished":{"testCaseStartedId":"21","testStepId":"16","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}}
+{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"22","testCaseId":"17","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"22","testStepId":"18","timestamp":{"seconds":0,"nanos":10000000}}}
+{"testStepFinished":{"testCaseStartedId":"22","testStepId":"18","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}}
+{"testStepStarted":{"testCaseStartedId":"22","testStepId":"19","timestamp":{"seconds":0,"nanos":12000000}}}
+{"testStepFinished":{"testCaseStartedId":"22","testStepId":"19","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/hooks/hooks.feature:8"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}}
+{"testStepStarted":{"testCaseStartedId":"22","testStepId":"20","timestamp":{"seconds":0,"nanos":14000000}}}
+{"testStepFinished":{"testCaseStartedId":"22","testStepId":"20","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}}
+{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}}
+{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":17000000},"success":false}}
diff --git a/spec/support/minimal.findAllPickles.results.json b/spec/support/minimal.findAllPickles.results.json
new file mode 100644
index 000000000..56a6051ca
--- /dev/null
+++ b/spec/support/minimal.findAllPickles.results.json
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/spec/support/minimal.findAllTestCases.results.json b/spec/support/minimal.findAllTestCases.results.json
new file mode 100644
index 000000000..56a6051ca
--- /dev/null
+++ b/spec/support/minimal.findAllTestCases.results.json
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/spec/support/minimal.findAllTestSteps.results.json b/spec/support/minimal.findAllTestSteps.results.json
new file mode 100644
index 000000000..56a6051ca
--- /dev/null
+++ b/spec/support/minimal.findAllTestSteps.results.json
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/spec/support/minimal.findPickleBy.results.json b/spec/support/minimal.findPickleBy.results.json
new file mode 100644
index 000000000..275ca2f31
--- /dev/null
+++ b/spec/support/minimal.findPickleBy.results.json
@@ -0,0 +1,14 @@
+{
+ "testCaseStarted" : [
+ "cukes"
+ ],
+ "testCaseFinished" : [
+ "cukes"
+ ],
+ "testStepStarted" : [
+ "cukes"
+ ],
+ "testStepFinished" : [
+ "cukes"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/minimal.findTestCaseBy.results.json b/spec/support/minimal.findTestCaseBy.results.json
new file mode 100644
index 000000000..e5c7e2f65
--- /dev/null
+++ b/spec/support/minimal.findTestCaseBy.results.json
@@ -0,0 +1,14 @@
+{
+ "testCaseStarted" : [
+ "6"
+ ],
+ "testCaseFinished" : [
+ "6"
+ ],
+ "testStepStarted" : [
+ "6"
+ ],
+ "testStepFinished" : [
+ "6"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/minimal.ndjson b/spec/support/minimal.ndjson
new file mode 100644
index 000000000..f35958f69
--- /dev/null
+++ b/spec/support/minimal.ndjson
@@ -0,0 +1,12 @@
+{"meta":{"protocolVersion":"28.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}}
+{"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","uri":"samples/minimal/minimal.feature","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"minimal","description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"cukes","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"I have 42 cukes in my belly"}],"examples":[]}}]},"comments":[],"uri":"samples/minimal/minimal.feature"}}
+{"pickle":{"id":"3","uri":"samples/minimal/minimal.feature","astNodeIds":["1"],"tags":[],"name":"cukes","language":"en","steps":[{"id":"2","text":"I have 42 cukes in my belly","type":"Context","astNodeIds":["0"]}]}}
+{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I have {int} cukes in my belly"},"sourceReference":{"uri":"samples/minimal/minimal.ts","location":{"line":3}}}}
+{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}}
+{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":7,"value":"42","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"5"}}
+{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}}
+{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}}
+{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}}
+{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":true}}
diff --git a/spec/support/rules.findAllPickles.results.json b/spec/support/rules.findAllPickles.results.json
new file mode 100644
index 000000000..e440e5c84
--- /dev/null
+++ b/spec/support/rules.findAllPickles.results.json
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/spec/support/rules.findAllTestCases.results.json b/spec/support/rules.findAllTestCases.results.json
new file mode 100644
index 000000000..e440e5c84
--- /dev/null
+++ b/spec/support/rules.findAllTestCases.results.json
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/spec/support/rules.findAllTestSteps.results.json b/spec/support/rules.findAllTestSteps.results.json
new file mode 100644
index 000000000..3cacc0b93
--- /dev/null
+++ b/spec/support/rules.findAllTestSteps.results.json
@@ -0,0 +1 @@
+12
\ No newline at end of file
diff --git a/spec/support/rules.findPickleBy.results.json b/spec/support/rules.findPickleBy.results.json
new file mode 100644
index 000000000..17a87cba5
--- /dev/null
+++ b/spec/support/rules.findPickleBy.results.json
@@ -0,0 +1,40 @@
+{
+ "testCaseStarted" : [
+ "Not enough money",
+ "Enough money",
+ "No chocolates left"
+ ],
+ "testCaseFinished" : [
+ "Not enough money",
+ "Enough money",
+ "No chocolates left"
+ ],
+ "testStepStarted" : [
+ "Not enough money",
+ "Not enough money",
+ "Not enough money",
+ "Not enough money",
+ "Enough money",
+ "Enough money",
+ "Enough money",
+ "Enough money",
+ "No chocolates left",
+ "No chocolates left",
+ "No chocolates left",
+ "No chocolates left"
+ ],
+ "testStepFinished" : [
+ "Not enough money",
+ "Not enough money",
+ "Not enough money",
+ "Not enough money",
+ "Enough money",
+ "Enough money",
+ "Enough money",
+ "Enough money",
+ "No chocolates left",
+ "No chocolates left",
+ "No chocolates left",
+ "No chocolates left"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/rules.findTestCaseBy.results.json b/spec/support/rules.findTestCaseBy.results.json
new file mode 100644
index 000000000..06e9bbc19
--- /dev/null
+++ b/spec/support/rules.findTestCaseBy.results.json
@@ -0,0 +1,40 @@
+{
+ "testCaseStarted" : [
+ "40",
+ "45",
+ "50"
+ ],
+ "testCaseFinished" : [
+ "40",
+ "45",
+ "50"
+ ],
+ "testStepStarted" : [
+ "40",
+ "40",
+ "40",
+ "40",
+ "45",
+ "45",
+ "45",
+ "45",
+ "50",
+ "50",
+ "50",
+ "50"
+ ],
+ "testStepFinished" : [
+ "40",
+ "40",
+ "40",
+ "40",
+ "45",
+ "45",
+ "45",
+ "45",
+ "50",
+ "50",
+ "50",
+ "50"
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/rules.ndjson b/spec/support/rules.ndjson
new file mode 100644
index 000000000..81bb984ba
--- /dev/null
+++ b/spec/support/rules.ndjson
@@ -0,0 +1,47 @@
+{"meta":{"protocolVersion":"28.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}}
+{"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","uri":"samples/rules/rules.feature","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Usage of a `Rule`","description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","children":[{"rule":{"id":"10","location":{"line":7,"column":3},"keyword":"Rule","name":"A sale cannot happen if the customer does not have enough money","description":"","children":[{"scenario":{"id":"4","tags":[],"location":{"line":9,"column":5},"keyword":"Example","name":"Not enough money","description":"","steps":[{"id":"0","location":{"line":10,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"1","location":{"line":11,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"2","location":{"line":12,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"3","location":{"line":13,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":16,"column":5},"keyword":"Example","name":"Enough money","description":"","steps":[{"id":"5","location":{"line":17,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"6","location":{"line":18,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"7","location":{"line":19,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"8","location":{"line":20,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should happen"}],"examples":[]}}],"tags":[]}},{"rule":{"id":"17","location":{"line":23,"column":3},"keyword":"Rule","name":"a sale cannot happen if there is no stock","description":"","children":[{"scenario":{"id":"15","tags":[],"location":{"line":25,"column":5},"keyword":"Example","name":"No chocolates left","description":"","steps":[{"id":"11","location":{"line":26,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"12","location":{"line":27,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are no chocolate bars in stock"},{"id":"13","location":{"line":28,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"14","location":{"line":29,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}}],"tags":[{"location":{"line":22,"column":3},"name":"@some-tag","id":"16"}]}}]},"comments":[{"location":{"line":8,"column":1},"text":" # Unhappy path"},{"location":{"line":15,"column":1},"text":" # Happy path"},{"location":{"line":24,"column":1},"text":" # Unhappy path"}],"uri":"samples/rules/rules.feature"}}
+{"pickle":{"id":"22","uri":"samples/rules/rules.feature","astNodeIds":["4"],"tags":[],"name":"Not enough money","language":"en","steps":[{"id":"18","text":"the customer has 100 cents","type":"Context","astNodeIds":["0"]},{"id":"19","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["1"]},{"id":"20","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action","astNodeIds":["2"]},{"id":"21","text":"the sale should not happen","type":"Outcome","astNodeIds":["3"]}]}}
+{"pickle":{"id":"27","uri":"samples/rules/rules.feature","astNodeIds":["9"],"tags":[],"name":"Enough money","language":"en","steps":[{"id":"23","text":"the customer has 100 cents","type":"Context","astNodeIds":["5"]},{"id":"24","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["6"]},{"id":"25","text":"the customer tries to buy a 75 cent chocolate bar","type":"Action","astNodeIds":["7"]},{"id":"26","text":"the sale should happen","type":"Outcome","astNodeIds":["8"]}]}}
+{"pickle":{"id":"32","uri":"samples/rules/rules.feature","astNodeIds":["15"],"tags":[{"name":"@some-tag","astNodeId":"16"}],"name":"No chocolates left","language":"en","steps":[{"id":"28","text":"the customer has 100 cents","type":"Context","astNodeIds":["11"]},{"id":"29","text":"there are no chocolate bars in stock","type":"Context","astNodeIds":["12"]},{"id":"30","text":"the customer tries to buy a 1 cent chocolate bar","type":"Action","astNodeIds":["13"]},{"id":"31","text":"the sale should not happen","type":"Outcome","astNodeIds":["14"]}]}}
+{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer has {int} cents"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":4}}}}
+{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":8}}}}
+{"stepDefinition":{"id":"35","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are no chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":12}}}}
+{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer tries to buy a {int} cent chocolate bar"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":16}}}}
+{"stepDefinition":{"id":"37","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should not happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":22}}}}
+{"stepDefinition":{"id":"38","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":26}}}}
+{"testRunStarted":{"id":"39","timestamp":{"seconds":0,"nanos":0}}}
+{"testCase":{"id":"40","pickleId":"22","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"19","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"43","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"125","children":[]},"parameterTypeName":"int"}]}]},{"id":"44","pickleStepId":"21","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}}
+{"testCase":{"id":"45","pickleId":"27","testSteps":[{"id":"46","pickleStepId":"23","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"24","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"48","pickleStepId":"25","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"75","children":[]},"parameterTypeName":"int"}]}]},{"id":"49","pickleStepId":"26","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}}
+{"testCase":{"id":"50","pickleId":"32","testSteps":[{"id":"51","pickleStepId":"28","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"53","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"54","pickleStepId":"31","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}}
+{"testCaseStarted":{"id":"55","testCaseId":"40","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"55","testStepId":"41","timestamp":{"seconds":0,"nanos":2000000}}}
+{"testStepFinished":{"testCaseStartedId":"55","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}}
+{"testStepStarted":{"testCaseStartedId":"55","testStepId":"42","timestamp":{"seconds":0,"nanos":4000000}}}
+{"testStepFinished":{"testCaseStartedId":"55","testStepId":"42","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}}
+{"testStepStarted":{"testCaseStartedId":"55","testStepId":"43","timestamp":{"seconds":0,"nanos":6000000}}}
+{"testStepFinished":{"testCaseStartedId":"55","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}}
+{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"seconds":0,"nanos":8000000}}}
+{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}}
+{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"56","testCaseId":"45","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"56","testStepId":"46","timestamp":{"seconds":0,"nanos":12000000}}}
+{"testStepFinished":{"testCaseStartedId":"56","testStepId":"46","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}}
+{"testStepStarted":{"testCaseStartedId":"56","testStepId":"47","timestamp":{"seconds":0,"nanos":14000000}}}
+{"testStepFinished":{"testCaseStartedId":"56","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}}
+{"testStepStarted":{"testCaseStartedId":"56","testStepId":"48","timestamp":{"seconds":0,"nanos":16000000}}}
+{"testStepFinished":{"testCaseStartedId":"56","testStepId":"48","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}}
+{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":18000000}}}
+{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}}
+{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}}
+{"testCaseStarted":{"id":"57","testCaseId":"50","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}}
+{"testStepStarted":{"testCaseStartedId":"57","testStepId":"51","timestamp":{"seconds":0,"nanos":22000000}}}
+{"testStepFinished":{"testCaseStartedId":"57","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}}
+{"testStepStarted":{"testCaseStartedId":"57","testStepId":"52","timestamp":{"seconds":0,"nanos":24000000}}}
+{"testStepFinished":{"testCaseStartedId":"57","testStepId":"52","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}}
+{"testStepStarted":{"testCaseStartedId":"57","testStepId":"53","timestamp":{"seconds":0,"nanos":26000000}}}
+{"testStepFinished":{"testCaseStartedId":"57","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}}
+{"testStepStarted":{"testCaseStartedId":"57","testStepId":"54","timestamp":{"seconds":0,"nanos":28000000}}}
+{"testStepFinished":{"testCaseStartedId":"57","testStepId":"54","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}}
+{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":30000000},"willBeRetried":false}}
+{"testRunFinished":{"testRunStartedId":"39","timestamp":{"seconds":0,"nanos":31000000},"success":true}}