cucumber 3.1.2 → 7.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.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +1780 -1146
  3. data/CONTRIBUTING.md +224 -61
  4. data/README.md +144 -22
  5. data/bin/cucumber +1 -1
  6. data/lib/autotest/cucumber_mixin.rb +46 -53
  7. data/lib/cucumber/cli/configuration.rb +28 -6
  8. data/lib/cucumber/cli/main.rb +12 -12
  9. data/lib/cucumber/cli/options.rb +103 -77
  10. data/lib/cucumber/cli/profile_loader.rb +49 -26
  11. data/lib/cucumber/configuration.rb +44 -29
  12. data/lib/cucumber/constantize.rb +2 -5
  13. data/lib/cucumber/deprecate.rb +31 -7
  14. data/lib/cucumber/errors.rb +5 -7
  15. data/lib/cucumber/events/envelope.rb +9 -0
  16. data/lib/cucumber/events/gherkin_source_parsed.rb +11 -0
  17. data/lib/cucumber/events/hook_test_step_created.rb +13 -0
  18. data/lib/cucumber/events/step_activated.rb +2 -1
  19. data/lib/cucumber/events/test_case_created.rb +13 -0
  20. data/lib/cucumber/events/test_case_ready.rb +12 -0
  21. data/lib/cucumber/events/test_step_created.rb +13 -0
  22. data/lib/cucumber/events/undefined_parameter_type.rb +10 -0
  23. data/lib/cucumber/events.rb +14 -7
  24. data/lib/cucumber/file_specs.rb +6 -6
  25. data/lib/cucumber/filters/activate_steps.rb +5 -3
  26. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  27. data/lib/cucumber/filters/prepare_world.rb +5 -9
  28. data/lib/cucumber/filters/quit.rb +1 -3
  29. data/lib/cucumber/filters/tag_limits/verifier.rb +2 -4
  30. data/lib/cucumber/filters.rb +1 -0
  31. data/lib/cucumber/formatter/ansicolor.rb +40 -52
  32. data/lib/cucumber/formatter/ast_lookup.rb +163 -0
  33. data/lib/cucumber/formatter/backtrace_filter.rb +10 -8
  34. data/lib/cucumber/formatter/console.rb +69 -69
  35. data/lib/cucumber/formatter/console_counts.rb +4 -9
  36. data/lib/cucumber/formatter/console_issues.rb +6 -3
  37. data/lib/cucumber/formatter/duration.rb +1 -1
  38. data/lib/cucumber/formatter/duration_extractor.rb +3 -1
  39. data/lib/cucumber/formatter/errors.rb +6 -0
  40. data/lib/cucumber/formatter/fanout.rb +2 -0
  41. data/lib/cucumber/formatter/html.rb +11 -598
  42. data/lib/cucumber/formatter/http_io.rb +147 -0
  43. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  44. data/lib/cucumber/formatter/interceptor.rb +11 -30
  45. data/lib/cucumber/formatter/io.rb +55 -13
  46. data/lib/cucumber/formatter/json.rb +115 -122
  47. data/lib/cucumber/formatter/junit.rb +72 -55
  48. data/lib/cucumber/formatter/message.rb +23 -0
  49. data/lib/cucumber/formatter/message_builder.rb +255 -0
  50. data/lib/cucumber/formatter/pretty.rb +360 -153
  51. data/lib/cucumber/formatter/progress.rb +30 -32
  52. data/lib/cucumber/formatter/publish_banner_printer.rb +77 -0
  53. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  54. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  55. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  56. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  57. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  58. data/lib/cucumber/formatter/rerun.rb +22 -4
  59. data/lib/cucumber/formatter/stepdefs.rb +1 -2
  60. data/lib/cucumber/formatter/steps.rb +8 -6
  61. data/lib/cucumber/formatter/summary.rb +16 -8
  62. data/lib/cucumber/formatter/unicode.rb +15 -17
  63. data/lib/cucumber/formatter/url_reporter.rb +17 -0
  64. data/lib/cucumber/formatter/usage.rb +17 -14
  65. data/lib/cucumber/gherkin/data_table_parser.rb +17 -6
  66. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +13 -17
  67. data/lib/cucumber/gherkin/formatter/escaping.rb +2 -2
  68. data/lib/cucumber/gherkin/steps_parser.rb +17 -8
  69. data/lib/cucumber/glue/dsl.rb +19 -0
  70. data/lib/cucumber/glue/hook.rb +34 -11
  71. data/lib/cucumber/glue/invoke_in_world.rb +13 -18
  72. data/lib/cucumber/glue/proto_world.rb +37 -44
  73. data/lib/cucumber/glue/registry_and_more.rb +71 -12
  74. data/lib/cucumber/glue/registry_wrapper.rb +31 -0
  75. data/lib/cucumber/glue/snippet.rb +23 -22
  76. data/lib/cucumber/glue/step_definition.rb +42 -20
  77. data/lib/cucumber/glue/world_factory.rb +1 -1
  78. data/lib/cucumber/hooks.rb +11 -11
  79. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +2 -2
  80. data/lib/cucumber/multiline_argument/data_table.rb +97 -64
  81. data/lib/cucumber/multiline_argument/doc_string.rb +1 -1
  82. data/lib/cucumber/multiline_argument.rb +4 -6
  83. data/lib/cucumber/platform.rb +3 -3
  84. data/lib/cucumber/rake/task.rb +16 -18
  85. data/lib/cucumber/rspec/disable_option_parser.rb +9 -8
  86. data/lib/cucumber/rspec/doubles.rb +3 -5
  87. data/lib/cucumber/running_test_case.rb +2 -53
  88. data/lib/cucumber/runtime/after_hooks.rb +8 -4
  89. data/lib/cucumber/runtime/before_hooks.rb +8 -4
  90. data/lib/cucumber/runtime/for_programming_languages.rb +4 -2
  91. data/lib/cucumber/runtime/step_hooks.rb +6 -2
  92. data/lib/cucumber/runtime/support_code.rb +13 -15
  93. data/lib/cucumber/runtime/user_interface.rb +6 -16
  94. data/lib/cucumber/runtime.rb +77 -59
  95. data/lib/cucumber/step_definition_light.rb +4 -3
  96. data/lib/cucumber/step_definitions.rb +2 -2
  97. data/lib/cucumber/step_match.rb +12 -17
  98. data/lib/cucumber/step_match_search.rb +2 -1
  99. data/lib/cucumber/term/ansicolor.rb +9 -9
  100. data/lib/cucumber/term/banner.rb +56 -0
  101. data/lib/cucumber/version +1 -1
  102. data/lib/cucumber.rb +1 -1
  103. metadata +272 -81
  104. data/lib/cucumber/core_ext/string.rb +0 -11
  105. data/lib/cucumber/events/gherkin_source_parsed.rb~ +0 -14
  106. data/lib/cucumber/formatter/ast_lookup.rb~ +0 -9
  107. data/lib/cucumber/formatter/cucumber.css +0 -286
  108. data/lib/cucumber/formatter/cucumber.sass +0 -247
  109. data/lib/cucumber/formatter/hook_query_visitor.rb +0 -42
  110. data/lib/cucumber/formatter/html_builder.rb +0 -121
  111. data/lib/cucumber/formatter/inline-js.js +0 -30
  112. data/lib/cucumber/formatter/jquery-min.js +0 -154
  113. data/lib/cucumber/formatter/json_pretty.rb +0 -11
  114. data/lib/cucumber/formatter/legacy_api/adapter.rb +0 -1028
  115. data/lib/cucumber/formatter/legacy_api/ast.rb +0 -394
  116. data/lib/cucumber/formatter/legacy_api/results.rb +0 -50
  117. data/lib/cucumber/formatter/legacy_api/runtime_facade.rb +0 -32
  118. data/lib/cucumber/step_argument.rb +0 -25
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
3
+ require 'json'
4
4
  require 'base64'
