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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cucumber/tag_expressions"
|
|
4
|
+
|
|
5
|
+
module GherkinSystem
|
|
6
|
+
# One Before, After, or Around hook.
|
|
7
|
+
class Hook
|
|
8
|
+
# @return [Proc]
|
|
9
|
+
attr_reader :block
|
|
10
|
+
|
|
11
|
+
# @param expression [Object, nil] parsed tag expression, or nil to match every scenario
|
|
12
|
+
# @param block [Proc]
|
|
13
|
+
def initialize(expression, block)
|
|
14
|
+
@expression = expression
|
|
15
|
+
@block = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param tags [Array<String>]
|
|
19
|
+
# @return [Boolean]
|
|
20
|
+
def applies_to?(tags)
|
|
21
|
+
@expression.nil? || @expression.evaluate(tags)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Hooks registered by {Hooks}.
|
|
26
|
+
class HookRegistry
|
|
27
|
+
def initialize
|
|
28
|
+
@hooks = { before: [], after: [], around: [] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param kind [Symbol] :before, :after, or :around
|
|
32
|
+
# @param tag_expression [String, nil]
|
|
33
|
+
# @param block [Proc]
|
|
34
|
+
# @return [Hook]
|
|
35
|
+
def add(kind, tag_expression, block)
|
|
36
|
+
hook = Hook.new(parse(tag_expression), block)
|
|
37
|
+
@hooks.fetch(kind) << hook
|
|
38
|
+
hook
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param kind [Symbol]
|
|
42
|
+
# @param tags [Array<String>]
|
|
43
|
+
# @return [Array<Hook>]
|
|
44
|
+
def matching(kind, tags)
|
|
45
|
+
@hooks.fetch(kind).select { |hook| hook.applies_to?(tags) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def parse(expression)
|
|
51
|
+
return nil if expression.nil? || expression.to_s.strip.empty?
|
|
52
|
+
|
|
53
|
+
Cucumber::TagExpressions::Parser.new.parse(expression)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise CompilationError, "Malformed hook tag expression #{expression.inspect}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# DSL extended by hook modules.
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# module CheckoutHooks
|
|
63
|
+
# extend GherkinSystem::Hooks
|
|
64
|
+
#
|
|
65
|
+
# Before("@admin") { visit admin_root_path }
|
|
66
|
+
# end
|
|
67
|
+
module Hooks
|
|
68
|
+
# @param tag_expression [String, nil]
|
|
69
|
+
# @return [Hook]
|
|
70
|
+
def Before(tag_expression = nil, &block)
|
|
71
|
+
GherkinSystem.hook_registry.add(:before, tag_expression, block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param tag_expression [String, nil]
|
|
75
|
+
# @return [Hook]
|
|
76
|
+
def After(tag_expression = nil, &block)
|
|
77
|
+
GherkinSystem.hook_registry.add(:after, tag_expression, block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param tag_expression [String, nil]
|
|
81
|
+
# @return [Hook]
|
|
82
|
+
def Around(tag_expression = nil, &block)
|
|
83
|
+
GherkinSystem.hook_registry.add(:around, tag_expression, block)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cucumber/cucumber_expressions/parameter_type"
|
|
4
|
+
require "cucumber/cucumber_expressions/parameter_type_registry"
|
|
5
|
+
|
|
6
|
+
module GherkinSystem
|
|
7
|
+
# Custom Cucumber Expressions parameter types.
|
|
8
|
+
class ParameterRegistry
|
|
9
|
+
# @return [Cucumber::CucumberExpressions::ParameterTypeRegistry]
|
|
10
|
+
attr_reader :registry
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@registry = Cucumber::CucumberExpressions::ParameterTypeRegistry.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param name [String]
|
|
17
|
+
# @param regexp [Regexp, String]
|
|
18
|
+
# @param transformer [Proc]
|
|
19
|
+
# @param type [Class]
|
|
20
|
+
# @param prefer_for_regexp_match [Boolean]
|
|
21
|
+
# @return [void]
|
|
22
|
+
def define(name:, regexp:, transformer:, type: String, prefer_for_regexp_match: false)
|
|
23
|
+
parameter = Cucumber::CucumberExpressions::ParameterType.new(
|
|
24
|
+
name,
|
|
25
|
+
regexp,
|
|
26
|
+
type || String,
|
|
27
|
+
transformer,
|
|
28
|
+
true,
|
|
29
|
+
prefer_for_regexp_match
|
|
30
|
+
)
|
|
31
|
+
registry.define_parameter_type(parameter)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gherkin/pickles/compiler"
|
|
4
|
+
require "gherkin/parser"
|
|
5
|
+
|
|
6
|
+
module GherkinSystem
|
|
7
|
+
# Turns a feature file into {Scenario} objects. Cucumber messages do not leave this class.
|
|
8
|
+
class Parser
|
|
9
|
+
# @param path [String]
|
|
10
|
+
# @return [Array<Scenario>]
|
|
11
|
+
def self.parse_file(path)
|
|
12
|
+
parse(File.read(path), path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param source_text [String]
|
|
16
|
+
# @param uri [String]
|
|
17
|
+
# @return [Array<Scenario>]
|
|
18
|
+
def self.parse(source_text, uri)
|
|
19
|
+
document = Gherkin::Parser.new.parse(source_text)
|
|
20
|
+
new(document, uri).scenarios
|
|
21
|
+
rescue Gherkin::ParserError => e
|
|
22
|
+
raise CompilationError, "#{uri}: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(document, uri)
|
|
26
|
+
@document = document
|
|
27
|
+
@uri = uri.to_s
|
|
28
|
+
@step_lines = {}
|
|
29
|
+
@step_keywords = {}
|
|
30
|
+
@example_rows = {}
|
|
31
|
+
@scenario_lines = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Array<Scenario>]
|
|
35
|
+
def scenarios
|
|
36
|
+
return [] unless feature
|
|
37
|
+
|
|
38
|
+
index_children(feature.children)
|
|
39
|
+
pickles.map { |pickle| build_scenario(pickle) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def feature
|
|
45
|
+
@document.feature
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def pickles
|
|
49
|
+
source = Struct.new(:uri).new(@uri)
|
|
50
|
+
generator = Cucumber::Messages::Helpers::IdGenerator::Incrementing.new
|
|
51
|
+
Gherkin::Pickles::Compiler.new(generator).compile(@document, source)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def index_children(children)
|
|
55
|
+
children.each do |child|
|
|
56
|
+
index_children(child.rule.children) if child.rule
|
|
57
|
+
index_steps(child.background.steps) if child.background
|
|
58
|
+
index_scenario(child.scenario) if child.scenario
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def index_scenario(scenario)
|
|
63
|
+
@scenario_lines[scenario.id] = scenario.location.line
|
|
64
|
+
index_steps(scenario.steps)
|
|
65
|
+
scenario.examples.each do |examples|
|
|
66
|
+
Array(examples.table_body).each do |row|
|
|
67
|
+
@example_rows[row.id] = row.cells.map(&:value)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def index_steps(steps)
|
|
73
|
+
steps.each do |step|
|
|
74
|
+
@step_lines[step.id] = step.location.line
|
|
75
|
+
@step_keywords[step.id] = step.keyword.to_s.strip
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_scenario(pickle)
|
|
80
|
+
Scenario.new(
|
|
81
|
+
uri: @uri,
|
|
82
|
+
feature_name: feature.name,
|
|
83
|
+
name: pickle.name,
|
|
84
|
+
tags: pickle.tags.map(&:name),
|
|
85
|
+
example_values: example_values(pickle),
|
|
86
|
+
line: pickle.location.line,
|
|
87
|
+
scenario_line: scenario_line(pickle),
|
|
88
|
+
steps: pickle.steps.map { |step| build_step(step) }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def example_values(pickle)
|
|
93
|
+
pickle.ast_node_ids.filter_map { |id| @example_rows[id] }.first || []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def scenario_line(pickle)
|
|
97
|
+
pickle.ast_node_ids.filter_map { |id| @scenario_lines[id] }.first || pickle.location.line
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_step(pickle_step)
|
|
101
|
+
step_id = pickle_step.ast_node_ids.first
|
|
102
|
+
Step.new(
|
|
103
|
+
text: pickle_step.text,
|
|
104
|
+
keyword: @step_keywords[step_id] || "Given",
|
|
105
|
+
line: @step_lines[step_id],
|
|
106
|
+
table: table_from(pickle_step),
|
|
107
|
+
doc_string: doc_from(pickle_step)
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def table_from(pickle_step)
|
|
112
|
+
table = pickle_step.argument&.data_table
|
|
113
|
+
return nil unless table
|
|
114
|
+
|
|
115
|
+
DataTable.new(table.rows.map { |row| row.cells.map(&:value) })
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def doc_from(pickle_step)
|
|
119
|
+
pickle_step.argument&.doc_string&.content
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# One executable Pickle.
|
|
5
|
+
Scenario = Data.define(
|
|
6
|
+
:uri,
|
|
7
|
+
:feature_name,
|
|
8
|
+
:name,
|
|
9
|
+
:tags,
|
|
10
|
+
:example_values,
|
|
11
|
+
:line,
|
|
12
|
+
:scenario_line,
|
|
13
|
+
:steps
|
|
14
|
+
) do
|
|
15
|
+
# @return [String] Minitest-facing scenario label
|
|
16
|
+
def test_name
|
|
17
|
+
label = "#{feature_name}: #{name}"
|
|
18
|
+
return label if example_values.empty?
|
|
19
|
+
|
|
20
|
+
"#{label} [#{example_values.join(", ")}]"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param number [Integer]
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def covers_line?(number)
|
|
26
|
+
lines.include?(number)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<Integer>]
|
|
30
|
+
def lines
|
|
31
|
+
[line, scenario_line, *steps.map(&:line)].compact.uniq
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# One step inside a {Scenario}.
|
|
36
|
+
Step = Data.define(:text, :keyword, :line, :table, :doc_string)
|
|
37
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Object yielded to hooks. Around hooks continue the scenario with {#run}.
|
|
5
|
+
class ScenarioRun
|
|
6
|
+
# @return [String]
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
# @return [String]
|
|
10
|
+
attr_reader :feature_name
|
|
11
|
+
|
|
12
|
+
# @return [Array<String>]
|
|
13
|
+
attr_reader :tags
|
|
14
|
+
|
|
15
|
+
# @return [Exception, nil]
|
|
16
|
+
attr_accessor :exception
|
|
17
|
+
|
|
18
|
+
# @return [Symbol]
|
|
19
|
+
attr_accessor :status
|
|
20
|
+
|
|
21
|
+
# @param scenario [Scenario]
|
|
22
|
+
def initialize(scenario)
|
|
23
|
+
@name = scenario.name
|
|
24
|
+
@feature_name = scenario.feature_name
|
|
25
|
+
@tags = scenario.tags
|
|
26
|
+
@status = :pending
|
|
27
|
+
@exception = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Continues the hook chain or the scenario body.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def run
|
|
34
|
+
@continuation.call
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [void]
|
|
38
|
+
def continue_with(&block)
|
|
39
|
+
@continuation = block
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Builds a step-definition snippet for an undefined step.
|
|
5
|
+
module Snippet
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param step [Step]
|
|
9
|
+
# @return [String]
|
|
10
|
+
def for(step)
|
|
11
|
+
pattern, names = pattern_for(step.text)
|
|
12
|
+
args = names.empty? ? "" : " |#{names.join(", ")}|"
|
|
13
|
+
"#{step.keyword}(\"#{pattern}\") do#{args}\n pending\nend"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pattern_for(text)
|
|
17
|
+
names = []
|
|
18
|
+
pattern = text.gsub(/"[^"]*"|'[^']*'/) { names << "string" and "{string}" }
|
|
19
|
+
pattern = pattern.gsub(/\b\d+\b/) { names << "int" and "{int}" }
|
|
20
|
+
[pattern, names]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cucumber/cucumber_expressions/cucumber_expression"
|
|
4
|
+
require "cucumber/cucumber_expressions/regular_expression"
|
|
5
|
+
|
|
6
|
+
module GherkinSystem
|
|
7
|
+
# One registered Given/When/Then block.
|
|
8
|
+
class StepDefinition
|
|
9
|
+
# @return [String, Regexp]
|
|
10
|
+
attr_reader :pattern
|
|
11
|
+
|
|
12
|
+
# @return [Proc]
|
|
13
|
+
attr_reader :block
|
|
14
|
+
|
|
15
|
+
# @return [Array(String, Integer)]
|
|
16
|
+
attr_reader :location
|
|
17
|
+
|
|
18
|
+
# @param pattern [String, Regexp]
|
|
19
|
+
# @param block [Proc]
|
|
20
|
+
# @param expression [Object]
|
|
21
|
+
# @param location [Array(String, Integer)]
|
|
22
|
+
def initialize(pattern, block, expression, location)
|
|
23
|
+
@pattern = pattern
|
|
24
|
+
@block = block
|
|
25
|
+
@expression = expression
|
|
26
|
+
@location = location
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param text [String]
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def matches?(text)
|
|
32
|
+
!match(text).nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param text [String]
|
|
36
|
+
# @param test [Object]
|
|
37
|
+
# @return [Array]
|
|
38
|
+
def captures(text, test)
|
|
39
|
+
match(text).map { |argument| argument.value(test) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String]
|
|
43
|
+
def location_label
|
|
44
|
+
"#{location[0]}:#{location[1]}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def match(text)
|
|
50
|
+
@expression.match(text)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# Step patterns registered by {Steps}.
|
|
5
|
+
class StepRegistry
|
|
6
|
+
def initialize(parameter_registry)
|
|
7
|
+
@parameter_registry = parameter_registry
|
|
8
|
+
@definitions = []
|
|
9
|
+
@keys = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param pattern [String, Regexp]
|
|
13
|
+
# @param block [Proc]
|
|
14
|
+
# @return [StepDefinition]
|
|
15
|
+
def register(pattern, &block)
|
|
16
|
+
key = pattern.is_a?(Regexp) ? pattern.inspect : pattern.to_s
|
|
17
|
+
raise DuplicateStep, "Duplicate step definition: #{key}" if @keys.key?(key)
|
|
18
|
+
|
|
19
|
+
definition = StepDefinition.new(pattern, block, expression_for(pattern), block.source_location)
|
|
20
|
+
@keys[key] = definition
|
|
21
|
+
@definitions << definition
|
|
22
|
+
definition
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param step [Step]
|
|
26
|
+
# @param scenario [Scenario]
|
|
27
|
+
# @return [StepDefinition]
|
|
28
|
+
def resolve(step, scenario)
|
|
29
|
+
matches = @definitions.select { |definition| definition.matches?(step.text) }
|
|
30
|
+
return matches.first if matches.one?
|
|
31
|
+
raise UndefinedStep.new(scenario, step) if matches.empty?
|
|
32
|
+
|
|
33
|
+
raise AmbiguousStep.new(scenario, step, matches)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def expression_for(pattern)
|
|
39
|
+
registry = @parameter_registry.registry
|
|
40
|
+
if pattern.is_a?(Regexp)
|
|
41
|
+
Cucumber::CucumberExpressions::RegularExpression.new(pattern, registry)
|
|
42
|
+
else
|
|
43
|
+
Cucumber::CucumberExpressions::CucumberExpression.new(pattern.to_s, registry)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GherkinSystem
|
|
4
|
+
# DSL extended by step-definition modules.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# module AuthenticationSteps
|
|
8
|
+
# extend GherkinSystem::Steps
|
|
9
|
+
#
|
|
10
|
+
# Given("I am signed in") { sign_in users(:customer) }
|
|
11
|
+
# end
|
|
12
|
+
module Steps
|
|
13
|
+
# @param pattern [String, Regexp]
|
|
14
|
+
# @return [StepDefinition]
|
|
15
|
+
def Given(pattern, &)
|
|
16
|
+
GherkinSystem.step_registry.register(pattern, &)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
alias When Given
|
|
20
|
+
alias Then Given
|
|
21
|
+
alias And Given
|
|
22
|
+
alias But Given
|
|
23
|
+
|
|
24
|
+
# @param name [String]
|
|
25
|
+
# @param regexp [Regexp, String]
|
|
26
|
+
# @param type [Class]
|
|
27
|
+
# @param transformer [Proc]
|
|
28
|
+
# @return [void]
|
|
29
|
+
def ParameterType(name:, regexp:, transformer:, type: String, prefer_for_regexp_match: false)
|
|
30
|
+
GherkinSystem.parameter_registry.define(
|
|
31
|
+
name: name,
|
|
32
|
+
regexp: regexp,
|
|
33
|
+
transformer: transformer,
|
|
34
|
+
type: type,
|
|
35
|
+
prefer_for_regexp_match: prefer_for_regexp_match
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cucumber/tag_expressions"
|
|
4
|
+
|
|
5
|
+
module GherkinSystem
|
|
6
|
+
# Selects scenarios with a Cucumber tag expression.
|
|
7
|
+
module TagFilter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# @param scenarios [Array<Scenario>]
|
|
11
|
+
# @param expression [String, nil]
|
|
12
|
+
# @return [Array<Scenario>]
|
|
13
|
+
def apply(scenarios, expression)
|
|
14
|
+
return scenarios if expression.nil? || expression.strip.empty?
|
|
15
|
+
|
|
16
|
+
parsed = Cucumber::TagExpressions::Parser.new.parse(expression)
|
|
17
|
+
scenarios.select { |scenario| parsed.evaluate(scenario.tags) }
|
|
18
|
+
rescue CompilationError
|
|
19
|
+
raise
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
raise CompilationError, "Invalid tag expression #{expression.inspect}: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module GherkinSystem
|
|
6
|
+
# Namespace for generated test classes.
|
|
7
|
+
module Suite
|
|
8
|
+
# Removes generated classes. Used by tests.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def self.reset!
|
|
12
|
+
constants(false).each { |name| remove_const(name) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Defines one Minitest class per feature file.
|
|
17
|
+
class TestClassBuilder
|
|
18
|
+
# @param base [Class]
|
|
19
|
+
def initialize(base)
|
|
20
|
+
@base = base
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param scenarios [Array<Scenario>]
|
|
24
|
+
# @return [Array<Class>]
|
|
25
|
+
def build(scenarios)
|
|
26
|
+
scenarios.group_by(&:uri).map { |uri, group| build_class(uri, group) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_class(uri, scenarios)
|
|
32
|
+
klass = Class.new(@base)
|
|
33
|
+
assign_constant(klass, scenarios.first.feature_name, uri)
|
|
34
|
+
include_step_modules(klass)
|
|
35
|
+
record_scenarios(klass, define_tests(klass, scenarios))
|
|
36
|
+
define_preflight(klass)
|
|
37
|
+
klass
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def assign_constant(klass, feature_name, uri)
|
|
41
|
+
name = constant_name(feature_name, uri)
|
|
42
|
+
Suite.send(:remove_const, name) if Suite.const_defined?(name, false)
|
|
43
|
+
Suite.const_set(name, klass)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def constant_name(feature_name, uri)
|
|
47
|
+
stem = feature_name.to_s.scan(/[A-Za-z0-9]+/).map(&:capitalize).join
|
|
48
|
+
stem = "Feature#{stem}" unless stem.match?(/\A[A-Z]/)
|
|
49
|
+
suffix = Digest::SHA1.hexdigest(uri.to_s)[0, 6]
|
|
50
|
+
"#{stem}#{suffix}Test"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def include_step_modules(klass)
|
|
54
|
+
GherkinSystem.config.step_modules.each { |mod| klass.include(mod) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def define_tests(klass, scenarios)
|
|
58
|
+
scenarios.each_with_index.to_h do |scenario, index|
|
|
59
|
+
method_name = method_name_for(scenario, index)
|
|
60
|
+
klass.define_method(method_name) { GherkinSystem::Executor.call(self) }
|
|
61
|
+
[method_name, scenario]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_name_for(scenario, index)
|
|
66
|
+
slug = scenario.test_name.gsub(/[^A-Za-z0-9]+/, "_").gsub(/\A_|_\z/, "")
|
|
67
|
+
slug = "scenario" if slug.empty?
|
|
68
|
+
"test_#{slug}_#{index}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def record_scenarios(klass, scenarios_by_method)
|
|
72
|
+
klass.define_singleton_method(:gherkin_scenarios) { scenarios_by_method }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def define_preflight(klass)
|
|
76
|
+
klass.define_method(:before_setup) do
|
|
77
|
+
GherkinSystem::Executor.validate!(self) if GherkinSystem.config.strict
|
|
78
|
+
super() if defined?(super)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gherkin_system/version"
|
|
4
|
+
require_relative "gherkin_system/errors"
|
|
5
|
+
require_relative "gherkin_system/configuration"
|
|
6
|
+
require_relative "gherkin_system/data_table"
|
|
7
|
+
require_relative "gherkin_system/scenario"
|
|
8
|
+
require_relative "gherkin_system/snippet"
|
|
9
|
+
require_relative "gherkin_system/parameter_registry"
|
|
10
|
+
require_relative "gherkin_system/step_definition"
|
|
11
|
+
require_relative "gherkin_system/step_registry"
|
|
12
|
+
require_relative "gherkin_system/steps"
|
|
13
|
+
require_relative "gherkin_system/hooks"
|
|
14
|
+
require_relative "gherkin_system/tag_filter"
|
|
15
|
+
require_relative "gherkin_system/parser"
|
|
16
|
+
require_relative "gherkin_system/feature_loader"
|
|
17
|
+
require_relative "gherkin_system/test_class_builder"
|
|
18
|
+
require_relative "gherkin_system/scenario_run"
|
|
19
|
+
require_relative "gherkin_system/executor"
|
|
20
|
+
require_relative "gherkin_system/checker"
|
|
21
|
+
require_relative "gherkin_system/cli"
|
|
22
|
+
|
|
23
|
+
# Compiles Gherkin features into Minitest methods on the application's system test class.
|
|
24
|
+
#
|
|
25
|
+
# @example Load features
|
|
26
|
+
# GherkinSystem.load!(base: ApplicationSystemTestCase)
|
|
27
|
+
module GherkinSystem
|
|
28
|
+
class << self
|
|
29
|
+
# @return [Configuration]
|
|
30
|
+
def config
|
|
31
|
+
@config ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @yieldparam config [Configuration]
|
|
35
|
+
# @return [void]
|
|
36
|
+
def configure
|
|
37
|
+
yield(config)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Clears configuration, step definitions, hooks, and generated test classes.
|
|
41
|
+
#
|
|
42
|
+
# @return [void]
|
|
43
|
+
def reset_configuration!
|
|
44
|
+
@config = nil
|
|
45
|
+
@step_registry = nil
|
|
46
|
+
@parameter_registry = nil
|
|
47
|
+
@hook_registry = nil
|
|
48
|
+
Suite.reset!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param glob [String, Pathname, nil]
|
|
52
|
+
# @param base [Class, nil] system test superclass. Required when config.base_test_class is unset.
|
|
53
|
+
# @return [Array<Class>] generated test classes
|
|
54
|
+
def load!(glob = nil, base: nil)
|
|
55
|
+
test_class = base || config.base_test_class
|
|
56
|
+
raise ConfigurationError, "Pass base: or set config.base_test_class" if test_class.nil?
|
|
57
|
+
|
|
58
|
+
scenarios = TagFilter.apply(FeatureLoader.load(glob), config.tags)
|
|
59
|
+
TestClassBuilder.new(test_class).build(scenarios)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [StepRegistry]
|
|
63
|
+
def step_registry
|
|
64
|
+
@step_registry ||= StepRegistry.new(parameter_registry)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [ParameterRegistry]
|
|
68
|
+
def parameter_registry
|
|
69
|
+
@parameter_registry ||= ParameterRegistry.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [HookRegistry]
|
|
73
|
+
def hook_registry
|
|
74
|
+
@hook_registry ||= HookRegistry.new
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
require_relative "gherkin_system/railtie" if defined?(Rails::Railtie)
|