cucumber 2.3.3 → 2.4.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +48 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
  4. data/.travis.yml +4 -5
  5. data/Gemfile +3 -1
  6. data/History.md +18 -2
  7. data/LICENSE +1 -1
  8. data/README.md +5 -2
  9. data/cucumber.gemspec +2 -13
  10. data/examples/i18n/eo/features/adicio.feature +6 -6
  11. data/examples/i18n/eo/features/divido.feature +6 -6
  12. data/examples/i18n/eo/features/step_definitions/calculator_steps.rb +2 -2
  13. data/features/docs/cli/randomize.feature +91 -15
  14. data/features/docs/cli/retry_failing_tests.feature +32 -0
  15. data/features/docs/defining_steps/snippets.feature +1 -1
  16. data/features/docs/formatters/json_formatter.feature +2 -6
  17. data/features/docs/formatters/junit_formatter.feature +47 -1
  18. data/features/docs/writing_support_code/after_step_hooks.feature +53 -0
  19. data/features/docs/{defining_steps → writing_support_code}/transforms.feature +42 -7
  20. data/features/lib/step_definitions/cucumber_steps.rb +28 -0
  21. data/features/lib/step_definitions/retry_steps.rb +35 -0
  22. data/lib/cucumber/cli/configuration.rb +4 -0
  23. data/lib/cucumber/cli/options.rb +7 -2
  24. data/lib/cucumber/cli/rerun_file.rb +1 -1
  25. data/lib/cucumber/configuration.rb +4 -0
  26. data/lib/cucumber/events/finished_testing.rb +9 -0
  27. data/lib/cucumber/filters/randomizer.rb +6 -1
  28. data/lib/cucumber/filters/retry.rb +32 -0
  29. data/lib/cucumber/formatter/cucumber.css +0 -0
  30. data/lib/cucumber/formatter/cucumber.sass +0 -0
  31. data/lib/cucumber/formatter/event_bus_report.rb +1 -0
  32. data/lib/cucumber/formatter/json.rb +18 -8
  33. data/lib/cucumber/formatter/junit.rb +56 -42
  34. data/lib/cucumber/gherkin/data_table_parser.rb +17 -9
  35. data/lib/cucumber/gherkin/steps_parser.rb +13 -21
  36. data/lib/cucumber/platform.rb +1 -0
  37. data/lib/cucumber/rake/task.rb +1 -1
  38. data/lib/cucumber/rb_support/rb_hook.rb +1 -1
  39. data/lib/cucumber/rb_support/snippet.rb +1 -1
  40. data/lib/cucumber/runtime.rb +4 -5
  41. data/lib/cucumber/runtime/for_programming_languages.rb +2 -0
  42. data/lib/cucumber/runtime/step_hooks.rb +1 -1
  43. data/lib/cucumber/step_match.rb +1 -1
  44. data/lib/cucumber/version +1 -1
  45. data/spec/cucumber/cli/configuration_spec.rb +7 -0
  46. data/spec/cucumber/cli/options_spec.rb +14 -0
  47. data/spec/cucumber/cli/rerun_spec.rb +3 -7
  48. data/spec/cucumber/filters/retry_spec.rb +79 -0
  49. data/spec/cucumber/formatter/event_bus_report_spec.rb +9 -0
  50. data/spec/cucumber/formatter/json_spec.rb +1 -1
  51. data/spec/cucumber/formatter/junit_spec.rb +2 -2
  52. data/spec/cucumber/formatter/spec_helper.rb +6 -1
  53. data/spec/cucumber/rake/task_spec.rb +85 -0
  54. data/spec/cucumber/rb_support/snippet_spec.rb +2 -2
  55. metadata +19 -139
File without changes
File without changes
@@ -29,6 +29,7 @@ module Cucumber
29
29
  end
30
30
 
31
31
  def done
32
+ @config.notify Events::FinishedTesting.new
32
33
  end
33
34
  end
34
35
 
