cucumber 4.0.0.rc.1 → 4.0.0.rc.6

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 (66) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +97 -4
  3. data/CONTRIBUTING.md +1 -18
  4. data/README.md +4 -5
  5. data/lib/autotest/cucumber_mixin.rb +2 -10
  6. data/lib/cucumber.rb +1 -1
  7. data/lib/cucumber/cli/configuration.rb +1 -1
  8. data/lib/cucumber/cli/main.rb +1 -0
  9. data/lib/cucumber/cli/options.rb +18 -13
  10. data/lib/cucumber/cli/profile_loader.rb +23 -12
  11. data/lib/cucumber/configuration.rb +11 -2
  12. data/lib/cucumber/deprecate.rb +29 -5
  13. data/lib/cucumber/errors.rb +5 -2
  14. data/lib/cucumber/events.rb +12 -7
  15. data/lib/cucumber/events/envelope.rb +9 -0
  16. data/lib/cucumber/events/hook_test_step_created.rb +13 -0
  17. data/lib/cucumber/events/test_case_created.rb +13 -0
  18. data/lib/cucumber/events/test_case_ready.rb +12 -0
  19. data/lib/cucumber/events/test_step_created.rb +13 -0
  20. data/lib/cucumber/filters.rb +1 -0
  21. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  22. data/lib/cucumber/formatter/ast_lookup.rb +43 -38
  23. data/lib/cucumber/formatter/backtrace_filter.rb +4 -1
  24. data/lib/cucumber/formatter/console.rb +4 -9
  25. data/lib/cucumber/formatter/console_issues.rb +1 -1
  26. data/lib/cucumber/formatter/duration.rb +1 -1
  27. data/lib/cucumber/formatter/duration_extractor.rb +2 -0
  28. data/lib/cucumber/formatter/errors.rb +6 -0
  29. data/lib/cucumber/formatter/html.rb +24 -0
  30. data/lib/cucumber/formatter/http_io.rb +146 -0
  31. data/lib/cucumber/formatter/interceptor.rb +3 -21
  32. data/lib/cucumber/formatter/io.rb +14 -8
  33. data/lib/cucumber/formatter/json.rb +46 -36
  34. data/lib/cucumber/formatter/junit.rb +13 -11
  35. data/lib/cucumber/formatter/message.rb +22 -0
  36. data/lib/cucumber/formatter/message_builder.rb +243 -0
  37. data/lib/cucumber/formatter/pretty.rb +65 -60
  38. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  39. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  40. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  41. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  42. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  43. data/lib/cucumber/formatter/summary.rb +1 -1
  44. data/lib/cucumber/formatter/usage.rb +3 -3
  45. data/lib/cucumber/gherkin/data_table_parser.rb +12 -3
  46. data/lib/cucumber/gherkin/steps_parser.rb +13 -3
  47. data/lib/cucumber/glue/hook.rb +18 -2
  48. data/lib/cucumber/glue/proto_world.rb +30 -18
  49. data/lib/cucumber/glue/registry_and_more.rb +40 -3
  50. data/lib/cucumber/glue/snippet.rb +2 -2
  51. data/lib/cucumber/glue/step_definition.rb +28 -4
  52. data/lib/cucumber/hooks.rb +8 -8
  53. data/lib/cucumber/multiline_argument.rb +1 -1
  54. data/lib/cucumber/multiline_argument/data_table.rb +17 -13
  55. data/lib/cucumber/platform.rb +1 -1
  56. data/lib/cucumber/rake/task.rb +3 -0
  57. data/lib/cucumber/runtime.rb +29 -3
  58. data/lib/cucumber/runtime/after_hooks.rb +6 -2
  59. data/lib/cucumber/runtime/before_hooks.rb +6 -2
  60. data/lib/cucumber/runtime/for_programming_languages.rb +1 -0
  61. data/lib/cucumber/runtime/step_hooks.rb +3 -2
  62. data/lib/cucumber/runtime/support_code.rb +3 -3
  63. data/lib/cucumber/runtime/user_interface.rb +2 -10
  64. data/lib/cucumber/step_definitions.rb +2 -2
  65. data/lib/cucumber/version +1 -1
  66. metadata +227 -73
@@ -10,8 +10,8 @@ module Cucumber
10
10
 
11
11
  begin
12
12
  raise new(with_prefix(step_name)) # rubocop:disable Style/RaiseArgs
13
- rescue StandardError => exception
14
- return exception
13
+ rescue StandardError => e
14
+ return e
15
15
  end
16
16
  end
17
17
 
