cucumber-cucumber-expressions 8.3.1

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE.md +5 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
  4. data/.rspec +1 -0
  5. data/.rsync +4 -0
  6. data/.subrepo +1 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +21 -0
  9. data/Makefile +1 -0
  10. data/README.md +5 -0
  11. data/Rakefile +27 -0
  12. data/cucumber-cucumber-expressions.gemspec +33 -0
  13. data/default.mk +70 -0
  14. data/examples.txt +31 -0
  15. data/lib/cucumber/cucumber_expressions/argument.rb +37 -0
  16. data/lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rb +49 -0
  17. data/lib/cucumber/cucumber_expressions/cucumber_expression.rb +119 -0
  18. data/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +105 -0
  19. data/lib/cucumber/cucumber_expressions/errors.rb +40 -0
  20. data/lib/cucumber/cucumber_expressions/generated_expression.rb +31 -0
  21. data/lib/cucumber/cucumber_expressions/group.rb +18 -0
  22. data/lib/cucumber/cucumber_expressions/group_builder.rb +42 -0
  23. data/lib/cucumber/cucumber_expressions/parameter_type.rb +81 -0
  24. data/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb +59 -0
  25. data/lib/cucumber/cucumber_expressions/parameter_type_registry.rb +70 -0
  26. data/lib/cucumber/cucumber_expressions/regular_expression.rb +48 -0
  27. data/lib/cucumber/cucumber_expressions/tree_regexp.rb +76 -0
  28. data/scripts/update-gemspec +32 -0
  29. data/spec/capture_warnings.rb +74 -0
  30. data/spec/coverage.rb +7 -0
  31. data/spec/cucumber/cucumber_expressions/argument_spec.rb +17 -0
  32. data/spec/cucumber/cucumber_expressions/combinatorial_generated_expression_factory_test.rb +43 -0
  33. data/spec/cucumber/cucumber_expressions/cucumber_expression_generator_spec.rb +231 -0
  34. data/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb +57 -0
  35. data/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +212 -0
  36. data/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +202 -0
  37. data/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +30 -0
  38. data/spec/cucumber/cucumber_expressions/parameter_type_registry_spec.rb +86 -0
  39. data/spec/cucumber/cucumber_expressions/parameter_type_spec.rb +15 -0
  40. data/spec/cucumber/cucumber_expressions/regular_expression_spec.rb +80 -0
  41. data/spec/cucumber/cucumber_expressions/tree_regexp_spec.rb +133 -0
  42. metadata +162 -0