@@ -1,5 +1,6 @@
1
1
  require 'multi_json'
2
2
  require 'base64'
3
+ require 'cucumber/formatter/backtrace_filter'
3
4
  require 'cucumber/formatter/io'
4
5
  require 'cucumber/formatter/hook_query_visitor'
5
6
 
@@ -9,13 +10,18 @@ module Cucumber
9
10
  class Json
10
11
  include Io
11
12
 
12
- def initialize(runtime, io, _options)
13
- @runtime = runtime
14
- @io = ensure_io(io)
13
+ def initialize(config)
14
+ config.on_event :before_test_case, &method(:on_before_test_case)
15
+ config.on_event :after_test_case, &method(:on_after_test_case)
16
+ config.on_event :before_test_step, &method(:on_before_test_step)
17
+ config.on_event :after_test_step, &method(:on_after_test_step)
18
+ config.on_event :finished_testing, &method(:on_finished_testing)
19
+ @io = ensure_io(config.out_stream)
15
20
  @feature_hashes = []
16
21
  end
17
22
 
18
- def before_test_case(test_case)
23
+ def on_before_test_case(event)
24
+ test_case = event.test_case
19
25
  builder = Builder.new(test_case)
20
26
  unless same_feature_as_previous_test_case?(test_case.feature)
21
27
  @feature_hash = builder.feature_hash
@@ -32,7 +38,8 @@ module Cucumber
32
38
  @any_step_failed = false
33
39
  end
34
40
 
35
- def before_test_step(test_step)
41
+ def on_before_test_step(event)
42
+ test_step = event.test_step
36
43
  return if internal_hook?(test_step)
37
44
  hook_query = HookQueryVisitor.new(test_step)
38
45
  if hook_query.hook?
@@ -49,17 +56,20 @@ module Cucumber
49
56
  @step_hash = @step_or_hook_hash
50
57
  end
51
58
 
52
- def after_test_step(test_step, result)
59
+ def on_after_test_step(event)
60
+ test_step = event.test_step
61
+ result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
53
62
  return if internal_hook?(test_step)
54
63
  add_match_and_result(test_step, result)
55
64
  @any_step_failed = true if result.failed?
56
65
  end
57
66
 
58
- def after_test_case(test_case, result)
67
+ def on_after_test_case(event)
68
+ result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
59
69
  add_failed_around_hook(result) if result.failed? && !@any_step_failed
60
70
  end
61
71
 
62
- def done
72
+ def on_finished_testing(event)
63
73
  @io.write(MultiJson.dump(@feature_hashes, pretty: true))
64
74
  end
65
75
 
@@ -1,4 +1,5 @@
1
1
  require 'builder'
2
+ require 'cucumber/formatter/backtrace_filter'
2
3
  require 'cucumber/formatter/io'
3
4
  require 'cucumber/formatter/interceptor'
4
5
  require 'fileutils'
@@ -16,14 +17,27 @@ module Cucumber
16
17
  end
17
18
  end
18
19
 
19
- def initialize(_runtime, io, options)
20
- @reportdir = ensure_dir(io, "junit")
21
- @options = options
22
- end
23
-
24
- def before_test_case(test_case)
20
+ def initialize(config)
21
+ config.on_event :before_test_case, &method(:on_before_test_case)
22
+ config.on_event :after_test_case, &method(:on_after_test_case)
23
+ config.on_event :after_test_step, &method(:on_after_test_step)
24
+ config.on_event :finished_testing, &method(:on_finished_testing)
25
+ @reportdir = ensure_dir(config.out_stream, "junit")
26
+ @config = config
27
+ @features_data = Hash.new { |h,k| h[k] = {
28
+ feature: nil,
29
+ failures: 0,
30
+ errors: 0,
31
+ tests: 0,
32
+ skipped: 0,
33
+ time: 0,
34
+ builder: Builder::XmlMarkup.new(:indent => 2)
35
+ }}
36
+ end
37
+
38
+ def on_before_test_case(event)
39
+ test_case = event.test_case
25
40
  unless same_feature_as_previous_test_case?(test_case.feature)