5
5
  require 'cucumber/formatter/backtrace_filter'
6
6
  require 'cucumber/formatter/io'
7
- require 'cucumber/formatter/hook_query_visitor'
7
+ require 'cucumber/formatter/ast_lookup'
8
8
 
9
9
  module Cucumber
10
10
  module Formatter
@@ -13,7 +13,8 @@ module Cucumber
13
13
  include Io
14
14
 
15
15
  def initialize(config)
16
- @io = ensure_io(config.out_stream)
16
+ @io = ensure_io(config.out_stream, config.error_stream)
17
+ @ast_lookup = AstLookup.new(config)
17
18
  @feature_hashes = []
18
19
  @step_or_hook_hash = {}
19
20
  config.on_event :test_case_started, &method(:on_test_case_started)
@@ -25,36 +26,40 @@ module Cucumber
25
26
 
26
27
  def on_test_case_started(event)
27
28
  test_case = event.test_case
28
- builder = Builder.new(test_case)
29
- unless same_feature_as_previous_test_case?(test_case.feature)
29
+ builder = Builder.new(test_case, @ast_lookup)
30
+ unless same_feature_as_previous_test_case?(test_case)
30
31
  @feature_hash = builder.feature_hash
31
32
  @feature_hashes << @feature_hash
32
33
  end
