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