Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/release-rubygem.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Release RubyGems

permissions: {}

on:
push:
branches:
- release/*

jobs:
publish-rubygem:
name: Publish Ruby Gem
runs-on: ubuntu-latest
environment: Release
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
with:
ruby-version: 4.0.4
working-directory: ruby
- uses: rubygems/configure-rubygems-credentials@762a4b77c3300434bb57c7ce80b20e36231927aa # v2.0.0
- uses: cucumber/action-publish-rubygem@4e79bb9aed597c835e8438f57c04d0996ab80d72 # v2.0.0
with:
working-directory: ruby
52 changes: 52 additions & 0 deletions .github/workflows/test-ruby.yaml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recommendation for YAML files is to use the .yaml extension.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also copy over the the release-rubygem.yaml from any other project.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Matt's robot>
Updated in f01d586: renamed the Ruby test workflow to .github/workflows/test-ruby.yaml. The RubyGems release workflow is already present as .github/workflows/release-rubygem.yaml, copied in this PR.
</Matt's robot>

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: test-ruby

permissions: {}

on:
push:
branches:
- main
- renovate/**
paths:
- ruby/**
- testdata/**
- .github/**
pull_request:
branches:
- main
paths:
- ruby/**
- testdata/**
- .github/**

jobs:
test-ruby:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
ruby: ['3.2', '3.3', '3.4', '4.0']
include:
- os: macos-latest
ruby: '3.4'
- os: windows-latest
ruby: '3.4'

steps:
- name: set git core.autocrlf to 'input'
run: git config --global core.autocrlf input

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
working-directory: ruby

- run: bundle exec rake
working-directory: ruby
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea/
*.iml

ruby/Gemfile.lock
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [Ruby] Add Ruby implementation of cucumber-query ([#167](https://github.com/cucumber/query/pull/167)).

## [15.0.1] - 2026-01-22
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions ruby/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--require spec_helper
--format documentation
25 changes: 25 additions & 0 deletions ruby/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins:
- rubocop-performance
- rubocop-rake
- rubocop-rspec

AllCops:
NewCops: enable
TargetRubyVersion: 3.2

Layout/EndOfLine:
EnforcedStyle: lf

Style/Documentation:
Enabled: false

Gemspec/DevelopmentDependencies:
Enabled: false

RSpec/MultipleExpectations:
Exclude:
- spec/unit/**/*

RSpec/SpecFilePathFormat:
Exclude:
- spec/unit/**/*
7 changes: 7 additions & 0 deletions ruby/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'cucumber-messages', path: ENV.fetch('CUCUMBER_MESSAGES_RUBY_PATH') if ENV['CUCUMBER_MESSAGES_RUBY_PATH']

gemspec
161 changes: 161 additions & 0 deletions ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Cucumber Query for Ruby

`cucumber-query` builds an in-memory index of Cucumber Messages so formatters
and other message consumers can answer questions such as "which pickle belongs
to this test case?", "which step definition matched this test step?", or "what
was the most severe result for this scenario?".

The Ruby implementation follows the public API shape used by the other
`cucumber-query` packages while using Ruby snake_case method names.

## Installation

```ruby
gem 'cucumber-query', '~> 15.0'
```

Ruby 3.2 or newer is required. The gem depends on `cucumber-messages` and
accepts `Cucumber::Messages::Envelope` objects.

## Basic usage

Create one query per message stream, then pass every envelope to `#update` in
the order it was produced:

```ruby
require 'cucumber/query'
require 'cucumber/messages/helpers/ndjson_to_message_enumerator'

query = Cucumber::Query::Query.new

File.open('cucumber-messages.ndjson', 'r') do |io|
Cucumber::Messages::Helpers::NdjsonToMessageEnumerator.new(io).each do |envelope|
query.update(envelope)
end
end

puts query.count_test_cases_started
puts query.find_all_pickles.map(&:name)
```

`#update` returns `nil`. After each update, the query indexes the parts of the
envelope it understands and keeps the original envelope in `#envelopes` for
inspection/debugging.

## Message flow and architecture

The query is deliberately stateful:

1. `meta`, `gherkin_document`, `pickle`, `hook`, `step_definition`, `test_case`
and other definition messages populate lookup tables.
2. Runtime messages such as `test_case_started`, `test_step_finished`,
`attachment`, `test_case_finished`, and global hook messages are linked back
to those definitions by id.
3. Query methods read those lookup tables and return message objects from
`cucumber-messages`, not wrapper objects.

Because Cucumber Messages refer to related data by id, feed the complete stream
to the query when possible. If a formatter asks for a pickle before the
corresponding `pickle` message has been seen, the lookup will return `nil`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this, it would be good to update the table in https://github.com/cucumber/query/blob/main/CONTRIBUTING.md#types, this gives good hints for when somebody wants to add a new method across languages.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, like, an additional column for Ruby with T | nil, Array and Hash?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep


## Common query methods

Counts and result aggregation:

```ruby
query.count_test_cases_started
query.count_most_severe_test_step_result_status
```

Top-level collections:

```ruby
query.find_all_pickles
query.find_all_pickle_steps
query.find_all_step_definitions
query.find_all_test_steps
query.find_all_test_case_started
query.find_all_test_case_finished
query.find_all_test_run_hook_started
query.find_all_test_run_hook_finished
query.find_all_undefined_parameter_types
```