@@ -0,0 +1,48 @@
1
+ require 'cucumber/cucumber_expressions/argument'
2
+ require 'cucumber/cucumber_expressions/parameter_type'
3
+ require 'cucumber/cucumber_expressions/tree_regexp'
4
+
5
+ module Cucumber
6
+ module CucumberExpressions
7
+ class RegularExpression
8
+
9
+ def initialize(expression_regexp, parameter_type_registry)
10
+ @expression_regexp = expression_regexp
11
+ @parameter_type_registry = parameter_type_registry
12
+ @tree_regexp = TreeRegexp.new(@expression_regexp)
13
+ end
14
+
15
+ def match(text)
16
+ parameter_types = @tree_regexp.group_builder.children.map do |group_builder|
17
+ parameter_type_regexp = group_builder.source
18
+ @parameter_type_registry.lookup_by_regexp(
19
+ parameter_type_regexp,
20
+ @expression_regexp,
21
+ text
22
+ ) || ParameterType.new(
23
+ nil,
24
+ parameter_type_regexp,
25
+ String,
26
+ lambda {|*s| s[0]},
27
+ false,
28
+ false
29
+ )
30
+ end
31
+
32
+ Argument.build(@tree_regexp, text, parameter_types)
33
+ end
34
+
35
+ def regexp
36
+ @expression_regexp
37
+ end
38
+
39
+ def source
40
+ @expression_regexp.source
41
+ end
42
+
43
+ def to_s
44
+ regexp.inspect
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,76 @@
1
+ require 'cucumber/cucumber_expressions/group_builder'
2
+ require 'cucumber/cucumber_expressions/errors'
3
+
4
+ module Cucumber
5
+ module CucumberExpressions
6
+ class TreeRegexp
7
+ attr_reader :regexp, :group_builder
8
+
9
+ def initialize(regexp)
10
+ @regexp = regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
11
+ @stack = [GroupBuilder.new]
12
+ group_start_stack = []
13
+ last = nil
14
+ escaping = false
15
+ @non_capturing_maybe = false
16
+ @name_capturing_maybe = false
17
+ char_class = false
18
+
19
+ @regexp.source.each_char.with_index do |c, n|
20
+ if c == '[' && !escaping
21
+ char_class = true
22
+ elsif c == ']' && !escaping
23
+ char_class = false
24
+ elsif c == '(' && !escaping && !char_class
25
+ @stack.push(GroupBuilder.new)
26
+ group_start_stack.push(n + 1)
27
+ @non_capturing_maybe = false
28
+ elsif c == ')' && !escaping && !char_class
29
+ gb = @stack.pop
30
+ group_start = group_start_stack.pop
31
+ if gb.capturing?
32
+ gb.source = @regexp.source[group_start...n]
33
+ @stack.last.add(gb)
34
+ else
35
+ gb.move_children_to(@stack.last)
36
+ end
37
+ end_group
38
+ elsif c == '?' && last == '('
39
+ @non_capturing_maybe = true
40
+ elsif (c == '<') && @non_capturing_maybe
41
+ @name_capturing_maybe = true
42
+ elsif (c == ':' || c == '!' || c == '=') && last == '?' && @non_capturing_maybe
43
+ end_non_capturing_group
44
+ elsif (c == '=' || c == '!') && last == '<' && @name_capturing_maybe
45
+ end_non_capturing_group
46
+ elsif @name_capturing_maybe
47
+ raise CucumberExpressionError.new("Named capture groups are not supported. See https://github.com/cucumber/cucumber/issues/329")
48
+ end
49
+
50
+ escaping = c == '\\' && !escaping
51
+ last = c
52
+ end
53
+ @group_builder = @stack.pop
54
+ end
55
+
56
+ def match(s)
57
+ match = @regexp.match(s)
58
+ return nil if match.nil?
59
+ group_indices = (0..match.length).to_a.to_enum
60
+ @group_builder.build(match, group_indices)
61
+ end
62
+
63
+ private
64
+
65
+ def end_non_capturing_group
66
+ @stack.last.set_non_capturing!
67
+ end_group
68
+ end
69
+
70
+ def end_group
71
+ @non_capturing_maybe = false
72
+ @name_capturing_maybe = false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Updates the *.gemspec in the current directory to use the latest releases of gems
4
+ #
5
+ set -uf -o pipefail
6
+ IFS=$'\n'
7
+
8
+ gemspec=$(find . -type f -maxdepth 1 -name "*.gemspec")
9
+ if [ "${gemspec}" = "" ]; then
10
+ exit 0
11
+ fi
12
+ add_dependency_lines=$(cat ${gemspec} | grep "s.add_[a-z_]*dependency '[^']*'")
13
+ if [ $? -ne 0 ]; then
14
+ # No add_dependency_lines found - nothing to do
15
+ exit 0
16
+ fi
17
+
18
+ set -e
19
+
20
+ gems=$(echo "${add_dependency_lines}" | tr -s ' ' | cut -d ' ' -f3 | cut -d"'" -f 2)
21
+ while read -r gem; do
22
+ echo "upgrading ${gem}"
23
+ if [ "${gem}" = "bundler" ]; then
24
+ cat "${gemspec}" | sed "s/\(s.add_[a-z_]*dependency\) '${gem}'.*/\1 '${gem}', '>= 1.16.2'/" > ${gemspec}.tmp
25
+ else
26
+ gem_line=$(gem list "${gem}" --remote --all --no-prerelease | grep "^${gem}\s")
27
+ latest_patch_version=$(echo "${gem_line}" | cut -d'(' -f2 | cut -d')' -f1 | cut -d',' -f1 | cut -d' ' -f1)
28
+ latest_minor_version=$(echo "${latest_patch_version}" | cut -d. -f1,2)
29
+ cat "${gemspec}" | sed "s/\(s.add_[a-z_]*dependency\) '${gem}'.*/\1 '${gem}', '~> ${latest_minor_version}', '>= ${latest_patch_version}'/" > ${gemspec}.tmp
30
+ fi
31
+ mv ${gemspec}.tmp ${gemspec}
32
+ done <<< "${gems}"
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ # With thanks to @myronmarston
3
+ # https://github.com/vcr/vcr/blob/master/spec/capture_warnings.rb
4
+
5
+ module CaptureWarnings
6
+ def report_warnings(&block)
7
+ current_dir = Dir.pwd
8
+ warnings, errors = capture_error(&block).partition { |line| line.include?('warning') }
9
+ project_warnings, other_warnings = warnings.uniq.partition { |line| line.include?(current_dir) }
10
+
11
+ if errors.any?
12
+ puts errors.join("\n")
13
+ end
14
+
15
+ if other_warnings.any?
16
+ puts "#{ other_warnings.count } warnings detected, set VIEW_OTHER_WARNINGS=true to see them."
17
+ print_warnings('other', other_warnings) if ENV['VIEW_OTHER_WARNINGS']
18
+ end
19
+
20
+ # Until they fix https://bugs.ruby-lang.org/issues/10661
21
+ if RUBY_VERSION == "2.2.0"
22
+ project_warnings = project_warnings.reject { |w| w =~ /warning: possible reference to past scope/ }
23
+ end
24
+
25
+ if project_warnings.any?
26
+ puts "#{ project_warnings.count } warnings detected"
27
+ print_warnings('cucumber-expressions', project_warnings)
28
+ fail "Please remove all cucumber-expressions warnings."
29
+ end
30
+
31
+ ensure_system_exit_if_required
32
+ end
33
+
34
+ def capture_error(&block)
35
+ old_stderr = STDERR.clone
36
+ pipe_r, pipe_w = IO.pipe
37
+ pipe_r.sync = true
38
+ error = String.new
39
+ reader = Thread.new do
40
+ begin
41
+ loop do
42
+ error << pipe_r.readpartial(1024)
43
+ end
44
+ rescue EOFError
45
+ end
46
+ end
47
+ STDERR.reopen(pipe_w)
48
+ block.call
49
+ ensure
50
+ capture_system_exit
51
+ STDERR.reopen(old_stderr)
52
+ pipe_w.close
53
+ reader.join
54
+ return error.split("\n")
55
+ end
56
+
57
+ def print_warnings(type, warnings)
58
+ puts
59
+ puts "-" * 30 + " #{type} warnings: " + "-" * 30
60
+ puts
61
+ puts warnings.join("\n")
62
+ puts
63
+ puts "-" * 75
64
+ puts
65
+ end
66
+
67
+ def ensure_system_exit_if_required
68
+ raise @system_exit if @system_exit
69
+ end
70
+
71
+ def capture_system_exit
72
+ @system_exit = $!
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'simplecov'
3
+ formatters = [ SimpleCov::Formatter::HTMLFormatter ]
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(*formatters)
6
+ SimpleCov.add_filter 'spec/'
7
+ SimpleCov.start
@@ -0,0 +1,17 @@
1
+ require 'cucumber/cucumber_expressions/argument'
2
+ require 'cucumber/cucumber_expressions/tree_regexp'
3
+ require 'cucumber/cucumber_expressions/parameter_type_registry'
4
+
5
+ module Cucumber
6
+ module CucumberExpressions
7
+ describe Argument do
8
+ it 'exposes parameter_type' do
9
+ tree_regexp = TreeRegexp.new(/three (.*) mice/)
10
+ parameter_type_registry = ParameterTypeRegistry.new
11
+ arguments = Argument.build(tree_regexp, "three blind mice", [parameter_type_registry.lookup_by_type_name("string")])
12
+ argument = arguments[0]
13
+ expect(argument.parameter_type.name).to eq("string")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ require 'cucumber/cucumber_expressions/parameter_type'
2
+ require 'cucumber/cucumber_expressions/combinatorial_generated_expression_factory'
3
+
4
+ module Cucumber
5
+ module CucumberExpressions
6
+
7
+ class Color; end
8
+ class CssColor; end
9
+ class Date; end
10
+ class DateTime; end
11
+ class Timestamp; end
12
+
13
+ describe CombinatorialGeneratedExpressionFactory do
14
+ it 'generates multiple expressions' do
15
+ parameter_type_combinations = [
16
+ [
17
+ ParameterType.new('color', /red|blue|yellow/, Color, lambda {|s| Color.new}, true, false),
18
+ ParameterType.new('csscolor', /red|blue|yellow/, CssColor, lambda {|s| CssColor.new}, true, false)
19
+ ],
20
+ [
21
+ ParameterType.new('date', /\d{4}-\d{2}-\d{2}/, Date, lambda {|s| Date.new}, true, false),
22
+ ParameterType.new('datetime', /\d{4}-\d{2}-\d{2}/, DateTime, lambda {|s| DateTime.new}, true, false),
23
+ ParameterType.new('timestamp', /\d{4}-\d{2}-\d{2}/, Timestamp, lambda {|s| Timestamp.new}, true, false)
24
+ ]
25
+ ]
26
+
27
+ factory = CombinatorialGeneratedExpressionFactory.new(
28
+ 'I bought a {%s} ball on {%s}',
29
+ parameter_type_combinations
30
+ )
31
+ expressions = factory.generate_expressions.map {|ge| ge.source}
32
+ expect(expressions).to eq([
33
+ 'I bought a {color} ball on {date}',
34
+ 'I bought a {color} ball on {datetime}',
35
+ 'I bought a {color} ball on {timestamp}',
36
+ 'I bought a {csscolor} ball on {date}',
37
+ 'I bought a {csscolor} ball on {datetime}',
38
+ 'I bought a {csscolor} ball on {timestamp}',
39
+ ])
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,231 @@
1
+ require 'cucumber/cucumber_expressions/cucumber_expression_generator'
2
+ require 'cucumber/cucumber_expressions/cucumber_expression'
3
+ require 'cucumber/cucumber_expressions/parameter_type'
4
+ require 'cucumber/cucumber_expressions/parameter_type_registry'
5
+
6
+ module Cucumber
7
+ module CucumberExpressions
8
+ describe CucumberExpressionGenerator do
9
+ class Currency
10
+ end
11
+
12
+ before do
13
+ @parameter_type_registry = ParameterTypeRegistry.new
14
+ @generator = CucumberExpressionGenerator.new(@parameter_type_registry)
15
+ end
16
+
17
+ it "documents expression generation" do
18
+ parameter_registry = ParameterTypeRegistry.new
19
+ ### [generate-expression]
20
+ generator = CucumberExpressionGenerator.new(parameter_registry)
21
+ undefined_step_text = "I have 2 cucumbers and 1.5 tomato"
22
+ generated_expression = generator.generate_expression(undefined_step_text)
23
+ expect(generated_expression.source).to eq("I have {int} cucumbers and {float} tomato")
24
+ expect(generated_expression.parameter_types[1].type).to eq(Float)
25
+ ### [generate-expression]
26
+ end
27
+
28
+ it "generates expression for no args" do
29
+ assert_expression("hello", [], "hello")
30
+ end
31
+
32
+ it "generates expression with escaped left parenthesis" do
33
+ assert_expression(
34
+ "\\(iii)", [],
35
+ "(iii)")
36
+ end
37
+
38
+ it "generates expression with escaped left curly brace" do
39
+ assert_expression(
40
+ "\\{iii}", [],
41
+ "{iii}")
42
+ end
43
+
44
+ it "generates expression with escaped slashes" do
45
+ assert_expression(
46
+ "The {int}\\/{int}\\/{int} hey", ["int", "int2", "int3"],
47
+ "The 1814/05/17 hey")
48
+ end
49
+
50
+ it "generates expression for int float arg" do
51
+ assert_expression(
52
+ "I have {int} cukes and {float} euro", ["int", "float"],
53
+ "I have 2 cukes and 1.5 euro")
54
+ end
55
+
56
+ it "generates expression for strings" do
57
+ assert_expression(
58
+ "I like {string} and {string}", ["string", "string2"],
59
+ 'I like "bangers" and \'mash\'')
60
+ end
61
+
62
+ it "generates expression with % sign" do
63
+ assert_expression(
64
+ "I am {int}% foobar", ["int"],
65
+ 'I am 20% foobar')
66
+ end
67
+
68
+ it "generates expression for just int" do
69
+ assert_expression(
70
+ "{int}", ["int"],
71
+ "99999")
72
+ end
73
+
74
+ it "numbers only second argument when builtin type is not reserved keyword" do
75
+ assert_expression(
76
+ "I have {int} cukes and {int} euro", ["int", "int2"],
77
+ "I have 2 cukes and 5 euro")
78
+ end
79
+
80
+ it "numbers only second argument when type is not reserved keyword" do
81
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
82
+ 'currency',
83
+ '[A-Z]{3}',
84
+ Currency,
85
+ lambda {|s| Currency.new(s)},
86
+ true,
87
+ true
88
+ ))
89
+
90
+ assert_expression(
91
+ "I have a {currency} account and a {currency} account", ["currency", "currency2"],
92
+ "I have a EUR account and a GBP account")
93
+ end
94
+
95
+ it "exposes parameters in generated expression" do
96
+ expression = @generator.generate_expression("I have 2 cukes and 1.5 euro")
97
+ types = expression.parameter_types.map(&:type)
98
+ expect(types).to eq([Integer, Float])
99
+ end
100
+
101
+ it "matches parameter types with optional capture groups" do
102
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
103
+ 'optional-flight',
104
+ /(1st flight)?/,
105
+ String,
106
+ lambda {|s| s},
107
+ true,
108
+ false
109
+ ))
110
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
111
+ 'optional-hotel',
112
+ /(1 hotel)?/,
113
+ String,
114
+ lambda {|s| s},
115
+ true,
116
+ false
117
+ ))
118
+
119
+ expression = @generator.generate_expressions("I reach Stage 4: 1st flight -1 hotel")[0]
120
+ # While you would expect this to be `I reach Stage {int}: {optional-flight} -{optional-hotel}`
121
+ # the `-1` causes {int} to match just before {optional-hotel}.
122
+ expect(expression.source).to eq("I reach Stage {int}: {optional-flight} {int} hotel")
123
+ end
124
+
125
+ it "generates at most 256 expressions" do
126
+ for i in 0..3
127
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
128
+ "my-type-#{i}",
129
+ /([a-z] )*?[a-z]/,
130
+ String,
131
+ lambda {|s| s},
132
+ true,
133
+ false
134
+ ))
135
+ end
136
+ # This would otherwise generate 4^11=4194300 expressions and consume just shy of 1.5GB.
137
+ expressions = @generator.generate_expressions("a s i m p l e s t e p")
138
+ expect(expressions.length).to eq(256)
139
+ end
140
+
141
+ it "prefers expression with longest non empty match" do
142
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
143
+ 'zero-or-more',
144
+ /[a-z]*/,
145
+ String,
146
+ lambda {|s| s},
147
+ true,
148
+ false
149
+ ))
150
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
151
+ 'exactly-one',
152
+ /[a-z]/,
153
+ String,
154
+ lambda {|s| s},
155
+ true,
156
+ false
157
+ ))
158
+
159
+ expressions = @generator.generate_expressions("a simple step")
160
+ expect(expressions.length).to eq(2)
161
+ expect(expressions[0].source).to eq("{exactly-one} {zero-or-more} {zero-or-more}")
162
+ expect(expressions[1].source).to eq("{zero-or-more} {zero-or-more} {zero-or-more}")
163
+ end
164
+
165
+ context "does not suggest parameter when match is" do
166
+ before do
167
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
168
+ 'direction',
169
+ /(up|down)/,
170
+ String,
171
+ lambda {|s| s},
172
+ true,
173
+ false
174
+ ))
175
+ end
176
+
177
+ it "at the beginning of a word" do
178
+ expect(@generator.generate_expression("When I download a picture").source).not_to eq("When I {direction}load a picture")
179
+ expect(@generator.generate_expression("When I download a picture").source).to eq("When I download a picture")
180
+ end
181
+
182
+ it "inside a word" do
183
+ expect(@generator.generate_expression("When I watch the muppet show").source).not_to eq("When I watch the m{direction}pet show")
184
+ expect(@generator.generate_expression("When I watch the muppet show").source).to eq("When I watch the muppet show")
185
+ end
186
+
187
+ it "at the end of a word" do
188
+ expect(@generator.generate_expression("When I create a group").source).not_to eq("When I create a gro{direction}")
189
+ expect(@generator.generate_expression("When I create a group").source).to eq("When I create a group")
190
+ end
191
+ end
192
+
193
+ context "does suggest parameter when match is" do
194
+ before do
195
+ @parameter_type_registry.define_parameter_type(ParameterType.new(
196
+ 'direction',
197
+ /(up|down)/,
198
+ String,
199
+ lambda {|s| s},
200
+ true,
201
+ false
202
+ ))
203
+ end
204
+
205
+ it "a full word" do
206
+ expect(@generator.generate_expression("When I go down the road").source).to eq("When I go {direction} the road")
207
+ expect(@generator.generate_expression("When I walk up the hill").source).to eq("When I walk {direction} the hill")
208
+ expect(@generator.generate_expression("up the hill, the road goes down").source).to eq("{direction} the hill, the road goes {direction}")
209
+ end
210
+
211
+ it 'wrapped around punctuation characters' do
212
+ expect(@generator.generate_expression("When direction is:down").source).to eq("When direction is:{direction}")
213
+ expect(@generator.generate_expression("Then direction is down.").source).to eq("Then direction is {direction}.")
214
+ end
215
+ end
216
+
217
+ def assert_expression(expected_expression, expected_argument_names, text)
218
+ generated_expression = @generator.generate_expression(text)
219
+ expect(generated_expression.parameter_names).to eq(expected_argument_names)
220
+ expect(generated_expression.source).to eq(expected_expression)
221
+
222
+ cucumber_expression = CucumberExpression.new(generated_expression.source, @parameter_type_registry)
223
+ match = cucumber_expression.match(text)
224
+ if match.nil?
225
+ raise "Expected text '#{text}' to match generated expression '#{generated_expression.source}'"
226
+ end
227
+ expect(match.length).to eq(expected_argument_names.length)
228
+ end
229
+ end
230
+ end
231
+ end