26
- end_feature if @current_feature
27
41
  start_feature(test_case.feature)
28
42
  end
29
43
  @failing_step_source = nil
@@ -33,13 +47,15 @@ module Cucumber
33
47
  @interceptederr = Interceptor::Pipe.wrap(:stderr)
34
48
  end
35
49
 
36
- def after_test_step(test_step, result)
50
+ def on_after_test_step(event)
37
51
  return if @failing_step_source
38
52
 
39
- @failing_step_source = test_step.source.last unless result.ok?(@options[:strict])
53
+ @failing_step_source = event.test_step.source.last unless event.result.ok?(@config.strict?)
40
54
  end
41
55
 
42
- def after_test_case(test_case, result)
56
+ def on_after_test_case(event)
57
+ test_case = event.test_case
58
+ result = event.result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
43
59
  test_case_name = NameBuilder.new(test_case)
44
60
  scenario = test_case_name.scenario_name
45
61
  scenario_designation = "#{scenario}#{test_case_name.name_suffix}"
@@ -50,43 +66,41 @@ module Cucumber
50
66
  Interceptor::Pipe.unwrap! :stderr
51
67
  end
52
68
 
53
- def done
54
- end_feature if @current_feature
69
+ def on_finished_testing(event)
70
+ @features_data.each { |file, data| end_feature(data) }
55
71
  end
56
72
 
57
73
  private
58
74
 
59
75
  def same_feature_as_previous_test_case?(feature)
60
- @current_feature && @current_feature.file == feature.file && @current_feature.location == feature.location
76
+ @current_feature_data && @current_feature_data[:feature].file == feature.file && @current_feature_data[:feature].location == feature.location
61
77
  end
62
78
 
63
79
  def start_feature(feature)
64
80
  raise UnNamedFeatureError.new(feature.file) if feature.name.empty?
65
- @current_feature = feature
66
- @failures = @errors = @tests = @skipped = 0
67
- @builder = Builder::XmlMarkup.new(:indent => 2)
68
- @time = 0
81
+ @current_feature_data = @features_data[feature.file]
82
+ @current_feature_data[:feature] = feature unless @current_feature_data[:feature]
69
83
  end
70
84
 
71
- def end_feature
85
+ def end_feature(feature_data)
72
86
  @testsuite = Builder::XmlMarkup.new(:indent => 2)
73
87
  @testsuite.instruct!
74
88
  @testsuite.testsuite(
75
- :failures => @failures,
76
- :errors => @errors,
77
- :skipped => @skipped,
78
- :tests => @tests,
79
- :time => "%.6f" % @time,
80
- :name => @current_feature.name ) do
81
- @testsuite << @builder.target!
89
+ :failures => feature_data[:failures],
90
+ :errors => feature_data[:errors],
91
+ :skipped => feature_data[:skipped],
92
+ :tests => feature_data[:tests],
93
+ :time => "%.6f" % feature_data[:time],
94
+ :name => feature_data[:feature].name ) do
95
+ @testsuite << feature_data[:builder].target!
82
96
  end
83
97
 
84
- write_file(feature_result_filename(@current_feature.file), @testsuite.target!)
98
+ write_file(feature_result_filename(feature_data[:feature].file), @testsuite.target!)
85
99
  end
86
100
 
87
101
  def create_output_string(test_case, scenario, result, row_name)
88
102
  output = "#{test_case.keyword}: #{scenario}\n\n"
89
- return output if result.ok?(@options[:strict])
103
+ return output if result.ok?(@config.strict?)
90
104
  if test_case.keyword == "Scenario"
91
105
  output += "#{@failing_step_source.keyword}" unless hook?(@failing_step_source)
92
106
  output += "#{@failing_step_source.name}\n"
@@ -102,31 +116,31 @@ module Cucumber
102
116
 