33
34
  @test_case_hash = builder.test_case_hash
34
- if builder.background?
35
- feature_elements << builder.background_hash
36
- @element_hash = builder.background_hash
37
- else
38
- feature_elements << @test_case_hash
39
- @element_hash = @test_case_hash
40
- end
35
+
36
+ @element_hash = nil
37
+ @element_background_hash = builder.background_hash
38
+ @in_background = builder.background?
39
+
41
40
  @any_step_failed = false
42
41
  end
43
42
 
44
43
  def on_test_step_started(event)
45
44
  test_step = event.test_step
46
45
  return if internal_hook?(test_step)
47
- hook_query = HookQueryVisitor.new(test_step)
48
- if hook_query.hook?
46
+
47
+ if @element_hash.nil?
48
+ @element_hash = create_element_hash(test_step)
49
+ feature_elements << @element_hash
50
+ end
51
+
52
+ if test_step.hook?
49
53
  @step_or_hook_hash = {}
50
- hooks_of_type(hook_query) << @step_or_hook_hash
54
+ hooks_of_type(test_step) << @step_or_hook_hash
51
55
  return
52
56
  end
53
57
  if first_step_after_background?(test_step)
58
+ @in_background = false
54
59
  feature_elements << @test_case_hash
55
60
  @element_hash = @test_case_hash
56
61
  end
57
- @step_or_hook_hash = create_step_hash(test_step.source.last)
62
+ @step_or_hook_hash = create_step_hash(test_step)
58
63
  steps << @step_or_hook_hash
59
64
  @step_hash = @step_or_hook_hash
60
65
  end
@@ -68,50 +73,47 @@ module Cucumber
68
73
  end
69
74
 
70
75
  def on_test_case_finished(event)
76
+ feature_elements << @test_case_hash if @in_background
77
+
71
78
  _test_case, result = *event.attributes
72
79
  result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
73
80
  add_failed_around_hook(result) if result.failed? && !@any_step_failed
74
81
  end
75
82
 
76
83
  def on_test_run_finished(_event)
77
- @io.write(MultiJson.dump(@feature_hashes, pretty: true))
78
- end
79
-
80
- def puts(message)
81
- test_step_output << message
84
+ @io.write(JSON.generate(@feature_hashes, pretty: true))
82
85
  end
83
86
 
84
- def embed(src, mime_type, _label)
85
- if File.file?(src)
86
- content = File.open(src, 'rb', &:read)
87
- data = encode64(content)
87
+ def attach(src, mime_type)
88
+ if mime_type == 'text/x.cucumber.log+plain'
89
+ test_step_output << src
90
+ return
91
+ end
92
+ if mime_type =~ /;base64$/
93
+ mime_type = mime_type[0..-8]
94
+ data = src
88
95
  else
89
- if mime_type =~ /;base64$/
90
- mime_type = mime_type[0..-8]
91
- data = src
92
- else
93
- data = encode64(src)
94
- end
96
+ data = encode64(src)
95
97
  end
96
98
  test_step_embeddings << { mime_type: mime_type, data: data }
97
99
  end
98
100
 
99
101
  private
100
102
 
101
- def same_feature_as_previous_test_case?(feature)
102
- current_feature[:uri] == feature.file && current_feature[:line] == feature.location.line
103
+ def same_feature_as_previous_test_case?(test_case)
104
+ current_feature[:uri] == test_case.location.file
103
105
  end
104
106
 
105
107
  def first_step_after_background?(test_step)
106
- test_step.source[1].to_s != @element_hash[:name]
108
+ @in_background && test_step.location.lines.max >= @test_case_hash[:line]
107
109
  end
108
110
 
109
111
  def internal_hook?(test_step)
110
- test_step.source.last.location.file.include?('lib/cucumber/')
112
+ test_step.location.file.include?('lib/cucumber/')
111
113
  end
112
114
 
113
115
  def current_feature
114
- @feature_hash ||= {}
116
+ @feature_hash ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
115
117
  end