@@ -49,4 +49,7 @@ module Cucumber
49
49
  super(messages.join("\n"))
50
50
  end
51
51
  end
52
+
53
+ class LogTypeInvalid < StandardError
54
+ end
52
55
  end
@@ -24,16 +24,21 @@ module Cucumber
24
24
 
25
25
  def self.registry
26
26
  Core::Events.build_registry(
27
- TestCaseStarted,
27
+ GherkinSourceParsed,
28
+ GherkinSourceRead,
29
+ HookTestStepCreated,
30
+ StepActivated,
31
+ StepDefinitionRegistered,
32
+ TestCaseCreated,
28
33
  TestCaseFinished,
34
+ TestCaseStarted,
35
+ TestCaseReady,
36
+ TestRunFinished,
37
+ TestRunStarted,
38
+ TestStepCreated,
29
39
  TestStepFinished,
30
40
  TestStepStarted,
31
- StepDefinitionRegistered,
32
- StepActivated,
33
- TestRunFinished,
34
- GherkinSourceRead,
35
- GherkinSourceParsed,
36
- TestRunStarted
41
+ Envelope
37
42
  )
38
43
  end
39
44
  end
@@ -0,0 +1,9 @@
1
+ require 'cucumber/core/events'
2
+
3
+ module Cucumber
4
+ module Events
5
+ class Envelope < Core::Event.new(:envelope)
6
+ attr_reader :envelope
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/events'
4
+
5
+ module Cucumber
6
+ module Events
7
+ # Event fired when a step is created from a hook
8
+ class HookTestStepCreated < Core::Event.new(:test_step, :hook)
9
+ attr_reader :test_step
10
+ attr_reader :hook
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/events'
4
+
5
+ module Cucumber
6
+ module Events
7
+ # Event fired when a Test::Case is created from a Pickle
8
+ class TestCaseCreated < Core::Event.new(:test_case, :pickle)
9
+ attr_reader :test_case
10
+ attr_reader :pickle
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/events'
4
+
5
+ module Cucumber
6
+ module Events
7
+ # Event fired when a Test::Case is ready to be ran (matching has been done, hooks added etc)
8
+ class TestCaseReady < Core::Event.new(:test_case)
9
+ attr_reader :test_case
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/events'
4
+
5
+ module Cucumber
6
+ module Events
7
+ # Event fired when a TestStep is created from a PickleStep
8
+ class TestStepCreated < Core::Event.new(:test_step, :pickle_step)
9
+ attr_reader :test_step
10
+ attr_reader :pickle_step
11
+ end
12
+ end
13
+ end
@@ -11,3 +11,4 @@ require 'cucumber/filters/quit'
11
11
  require 'cucumber/filters/randomizer'
12
12
  require 'cucumber/filters/retry'
13
13
  require 'cucumber/filters/tag_limits'
14
+ require 'cucumber/filters/broadcast_test_case_ready_event'
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cucumber
4
+ module Filters
5
+ class BroadcastTestCaseReadyEvent < Core::Filter.new(:config)
6
+ def test_case(test_case)
7
+ config.notify :test_case_ready, test_case
8
+ test_case.describe_to(receiver)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -12,7 +12,7 @@ module Cucumber
12
12
  end
13
13
 
14
14
  def on_gherkin_source_parsed(event)
15
- @gherkin_documents[event.gherkin_document[:uri]] = event.gherkin_document
15
+ @gherkin_documents[event.gherkin_document.uri] = event.gherkin_document
16
16
  end
17
17
 
18
18
  def gherkin_document(uri)
@@ -34,7 +34,7 @@ module Cucumber
34
34
  def snippet_step_keyword(test_step)
35
35
  uri = test_step.location.file
36
36
  document = gherkin_document(uri)
37
- dialect = ::Gherkin::Dialect.for(document[:feature][:language])
37
+ dialect = ::Gherkin::Dialect.for(document.feature.language)
38
38
  given_when_then_keywords = [dialect.given_keywords, dialect.when_keywords, dialect.then_keywords].flatten.uniq.reject { |kw| kw == '* ' }
39
39
  keyword_lookup = step_keyword_lookup(uri)
40
40
  keyword = nil
@@ -69,25 +69,28 @@ module Cucumber
69
69
 
70
70
  def initialize(gherkin_document)
71
71
  @lookup_hash = {}
72
- process_scenario_container(gherkin_document[:feature])
72
+ process_scenario_container(gherkin_document.feature)
73
73
  end
74
74
 
75
75
  private
76
76
 
77
77
  def process_scenario_container(container)
