diff --git a/ruby/.gitignore b/ruby/.gitignore new file mode 100644 index 00000000..7ae6fcfb --- /dev/null +++ b/ruby/.gitignore @@ -0,0 +1,2 @@ +Gemfile.lock +*.gem diff --git a/ruby/.rspec b/ruby/.rspec new file mode 100644 index 00000000..3687797e --- /dev/null +++ b/ruby/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--color diff --git a/ruby/.rubocop.yml b/ruby/.rubocop.yml new file mode 100644 index 00000000..4c19d3f7 --- /dev/null +++ b/ruby/.rubocop.yml @@ -0,0 +1,21 @@ +inherit_from: .rubocop_todo.yml + +plugins: + - rubocop-performance + - rubocop-rspec + +inherit_mode: + merge: + - Exclude + +AllCops: + TargetRubyVersion: 3.2 + NewCops: enable + +# Stylistic preference for cucumber +Gemspec/DevelopmentDependencies: + Enabled: false + +# Disabled on our repo's to enable polyglot-release +Gemspec/RequireMFA: + Enabled: false diff --git a/ruby/.rubocop_todo.yml b/ruby/.rubocop_todo.yml new file mode 100644 index 00000000..4f132b72 --- /dev/null +++ b/ruby/.rubocop_todo.yml @@ -0,0 +1,128 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2025-09-05 19:34:38 UTC using RuboCop version 1.80.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines +Layout/EmptyLinesAroundModuleBody: + Exclude: + - 'spec/support/runner_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb' + +# Offense count: 8 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/cucumber/query/legacy/hook_by_test_step_spec.rb' + - 'spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# NotImplementedExceptions: NotImplementedError +Lint/UnusedMethodArgument: + Exclude: + - 'lib/cucumber/query/query.rb' + +# Offense count: 4 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 43 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 14 + +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 24 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 14 + +# Offense count: 8 +Performance/MethodObjectAsBlock: + Exclude: + - 'lib/cucumber/query/legacy/hook_by_test_step.rb' + - 'lib/cucumber/query/legacy/pickle_by_test.rb' + - 'lib/cucumber/query/legacy/pickle_step_by_test_step.rb' + - 'lib/cucumber/query/legacy/step_definitions_by_test_step.rb' + - 'lib/cucumber/query/legacy/test_case_started_by_test_case.rb' + +# Offense count: 1 +# Configuration parameters: Prefixes, AllowedPatterns. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/cucumber/query/legacy/hook_by_test_step_spec.rb' + +# Offense count: 1 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 7 + +# Offense count: 32 +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/cucumber/query/legacy/hook_by_test_step_spec.rb' + - 'spec/cucumber/query/legacy/pickle_by_test_spec.rb' + - 'spec/cucumber/query/legacy/pickle_step_by_test_step_spec.rb' + - 'spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb' + - 'spec/cucumber/query/legacy/test_case_started_by_test_case_spec.rb' + +# Offense count: 3 +RSpec/MultipleExpectations: + Max: 3 + +# Offense count: 12 +# Configuration parameters: AllowedGroups. +RSpec/NestedGroups: + Max: 5 + +# Offense count: 8 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'lib/cucumber/query/helpers.rb' + - 'lib/cucumber/query/legacy/hook_by_test_step.rb' + - 'lib/cucumber/query/legacy/pickle_by_test.rb' + - 'lib/cucumber/query/legacy/pickle_step_by_test_step.rb' + - 'lib/cucumber/query/legacy/step_definitions_by_test_step.rb' + - 'lib/cucumber/query/legacy/test_case_started_by_test_case.rb' + - 'lib/cucumber/query/legacy/test_run_started.rb' + - 'lib/cucumber/query/query.rb' + +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Exclude: + - 'lib/cucumber/query/helpers.rb' + - 'lib/cucumber/query/query.rb' + - 'spec/support/gherkin_helper.rb' + - 'spec/support/runner_helper.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 198 diff --git a/ruby/Gemfile b/ruby/Gemfile new file mode 100644 index 00000000..7f4f5e95 --- /dev/null +++ b/ruby/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock new file mode 100644 index 00000000..a65432fe --- /dev/null +++ b/ruby/Gemfile.lock @@ -0,0 +1,112 @@ +PATH + remote: . + specs: + cucumber-query (21.0.0) + cucumber-messages (> 25, < 30) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + base64 (0.3.0) + bigdecimal (3.2.3) + builder (3.3.0) + cucumber (10.1.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.2.1) + cucumber-gherkin (> 27, < 33) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 7) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (32.2.0) + cucumber-messages (> 25, < 28) + cucumber-html-formatter (21.14.0) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-tag-expressions (6.1.2) + diff-lcs (1.5.1) + ffi (1.17.2) + ffi (1.17.2-x86_64-linux-gnu) + json (2.9.1) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + logger (1.7.0) + memoist3 (1.0.0) + mini_mime (1.1.5) + multi_test (1.1.0) + parallel (1.26.3) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + prism (1.4.0) + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.10.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + rubocop (1.80.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.7.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + ruby-progressbar (1.13.0) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + cucumber (~> 10.1) + cucumber-query! + rspec (~> 3.13) + rubocop (~> 1.80.0) + rubocop-performance (~> 1.25.0) + rubocop-rspec (~> 3.7.0) + +BUNDLED WITH + 2.6.2 diff --git a/ruby/LICENSE b/ruby/LICENSE new file mode 100644 index 00000000..d23a133d --- /dev/null +++ b/ruby/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Cucumber Ltd and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ruby/README.md b/ruby/README.md new file mode 100644 index 00000000..faeb97a2 --- /dev/null +++ b/ruby/README.md @@ -0,0 +1,81 @@ +# The Cucumber Compatibility Kit for Ruby + +The CCK - aka. Cucumber Compatibility Kit - is a set of features and Messages. +It aims to validate an implementation of the +[Cucumber Messages protocol](https://github.com/cucumber/common/tree/main/messages#cucumber-messages). + +## Overview + +This kit (gem), consists of a set of features, miscellaneous files and messages: + +- Each area will contain one feature, which, once executed, will emit an exhaustive set of messages +as specified by the protocol +- Some of these areas may "also" require miscellaneous files to be used when testing functions +such as attaching images or documents or reading data from files +- Each area will contain a set of messages - serialized as a single `.ndjson` file +This is the reference for the CCK, that a given feature from the kit, when executed using any dedicated +step definitions, must emit the **exact** corresponding messages + +## Installation and Usage + +Add `cucumber-compatibility-kit` to your `Gemfile` as a development dependency, and +install it: + + bundle install + +Then add a spec that could look like this: + +```ruby +# spec/my_compatibility_checks_spec.rb +require 'cucumber/compatibility_kit' + +describe Cucumber::CompatibilityKit, type: :feature do + let(:cucumber_command) { 'bundle exec cucumber --publish-quiet --profile none --format message' } + + # Don't run the retry or skipped CCK Examples (For whatever reason) + examples = Cucumber::CompatibilityKit.gherkin.reject { |example| example == 'retry' || example == 'skipped' } + + examples.each do |example_name| + describe "'#{example_name}' example" do + include_examples 'cucumber compatibility kit' do + let(:example) { example_name } + # You will need to specify the relative support code and cck paths + let(:messages) { `#{cucumber_command} --require #{support_code_path} #{cck_path}` } + end + end + end +end +``` + +`CucumberCompatibilityKit.gherkin` will return an array that lists all the gherkin examples available within the CCK. +Here, we want to execute all of them except the `retry` and `skipped` ones (For whatever reason). + +`let(:messages)` will execute the cucumber command. As we are using the `message` formatter, `messages` will +then contain the messages as a `ndjson` document with one message per line. + +You can use `gem open cucumber-compatibility-kit` in order to take a look at the features and the +expected messages they should produce. They are available in the `features` folder within the gem. + +## More info + +The Cucumber Compatibility Kit is part of the development tools of [Cucumber](https://cucumber.io). +It allows us to make sure that all our implementations are properly supporting our internal protocol +and thus are compatible (and consistent), with each other and our common tools like the [html-formatter](https://github.com/cucumber/html-formatter). + +It can be a valuable tool if you are developing integration with cucumber, or your own implementation of it. + +Join us on [github/cucumber/compatibility-kit](https://github.com/cucumber/compatibility-kit) +to get more help if you need to. + +You can also take a look on [cucumber-ruby](https://github.com/cucumber/cucumber-ruby/blob/v9.2.0/compatibility/cck_spec.rb) +to see how the kit is used there. + +## Development + +Before building this project locally, the samples must be copied from the `devkit`. Use: + +``` +cd ../devkit +npm ci && npm run copy-to:ruby +cd ../ruby +``` diff --git a/ruby/VERSION b/ruby/VERSION new file mode 100644 index 00000000..fb5b5130 --- /dev/null +++ b/ruby/VERSION @@ -0,0 +1 @@ +21.0.0 diff --git a/ruby/cucumber-query.gemspec b/ruby/cucumber-query.gemspec new file mode 100644 index 00000000..b152a6a4 --- /dev/null +++ b/ruby/cucumber-query.gemspec @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = 'cucumber-query' + s.version = File.read(File.expand_path('VERSION', __dir__)).strip + s.authors = ['Luke Hill'] + s.description = 'Cucumber Query - query messages' + s.summary = "#{s.name}-#{s.version}" + s.email = 'cukebot@cucumber.io' + s.homepage = 'https://github.com/cucumber/query' + 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/blob/main/CONTRIBUTING.md', + 'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/cukes', + 'source_code_uri' => 'https://github.com/cucumber/query/blob/main/ruby' + } + + s.add_dependency 'cucumber-messages', '> 25', '< 30' + + s.add_development_dependency 'cucumber', '~> 10.1' + s.add_development_dependency 'rspec', '~> 3.13' + s.add_development_dependency 'rubocop', '~> 1.80.0' + s.add_development_dependency 'rubocop-performance', '~> 1.25.0' + s.add_development_dependency 'rubocop-rspec', '~> 3.7.0' + + s.files = Dir['README.md', 'LICENSE', 'lib/**/*'] + s.rdoc_options = ['--charset=UTF-8'] + s.require_path = 'lib' +end diff --git a/ruby/lib/cucumber/query/helpers.rb b/ruby/lib/cucumber/query/helpers.rb new file mode 100644 index 00000000..0c59d7cf --- /dev/null +++ b/ruby/lib/cucumber/query/helpers.rb @@ -0,0 +1,23 @@ +require 'cucumber/messages/test_step_result_status' + +module Cucumber + module Query + module Helpers + def status_ordinal(status) + [ + TestStepResultStatus::UNKNOWN, + TestStepResultStatus::PASSED, + TestStepResultStatus::SKIPPED, + TestStepResultStatus::PENDING, + TestStepResultStatus::UNDEFINED, + TestStepResultStatus::AMBIGUOUS, + TestStepResultStatus::FAILED + ].index(status) + end + + def assert(target, failure_message) + raise StandardError, failure_message unless target + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/errors.rb b/ruby/lib/cucumber/query/legacy/errors.rb new file mode 100644 index 00000000..fc94315d --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/errors.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Cucumber + module Query + module Legacy + TestCaseUnknownError = Class.new(StandardError) + TestStepUnknownError = Class.new(StandardError) + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/hook_by_test_step.rb b/ruby/lib/cucumber/query/legacy/hook_by_test_step.rb new file mode 100644 index 00000000..7953684a --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/hook_by_test_step.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class HookByTestStep + def initialize(config) + @hook_id_by_test_step_id = {} + + config.on_event(:test_step_created, &method(:on_test_step_created)) + config.on_event(:hook_test_step_created, &method(:on_hook_test_step_created)) + end + + def hook_id(test_step) + return @hook_id_by_test_step_id[test_step.id] if @hook_id_by_test_step_id.key?(test_step.id) + + raise TestStepUnknownError, "No hook found for #{test_step.id} }. Known: #{@hook_id_by_test_step_id.keys}" + end + + private + + def on_test_step_created(event) + @hook_id_by_test_step_id[event.test_step.id] = nil + end + + def on_hook_test_step_created(event) + @hook_id_by_test_step_id[event.test_step.id] = event.hook.id + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/pickle_by_test.rb b/ruby/lib/cucumber/query/legacy/pickle_by_test.rb new file mode 100644 index 00000000..c9a5a35c --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/pickle_by_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class PickleByTest + def initialize(config) + @pickle_id_by_test_case_id = {} + + config.on_event(:test_case_created, &method(:on_test_case_created)) + end + + def pickle_id(test_case) + return @pickle_id_by_test_case_id[test_case.id] if @pickle_id_by_test_case_id.key?(test_case.id) + + raise TestCaseUnknownError, "No pickle found for #{test_case.id} }. Known: #{@pickle_id_by_test_case_id.keys}" + end + + private + + def on_test_case_created(event) + @pickle_id_by_test_case_id[event.test_case.id] = event.pickle.id + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/pickle_step_by_test_step.rb b/ruby/lib/cucumber/query/legacy/pickle_step_by_test_step.rb new file mode 100644 index 00000000..aee77f74 --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/pickle_step_by_test_step.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class PickleStepByTestStep + def initialize(config) + @pickle_id_step_by_test_step_id = {} + + config.on_event(:test_step_created, &method(:on_test_step_created)) + end + + def pickle_step_id(test_step) + return @pickle_id_step_by_test_step_id[test_step.id] if @pickle_id_step_by_test_step_id.key?(test_step.id) + + raise TestStepUnknownError, "No pickle step found for #{test_step.id} }. Known: #{@pickle_id_step_by_test_step_id.keys}" + end + + private + + def on_test_step_created(event) + @pickle_id_step_by_test_step_id[event.test_step.id] = event.pickle_step.id + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/step_definitions_by_test_step.rb b/ruby/lib/cucumber/query/legacy/step_definitions_by_test_step.rb new file mode 100644 index 00000000..ae82f7c6 --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/step_definitions_by_test_step.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class StepDefinitionsByTestStep + def initialize(config) + @step_definition_ids_by_test_step_id = {} + @step_match_arguments_by_test_step_id = {} + + config.on_event(:test_step_created, &method(:on_test_step_created)) + config.on_event(:step_activated, &method(:on_step_activated)) + end + + def step_definition_ids(test_step) + return @step_definition_ids_by_test_step_id[test_step.id] if @step_definition_ids_by_test_step_id.key?(test_step.id) + + raise TestStepUnknownError, "No step definition found for #{test_step.id} }. Known: #{@step_definition_ids_by_test_step_id.keys}" + end + + def step_match_arguments(test_step) + return @step_match_arguments_by_test_step_id[test_step.id] if @step_match_arguments_by_test_step_id.key?(test_step.id) + + raise TestStepUnknownError, "No step match arguments found for #{test_step.id} }. Known: #{@step_match_arguments_by_test_step_id.keys}" + end + + private + + def on_test_step_created(event) + @step_definition_ids_by_test_step_id[event.test_step.id] = [] + end + + def on_step_activated(event) + @step_definition_ids_by_test_step_id[event.test_step.id] << event.step_match.step_definition.id + @step_match_arguments_by_test_step_id[event.test_step.id] = event.step_match.step_arguments + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/test_case_started_by_test_case.rb b/ruby/lib/cucumber/query/legacy/test_case_started_by_test_case.rb new file mode 100644 index 00000000..8153cc50 --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/test_case_started_by_test_case.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class TestCaseStartedByTestCase + def initialize(config) + @config = config + @attempts_by_test_case_id = {} + @test_case_started_id_by_test_case_id = {} + + config.on_event(:test_case_created, &method(:on_test_case_created)) + config.on_event(:test_case_started, &method(:on_test_case_started)) + end + + def attempt_by_test_case(test_case) + raise TestCaseUnknownError, "No test case found for #{test_case.id} }. Known: #{@attempts_by_test_case_id.keys}" unless @attempts_by_test_case_id.key?(test_case.id) + + @attempts_by_test_case_id[test_case.id] + end + + def test_case_started_id_by_test_case(test_case) + raise TestCaseUnknownError, "No test case found for #{test_case.id} }. Known: #{@test_case_started_id_by_test_case_id.keys}" unless @test_case_started_id_by_test_case_id.key?(test_case.id) + + @test_case_started_id_by_test_case_id[test_case.id] + end + + private + + def on_test_case_created(event) + @attempts_by_test_case_id[event.test_case.id] = 0 + @test_case_started_id_by_test_case_id[event.test_case.id] = nil + end + + def on_test_case_started(event) + @attempts_by_test_case_id[event.test_case.id] += 1 + @test_case_started_id_by_test_case_id[event.test_case.id] = @config.id_generator.new_id + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/legacy/test_run_started.rb b/ruby/lib/cucumber/query/legacy/test_run_started.rb new file mode 100644 index 00000000..ccc1a162 --- /dev/null +++ b/ruby/lib/cucumber/query/legacy/test_run_started.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative 'errors' + +module Cucumber + module Query + module Legacy + class TestRunStarted + def initialize(config) + @config = config + end + + def id + @id ||= @config.id_generator.new_id + end + end + end + end +end diff --git a/ruby/lib/cucumber/query/query.rb b/ruby/lib/cucumber/query/query.rb new file mode 100644 index 00000000..319e38ee --- /dev/null +++ b/ruby/lib/cucumber/query/query.rb @@ -0,0 +1,80 @@ +require_relative 'helpers' + +module Cucumber + class Query + def initialize + @test_step_result_by_pickle_id = [{}] + @test_step_results_by_pickle_step_id = [[]] + @test_case_by_pickle_id = [] + @pickle_id_by_test_step_id = [] + @pickle_step_id_by_test_step_id = [] + @test_step_results_by_test_step_id = [[]] + @test_step_ids_by_pickle_step_id = [{}] + @hooks_by_id = {} + @attachments_by_test_step_id = [] + @step_match_arguments_lists_by_pickle_step_id = [{}] + @meta = :not_sure_on_this + @test_run_started = :not_sure_on_this + @test_run_finished = :not_sure_on_this + @test_case_started_by_id = [] + @lineage_by_id = [] + @step_by_id = [] + @pickle_by_id = [] + @pickle_step_by_id = [] + @step_definition_by_id = {} + @test_case_by_id = [] + @test_case_finished_by_test_case_started_id = [[]] + @test_step_started_by_test_case_started_id = [[]] + @test_step_finished_by_test_case_started_id = [[]] + @attachments_by_test_case_started_id = [[]] + end + + def update(envelope) + @meta = envelope.meta if envelope.meta + update_gherkin_document(envelope.gherkin_document) if envelope.gherkin_document + update_pickle(envelope.pickle) if envelope.gherkin_document + @hooks_by_id[envelope.hook.id] = envelope.hook if envelope.hook + @step_definition_by_id[envelope.step_definition.id] = envelope.step_definition if envelope.step_definition + @test_run_started = envelope.test_run_started if envelope.test_run_started + update_test_case(envelope.test_case) if envelope.test_case + update_test_case_started(envelope.test_case_started) if envelope.test_case_started + update_test_step_started(envelope.test_step_started) if envelope.test_step_started + update_attachment(envelope.attachment) if envelope.attachment + update_test_step_finished(envelope.test_step_finished) if envelope.test_step_finished + update_test_case_finished(envelope.test_case_finished) if envelope.test_case_finished + @test_run_finished = envelope.test_run_finished if envelope.test_run_finished + end + + def update_gherkin_document(gherkin_document) + :not_yet_implemented + end + + def update_pickle(pickle) + :not_yet_implemented + end + + def update_test_case(test_case) + :not_yet_implemented + end + + def update_test_case_started(test_case_started) + :not_yet_implemented + end + + def update_test_step_started(test_step_started) + :not_yet_implemented + end + + def update_attachment(attachment) + :not_yet_implemented + end + + def update_test_step_finished(test_step_finished) + :not_yet_implemented + end + + def update_test_case_finished(test_case_finished) + :not_yet_implemented + end + end +end diff --git a/ruby/spec/cucumber/query/legacy/hook_by_test_step_spec.rb b/ruby/spec/cucumber/query/legacy/hook_by_test_step_spec.rb new file mode 100644 index 00000000..b7100eed --- /dev/null +++ b/ruby/spec/cucumber/query/legacy/hook_by_test_step_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'cucumber/query/legacy/hook_by_test_step' + +describe Cucumber::Query::Legacy::HookByTestStep do + before do + @test_cases = [] + @hook_ids = [] + + config.on_event(:test_case_started) do |event| + @test_cases << event.test_case + end + + config.on_event(:envelope) do |event| + next unless event.envelope.hook + + @hook_ids << event.envelope.hook.id + end + end + + let(:config) { actual_runtime.configuration.with_options(out_stream: StringIO.new) } + let(:first_test_case) { @test_cases.first } + let(:formatter) { described_class.new(config) } + + context 'given a single feature' do + before do + run_defined_feature + end + + context 'with a scenario' do + describe '#pickle_step_id' do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + define_steps do + Before() {} + After() {} + end + + it 'provides the ID of the Before Hook used to generate the Test::Step' do + expect(formatter.hook_id(first_test_case.test_steps.first)).to eq(@hook_ids.first) + end + + it 'provides the ID of the After Hook used to generate the Test::Step' do + expect(formatter.hook_id(first_test_case.test_steps.last)).to eq(@hook_ids.last) + end + + it 'returns nil if the step was not generated from a hook' do + expect(formatter.hook_id(first_test_case.test_steps[1])).to be_nil + end + + it 'raises an exception when the test_step is unknown' do + test_step = double + allow(test_step).to receive(:id).and_return('whatever-id') + + expect { formatter.hook_id(test_step) }.to raise_error(Cucumber::Query::Legacy::TestStepUnknownError) + end + end + end + + context 'with AfterStep hooks' do + describe '#pickle_step_id' do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + define_steps do + AfterStep() {} + end + + it 'provides the ID of the AfterStepHook used to generate the Test::Step' do + expect(formatter.hook_id(first_test_case.test_steps.last)).to eq(@hook_ids.first) + end + end + end + end +end diff --git a/ruby/spec/cucumber/query/legacy/pickle_by_test_spec.rb b/ruby/spec/cucumber/query/legacy/pickle_by_test_spec.rb new file mode 100644 index 00000000..e463b84d --- /dev/null +++ b/ruby/spec/cucumber/query/legacy/pickle_by_test_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'cucumber/query/legacy/pickle_by_test' + +describe Cucumber::Query::Legacy::PickleByTest do + before do + @test_cases = [] + @pickle_ids = [] + + config.on_event(:test_case_created) do |event| + @test_cases << event.test_case + end + + config.on_event(:envelope) do |event| + next unless event.envelope.pickle + + @pickle_ids << event.envelope.pickle.id + end + end + + let(:config) { actual_runtime.configuration.with_options(out_stream: StringIO.new) } + let(:formatter) { described_class.new(config) } + + describe 'given a single feature' do + before do + run_defined_feature + end + + describe 'with a scenario' do + describe '#pickle_id' do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'provides the ID of the pickle used to generate the Test::Case' do + expect(formatter.pickle_id(@test_cases.first)).to eq(@pickle_ids.first) + end + + it 'raises an error when the Test::Case is unknown' do + test_case = double + allow(test_case).to receive(:id).and_return('whatever-id') + + expect { formatter.pickle_id(test_case) }.to raise_error(Cucumber::Query::Legacy::TestCaseUnknownError) + end + end + end + end +end diff --git a/ruby/spec/cucumber/query/legacy/pickle_step_by_test_step_spec.rb b/ruby/spec/cucumber/query/legacy/pickle_step_by_test_step_spec.rb new file mode 100644 index 00000000..7b16c027 --- /dev/null +++ b/ruby/spec/cucumber/query/legacy/pickle_step_by_test_step_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'cucumber/query/legacy/pickle_step_by_test_step' + +describe Cucumber::Query::Legacy::PickleStepByTestStep do + before do + @test_cases = [] + @pickle_step_ids = [] + + config.on_event(:test_case_created) do |event| + @test_cases << event.test_case + end + + config.on_event(:envelope) do |event| + next unless event.envelope.pickle + + event.envelope.pickle.steps.each do |step| + @pickle_step_ids << step.id + end + end + end + + let(:config) { actual_runtime.configuration.with_options(out_stream: StringIO.new) } + let(:first_test_case) { @test_cases.first } + let(:formatter) { described_class.new(config) } + + describe 'given a single feature' do + before do + run_defined_feature + end + + describe 'with a scenario' do + describe '#pickle_step_id' do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'provides the ID of the PickleStep used to generate the Test::Step' do + test_step = first_test_case.test_steps.first + + expect(formatter.pickle_step_id(test_step)).to eq(@pickle_step_ids.first) + end + + it 'raises an exception when the test_step is unknown' do + test_step = double + allow(test_step).to receive(:id).and_return('whatever-id') + + expect { formatter.pickle_step_id(test_step) }.to raise_error(Cucumber::Query::Legacy::TestStepUnknownError) + end + end + end + end +end diff --git a/ruby/spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb b/ruby/spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb new file mode 100644 index 00000000..398d6623 --- /dev/null +++ b/ruby/spec/cucumber/query/legacy/step_definitions_by_test_step_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'cucumber/query/legacy/step_definitions_by_test_step' + +describe Cucumber::Query::Legacy::StepDefinitionsByTestStep do + before do + @test_cases = [] + @step_definition_ids = [] + + config.on_event(:test_case_created) do |event| + @test_cases << event.test_case + end + + config.on_event(:envelope) do |event| + next unless event.envelope.step_definition + + @step_definition_ids << event.envelope.step_definition.id + end + end + + let(:config) { actual_runtime.configuration.with_options(out_stream: StringIO.new) } + let(:first_test_case) { @test_cases.first } + let(:formatter) { described_class.new(config) } + + describe 'given a single feature' do + before do + run_defined_feature + end + + describe '#step_definition_ids' do + context 'with a matching step' do + define_steps do + Given(/^there are bananas$/) {} + end + + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'provides the ID of the StepDefinition that matches Test::Step' do + test_step = first_test_case.test_steps.first + + expect(formatter.step_definition_ids(test_step)).to eq([@step_definition_ids.first]) + end + end + + context 'with a step that was not activated' do + context 'when there is no match' do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'returns an empty array' do + test_step = first_test_case.test_steps.first + + expect(formatter.step_definition_ids(test_step)).to be_empty + end + end + + context 'when there are multiple matches' do + define_steps do + Given(/^there are bananas$/) {} + Given(/^there .* bananas$/) {} + end + + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'returns an empty array as the step is not activated' do + test_step = first_test_case.test_steps.first + + expect(formatter.step_definition_ids(test_step)).to be_empty + end + end + end + + context 'with an unknown step' do + define_feature 'Feature: Banana party' + + it 'raises an exception' do + test_step = double + allow(test_step).to receive(:id).and_return('whatever-id') + + expect { formatter.step_definition_ids(test_step) }.to raise_error(Cucumber::Query::Legacy::TestStepUnknownError) + end + end + end + + describe '#step_match_arguments' do + context 'with a matching step without arguments' do + define_steps do + Given(/^there are bananas$/) {} + end + + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'returns an empty list' do + test_step = first_test_case.test_steps.first + + expect(formatter.step_match_arguments(test_step)).to be_empty + end + end + + context 'with a matching step with arguments' do + define_steps do + Given(/^there are (.*)$/) {} + end + + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats banana + Given there are bananas + FEATURE + + it 'returns an empty list' do + test_step = first_test_case.test_steps.first + matches = formatter.step_match_arguments(test_step) + + expect(matches.count).to eq(1) + expect(matches.first).to be_a(Cucumber::CucumberExpressions::Argument) + expect(matches.first.group.value).to eq('bananas') + end + end + + context 'with an unknown step' do + define_feature 'Feature: Banana party' + + it 'raises an exception' do + test_step = double + allow(test_step).to receive(:id).and_return('whatever-id') + + expect { formatter.step_match_arguments(test_step) }.to raise_error(Cucumber::Query::Legacy::TestStepUnknownError) + end + end + end + end +end diff --git a/ruby/spec/cucumber/query/legacy/test_case_started_by_test_case_spec.rb b/ruby/spec/cucumber/query/legacy/test_case_started_by_test_case_spec.rb new file mode 100644 index 00000000..b27b6ebc --- /dev/null +++ b/ruby/spec/cucumber/query/legacy/test_case_started_by_test_case_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'cucumber/query/legacy/test_case_started_by_test_case' + +describe Cucumber::Query::Legacy::TestCaseStartedByTestCase do + let(:config) { actual_runtime.configuration.with_options(out_stream: StringIO.new) } + let(:formatter) { described_class.new(config) } + let(:unknown_test_case) do + test_case = double + allow(test_case).to receive(:id).and_return('whatever-id') + test_case + end + + describe '#attempt_by_test_case' do + it 'raises an error when the TestCase is unknown' do + expect { formatter.attempt_by_test_case(unknown_test_case) }.to raise_error(Cucumber::Query::Legacy::TestCaseUnknownError) + end + + context 'when the test case has been declared' do + before do + @test_case = double + allow(@test_case).to receive(:id).and_return('some-valid-id') + config.notify(:test_case_created, @test_case, nil) + end + + it 'returns 0 if no test_case_started event has been fired' do + expect(formatter.attempt_by_test_case(@test_case)).to eq(0) + end + + it 'increments the attemp on every test_case_started event' do + config.notify(:test_case_started, @test_case) + expect(formatter.attempt_by_test_case(@test_case)).to eq(1) + + config.notify(:test_case_started, @test_case) + expect(formatter.attempt_by_test_case(@test_case)).to eq(2) + end + end + end + + describe '#test_case_started_id_by_test_case' do + it 'raises an error when the TestCase is unknown' do + expect { formatter.test_case_started_id_by_test_case(unknown_test_case) }.to raise_error(Cucumber::Query::Legacy::TestCaseUnknownError) + end + + context 'when the test case has been declared' do + before do + @test_case = double + allow(@test_case).to receive(:id).and_return('some-valid-id') + config.notify(:test_case_created, @test_case, nil) + end + + it 'returns nil if no test_case_started event has been fired' do + expect(formatter.test_case_started_id_by_test_case(@test_case)).to be_nil + end + + it 'gives a new id when a test_case_started event is fired' do + config.notify(:test_case_started, @test_case) + + first_attempt_id = formatter.test_case_started_id_by_test_case(@test_case) + expect(first_attempt_id).not_to be_nil + + config.notify(:test_case_started, @test_case) + second_attempt_id = formatter.test_case_started_id_by_test_case(@test_case) + expect(second_attempt_id).not_to be_nil + + expect(second_attempt_id).not_to eq(first_attempt_id) + end + end + end +end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb new file mode 100644 index 00000000..3c684fd4 --- /dev/null +++ b/ruby/spec/spec_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'cucumber' +require 'cucumber/core' + +require_relative 'support/runner_helper' +require_relative 'support/gherkin_helper' + +RSpec.configure do |c| + c.include RunnerHelper + c.extend GherkinHelper +end diff --git a/ruby/spec/support/gherkin_helper.rb b/ruby/spec/support/gherkin_helper.rb new file mode 100644 index 00000000..29bc3e6c --- /dev/null +++ b/ruby/spec/support/gherkin_helper.rb @@ -0,0 +1,14 @@ +# TODO: This file is adapted / duplicated from SpecHelperDSL and SpecHelper in cucumber-ruby + +module GherkinHelper + attr_reader :feature_content, :step_definitions, :feature_filename + + def define_feature(string, feature_file = 'spec.feature') + @feature_content = string + @feature_filename = feature_file + end + + def define_steps(&block) + @step_definitions = block + end +end diff --git a/ruby/spec/support/runner_helper.rb b/ruby/spec/support/runner_helper.rb new file mode 100644 index 00000000..44d5f67b --- /dev/null +++ b/ruby/spec/support/runner_helper.rb @@ -0,0 +1,60 @@ +# TODO: This file is adapted / duplicated from SpecHelperDSL and SpecHelper in cucumber-ruby + +module RunnerHelper + + include Cucumber::Core + + def run_defined_feature + define_steps + actual_runtime.visitor = Cucumber::Formatter::Fanout.new([@formatter]) + receiver = Test::Runner.new(event_bus) + + event_bus.gherkin_source_read(gherkin_doc.uri, gherkin_doc.body) + + compile [gherkin_doc], receiver, filters, event_bus + + event_bus.test_run_finished + end + + def filters + [ + Cucumber::Filters::ActivateSteps.new( + Cucumber::StepMatchSearch.new(actual_runtime.support_code.registry.method(:step_matches), actual_runtime.configuration), + actual_runtime.configuration + ), + Cucumber::Filters::ApplyAfterStepHooks.new(actual_runtime.support_code), + Cucumber::Filters::ApplyBeforeHooks.new(actual_runtime.support_code), + Cucumber::Filters::ApplyAfterHooks.new(actual_runtime.support_code), + Cucumber::Filters::ApplyAroundHooks.new(actual_runtime.support_code), + Cucumber::Filters::BroadcastTestRunStartedEvent.new(actual_runtime.configuration), + Cucumber::Filters::BroadcastTestCaseReadyEvent.new(actual_runtime.configuration), + Cucumber::Filters::PrepareWorld.new(actual_runtime) + ] + end + + def gherkin_doc + Cucumber::Core::Gherkin::Document.new(self.class.feature_filename, gherkin) + end + + def gherkin + self.class.feature_content || raise('No feature content defined!') + end + + def actual_runtime + @actual_runtime ||= Cucumber::Runtime.new({}) + end + + def event_bus + actual_runtime.configuration.event_bus + end + + def define_steps + step_definitions = self.class.step_definitions + + return unless step_definitions + + dsl = Object.new + dsl.extend Cucumber::Glue::Dsl + dsl.instance_exec(&step_definitions) + end +end diff --git a/testdata/README.md b/testdata/README.md index ed981627..b2136edc 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -4,7 +4,7 @@ Query uses the examples from the [cucumber compatibility kit](https://github.com for acceptance testing. These examples consist of `.ndjson` files created by the [`fake-cucumber` reference implementation](https://github.com/cucumber/fake-cucumber). -* The `.njdon` files are copied in by running `npm install`. +* The `.ndjson` files are copied in by running `npm install`. * The expected `.xml` files are created by running the `QueryTest#updateExpectedXmlReportFiles` test.