116
118
 
117
119
  def feature_elements
@@ -122,16 +124,16 @@ module Cucumber
122
124
  @element_hash[:steps] ||= []
123
125
  end
124
126
 
125
- def hooks_of_type(hook_query)
126
- case hook_query.type
127
- when :before
128
- return before_hooks
129
- when :after
130
- return after_hooks
131
- when :after_step
132
- return after_step_hooks
127
+ def hooks_of_type(hook_step)
128
+ case hook_step.text
129
+ when 'Before hook'
130
+ before_hooks
131
+ when 'After hook'
132
+ after_hooks
133
+ when 'AfterStep hook'
134
+ after_step_hooks
133
135
  else
134
- fail 'Unknown hook type ' + hook_query.type.to_s
136
+ raise 'Unknown hook type ' + hook_step.to_s
135
137
  end
136
138
  end
137
139
 
@@ -159,20 +161,27 @@ module Cucumber
159
161
  @step_or_hook_hash[:embeddings] ||= []
160
162
  end
161
163
 
162
- def create_step_hash(step_source)
164
+ def create_element_hash(test_step)
165
+ return @element_background_hash if @in_background && !first_step_after_background?(test_step)
166
+
167
+ @in_background = false
168
+ @test_case_hash
169
+ end
170
+
171
+ def create_step_hash(test_step)
172
+ step_source = @ast_lookup.step_source(test_step).step
163
173
  step_hash = {
164
174
  keyword: step_source.keyword,
165
- name: step_source.to_s,
166
- line: step_source.original_location.line
175
+ name: test_step.text,
176
+ line: test_step.location.lines.min
167
177
  }
168
- step_hash[:comments] = Formatter.create_comments_array(step_source.comments) unless step_source.comments.empty?
169
- step_hash[:doc_string] = create_doc_string_hash(step_source.multiline_arg) if step_source.multiline_arg.doc_string?
170
- step_hash[:rows] = create_data_table_value(step_source.multiline_arg) if step_source.multiline_arg.data_table?
178
+ step_hash[:doc_string] = create_doc_string_hash(step_source.doc_string) unless step_source.doc_string.nil?
179
+ step_hash[:rows] = create_data_table_value(step_source.data_table) unless step_source.data_table.nil?
171
180
  step_hash
172
181
  end
173
182
 
174
183
  def create_doc_string_hash(doc_string)