78
- container[:children].each do |child|
79
- if !child[:rule].nil?
80
- process_scenario_container(child[:rule])
81
- elsif !child[:scenario].nil?
82
- if child[:scenario][:examples].empty?
83
- @lookup_hash[child[:scenario][:location][:line]] = ScenarioSource.new(:Scenario, child[:scenario])
84
-
85
- else
86
- child[:scenario][:examples].each do |examples|
87
- examples[:table_body].each do |row|
88
- @lookup_hash[row[:location][:line]] = ScenarioOutlineSource.new(:ScenarioOutline, child[:scenario], examples, row)
89
- end
90
- end
78
+ container.children.each do |child|
79
+ if child.respond_to?(:rule) && child.rule
80
+ process_scenario_container(child.rule)
81
+ elsif child.respond_to?(:scenario) && child.scenario
82
+ process_scenario(child)
83
+ end
84
+ end
85
+ end
86
+
87
+ def process_scenario(child)
88
+ if child.scenario.examples.empty?
89
+ @lookup_hash[child.scenario.location.line] = ScenarioSource.new(:Scenario, child.scenario)
90
+ else
91
+ child.scenario.examples.each do |examples|
92
+ examples.table_body.each do |row|
93
+ @lookup_hash[row.location.line] = ScenarioOutlineSource.new(:ScenarioOutline, child.scenario, examples, row)
91
94
  end
92
95
  end
93
96
  end
@@ -99,22 +102,22 @@ module Cucumber
99
102
 
100
103
  def initialize(gherkin_document)
101
104
  @lookup_hash = {}
102
- process_scenario_container(gherkin_document[:feature])
105
+ process_scenario_container(gherkin_document.feature)
103
106
  end
104
107
 
105
108
  private
106
109
 
107
110
  def process_scenario_container(container)
108
- container[:children].each do |child|
109
- if !child[:rule].nil?
110
- process_scenario_container(child[:rule])
111
- elsif !child[:scenario].nil?
112
- child[:scenario][:steps].each do |step|
113
- @lookup_hash[step[:location][:line]] = StepSource.new(:Step, step)
111
+ container.children.each do |child|
112
+ if child.respond_to?(:rule) && child.rule
113
+ process_scenario_container(child.rule)
114
+ elsif child.respond_to?(:scenario) && child.scenario
115
+ child.scenario.steps.each do |step|
116
+ @lookup_hash[step.location.line] = StepSource.new(:Step, step)
114
117
  end
115
- elsif !child[:background].nil?
116
- child[:background][:steps].each do |step|
117
- @lookup_hash[step[:location][:line]] = StepSource.new(:Step, step)
118
+ elsif !child.background.nil?
119
+ child.background.steps.each do |step|
120
+ @lookup_hash[step.location.line] = StepSource.new(:Step, step)
118
121
  end
119
122
  end
120
123
  end
@@ -128,32 +131,34 @@ module Cucumber
128
131
 
129
132
  def initialize(gherkin_document)
130
133
  @lookup_hash = {}
131
- process_scenario_container(gherkin_document[:feature], nil)
134
+ process_scenario_container(gherkin_document.feature, nil)
132
135
  end
133
136
 
134
137
  private
135
138
 
139
+ # rubocop:disable Metrics/PerceivedComplexity
136
140
  def process_scenario_container(container, original_previous_node)
137
- container[:children].each do |child|
141
+ container.children.each do |child|
138
142
  previous_node = original_previous_node
139
- if !child[:rule].nil?
140
- process_scenario_container(child[:rule], original_previous_node)
141
- elsif !child[:scenario].nil?
142
- child[:scenario][:steps].each do |step|
143
- node = KeywordSearchNode.new(step[:keyword], previous_node)
144
- @lookup_hash[step[:location][:line]] = node
143
+ if child.respond_to?(:rule) && child.rule
144
+ process_scenario_container(child.rule, original_previous_node)
145
+ elsif child.respond_to?(:scenario) && child.scenario
146
+ child.scenario.steps.each do |step|
147
+ node = KeywordSearchNode.new(step.keyword, previous_node)
148
+ @lookup_hash[step.location.line] = node
145
149
  previous_node = node
146
150
  end
147
- elsif !child[:background].nil?
148
- child[:background][:steps].each do |step|
149
- node = KeywordSearchNode.new(step[:keyword], previous_node)
150
- @lookup_hash[step[:location][:line]] = node
151
+ elsif child.respond_to?(:background) && child.background
152
+ child.background.steps.each do |step|
153
+ node = KeywordSearchNode.new(step.keyword, previous_node)
154
+ @lookup_hash[step.location.line] = node
151
155
  previous_node = node