103
117
  def build_testcase(result, scenario_designation, output)
104
118
  duration = ResultBuilder.new(result).test_case_duration
105
- @time += duration
106
- classname = @current_feature.name
119
+ @current_feature_data[:time] += duration
120
+ classname = @current_feature_data[:feature].name
107
121
  name = scenario_designation
108
122
 
109
- @builder.testcase(:classname => classname, :name => name, :time => "%.6f" % duration) do
110
- if !result.passed? && result.ok?(@options[:strict])
111
- @builder.skipped
112
- @skipped += 1
123
+ @current_feature_data[:builder].testcase(:classname => classname, :name => name, :time => "%.6f" % duration) do
124
+ if !result.passed? && result.ok?(@config.strict?)
125
+ @current_feature_data[:builder].skipped
126
+ @current_feature_data[:skipped] += 1
113
127
  elsif !result.passed?
114
128
  status = result.to_sym
115
129
  exception = get_backtrace_object(result)
116
- @builder.failure(:message => "#{status} #{name}", :type => status) do
117
- @builder.cdata! output
118
- @builder.cdata!(format_exception(exception)) if exception
130
+ @current_feature_data[:builder].failure(:message => "#{status} #{name}", :type => status) do
131
+ @current_feature_data[:builder].cdata! output
132
+ @current_feature_data[:builder].cdata!(format_exception(exception)) if exception
119
133
  end
120
- @failures += 1
134
+ @current_feature_data[:failures] += 1
121
135
  end
122
- @builder.tag!('system-out') do
123
- @builder.cdata! strip_control_chars(@interceptedout.buffer.join)
136
+ @current_feature_data[:builder].tag!('system-out') do
137
+ @current_feature_data[:builder].cdata! strip_control_chars(@interceptedout.buffer.join)
124
138
  end
125
- @builder.tag!('system-err') do
126
- @builder.cdata! strip_control_chars(@interceptederr.buffer.join)
139
+ @current_feature_data[:builder].tag!('system-err') do
140
+ @current_feature_data[:builder].cdata! strip_control_chars(@interceptederr.buffer.join)
127
141
  end
128
142
  end
129
- @tests += 1
143
+ @current_feature_data[:tests] += 1
130
144
  end
131
145
 
132
146
  def get_backtrace_object(result)
@@ -1,5 +1,6 @@
1
1
  require 'gherkin/token_scanner'
2
- require 'gherkin/token_matcher'
2
+ require 'gherkin/parser'
3
+ require 'gherkin/dialect'
3
4
 
4
5
  module Cucumber
5
6
  module Gherkin
@@ -7,17 +8,24 @@ module Cucumber
7
8
  def initialize(builder)
8
9
  @builder = builder
9
10
  end
11
+
10
12
  def parse(text)
11
- scanner = ::Gherkin::TokenScanner.new(text)
12
- matcher = ::Gherkin::TokenMatcher.new
13
- token = scanner.read
14
- until matcher.match_EOF(token) do
15
- if matcher.match_TableRow(token)
16
- @builder.row(token.matched_items.map { |cell_item| cell_item.text })
17
- end
18
- token = scanner.read
13
+ token_scanner = ::Gherkin::TokenScanner.new(feature_header + text)
14
+ parser = ::Gherkin::Parser.new
15
+ gherkin_document = parser.parse(token_scanner)
16
+
17
+ gherkin_document[:feature][:children][0][:steps][0][:argument][:rows].each do |row|
18
+ @builder.row(row[:cells].map { |cell| cell[:value] })
19
19
  end
20
20
  end
21
+
22
+ def feature_header
23
+ dialect = ::Gherkin::Dialect.for('en')
24
+ %(#{dialect.feature_keywords[0]}:
25
+ #{dialect.scenario_keywords[0]}:
26
+ #{dialect.given_keywords[0]} x
27
+ )
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -1,7 +1,7 @@
1
1
  require 'gherkin/token_scanner'