175
- content_type = doc_string.content_type ? doc_string.content_type : ''
184
+ content_type = doc_string.media_type || ''
176
185
  {
177
186
  value: doc_string.content,
178
187
  content_type: content_type,
@@ -181,14 +190,15 @@ module Cucumber
181
190
  end
182
191
 
183
192
  def create_data_table_value(data_table)
184
- data_table.raw.map do |row|
185
- { cells: row }
193
+ data_table.rows.map do |row|
194
+ { cells: row.cells.map(&:value) }
186
195
  end
187
196
  end
188
197
 
189
198
  def add_match_and_result(test_step, result)
190
199
  @step_or_hook_hash[:match] = create_match_hash(test_step, result)
191
200
  @step_or_hook_hash[:result] = create_result_hash(result)
201
+ result.embeddings.each { |e| embed(e['src'], e['mime_type'], e['label']) } if result.respond_to?(:embeddings)
192
202
  end
193
203
 
194
204
  def add_failed_around_hook(result)
@@ -226,113 +236,96 @@ module Cucumber
226
236
  class Builder
227
237
  attr_reader :feature_hash, :background_hash, :test_case_hash
228
238
 
229
- def initialize(test_case)
239
+ def initialize(test_case, ast_lookup)
230
240
  @background_hash = nil
231
- test_case.describe_source_to(self)
232
- test_case.feature.background.describe_to(self)
241
+ uri = test_case.location.file
242
+ feature = ast_lookup.gherkin_document(uri).feature
243
+ feature(feature, uri)
244
+ background(feature.children.first.background) unless feature.children.first.background.nil?
245
+ scenario(ast_lookup.scenario_source(test_case), test_case)
233
246
  end
234
247
 
235
248
  def background?
236
249
  @background_hash != nil
237
250
  end
238
251
 
239
- def feature(feature)
252
+ def feature(feature, uri)
240
253
  @feature_hash = {
241
- uri: feature.file,
242
- id: create_id(feature),
254
+ id: create_id(feature.name),
255
+ uri: uri,
243
256
  keyword: feature.keyword,
244
- name: feature.to_s,
245
- description: feature.description,
257
+ name: feature.name,
258
+ description: value_or_empty_string(feature.description),
246
259
  line: feature.location.line
247
260
  }
248
- unless feature.tags.empty?
249
- @feature_hash[:tags] = create_tags_array(feature.tags)
250
- @test_case_hash[:tags] = if @test_case_hash[:tags]
251
- @feature_hash[:tags] + @test_case_hash[:tags]
252
- else
253
- @feature_hash[:tags]
254
- end
255
- end
256
- @feature_hash[:comments] = Formatter.create_comments_array(feature.comments) unless feature.comments.empty?
257
- @test_case_hash[:id].insert(0, @feature_hash[:id] + ';')
261
+ return if feature.tags.empty?
262
+ @feature_hash[:tags] = create_tags_array_from_hash_array(feature.tags)
258
263
  end
259
264
 
260
265
  def background(background)
261
266
  @background_hash = {
262
267
  keyword: background.keyword,
263
- name: background.to_s,
264
- description: background.description,
268
+ name: background.name,
269
+ description: value_or_empty_string(background.description),
265
270
  line: background.location.line,
266
- type: 'background'
271
+ type: 'background',
272
+ steps: []
267
273
  }
268
- @background_hash[:comments] = Formatter.create_comments_array(background.comments) unless background.comments.empty?
269
274
  end
270
275
 
271
- def scenario(scenario)
276
+ def scenario(scenario_source, test_case)
277
+ scenario = scenario_source.type == :Scenario ? scenario_source.scenario : scenario_source.scenario_outline
272
278
  @test_case_hash = {
273
- id: create_id(scenario),
279
+ id: "#{@feature_hash[:id]};#{create_id_from_scenario_source(scenario_source)}",
274
280
  keyword: scenario.keyword,
275
- name: scenario.to_s,
276
- description: scenario.description,
277
- line: scenario.location.line,
278
- type: 'scenario'
281
+ name: test_case.name,
282
+ description: value_or_empty_string(scenario.description),
283
+ line: test_case.location.lines.max,
284
+ type: 'scenario',
285
+ steps: []
279
286
  }
280
- @test_case_hash[:tags] = create_tags_array(scenario.tags) unless scenario.tags.empty?
281
- @test_case_hash[:comments] = Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty?
287
+ @test_case_hash[:tags] = create_tags_array_from_tags_array(test_case.tags) unless test_case.tags.empty?
282
288
  end
283
289
 
284
- def scenario_outline(scenario)
285
- @test_case_hash = {
286
- id: create_id(scenario) + ';' + @example_id,
287
- keyword: scenario.keyword,
288
- name: scenario.to_s,
289
- description: scenario.description,
290
- line: @row.location.line,
291
- type: 'scenario'
292
- }
293
- tags = []
294
- tags += create_tags_array(scenario.tags) unless scenario.tags.empty?
295
- tags += @examples_table_tags if @examples_table_tags
296
- @test_case_hash[:tags] = tags unless tags.empty?
297
- comments = []
298
- comments += Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty?
299
- comments += @examples_table_comments if @examples_table_comments
300
- comments += @row_comments if @row_comments
301
- @test_case_hash[:comments] = comments unless comments.empty?
302
- end
290
+ private
303
291
 
304
- def examples_table(examples_table)
305
- # the json file have traditionally used the header row as row 1,
306
- # wheras cucumber-ruby-core used the first example row as row 1.
307
- @example_id = create_id(examples_table) + ";#{@row.number + 1}"
292
+ def value_or_empty_string(value)
293
+ value.nil? ? '' : value
294
+ end
308
295
 
309
- @examples_table_tags = create_tags_array(examples_table.tags) unless examples_table.tags.empty?
310
- @examples_table_comments = Formatter.create_comments_array(examples_table.comments) unless examples_table.comments.empty?
296
+ def create_id(name)
297
+ name.downcase.tr(' ', '-')
311
298
  end
312
299
 
313
- def examples_table_row(row)
314
- @row = row
315
- @row_comments = Formatter.create_comments_array(row.comments) unless row.comments.empty?
300
+ def create_id_from_scenario_source(scenario_source)
301
+ if scenario_source.type == :Scenario
302
+ create_id(scenario_source.scenario.name)
303
+ else
304
+ scenario_outline_name = scenario_source.scenario_outline.name
305
+ examples_name = scenario_source.examples.name
306
+ row_number = calculate_row_number(scenario_source)
307
+ "#{create_id(scenario_outline_name)};#{create_id(examples_name)};#{row_number}"
308
+ end
316
309
  end
317
310
 
318
- private
311
+ def calculate_row_number(scenario_source)
312
+ scenario_source.examples.table_body.each_with_index do |row, index|
313
+ return index + 2 if row == scenario_source.row
314
+ end
315
+ end
319
316
 
320
- def create_id(element)
321
- element.to_s.downcase.tr(' ', '-')
317
+ def create_tags_array_from_hash_array(tags)
318
+ tags_array = []
319
+ tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
320
+ tags_array
322
321
  end
323
322
 
324
- def create_tags_array(tags)
323
+ def create_tags_array_from_tags_array(tags)
325
324
  tags_array = []
326
325
  tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
327
326
  tags_array
328
327
  end
329
328
  end
330
329
  end
331
-
332
- def self.create_comments_array(comments)
333
- comments_array = []
334
- comments.each { |comment| comments_array << { value: comment.to_s.strip, line: comment.location.line } }
335
- comments_array
336
- end
337
330
  end
338
331
  end
@@ -5,6 +5,7 @@ require 'cucumber/formatter/backtrace_filter'
5
5
  require 'cucumber/formatter/io'
6
6
  require 'cucumber/formatter/interceptor'
7
7
  require 'fileutils'
8
+ require 'cucumber/formatter/ast_lookup'
8
9
 
9
10
  module Cucumber
10
11
  module Formatter
@@ -19,6 +20,7 @@ module Cucumber
19
20
  end
20
21
 
21
22
  def initialize(config)
23
+ @ast_lookup = AstLookup.new(config)
22
24
  config.on_event :test_case_started, &method(:on_test_case_started)
23
25
  config.on_event :test_case_finished, &method(:on_test_case_finished)
24
26
  config.on_event :test_step_finished, &method(:on_test_step_finished)
@@ -33,17 +35,15 @@ module Cucumber
33
35
  tests: 0,
34
36
  skipped: 0,
35
37
  time: 0,
36
- builder: Builder::XmlMarkup.new(:indent => 2)
38
+ builder: Builder::XmlMarkup.new(indent: 2)
37
39
  }