152
156
  original_previous_node = previous_node
153
157
  end
154
158
  end
155
159
  end
156
160
  end
161
+ # rubocop:enable Metrics/PerceivedComplexity
157
162
  end
158
163
  end
159
164
  end
@@ -10,13 +10,16 @@ module Cucumber
10
10
  bin/cucumber:
11
11
  lib/rspec
12
12
  gems/
13
+ site_ruby/
13
14
  minitest
14
15
  test/unit
15
16
  .gem/ruby
16
- lib/ruby/
17
17
  bin/bundle
18
18
  ]
19
19
 
20
+ @backtrace_filters << RbConfig::CONFIG['rubyarchdir'] if RbConfig::CONFIG['rubyarchdir']
21
+ @backtrace_filters << RbConfig::CONFIG['rubylibdir'] if RbConfig::CONFIG['rubylibdir']
22
+
20
23
  @backtrace_filters << 'org/jruby/' if ::Cucumber::JRUBY
21
24
 
22
25
  BACKTRACE_FILTER_PATTERNS = Regexp.new(@backtrace_filters.join('|'))
@@ -143,7 +143,7 @@ module Cucumber
143
143
  return unless config.wip?
144
144
  messages = passed_test_cases.map do |test_case|
145
145
  scenario_source = ast_lookup.scenario_source(test_case)
146
- keyword = scenario_source.type == :Scenario ? scenario_source.scenario[:keyword] : scenario_source.scenario_outline[:keyword]
146
+ keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
147
147
  linebreaks("#{test_case.location.on_line(test_case.location.lines.max)}:in `#{keyword}: #{test_case.name}'", ENV['CUCUMBER_TRUNCATE_OUTPUT'].to_i)
148
148
  end
149
149
  do_print_passing_wip(messages)
@@ -158,16 +158,11 @@ module Cucumber
158
158
  end
159
159
  end
160
160
 
161
- def embed(file, mime_type, label)
162
- # no-op
163
- end
164
-
165
- def puts(*messages)
161
+ def attach(src, media_type)
162
+ return unless media_type == 'text/x.cucumber.log+plain'
166
163
  return unless @io
167
164
  @io.puts
168
- messages.each do |message|
169
- @io.puts(format_string(message, :tag))
170
- end
165
+ @io.puts(format_string(src, :tag))
171
166
  @io.flush
172
167
  end
173
168
 
@@ -37,7 +37,7 @@ module Cucumber
37
37
  return [] if test_cases.empty?
38
38
  [format_string("#{type_heading(type)} Scenarios:", type)] + test_cases.map do |test_case|
39
39
  scenario_source = @ast_lookup.scenario_source(test_case)
40
- keyword = scenario_source.type == :Scenario ? scenario_source.scenario[:keyword] : scenario_source.scenario_outline[:keyword]
40
+ keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
41
41
  source = @config.source? ? format_string(" # #{keyword}: #{test_case.name}", :comment) : ''
42
42
  format_string("cucumber #{profiles_string}#{test_case.location.file}:#{test_case.location.lines.max}", type) + source
43
43
  end
@@ -8,7 +8,7 @@ module Cucumber
8
8
  # <tt>time</tt> format.
9
9
  def format_duration(seconds)
10
10
  m, s = seconds.divmod(60)
11
- "#{m}m#{format('%.3f', s)}s"
11
+ "#{m}m#{format('%<seconds>.3f', seconds: s)}s"
12
12
  end
13
13
  end
14
14
  end
@@ -24,6 +24,8 @@ module Cucumber
24
24
  def duration(duration, *)
25
25
  duration.tap { |dur| @result_duration = dur.nanoseconds / 10**9.0 }
26
26
  end
27
+
28
+ def attach(*) end
27
29
  end
28
30
  end
29
31
  end