2
2
  require 'gherkin/token_matcher'
3
- require 'gherkin/ast_builder'
4
3
  require 'gherkin/parser'
4
+ require 'gherkin/dialect'
5
5
 
6
6
  module Cucumber
7
7
  module Gherkin
@@ -10,29 +10,21 @@ module Cucumber
10
10
  @builder = builder
11
11
  @language = language
12
12
  end
13
- def parse(text)
14
- ast_builder = ::Gherkin::AstBuilder.new
15
- context = ::Gherkin::ParserContext.new(
16
- ::Gherkin::TokenScanner.new(text),
17
- ::Gherkin::TokenMatcher.new(@language),
18
- [],
19
- []
20
- )
21
- parser = ::Gherkin::Parser.new(ast_builder)
22
13
 
23
- parser.start_rule(context, :ScenarioDefinition)
24
- parser.start_rule(context, :Scenario)
25
- scenario = ast_builder.current_node
26
- state = 12
27
- token = nil
28
- begin
29
- token = parser.read_token(context)
30
- state = parser.match_token(state, token, context)
31
- end until(token.eof?)
14
+ def parse(text)
15
+ dialect = ::Gherkin::Dialect.for(@language)
16
+ token_matcher = ::Gherkin::TokenMatcher.new(@language)
17
+ token_scanner = ::Gherkin::TokenScanner.new(feature_header(dialect) + text)
18
+ parser = ::Gherkin::Parser.new
19
+ gherkin_document = parser.parse(token_scanner, token_matcher)
32
20
 
33
- raise CompositeParserException.new(context.errors) if context.errors.any?
21
+ @builder.steps(gherkin_document[:feature][:children][0][:steps])
22
+ end
34
23
 
35
- @builder.steps(ast_builder.get_steps(scenario))
24
+ def feature_header(dialect)
25
+ %(#{dialect.feature_keywords[0]}:
26
+ #{dialect.scenario_keywords[0]}:
27
+ )
36
28
  end
37
29
  end
38
30
  end
@@ -12,6 +12,7 @@ module Cucumber
12
12
  RUBY_BINARY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
13
13
  RUBY_2_2 = RUBY_VERSION =~ /^2\.2/
14
14
  RUBY_2_1 = RUBY_VERSION =~ /^2\.1/
15
+ RUBY_2_3 = RUBY_VERSION =~ /^2\.3/
15
16
 
16
17
  class << self
17
18
  attr_accessor :use_full_backtrace
@@ -153,7 +153,7 @@ module Cucumber
153
153
  end
154
154
 
155
155
  def cucumber_opts_with_profile #:nodoc:
156
- @profile ? [cucumber_opts, '--profile', @profile] : cucumber_opts
156
+ Array(cucumber_opts).concat Array(@profile).flat_map {|p| ["--profile", p] }
157
157
  end
158
158
 
159
159
  def feature_files #:nodoc:
@@ -12,7 +12,7 @@ module Cucumber
12
12
  end
13
13
 
14
14
  def invoke(pseudo_method, arguments, &block)
15
- @rb_language.current_world.cucumber_instance_exec(false, pseudo_method, *[arguments, block].compact, &@proc)
15
+ @rb_language.current_world.cucumber_instance_exec(false, pseudo_method, *[arguments, block].flatten.compact, &@proc)
16
16
  end
17
17
  end
18
18
  end
@@ -153,7 +153,7 @@ module Cucumber
153
153
  end
154
154
 
155
155
  def append_comment_to(string)
156
- string << " # table is a #{@table.class.to_s}\n"
156
+ string << " # table is a #{Cucumber::MultilineArgument::DataTable.to_s}\n"
157
157
  end
158
158
  end
159
159
 
@@ -1,7 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'fileutils'
3
3
  require 'multi_json'
4
- require 'multi_test'
5
4
  require 'cucumber/configuration'
6
5
  require 'cucumber/load_path'