38
40
  end
39
41
  end
40
42
 
41
43
  def on_test_case_started(event)
42
44
  test_case = event.test_case
43
- unless same_feature_as_previous_test_case?(test_case.feature)
44
- start_feature(test_case.feature)
45
- end
46
- @failing_step_source = nil
45
+ start_feature(test_case) unless same_feature_as_previous_test_case?(test_case)
46
+ @failing_test_step = nil
47
47
  # In order to fill out <system-err/> and <system-out/>, we need to
48
48
  # intercept the $stderr and $stdout
49
49
  @interceptedout = Interceptor::Pipe.wrap(:stdout)
@@ -52,15 +52,15 @@ module Cucumber
52
52
 
53
53
  def on_test_step_finished(event)
54
54
  test_step, result = *event.attributes
55
- return if @failing_step_source
55
+ return if @failing_test_step
56
56
 
57
- @failing_step_source = test_step.source.last unless result.ok?(@config.strict)
57
+ @failing_test_step = test_step unless result.ok?(@config.strict)
58
58
  end
59
59
 
60
60
  def on_test_case_finished(event)
61
61
  test_case, result = *event.attributes
62
62
  result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
63
- test_case_name = NameBuilder.new(test_case)
63
+ test_case_name = NameBuilder.new(test_case, @ast_lookup)
64
64
  scenario = test_case_name.scenario_name
65
65
  scenario_designation = "#{scenario}#{test_case_name.name_suffix}"
66
66
  output = create_output_string(test_case, scenario, result, test_case_name.row_name)
@@ -71,45 +71,54 @@ module Cucumber
71
71
  end
72
72
 
73
73
  def on_test_run_finished(_event)
74
- @features_data.each { |file, data| end_feature(data) }
74
+ @features_data.each { |_file, data| end_feature(data) }
75
75
  end
76
76
 
77
77
  private
78
78
 
79
- def same_feature_as_previous_test_case?(feature)
80
- @current_feature_data && @current_feature_data[:feature].file == feature.file && @current_feature_data[:feature].location == feature.location
79
+ def same_feature_as_previous_test_case?(test_case)
80
+ @current_feature_data && @current_feature_data[:uri] == test_case.location.file
81
81
  end