@@ -0,0 +1,6 @@
1
+ module Cucumber
2
+ module Formatter
3
+ class TestCaseUnknownError < StandardError; end
4
+ class TestStepUnknownError < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ require 'cucumber/formatter/io'
2
+ require 'cucumber/html_formatter'
3
+ require 'cucumber/formatter/message_builder'
4
+
5
+ module Cucumber
6
+ module Formatter
7
+ class HTML < MessageBuilder
8
+ include Io
9
+
10
+ def initialize(config)
11
+ @io = ensure_io(config.out_stream)
12
+ @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io)
13
+ @html_formatter.write_pre_message
14
+
15
+ super(config)
16
+ end
17
+
18
+ def output_envelope(envelope)
19
+ @html_formatter.write_message(envelope)
20
+ @html_formatter.write_post_message if envelope.test_run_finished
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,146 @@
1
+ require 'net/http'
2
+ require 'tempfile'
3
+
4
+ module Cucumber
5
+ module Formatter
6
+ class HTTPIO
7
+ class << self
8
+ # Returns an IO that will write to a HTTP request's body
9
+ def open(url, https_verify_mode = nil)
10
+ @https_verify_mode = https_verify_mode
11
+ uri, method, headers = CurlOptionParser.parse(url)
12
+ IOHTTPBuffer.new(uri, method, headers, https_verify_mode)
13
+ end
14
+ end
15
+ end
16
+
17
+ class CurlOptionParser
18
+ def self.parse(options)
19
+ chunks = options.split(/\s/).compact
20
+ http_method = 'PUT'
21
+ url = chunks[0]
22
+ headers = ''
23
+
24
+ last_flag = nil
25
+ chunks.each do |chunk|
26
+ if ['-X', '--request'].include?(chunk)
27
+ last_flag = '-X'
28
+ next
29
+ end
30
+
31
+ if chunk == '-H'
32
+ last_flag = '-H'
33
+ next
34
+ end
35
+
36
+ if last_flag == '-X'
37
+ http_method = chunk
38
+ last_flag = nil
39
+ end
40
+
41
+ headers += chunk if last_flag == '-H'
42
+ end
43
+
44
+ [
45
+ url,
46
+ http_method,
47
+ make_headers(headers)
48
+ ]
49
+ end
50
+
51
+ def self.make_headers(headers)
52
+ hash_headers = {}
53
+ str_scanner = /("(?<key>[^":]+)\s*:\s*(?<value>[^":]+)")|('(?<key1>[^':]+)\s*:\s*(?<value1>[^':]+)')/
54
+
55
+ headers.scan(str_scanner) do |header|
56
+ header = header.compact!
57
+ hash_headers[header[0]] = header[1]&.strip
58
+ end
59
+
60
+ hash_headers
61
+ end
62
+ end
63
+
64
+ class IOHTTPBuffer
65
+ attr_reader :uri, :method, :headers
66
+
67
+ def initialize(uri, method, headers = {}, https_verify_mode = nil)
68
+ @uri = URI(uri)
69
+ @method = method
70
+ @headers = headers
71
+ @write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
72
+ @https_verify_mode = https_verify_mode
73
+ end
74
+
75
+ def close
76
+ post_content(@uri, @method, @headers)
77
+ @write_io.close
78
+ end
79
+
80
+ def write(data)
81
+ @write_io.write(data)
82
+ end
83
+
84
+ def flush
85
+ @write_io.flush
86
+ end
87
+
88
+ def closed?
89
+ @write_io.closed?
90
+ end
91
+
92
+ private
93
+
94
+ def post_content(uri, method, headers, attempt = 10)
95
+ content = @write_io
96
+ http = build_client(uri, @https_verify_mode)
97
+
98
+ raise StandardError, "request to #{uri} failed (too many redirections)" if attempt <= 0
99
+ req = build_request(
100
+ uri,
101
+ method,
102
+ headers.merge(
103
+ 'Content-Length' => content.size.to_s
104
+ )
105
+ )
106
+
107
+ content.rewind
108
+ req.body_stream = content
109
+
110
+ begin
111
+ response = http.request(req)
112
+ rescue SystemCallError
113
+ # We may get the redirect response before pushing the file.
114
+ response = http.request(build_request(uri, method, headers))
115
+ end
116
+
117
+ case response
118
+ when Net::HTTPSuccess
119
+ response
120
+ when Net::HTTPRedirection
121
+ post_content(URI(response['Location']), method, headers, attempt - 1)
122
+ else
123
+ raise StandardError, "request to #{uri} failed with status #{response.code}"
124
+ end
125
+ end
126
+
127
+ def build_request(uri, method, headers)
128
+ method_class_name = "#{method[0].upcase}#{method[1..-1].downcase}"
129
+ req = Net::HTTP.const_get(method_class_name).new(uri)
130
+ headers.each do |header, value|
131
+ req[header] = value
132
+ end
133
+ req
134
+ end
135
+
136
+ def build_client(uri, https_verify_mode)
137
+ http = Net::HTTP.new(uri.hostname, uri.port)
138
+ if uri.scheme == 'https'
139
+ http.use_ssl = true
140
+ http.verify_mode = https_verify_mode if https_verify_mode
141
+ end
142
+ http
143
+ end
144
+ end
145
+ end
146
+ end