Relationship lookups:

```ruby
query.find_pickle_by(test_case_started_or_finished)
query.find_location_of(pickle)
query.find_lineage_by(pickle_or_runtime_message)
query.find_test_case_by(test_case_started_or_finished)
query.find_test_case_started_by(test_case_finished_or_step_message)
query.find_test_case_finished_by(test_case_started)
query.find_test_step_by(test_step_started_or_finished)
query.find_pickle_step_by(test_step)
query.find_step_by(pickle_step)
query.find_hook_by(test_step_or_test_run_hook_message)
query.find_step_definitions_by(test_step)
query.find_unambiguous_step_definition_by(test_step)
query.find_attachments_by(test_step_finished_or_test_run_hook_finished)
query.find_suggestions_by(pickle_or_pickle_step)
```

Duration helpers:

```ruby
query.find_test_case_duration_by(test_case_started_or_finished)
query.find_test_run_duration
```

Ordering helpers:

```ruby
query.find_all_test_case_started_order_by(->(query, item) { query.find_pickle_by(item).name }, ->(a, b) { a <=> b })
query.find_all_test_case_finished_order_by(->(query, item) { item.timestamp.seconds }, ->(a, b) { a <=> b })
```

The ordering methods receive a projection callable and a comparison callable.
Items whose projection is `nil` sort after items with a value.

## Local development dependencies

Use released gems by default. To test against a local `cucumber-messages`
checkout without editing this repository, set an environment variable before
running Bundler:

```sh
cd repos/query/ruby
CUCUMBER_MESSAGES_RUBY_PATH=../../messages/ruby bundle install
```

Run the Ruby checks from this directory:

```sh
bundle exec rake
```

The acceptance specs load shared NDJSON fixtures from `../testdata/src` and
compare the Ruby query results with the shared expected JSON files.

## Performance and thread-safety notes

`Cucumber::Query::Query` keeps indexes for the whole message stream in memory.
This is the intended trade-off for formatters that need to correlate runtime
messages with source, pickle, hook, attachment, and suggestion messages.

A query instance is mutable and is not designed for concurrent `#update` calls.
Use one query per formatter/message stream, or synchronize access externally if
you share an instance across threads.

## Troubleshooting

- A lookup returning `nil` usually means the related message has not been fed to
the query yet, or the input stream is incomplete.
- Retried scenarios whose `test_case_finished.will_be_retried` flag is true are
excluded from final scenario/result collections such as
`find_all_test_case_started` and `find_all_test_case_finished`.
- Status aggregation uses Cucumber's severity order:
`UNKNOWN`, `PASSED`, `SKIPPED`, `PENDING`, `UNDEFINED`, `AMBIGUOUS`, `FAILED`.
- The Ruby package is developed against shared Cucumber Query testdata; when a
new shared fixture appears, add/adjust Ruby acceptance coverage rather than
creating Ruby-only behaviour.
10 changes: 10 additions & 0 deletions ruby/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'

RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new(:rubocop)

task default: %i[spec rubocop]
1 change: 1 addition & 0 deletions ruby/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
15.0.1
38 changes: 38 additions & 0 deletions ruby/cucumber-query.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

version = File.read(File.expand_path('VERSION', __dir__)).strip

Gem::Specification.new do |s|
s.name = 'cucumber-query'
s.version = version
s.authors = ['Matt Wynne']
s.description = 'Query and correlate Cucumber messages'
s.summary = "cucumber-query-#{s.version}"
s.email = 'hello@cucumber.io'
s.homepage = 'https://github.com/cucumber/query#readme'
s.platform = Gem::Platform::RUBY
s.license = 'MIT'
s.required_ruby_version = '>= 3.2'
s.required_rubygems_version = '>= 3.2.8'

s.metadata = {
'bug_tracker_uri' => 'https://github.com/cucumber/query/issues',
'changelog_uri' => 'https://github.com/cucumber/query/blob/main/CHANGELOG.md',
'documentation_uri' => 'https://github.com/cucumber/query/tree/main/ruby',
'rubygems_mfa_required' => 'true',
'source_code_uri' => 'https://github.com/cucumber/query'
}

s.add_dependency 'cucumber-messages', '>= 32.0.0', '< 33.0.0'

s.add_development_dependency 'rake', '~> 13.1'
s.add_development_dependency 'rspec', '~> 3.13'
s.add_development_dependency 'rubocop', '~> 1.80'
s.add_development_dependency 'rubocop-performance', '~> 1.24'
s.add_development_dependency 'rubocop-rake', '~> 0.6'
s.add_development_dependency 'rubocop-rspec', '~> 3.7'

s.files = Dir['README.md', 'VERSION', 'lib/**/*']
s.rdoc_options = ['--charset=UTF-8']
s.require_path = 'lib'
end
15 changes: 15 additions & 0 deletions ruby/lib/cucumber/query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

require_relative 'query/version'
require_relative 'query/state'
require_relative 'query/gherkin_indexer'
require_relative 'query/envelope_indexer'
require_relative 'query/result_order'
require_relative 'query/naming_strategy'
require_relative 'query/finders'
require_relative 'query/query'

module Cucumber
module Query
end
end
Loading
Loading