82
82
 
83
- def start_feature(feature)
84
- raise UnNamedFeatureError.new(feature.file) if feature.name.empty?
85
- @current_feature_data = @features_data[feature.file]
83
+ def start_feature(test_case)
84
+ uri = test_case.location.file
85
+ feature = @ast_lookup.gherkin_document(uri).feature
86
+ raise UnNamedFeatureError, uri if feature.name.empty?
87
+ @current_feature_data = @features_data[uri]
88
+ @current_feature_data[:uri] = uri unless @current_feature_data[:uri]
86
89
  @current_feature_data[:feature] = feature unless @current_feature_data[:feature]
87
90
  end
88
91
 
89
92
  def end_feature(feature_data)
90
- @testsuite = Builder::XmlMarkup.new(:indent => 2)
93
+ @testsuite = Builder::XmlMarkup.new(indent: 2)
91
94
  @testsuite.instruct!
92
95
  @testsuite.testsuite(
93
- :failures => feature_data[:failures],
94
- :errors => feature_data[:errors],
95
- :skipped => feature_data[:skipped],
96
- :tests => feature_data[:tests],
97
- :time => format('%.6f', feature_data[:time]),
98
- :name => feature_data[:feature].name
96
+ failures: feature_data[:failures],
97
+ errors: feature_data[:errors],
98
+ skipped: feature_data[:skipped],
99
+ tests: feature_data[:tests],
100
+ time: format('%<time>.6f', time: feature_data[:time]),
101
+ name: feature_data[:feature].name
99
102
  ) do
100
103
  @testsuite << feature_data[:builder].target!
101
104
  end
102
105
 
103
- write_file(feature_result_filename(feature_data[:feature].file), @testsuite.target!)
106
+ write_file(feature_result_filename(feature_data[:uri]), @testsuite.target!)
104
107
  end
105
108
 
106
109
  def create_output_string(test_case, scenario, result, row_name)
107
- output = "#{test_case.keyword}: #{scenario}\n\n"
110
+ scenario_source = @ast_lookup.scenario_source(test_case)
111
+ keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
112
+ output = "#{keyword}: #{scenario}\n\n"
108
113
  return output if result.ok?(@config.strict)
109
- if test_case.keyword == 'Scenario'
110
- if @failing_step_source
111
- output += @failing_step_source.keyword.to_s unless hook?(@failing_step_source)
112
- output += "#{@failing_step_source}\n"
114
+ if scenario_source.type == :Scenario
115
+ if @failing_test_step
116
+ if @failing_test_step.hook?
117
+ output += "#{@failing_test_step.text} at #{@failing_test_step.location}\n"
118
+ else
119
+ step_source = @ast_lookup.step_source(@failing_test_step).step
120
+ output += "#{step_source.keyword}#{@failing_test_step.text}\n"
121
+ end
113
122
  else # An Around hook has failed
114
123
  output += "Around hook\n"
115
124
  end
@@ -119,24 +128,23 @@ module Cucumber
119
128
  output + "\nMessage:\n"
120
129
  end
121
130
 
122
- def hook?(step)
123
- ['Before hook', 'After hook', 'AfterStep hook'].include? step.text
124
- end
125
-
126
131
  def build_testcase(result, scenario_designation, output)
127
132
  duration = ResultBuilder.new(result).test_case_duration
128
133
  @current_feature_data[:time] += duration
129
134
  classname = @current_feature_data[:feature].name
135
+ filename = @current_feature_data[:uri]
130
136
  name = scenario_designation
131
137
 
132
- @current_feature_data[:builder].testcase(:classname => classname, :name => name, :time => format('%.6f', duration)) do
138
+ testcase_attributes = get_testcase_attributes(classname, name, duration, filename)
139
+
140
+ @current_feature_data[:builder].testcase(testcase_attributes) do
133
141
  if !result.passed? && result.ok?(@config.strict)
134
142
  @current_feature_data[:builder].skipped
135
143
  @current_feature_data[:skipped] += 1
136
144
  elsif !result.passed?
137
145
  status = result.to_sym
138
146
  exception = get_backtrace_object(result)
139
- @current_feature_data[:builder].failure(:message => "#{status} #{name}", :type => status) do
147
+ @current_feature_data[:builder].failure(message: "#{status} #{name}", type: status) do
140
148
  @current_feature_data[:builder].cdata! output
