gherkin_system 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/lib/gherkin_system/checker.rb +58 -0
- data/lib/gherkin_system/cli.rb +42 -0
- data/lib/gherkin_system/configuration.rb +64 -0
- data/lib/gherkin_system/data_table.rb +24 -0
- data/lib/gherkin_system/errors.rb +95 -0
- data/lib/gherkin_system/executor.rb +95 -0
- data/lib/gherkin_system/feature_loader.rb +52 -0
- data/lib/gherkin_system/hooks.rb +86 -0
- data/lib/gherkin_system/parameter_registry.rb +34 -0
- data/lib/gherkin_system/parser.rb +122 -0
- data/lib/gherkin_system/rails.rb +3 -0
- data/lib/gherkin_system/railtie.rb +7 -0
- data/lib/gherkin_system/scenario.rb +37 -0
- data/lib/gherkin_system/scenario_run.rb +42 -0
- data/lib/gherkin_system/snippet.rb +23 -0
- data/lib/gherkin_system/step_definition.rb +53 -0
- data/lib/gherkin_system/step_registry.rb +47 -0
- data/lib/gherkin_system/steps.rb +39 -0
- data/lib/gherkin_system/tag_filter.rb +24 -0
- data/lib/gherkin_system/test_class_builder.rb +82 -0
- data/lib/gherkin_system/version.rb +6 -0
- data/lib/gherkin_system.rb +79 -0
- data/lib/rails/commands/gherkin/gherkin_command.rb +68 -0
- data/sig/gherkin_system.rbs +64 -0
- data/skills/gherkin-system/SKILL.md +128 -0
- metadata +139 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 932f9e7927f4669a1dccd6af66d25a9f85fa446493d8612a65138558170d569d
|
|
4
|
+
data.tar.gz: 112171a83d2083bc8a8d1a68412e54dc4e0af3a43afdaaa2c653821df56a1080
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a4a601d9c86be652601a73aa4fa5a88b5db61dbff738316983ac8596e9cb7f5abe7c1ecd5d3111e9c6d3e1eeb2010d77f3270926d8ca91a8880ab65ec223d7f4
|
|
7
|
+
data.tar.gz: b39ecb4ff0463c325bcef5d0bd151d1d0cfc49a7d7d23f22e3f70322b9404cf6d9f074498f6bb6f09c56543fabed0cfe0f3a376587d09e8b83731590f2987bfd
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Initial Release
|
|
13
|
+
|
|
14
|
+
- Compile Gherkin features into Minitest methods on the application's system test class
|
|
15
|
+
- Cucumber Expressions, regex step definitions, and custom parameter types
|
|
16
|
+
- Before, After, and Around hooks, tags, data tables, and doc strings
|
|
17
|
+
- `bin/rails gherkin`, `gherkin:check`, and `gherkin:list`
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Paluy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Paluy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# gherkin_system
|
|
2
|
+
|
|
3
|
+
Compile Gherkin features into Rails system tests.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/gherkin_system)
|
|
6
|
+
[](https://github.com/dpaluy/gherkin_system/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
Gherkin describes behavior. Rails owns execution. Each scenario becomes a Minitest method on your `ApplicationSystemTestCase`, so Capybara, fixtures, routes, and screenshots stay the ones you already use. cucumber-rails is a separate runner. This gem is not.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "gherkin_system"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Agent skill
|
|
17
|
+
|
|
18
|
+
For agents writing Gherkin scenarios in a Rails app, install the skill with [`npx skills`](https://www.skills.sh/docs/cli):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx skills add dpaluy/gherkin_system
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `-g` for a global install, `-a` to target specific agents, and `-y` to skip prompts. The skill source is [`skills/gherkin-system`](skills/gherkin-system/SKILL.md).
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# config/initializers/gherkin_system.rb
|
|
30
|
+
GherkinSystem.configure do |config|
|
|
31
|
+
config.features = Rails.root.join("features/system/**/*.feature")
|
|
32
|
+
config.base_test_class = ApplicationSystemTestCase
|
|
33
|
+
config.include_steps AuthenticationSteps
|
|
34
|
+
config.strict = true
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Option | ENV variable | Default |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `features` | | `features/system/**/*.feature` |
|
|
41
|
+
| `tags` | `GHERKIN_TAGS` | unset → every scenario is compiled |
|
|
42
|
+
| `strict` | | `true`. A broken scenario fails itself. The rest still run |
|
|
43
|
+
| `base_test_class` | | required by `load!` when `base:` is omitted |
|
|
44
|
+
| `test_file` | | `test/system/gherkin_test.rb` |
|
|
45
|
+
|
|
46
|
+
`config.tags` / `GHERKIN_TAGS` is a [Cucumber tag expression](https://github.com/cucumber/tag-expressions). Only matching scenarios are compiled into Minitest methods:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
GHERKIN_TAGS="@critical and not @slow" bin/rails test:system
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Same filter via the CLI: `bin/rails gherkin --tags "@critical and not @slow"` (sets `GHERKIN_TAGS` for that run). Leave it unset to compile every scenario. If the env var stays set, later `bin/rails test:system` runs keep the filter.
|
|
53
|
+
|
|
54
|
+
## Rails setup
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# test/system/gherkin_test.rb
|
|
58
|
+
require "application_system_test_case"
|
|
59
|
+
require "gherkin_system/rails"
|
|
60
|
+
|
|
61
|
+
GherkinSystem.load!(base: ApplicationSystemTestCase)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Generated tests subclass the class you pass. The gem does not install its own system-test superclass.
|
|
65
|
+
|
|
66
|
+
## Steps
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
module AuthenticationSteps
|
|
70
|
+
extend GherkinSystem::Steps
|
|
71
|
+
|
|
72
|
+
Given("I am signed in") do
|
|
73
|
+
sign_in users(:customer)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
When("I purchase the {string} plan") do |plan|
|
|
77
|
+
visit plans_path
|
|
78
|
+
click_on plan
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Then(/^I have (\d+) products$/) do |count|
|
|
82
|
+
assert_equal count.to_i, Product.count
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
ParameterType(
|
|
86
|
+
name: "user",
|
|
87
|
+
regexp: /alice|bob/,
|
|
88
|
+
type: User,
|
|
89
|
+
transformer: ->(name) { users(name.to_sym) }
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`Given`, `When`, `Then`, `And`, and `But` share one registry. Blocks run with `instance_exec` on the system test, so instance variables set in one step are visible in the next.
|
|
95
|
+
|
|
96
|
+
## Hooks
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
module CheckoutHooks
|
|
100
|
+
extend GherkinSystem::Hooks
|
|
101
|
+
|
|
102
|
+
Before do
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Before("@admin") do
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
After do |scenario|
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Around("@external") do |scenario|
|
|
112
|
+
scenario.run
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Rails `setup` runs first. Gherkin hooks and steps run inside the test method. Rails `teardown` and failure screenshots run after.
|
|
118
|
+
|
|
119
|
+
## Commands
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
bin/rails gherkin
|
|
123
|
+
bin/rails gherkin features/system/checkout.feature
|
|
124
|
+
bin/rails gherkin features/system/checkout.feature:42
|
|
125
|
+
bin/rails gherkin --tags "@critical and not @slow"
|
|
126
|
+
bin/rails gherkin:check
|
|
127
|
+
bin/rails gherkin:list
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`gherkin:check` parses features and resolves steps. It does not start a browser. `bin/rails test:system` and `bin/rails test:all` run the generated tests as normal Minitest tests.
|
|
131
|
+
|
|
132
|
+
Tag filters use [Cucumber tag expressions](https://github.com/cucumber/tag-expressions).
|
|
133
|
+
|
|
134
|
+
## Error handling
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
begin
|
|
138
|
+
GherkinSystem.load!(base: ApplicationSystemTestCase)
|
|
139
|
+
rescue GherkinSystem::ConfigurationError => e
|
|
140
|
+
warn e.message
|
|
141
|
+
rescue GherkinSystem::CompilationError => e
|
|
142
|
+
warn e.message
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Class | When |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `ConfigurationError` | Missing base class or test file |
|
|
149
|
+
| `CompilationError` | Invalid Gherkin, tag expression, or hook |
|
|
150
|
+
| `UndefinedStep` | No definition matches the step |
|
|
151
|
+
| `AmbiguousStep` | More than one definition matches |
|
|
152
|
+
| `DuplicateStep` | The same pattern is registered twice |
|
|
153
|
+
| `StepFailure` | A step raised. `cause` is the original exception |
|
|
154
|
+
|
|
155
|
+
Undefined and ambiguous steps raise in `before_setup`, before the driver starts. The message names the feature, scenario, step, feature line, and a suggested definition.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bin/setup
|
|
161
|
+
bundle exec rake
|
|
162
|
+
bundle exec rake test
|
|
163
|
+
bundle exec rubocop
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
Issues and pull requests: [github.com/dpaluy/gherkin_system](https://github.com/dpaluy/gherkin_system/issues).
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
173
|
+
|
|
174
|
+
Supported by [Majestic Labs](https://majesticlabs.dev/).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Static validation. Does not execute steps or start a browser.
|
|
5
|
+
class Checker
|
|
6
|
+
# Collected authoring problems.
|
|
7
|
+
Result = Data.define(:issues) do
|
|
8
|
+
# @return [Boolean]
|
|
9
|
+
def ok?
|
|
10
|
+
issues.empty?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [String]
|
|
14
|
+
def report
|
|
15
|
+
issues.map(&:message).join("\n\n")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param glob [String, Pathname, nil]
|
|
20
|
+
# @return [Result]
|
|
21
|
+
def self.call(glob = nil)
|
|
22
|
+
new(glob).call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param glob [String, Pathname, nil]
|
|
26
|
+
def initialize(glob)
|
|
27
|
+
@glob = glob
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Result]
|
|
31
|
+
def call
|
|
32
|
+
Result.new(issues)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def issues
|
|
38
|
+
scenarios.flat_map { |scenario| step_issues(scenario) }
|
|
39
|
+
rescue CompilationError => e
|
|
40
|
+
[e]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def scenarios
|
|
44
|
+
TagFilter.apply(FeatureLoader.load(@glob), GherkinSystem.config.tags)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def step_issues(scenario)
|
|
48
|
+
scenario.steps.filter_map { |step| step_issue(scenario, step) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def step_issue(scenario, step)
|
|
52
|
+
GherkinSystem.step_registry.resolve(step, scenario)
|
|
53
|
+
nil
|
|
54
|
+
rescue UndefinedStep, AmbiguousStep => e
|
|
55
|
+
e
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Shared argument handling for the Rails commands.
|
|
5
|
+
module Cli
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param args [Array<String>]
|
|
9
|
+
# @param tags [String, nil]
|
|
10
|
+
# @return [Array<String>] remaining arguments
|
|
11
|
+
def apply_filters!(args, tags:)
|
|
12
|
+
feature = args.shift if args.first && !args.first.start_with?("-")
|
|
13
|
+
apply_feature!(feature) if feature
|
|
14
|
+
ENV["GHERKIN_TAGS"] = tags if tags
|
|
15
|
+
args
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def apply_feature!(feature)
|
|
19
|
+
path, line = split_location(feature)
|
|
20
|
+
ENV["GHERKIN_FEATURES"] = path
|
|
21
|
+
ENV["GHERKIN_LINE"] = line if line
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def split_location(feature)
|
|
25
|
+
match = feature.match(/\A(.+):(\d+)\z/)
|
|
26
|
+
return [match[1], match[2]] if match
|
|
27
|
+
|
|
28
|
+
[feature, nil]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String]
|
|
32
|
+
def test_file
|
|
33
|
+
root = defined?(Rails) ? Rails.root : Dir.pwd
|
|
34
|
+
File.join(root.to_s, GherkinSystem.config.test_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [void]
|
|
38
|
+
def prepare_test_env!
|
|
39
|
+
ENV["RAILS_ENV"] = "test"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Global settings for feature discovery and generated tests.
|
|
5
|
+
class Configuration
|
|
6
|
+
# @return [Array<Module>] modules included into generated test classes
|
|
7
|
+
attr_reader :step_modules
|
|
8
|
+
|
|
9
|
+
# @return [Class, nil] superclass for generated tests
|
|
10
|
+
# @return [Boolean] when true, undefined steps fail before the driver starts
|
|
11
|
+
# @return [String] Rails test file that calls {.load!}
|
|
12
|
+
attr_accessor :base_test_class, :strict, :test_file
|
|
13
|
+
|
|
14
|
+
attr_writer :features
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@step_modules = []
|
|
18
|
+
@tags_set = false
|
|
19
|
+
@strict = true
|
|
20
|
+
@test_file = "test/system/gherkin_test.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] glob of feature files
|
|
24
|
+
def features
|
|
25
|
+
@features ||= default_features
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Tag expression used when compiling. Unset values follow ENV["GHERKIN_TAGS"].
|
|
29
|
+
#
|
|
30
|
+
# @return [String, nil]
|
|
31
|
+
def tags
|
|
32
|
+
return @tags if @tags_set
|
|
33
|
+
|
|
34
|
+
ENV.fetch("GHERKIN_TAGS", nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param value [String, nil]
|
|
38
|
+
# @return [void]
|
|
39
|
+
def tags=(value)
|
|
40
|
+
@tags_set = true
|
|
41
|
+
@tags = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Include helper methods from a step module into generated test classes.
|
|
45
|
+
#
|
|
46
|
+
# @param mod [Module]
|
|
47
|
+
# @return [void]
|
|
48
|
+
def include_steps(mod)
|
|
49
|
+
@step_modules << mod
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def default_features
|
|
55
|
+
File.join(root.to_s, "features/system/**/*.feature")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def root
|
|
59
|
+
return Rails.root if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
60
|
+
|
|
61
|
+
Dir.pwd
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Cucumber-compatible data table passed to a step block.
|
|
5
|
+
class DataTable
|
|
6
|
+
# @param rows [Array<Array<String>>]
|
|
7
|
+
def initialize(rows)
|
|
8
|
+
@rows = rows.map { |row| row.map(&:to_s) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [Array<Array<String>>] every row, including the header
|
|
12
|
+
def raw
|
|
13
|
+
@rows
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Array<Hash{String => String}>] body rows keyed by the header
|
|
17
|
+
def hashes
|
|
18
|
+
header, *body = @rows
|
|
19
|
+
return [] if header.nil?
|
|
20
|
+
|
|
21
|
+
body.map { |row| header.zip(row).to_h }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Base error for gherkin_system failures.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when load configuration is missing or inconsistent.
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a feature, tag expression, or hook cannot be compiled.
|
|
11
|
+
class CompilationError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when the same step pattern is registered twice.
|
|
14
|
+
class DuplicateStep < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when no step definition matches.
|
|
17
|
+
class UndefinedStep < Error
|
|
18
|
+
# @param scenario [Scenario]
|
|
19
|
+
# @param step [Step]
|
|
20
|
+
def initialize(scenario, step)
|
|
21
|
+
super(self.class.format(scenario, step))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.format(scenario, step)
|
|
25
|
+
[
|
|
26
|
+
"Undefined step:",
|
|
27
|
+
"",
|
|
28
|
+
" #{step.keyword} #{step.text}",
|
|
29
|
+
"",
|
|
30
|
+
"#{scenario.uri}:#{step.line}",
|
|
31
|
+
"",
|
|
32
|
+
"Suggested definition:",
|
|
33
|
+
"",
|
|
34
|
+
Snippet.for(step)
|
|
35
|
+
].join("\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raised when more than one step definition matches.
|
|
40
|
+
class AmbiguousStep < Error
|
|
41
|
+
# @param scenario [Scenario]
|
|
42
|
+
# @param step [Step]
|
|
43
|
+
# @param definitions [Array<StepDefinition>]
|
|
44
|
+
def initialize(scenario, step, definitions)
|
|
45
|
+
super(self.class.format(scenario, step, definitions))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.format(scenario, step, definitions)
|
|
49
|
+
locations = definitions.map { |definition| " #{definition.location_label}" }
|
|
50
|
+
[
|
|
51
|
+
"Ambiguous step:",
|
|
52
|
+
"",
|
|
53
|
+
" #{step.keyword} #{step.text}",
|
|
54
|
+
"",
|
|
55
|
+
"#{scenario.uri}:#{step.line}",
|
|
56
|
+
"",
|
|
57
|
+
"Matching definitions:",
|
|
58
|
+
*locations
|
|
59
|
+
].join("\n")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Wraps a step exception with the feature and definition locations.
|
|
64
|
+
class StepFailure < Error
|
|
65
|
+
# @return [Exception] the original failure
|
|
66
|
+
attr_reader :cause
|
|
67
|
+
|
|
68
|
+
# @param scenario [Scenario]
|
|
69
|
+
# @param step [Step]
|
|
70
|
+
# @param definition [StepDefinition]
|
|
71
|
+
# @param cause [Exception]
|
|
72
|
+
def initialize(scenario, step, definition, cause)
|
|
73
|
+
super(self.class.format(scenario, step, definition, cause))
|
|
74
|
+
@cause = cause
|
|
75
|
+
set_backtrace(cause.backtrace)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.format(scenario, step, definition, cause)
|
|
79
|
+
[
|
|
80
|
+
scenario.feature_name,
|
|
81
|
+
" #{scenario.name}",
|
|
82
|
+
"",
|
|
83
|
+
"FAILED",
|
|
84
|
+
"",
|
|
85
|
+
"#{step.keyword} #{step.text}",
|
|
86
|
+
"#{scenario.uri}:#{step.line}",
|
|
87
|
+
"",
|
|
88
|
+
cause.message.to_s,
|
|
89
|
+
"",
|
|
90
|
+
"Step definition:",
|
|
91
|
+
definition.location_label
|
|
92
|
+
].join("\n")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Runs one compiled scenario against a test instance.
|
|
5
|
+
class Executor
|
|
6
|
+
# @param test [Minitest::Test]
|
|
7
|
+
# @return [void]
|
|
8
|
+
def self.validate!(test)
|
|
9
|
+
new(test).validate!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param test [Minitest::Test]
|
|
13
|
+
# @return [void]
|
|
14
|
+
def self.call(test)
|
|
15
|
+
new(test).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param test [Minitest::Test]
|
|
19
|
+
def initialize(test)
|
|
20
|
+
@test = test
|
|
21
|
+
@scenario = test.class.gherkin_scenarios.fetch(test.name.to_s)
|
|
22
|
+
@run = ScenarioRun.new(@scenario)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [void]
|
|
26
|
+
def validate!
|
|
27
|
+
@scenario.steps.each { |step| registry.resolve(step, @scenario) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [void]
|
|
31
|
+
def call
|
|
32
|
+
invoke_around(matching(:around)) { run_body }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def registry
|
|
38
|
+
GherkinSystem.step_registry
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def matching(kind)
|
|
42
|
+
GherkinSystem.hook_registry.matching(kind, @scenario.tags)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def invoke_around(hooks, &body)
|
|
46
|
+
hook = hooks.first
|
|
47
|
+
return body.call unless hook
|
|
48
|
+
|
|
49
|
+
@run.continue_with { invoke_around(hooks.drop(1), &body) }
|
|
50
|
+
@test.instance_exec(@run, &hook.block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def run_body
|
|
54
|
+
run_before
|
|
55
|
+
run_steps
|
|
56
|
+
@run.status = :passed
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
@run.status = :failed
|
|
59
|
+
@run.exception = e
|
|
60
|
+
raise
|
|
61
|
+
ensure
|
|
62
|
+
run_after
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run_before
|
|
66
|
+
matching(:before).each { |hook| @test.instance_exec(@run, &hook.block) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run_steps
|
|
70
|
+
@scenario.steps.each { |step| run_step(step) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def run_step(step)
|
|
74
|
+
definition = registry.resolve(step, @scenario)
|
|
75
|
+
@test.instance_exec(*arguments(definition, step), &definition.block)
|
|
76
|
+
rescue UndefinedStep, AmbiguousStep
|
|
77
|
+
raise
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
raise StepFailure.new(@scenario, step, definition, e)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def arguments(definition, step)
|
|
83
|
+
values = definition.captures(step.text, @test)
|
|
84
|
+
values << step.table if step.table
|
|
85
|
+
values << step.doc_string if step.doc_string
|
|
86
|
+
values
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_after
|
|
90
|
+
matching(:after).reverse_each { |hook| @test.instance_exec(@run, &hook.block) }
|
|
91
|
+
rescue StandardError
|
|
92
|
+
raise unless @run.exception
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Discovers feature files and applies path and line filters from the environment.
|
|
5
|
+
class FeatureLoader
|
|
6
|
+
# @param glob [String, Pathname, nil]
|
|
7
|
+
# @return [Array<Scenario>]
|
|
8
|
+
def self.load(glob = nil)
|
|
9
|
+
new(glob || GherkinSystem.config.features).scenarios
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param glob [String, Pathname]
|
|
13
|
+
def initialize(glob)
|
|
14
|
+
@glob = glob.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Array<Scenario>]
|
|
18
|
+
def scenarios
|
|
19
|
+
selected = files.flat_map { |path| Parser.parse_file(path) }
|
|
20
|
+
filter_line(filter_path(selected))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def files
|
|
26
|
+
requested = ENV.fetch("GHERKIN_FEATURES", nil)
|
|
27
|
+
return [requested] if requested && File.file?(requested)
|
|
28
|
+
|
|
29
|
+
paths = Dir.glob(@glob)
|
|
30
|
+
return paths if requested.nil? || requested.empty?
|
|
31
|
+
|
|
32
|
+
expanded = File.expand_path(requested)
|
|
33
|
+
paths.select { |path| File.expand_path(path) == expanded }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def filter_path(scenarios)
|
|
37
|
+
requested = ENV.fetch("GHERKIN_FEATURES", nil)
|
|
38
|
+
return scenarios if requested.nil? || requested.empty?
|
|
39
|
+
|
|
40
|
+
expanded = File.expand_path(requested)
|
|
41
|
+
scenarios.select { |scenario| File.expand_path(scenario.uri) == expanded }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def filter_line(scenarios)
|
|
45
|
+
line = ENV.fetch("GHERKIN_LINE", nil)
|
|
46
|
+
return scenarios if line.nil? || line.empty?
|
|
47
|
+
|
|
48
|
+
number = Integer(line)
|
|
49
|
+
scenarios.select { |scenario| scenario.covers_line?(number) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|