cucumber-cucumber-expressions 8.3.1

Sign up to get free protection for your applications and to get access to all the features.
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