141
149
  @current_feature_data[:builder].cdata!(format_exception(exception)) if exception
142
150
  end
@@ -152,13 +160,25 @@ module Cucumber
152
160
  @current_feature_data[:tests] += 1
153
161
  end
154
162
 
163
+ def get_testcase_attributes(classname, name, duration, filename)
164
+ { classname: classname, name: name, time: format('%<duration>.6f', duration: duration) }.tap do |attributes|
165
+ attributes[:file] = filename if add_fileattribute?
166
+ end
167
+ end
168
+
169
+ def add_fileattribute?
170
+ return false if @config.formats.nil? || @config.formats.empty?
171
+
172
+ !!@config.formats.find do |format|
173
+ format.first == 'junit' && format.dig(1, 'fileattribute') == 'true'
174
+ end
175
+ end
176
+
155
177
  def get_backtrace_object(result)
156
178
  if result.failed?
157
- return result.exception
179
+ result.exception
158
180
  elsif result.backtrace
159
- return result
160
- else
161
- return nil
181
+ result
162
182
  end
163
183
  end
164
184
 
@@ -171,7 +191,7 @@ module Cucumber
171
191
  end
172
192
 
173
193
  def basename(feature_file)
174
- File.basename(feature_file.gsub(/[\\\/]/, '-'), '.feature')
194
+ File.basename(feature_file.gsub(/[\\\/]/, '-'), '.feature') # rubocop:disable Style/RegexpLiteral
175
195
  end
176
196
 
177
197
  def write_file(feature_filename, data)
@@ -187,34 +207,29 @@ module Cucumber
187
207
  class NameBuilder
188
208
  attr_reader :scenario_name, :name_suffix, :row_name
189
209
 
190
- def initialize(test_case)
210
+ def initialize(test_case, ast_lookup)
191
211
  @name_suffix = ''
192
212
  @row_name = ''
193
- test_case.describe_source_to self
194
- end
195
-
196
- def feature(*)
197
- self
213
+ scenario_source = ast_lookup.scenario_source(test_case)
214
+ if scenario_source.type == :Scenario
215
+ scenario(scenario_source.scenario)
216
+ else
217
+ scenario_outline(scenario_source.scenario_outline)
218
+ examples_table_row(scenario_source.row)
219
+ end
198
220
  end
199
221
 
200
222
  def scenario(scenario)
201
- @scenario_name = (scenario.name.nil? || scenario.name == '') ? 'Unnamed scenario' : scenario.name
202
- self
223
+ @scenario_name = scenario.name.empty? ? 'Unnamed scenario' : scenario.name
203
224
  end
204
225
 
205
226
  def scenario_outline(outline)
206
- @scenario_name = (outline.name.nil? || outline.name == '') ? 'Unnamed scenario outline' : outline.name
207
- self
208
- end
209
-
210
- def examples_table(*)
211
- self
227
+ @scenario_name = outline.name.empty? ? 'Unnamed scenario outline' : outline.name
212
228
  end
213
229
 
214
230
  def examples_table_row(row)
215
- @row_name = '| ' + row.values.join(' | ') + ' |'
231
+ @row_name = '| ' + row.cells.map(&:value).join(' | ') + ' |'
216
232
  @name_suffix = " (outline example : #{@row_name})"
217
- self
218
233
  end
219
234
  end
220
235
 
@@ -238,8 +253,10 @@ module Cucumber
238
253
  def exception(*) end
239
254
 
240
255
  def duration(duration, *)
241
- duration.tap { |duration| @test_case_duration = duration.nanoseconds / 10**9.0 }
256
+ duration.tap { |dur| @test_case_duration = dur.nanoseconds / 10**9.0 }
242
257
  end
258
+
259
+ def attach(*) end
243
260
  end
244
261
  end
245
262
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/formatter/io'
4
+ require 'cucumber/formatter/message_builder'
5
+
6
+ module Cucumber
7
+ module Formatter
8
+ # The formatter used for <tt>--format message</tt>
9
+ class Message < MessageBuilder
10
+ include Io
11
+
12
+ def initialize(config)
13
+ @io = ensure_io(config.out_stream, config.error_stream)
14
+ super(config)
15
+ end
16
+
17
+ def output_envelope(envelope)
18
+ @io.write(envelope.to_json)
19
+ @io.write("\n")
20
+ end
21
+ end
22
+ end
23
+ end