7
6
  require 'cucumber/formatter/duration'
@@ -32,7 +31,7 @@ module Cucumber
32
31
  end
33
32
 
34
33
  class FeatureFolderNotFoundException < FileException
35
- include FixRuby21Bug9285 if Cucumber::RUBY_2_1 || Cucumber::RUBY_2_2
34
+ include FixRuby21Bug9285 if Cucumber::RUBY_2_1 || Cucumber::RUBY_2_2 || Cucumber::RUBY_2_3
36
35
  end
37
36
 
38
37
  require 'cucumber/core'
@@ -195,8 +194,8 @@ module Cucumber
195
194
 
196
195
  def create_formatter(factory, path_or_io, options)
197
196
  if !legacy_formatter?(factory)
198
- out_stream = Cucumber::Formatter::Io.ensure_io(path_or_io)
199
- return factory.new(@configuration.with_options(out_stream: out_stream))
197
+ return factory.new(@configuration) if path_or_io.nil?
198
+ return factory.new(@configuration.with_options(out_stream: path_or_io))
200
199
  end
201
200
  results = Formatter::LegacyApi::Results.new
202
201
  runtime_facade = Formatter::LegacyApi::RuntimeFacade.new(results, @support_code, @configuration)
@@ -226,11 +225,11 @@ module Cucumber
226
225
  name_regexps = @configuration.name_regexps
227
226
  tag_limits = @configuration.tag_limits
228
227
  [].tap do |filters|
229
- filters << Filters::Randomizer.new(@configuration.seed) if @configuration.randomize?
230
228
  filters << Filters::TagLimits.new(tag_limits) if tag_limits.any?
231
229
  filters << Cucumber::Core::Test::TagFilter.new(tag_expressions)
232
230
  filters << Cucumber::Core::Test::NameFilter.new(name_regexps)
233
231
  filters << Cucumber::Core::Test::LocationsFilter.new(filespecs.locations)
232
+ filters << Filters::Randomizer.new(@configuration.seed) if @configuration.randomize?
234
233
  filters << Filters::Quit.new
235
234
  # TODO: can we just use RbLanguages's step definitions directly?
236
235
  step_match_search = StepMatchSearch.new(@support_code.ruby.method(:step_matches), @configuration)
@@ -11,6 +11,8 @@ module Cucumber
11
11
  class ForProgrammingLanguages
12
12
  extend Forwardable
13
13
 
14
+ attr_reader :support_code
15
+
14
16
  def initialize(support_code, user_interface)
15
17
  @support_code, @user_interface = support_code, user_interface
16
18
  end
@@ -14,7 +14,7 @@ module Cucumber
14
14
  private
15
15
  def after_step_hooks(test_step)
16
16
  @hooks.map do |hook|
17
- action = ->(*args) { hook.invoke('AfterStep', args) }
17
+ action = ->(*args) { hook.invoke('AfterStep', [args, test_step]) }
18
18
  Hooks.after_step_hook(test_step.source, hook.location, &action)
19
19
  end
20
20
  end
@@ -2,7 +2,7 @@ require 'cucumber/multiline_argument'
2
2
 
3
3
  module Cucumber
4
4
 
5
- # Represents the match found between a Test Step and it's activation
5
+ # Represents the match found between a Test Step and its activation
6
6
  class StepMatch #:nodoc:
7
7
  attr_reader :step_definition, :step_arguments
8
8
 
@@ -1 +1 @@
1
- 2.3.3
1
+ 2.4.0
@@ -423,6 +423,13 @@ END_OF_MESSAGE
423
423
  expect(config.snippet_type).to eq :regexp
424
424
  end
425
425
  end
426
+
427
+ describe "#retry_attempts" do
428
+ it "returns the specified number of retries" do
429
+ config.parse!(['--retry=3'])
430
+ expect(config.retry_attempts).to eql 3
431
+ end
432
+ end
426
433
  end
427
434
  end
428
435
  end