cucumber 3.0.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +216 -17
  3. data/CONTRIBUTING.md +4 -21
  4. data/README.md +8 -10
  5. data/bin/cucumber +1 -1
  6. data/lib/autotest/cucumber.rb +1 -0
  7. data/lib/autotest/cucumber_mixin.rb +35 -39
  8. data/lib/autotest/cucumber_rails.rb +1 -0
  9. data/lib/autotest/cucumber_rails_rspec.rb +1 -0
  10. data/lib/autotest/cucumber_rails_rspec2.rb +1 -0
  11. data/lib/autotest/cucumber_rspec.rb +1 -0
  12. data/lib/autotest/cucumber_rspec2.rb +1 -0
  13. data/lib/autotest/discover.rb +1 -0
  14. data/lib/cucumber.rb +2 -1
  15. data/lib/cucumber/cli/configuration.rb +6 -5
  16. data/lib/cucumber/cli/main.rb +14 -14
  17. data/lib/cucumber/cli/options.rb +113 -116
  18. data/lib/cucumber/cli/profile_loader.rb +50 -29
  19. data/lib/cucumber/cli/rerun_file.rb +1 -0
  20. data/lib/cucumber/configuration.rb +38 -29
  21. data/lib/cucumber/constantize.rb +8 -10
  22. data/lib/cucumber/core_ext/string.rb +1 -0
  23. data/lib/cucumber/deprecate.rb +32 -8
  24. data/lib/cucumber/encoding.rb +2 -1
  25. data/lib/cucumber/errors.rb +6 -7
  26. data/lib/cucumber/events.rb +14 -7
  27. data/lib/cucumber/events/envelope.rb +9 -0
  28. data/lib/cucumber/events/gherkin_source_parsed.rb +11 -0
  29. data/lib/cucumber/events/gherkin_source_read.rb +1 -4
  30. data/lib/cucumber/events/hook_test_step_created.rb +13 -0
  31. data/lib/cucumber/events/step_activated.rb +6 -6
  32. data/lib/cucumber/events/step_definition_registered.rb +4 -8
  33. data/lib/cucumber/events/test_case_created.rb +13 -0
  34. data/lib/cucumber/events/test_case_finished.rb +0 -4
  35. data/lib/cucumber/events/test_case_ready.rb +12 -0
  36. data/lib/cucumber/events/test_case_started.rb +0 -4
  37. data/lib/cucumber/events/test_run_finished.rb +2 -3
  38. data/lib/cucumber/events/test_run_started.rb +2 -4
  39. data/lib/cucumber/events/test_step_created.rb +13 -0
  40. data/lib/cucumber/events/test_step_finished.rb +0 -4
  41. data/lib/cucumber/events/test_step_started.rb +1 -5
  42. data/lib/cucumber/events/undefined_parameter_type.rb +10 -0
  43. data/lib/cucumber/file_specs.rb +7 -6
  44. data/lib/cucumber/filters.rb +2 -0
  45. data/lib/cucumber/filters/activate_steps.rb +6 -4
  46. data/lib/cucumber/filters/apply_after_hooks.rb +1 -0
  47. data/lib/cucumber/filters/apply_after_step_hooks.rb +1 -0
  48. data/lib/cucumber/filters/apply_around_hooks.rb +1 -0
  49. data/lib/cucumber/filters/apply_before_hooks.rb +1 -0
  50. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  51. data/lib/cucumber/filters/broadcast_test_run_started_event.rb +2 -1
  52. data/lib/cucumber/filters/gated_receiver.rb +1 -2
  53. data/lib/cucumber/filters/prepare_world.rb +6 -13
  54. data/lib/cucumber/filters/quit.rb +3 -6
  55. data/lib/cucumber/filters/randomizer.rb +6 -7
  56. data/lib/cucumber/filters/retry.rb +2 -2
  57. data/lib/cucumber/filters/tag_limits.rb +2 -2
  58. data/lib/cucumber/filters/tag_limits/test_case_index.rb +1 -2
  59. data/lib/cucumber/filters/tag_limits/verifier.rb +3 -6
  60. data/lib/cucumber/formatter/ansicolor.rb +33 -37
  61. data/lib/cucumber/formatter/ast_lookup.rb +165 -0
  62. data/lib/cucumber/formatter/backtrace_filter.rb +10 -10
  63. data/lib/cucumber/formatter/console.rb +65 -74
  64. data/lib/cucumber/formatter/console_counts.rb +4 -9
  65. data/lib/cucumber/formatter/console_issues.rb +9 -6
  66. data/lib/cucumber/formatter/duration.rb +2 -1
  67. data/lib/cucumber/formatter/duration_extractor.rb +4 -2
  68. data/lib/cucumber/formatter/errors.rb +6 -0
  69. data/lib/cucumber/formatter/fail_fast.rb +9 -6
  70. data/lib/cucumber/formatter/fanout.rb +3 -3
  71. data/lib/cucumber/formatter/html.rb +11 -602
  72. data/lib/cucumber/formatter/http_io.rb +146 -0
  73. data/lib/cucumber/formatter/ignore_missing_messages.rb +2 -3
  74. data/lib/cucumber/formatter/interceptor.rb +11 -18
  75. data/lib/cucumber/formatter/io.rb +18 -11
  76. data/lib/cucumber/formatter/json.rb +102 -109
  77. data/lib/cucumber/formatter/junit.rb +73 -68
  78. data/lib/cucumber/formatter/message.rb +22 -0
  79. data/lib/cucumber/formatter/message_builder.rb +255 -0
  80. data/lib/cucumber/formatter/pretty.rb +360 -153
  81. data/lib/cucumber/formatter/progress.rb +31 -32
  82. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  83. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  84. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  85. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  86. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  87. data/lib/cucumber/formatter/rerun.rb +23 -4
  88. data/lib/cucumber/formatter/stepdefs.rb +2 -2
  89. data/lib/cucumber/formatter/steps.rb +4 -5
  90. data/lib/cucumber/formatter/summary.rb +17 -9
  91. data/lib/cucumber/formatter/unicode.rb +16 -18
  92. data/lib/cucumber/formatter/usage.rb +30 -26
  93. data/lib/cucumber/gherkin/data_table_parser.rb +18 -6
  94. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +83 -86
  95. data/lib/cucumber/gherkin/formatter/escaping.rb +13 -12
  96. data/lib/cucumber/gherkin/i18n.rb +1 -0
  97. data/lib/cucumber/gherkin/steps_parser.rb +18 -8
  98. data/lib/cucumber/glue/dsl.rb +2 -1
  99. data/lib/cucumber/glue/hook.rb +35 -11
  100. data/lib/cucumber/glue/invoke_in_world.rb +15 -20
  101. data/lib/cucumber/glue/proto_world.rb +47 -39
  102. data/lib/cucumber/glue/registry_and_more.rb +54 -23
  103. data/lib/cucumber/glue/snippet.rb +24 -27
  104. data/lib/cucumber/glue/step_definition.rb +51 -28
  105. data/lib/cucumber/glue/world_factory.rb +1 -3
  106. data/lib/cucumber/hooks.rb +24 -14
  107. data/lib/cucumber/load_path.rb +1 -0
  108. data/lib/cucumber/multiline_argument.rb +6 -8
  109. data/lib/cucumber/multiline_argument/data_table.rb +106 -73
  110. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +8 -11
  111. data/lib/cucumber/multiline_argument/doc_string.rb +2 -1
  112. data/lib/cucumber/platform.rb +4 -3
  113. data/lib/cucumber/project_initializer.rb +1 -1
  114. data/lib/cucumber/rake/task.rb +21 -18
  115. data/lib/cucumber/rspec/disable_option_parser.rb +10 -8
  116. data/lib/cucumber/rspec/doubles.rb +1 -0
  117. data/lib/cucumber/running_test_case.rb +4 -54
  118. data/lib/cucumber/runtime.rb +57 -61
  119. data/lib/cucumber/runtime/after_hooks.rb +9 -4
  120. data/lib/cucumber/runtime/before_hooks.rb +9 -4
  121. data/lib/cucumber/runtime/for_programming_languages.rb +12 -9
  122. data/lib/cucumber/runtime/step_hooks.rb +5 -2
  123. data/lib/cucumber/runtime/support_code.rb +16 -22
  124. data/lib/cucumber/runtime/user_interface.rb +8 -19
  125. data/lib/cucumber/step_definition_light.rb +6 -4
  126. data/lib/cucumber/step_definitions.rb +3 -2
  127. data/lib/cucumber/step_match.rb +20 -18
  128. data/lib/cucumber/step_match_search.rb +9 -9
  129. data/lib/cucumber/term/ansicolor.rb +39 -39
  130. data/lib/cucumber/unit.rb +1 -0
  131. data/lib/cucumber/version +1 -1
  132. data/lib/simplecov_setup.rb +1 -0
  133. metadata +214 -127
  134. data/lib/cucumber/formatter/cucumber.css +0 -286
  135. data/lib/cucumber/formatter/cucumber.sass +0 -247
  136. data/lib/cucumber/formatter/hook_query_visitor.rb +0 -41
  137. data/lib/cucumber/formatter/html_builder.rb +0 -120
  138. data/lib/cucumber/formatter/inline-js.js +0 -30
  139. data/lib/cucumber/formatter/jquery-min.js +0 -154
  140. data/lib/cucumber/formatter/json_pretty.rb +0 -10
  141. data/lib/cucumber/formatter/legacy_api/adapter.rb +0 -1028
  142. data/lib/cucumber/formatter/legacy_api/ast.rb +0 -394
  143. data/lib/cucumber/formatter/legacy_api/results.rb +0 -50
  144. data/lib/cucumber/formatter/legacy_api/runtime_facade.rb +0 -32
  145. data/lib/cucumber/step_argument.rb +0 -24
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ModuleLength
4
+
2
5
  require 'cucumber/formatter/ansicolor'
3
6
  require 'cucumber/formatter/duration'
4
7
  require 'cucumber/gherkin/i18n'
5
8
 
6
9
  module Cucumber
7
10
  module Formatter
8
-
9
11
  # This module contains helper methods that are used by formatters that
10
12
  # print output to the terminal.
11
13
  #
@@ -46,7 +48,7 @@ module Cucumber
46
48
  def format_string(o, status)
47
49
  fmt = format_for(status)
48
50
  o.to_s.split("\n").map do |line|
49
- if Proc === fmt
51
+ if Proc == fmt.class
50
52
  fmt.call(line)
51
53
  else
52
54
  fmt % line
@@ -54,10 +56,6 @@ module Cucumber
54
56
  end.join("\n")
55
57
  end
56
58
 
57
- def print_steps(status)
58
- print_elements(runtime.steps(status), status, 'steps')
59
- end
60
-
61
59
  def print_elements(elements, status, kind)
62
60
  return if elements.empty?
63
61
 
@@ -109,28 +107,32 @@ module Cucumber
109
107
  end
110
108
 
111
109
  # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/10655
112
- def linebreaks(s, max)
113
- return s unless max && max > 0
114
- s.gsub(/.{1,#{max}}(?:\s|\Z)/){($& + 5.chr).gsub(/\n\005/,"\n").gsub(/\005/,"\n")}.rstrip
110
+ def linebreaks(msg, max)
111
+ return msg unless max && max > 0
112
+ msg.gsub(/.{1,#{max}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") }.rstrip
115
113
  end
116
114
 
117
- def collect_snippet_data(test_step, result)
115
+ def collect_snippet_data(test_step, ast_lookup)
118
116
  # collect snippet data for undefined steps
119
- return if hook?(test_step)
120
- keyword = test_step.source.last.actual_keyword(@previous_step_keyword)
121
- @previous_step_keyword = keyword
122
- return unless result.undefined?
123
- @snippets_input << Console::SnippetData.new(keyword, test_step.source.last)
117
+ keyword = ast_lookup.snippet_step_keyword(test_step)
118
+ @snippets_input << Console::SnippetData.new(keyword, test_step)
119
+ end
120
+
121
+ def collect_undefined_parameter_type_names(undefined_parameter_type)
122
+ @undefined_parameter_types << undefined_parameter_type.type_name
124
123
  end
125
124
 
126
125
  def print_snippets(options)
127
126
  return unless options[:snippets]
128
- return if runtime.steps(:undefined).empty?
129
127
 
130
- snippet_text_proc = lambda { |step_keyword, step_name, multiline_arg|
131
- runtime.snippet_text(step_keyword, step_name, multiline_arg)
132
- }
133
- do_print_snippets(snippet_text_proc)
128
+ snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg|
129
+ snippet_text(step_keyword, step_name, multiline_arg)
130
+ end
131
+ do_print_snippets(snippet_text_proc) unless @snippets_input.empty?
132
+
133
+ @undefined_parameter_types.map do |type_name|
134
+ do_print_undefined_parameter_type_snippet(type_name)
135
+ end
134
136
  end
135
137
 
136
138
  def do_print_snippets(snippet_text_proc)
@@ -146,10 +148,14 @@ module Cucumber
146
148
  @io.flush
147
149
  end
148
150
 
149
- def print_passing_wip(options)
150
- return unless options[:wip]
151
- passed_messages = element_messages(runtime.scenarios(:passed), :passed)
152
- do_print_passing_wip(passed_messages)
151
+ def print_passing_wip(config, passed_test_cases, ast_lookup)
152
+ return unless config.wip?
153
+ messages = passed_test_cases.map do |test_case|
154
+ scenario_source = ast_lookup.scenario_source(test_case)
155
+ keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
156
+ linebreaks("#{test_case.location.on_line(test_case.location.lines.max)}:in `#{keyword}: #{test_case.name}'", ENV['CUCUMBER_TRUNCATE_OUTPUT'].to_i)
157
+ end
158
+ do_print_passing_wip(messages)
153
159
  end
154
160
 
155
161
  def do_print_passing_wip(passed_messages)
@@ -161,62 +167,50 @@ module Cucumber
161
167
  end
162
168
  end
163
169
 
164
- def embed(file, mime_type, label)
165
- # no-op
166
- end
167
-
168
- #define @delayed_messages = [] in your Formatter if you want to
169
- #activate this feature
170
- def puts(*messages)
171
- if @delayed_messages
172
- @delayed_messages += messages
173
- else
174
- if @io
175
- @io.puts
176
- messages.each do |message|
177
- @io.puts(format_string(message, :tag))
178
- end
179
- @io.flush
180
- end
181
- end
182
- end
183
-
184
- def print_messages
185
- @delayed_messages.each {|message| print_message(message)}
186
- empty_messages
187
- end
188
-
189
- def print_table_row_messages
190
- return if @delayed_messages.empty?
191
- @io.print(format_string(@delayed_messages.join(', '), :tag).indent(2))
192
- @io.flush
193
- empty_messages
194
- end
195
-
196
- def print_message(message)
197
- @io.puts(format_string(message, :tag).indent(@indent))
170
+ def attach(src, media_type)
171
+ return unless media_type == 'text/x.cucumber.log+plain'
172
+ return unless @io
173
+ @io.puts
174
+ @io.puts(format_string(src, :tag))
198
175
  @io.flush
199
176
  end
200
177
 
201
- def empty_messages
202
- @delayed_messages = []
203
- end
204
-
205
178
  def print_profile_information
206
179
  return if @options[:skip_profile_information] || @options[:profiles].nil? || @options[:profiles].empty?
207
180
  do_print_profile_information(@options[:profiles])
208
181
  end
209
182
 
210
183
  def do_print_profile_information(profiles)
211
- profiles_sentence = profiles.size == 1 ? profiles.first :
212
- "#{profiles[0...-1].join(', ')} and #{profiles.last}"
184
+ profiles_sentence = if profiles.size == 1
185
+ profiles.first
186
+ else
187
+ "#{profiles[0...-1].join(', ')} and #{profiles.last}"
188
+ end
189
+
190
+ @io.puts "Using the #{profiles_sentence} profile#{'s' if profiles.size > 1}..."
191
+ end
192
+
193
+ def do_print_undefined_parameter_type_snippet(type_name)
194
+ camelized = type_name.split(/_|-/).collect(&:capitalize).join
213
195
 
214
- @io.puts "Using the #{profiles_sentence} profile#{'s' if profiles.size> 1}..."
196
+ @io.puts [
197
+ "The parameter #{type_name} is not defined. You can define a new one with:",
198
+ '',
199
+ 'ParameterType(',
200
+ " name: '#{type_name}',",
201
+ ' regexp: /some regexp here/,',
202
+ " type: #{camelized},",
203
+ ' # The transformer takes as many arguments as there are capture groups in the regexp,',
204
+ ' # or just one if there are none.',
205
+ " transformer: ->(s) { #{camelized}.new(s) }",
206
+ ')',
207
+ ''
208
+ ].join("\n")
215
209
  end
216
210
 
217
211
  private
218
212
 
219
- FORMATS = Hash.new{ |hash, format| hash[format] = method(format).to_proc }
213
+ FORMATS = Hash.new { |hash, format| hash[format] = method(format).to_proc }
220
214
 
221
215
  def format_for(*keys)
222
216
  key = keys.join('_').to_sym
@@ -225,10 +219,6 @@ module Cucumber
225
219
  fmt
226
220
  end
227
221
 
228
- def hook?(test_step)
229
- not test_step.source.last.respond_to?(:actual_keyword)
230
- end
231
-
232
222
  def element_messages(elements, status)
233
223
  elements.map do |element|
234
224
  if status == :failed
@@ -241,18 +231,19 @@ module Cucumber
241
231
 
242
232
  def snippet_text(step_keyword, step_name, multiline_arg)
243
233
  keyword = Cucumber::Gherkin::I18n.code_keyword_for(step_keyword).strip
244
- config.snippet_generators.map { |generator|
234
+ config.snippet_generators.map do |generator|
245
235
  generator.call(keyword, step_name, multiline_arg, config.snippet_type)
246
- }.join("\n")
236
+ end.join("\n")
247
237
  end
248
238
 
249
239
  class SnippetData
250
240
  attr_reader :actual_keyword, :step
251
241
  def initialize(actual_keyword, step)
252
- @actual_keyword, @step = actual_keyword, step
242
+ @actual_keyword = actual_keyword
243
+ @step = step
253
244
  end
254
245
  end
255
-
256
246
  end
257
247
  end
258
248
  end
249
+ # rubocop:enable Metrics/ModuleLength
@@ -29,15 +29,10 @@ module Cucumber
29
29
  end
30
30
 
31
31
  def status_counts(summary)
32
- counts = Core::Test::Result::TYPES.map { |status|
33
- count = summary.total(status)
34
- [status, count]
35
- }.select { |status, count|
36
- count > 0
37
- }.map { |status, count|
38
- format_string("#{count} #{status}", status)
39
- }
40
- "(#{counts.join(", ")})" if counts.any?
32
+ counts = Core::Test::Result::TYPES.map { |status| [status, summary.total(status)] }
33
+ counts = counts.select { |_status, count| count > 0 }
34
+ counts = counts.map { |status, count| format_string("#{count} #{status}", status) }
35
+ "(#{counts.join(', ')})" if counts.any?
41
36
  end
42
37
  end
43
38
  end
@@ -5,7 +5,7 @@ module Cucumber
5
5
  class ConsoleIssues
6
6
  include Console
7
7
 
8
- def initialize(config)
8
+ def initialize(config, ast_lookup = AstLookup.new(config))
9
9
  @previous_test_case = nil
10
10
  @issues = Hash.new { |h, k| h[k] = [] }
11
11
  @config = config
@@ -14,10 +14,11 @@ module Cucumber
14
14
  @previous_test_case = event.test_case
15
15
  @issues[event.result.to_sym] << event.test_case unless event.result.ok?(@config.strict)
16
16
  elsif event.result.passed?
17
- @issues[:flaky] << event.test_case unless Core::Test::Result::Flaky.ok?(@config.strict)
17
+ @issues[:flaky] << event.test_case unless Core::Test::Result::Flaky.ok?(@config.strict.strict?(:flaky))
18
18
  @issues[:failed].delete(event.test_case)
19
19
  end
20
20
  end
21
+ @ast_lookup = ast_lookup
21
22
  end
22
23
 
23
24
  def to_s
@@ -34,10 +35,12 @@ module Cucumber
34
35
 
35
36
  def scenario_listing(type, test_cases)
36
37
  return [] if test_cases.empty?
37
- [ format_string("#{type_heading(type)} Scenarios:", type) ] + test_cases.map { |test_case|
38
- source = @config.source? ? format_string(" # #{test_case.keyword}: #{test_case.name}", :comment) : ''
39
- format_string("cucumber #{profiles_string}" + test_case.location, type) + source
40
- }
38
+ [format_string("#{type_heading(type)} Scenarios:", type)] + test_cases.map do |test_case|
39
+ scenario_source = @ast_lookup.scenario_source(test_case)
40
+ keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
41
+ source = @config.source? ? format_string(" # #{keyword}: #{test_case.name}", :comment) : ''
42
+ format_string("cucumber #{profiles_string}#{test_case.location.file}:#{test_case.location.lines.max}", type) + source
43
+ end
41
44
  end
42
45
 
43
46
  def type_heading(type)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Cucumber
3
4
  module Formatter
4
5
  module Duration
@@ -7,7 +8,7 @@ module Cucumber
7
8
  # <tt>time</tt> format.
8
9
  def format_duration(seconds)
9
10
  m, s = seconds.divmod(60)
10
- "#{m}m#{format('%.3f', s)}s"
11
+ "#{m}m#{format('%<seconds>.3f', seconds: s)}s"
11
12
  end
12
13
  end
13
14
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Cucumber
3
4
  module Formatter
4
-
5
5
  class DurationExtractor
6
6
  attr_reader :result_duration
7
7
  def initialize(result)
@@ -22,8 +22,10 @@ module Cucumber
22
22
  def exception(*) end
23
23
 
24
24
  def duration(duration, *)
25
- duration.tap { |duration| @result_duration = duration.nanoseconds / 10**9.0 }
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
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'cucumber/formatter/io'
3
4
  require 'cucumber/formatter/console'
4
5
 
5
6
  module Cucumber
6
7
  module Formatter
7
-
8
8
  class FailFast
9
-
10
9
  def initialize(configuration)
10
+ @previous_test_case = nil
11
11
  configuration.on_event :test_case_finished do |event|
12
- _test_case, result = *event.attributes
13
- Cucumber.wants_to_quit = true unless result.ok?(configuration.strict)
12
+ test_case, result = *event.attributes
13
+ if test_case != @previous_test_case
14
+ @previous_test_case = event.test_case
15
+ Cucumber.wants_to_quit = true unless result.ok?(configuration.strict)
16
+ elsif result.passed?
17
+ Cucumber.wants_to_quit = false
18
+ end
14
19
  end
15
20
  end
16
-
17
21
  end
18
-
19
22
  end
20
23
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Cucumber
3
4
  module Formatter
4
-
5
5
  # Forwards any messages sent to this object to all recipients
6
6
  # that respond to that message.
7
7
  class Fanout < BasicObject
@@ -13,6 +13,8 @@ module Cucumber
13
13
  end
14
14
 
15
15
  def method_missing(message, *args)
16
+ super unless recipients
17
+
16
18
  recipients.each do |recipient|
17
19
  recipient.send(message, *args) if recipient.respond_to?(message)
18
20
  end
@@ -21,8 +23,6 @@ module Cucumber
21
23
  def respond_to_missing?(name, include_private = false)
22
24
  recipients.any? { |recipient| recipient.respond_to?(name, include_private) }
23
25
  end
24
-
25
26
  end
26
-
27
27
  end
28
28
  end
@@ -1,614 +1,23 @@
1
- # frozen_string_literal: true
2
- require 'erb'
3
- require 'cucumber/formatter/duration'
4
1
  require 'cucumber/formatter/io'
5
- require 'cucumber/formatter/html_builder'
6
- require 'pathname'
2
+ require 'cucumber/html_formatter'
3
+ require 'cucumber/formatter/message_builder'
7
4
 
8
5
  module Cucumber
9
6
  module Formatter
10
- class Html
11
-
12
- # TODO: remove coupling to types
13
- AST_CLASSES = {
14
- Cucumber::Core::Ast::Scenario => 'scenario',
15
- Cucumber::Core::Ast::ScenarioOutline => 'scenario outline'
16
- }
17
-
18
- AST_DATA_TABLE = LegacyApi::Ast::MultilineArg::DataTable
19
-
20
- include ERB::Util # for the #h method
21
- include Duration
7
+ class HTML < MessageBuilder
22
8
  include Io
23
9
 
24
- attr_reader :builder
25
- private :builder
26
-
27
- def initialize(runtime, path_or_io, options)
28
- @io = ensure_io(path_or_io)
29
- @runtime = runtime
30
- @options = options
31
- @buffer = {}
32
- @builder = HtmlBuilder.new(target: @io, indent: 0)
33
- @feature_number = 0
34
- @scenario_number = 0
35
- @step_number = 0
36
- @header_red = nil
37
- @delayed_messages = []
38
- @inside_outline = false
39
- @previous_step_keyword = nil
40
- end
41
-
42
- def embed(src, mime_type, label)
43
- if image?(mime_type)
44
- src = src_is_file_or_data?(src) ? src : "data:#{standardize_mime_type(mime_type)},#{src}"
45
- builder.embed(type: :image, src: set_path(src), label: label, id: next_id(:img))
46
- else
47
- builder.embed(type: :text, src: src, label: label, id: next_id(:text))
48
- end
49
- end
50
-
51
- def set_path(src)
52
- if @io.respond_to?(:path) && File.file?(src)
53
- out_dir = Pathname.new(File.dirname(File.absolute_path(@io.path)))
54
- src = Pathname.new(File.absolute_path(src)).relative_path_from(out_dir)
55
- end
56
-
57
- src
58
- end
59
-
60
- def standardize_mime_type(mime_type)
61
- mime_type =~ /;base[0-9]+$/ ? mime_type : mime_type + ';base64'
62
- end
63
-
64
- def src_is_file_or_data?(src)
65
- File.file?(src) || src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
66
- end
67
-
68
- def image?(mime_type)
69
- mime_type =~ /^image\/(png|gif|jpg|jpeg)/
70
- end
71
-
72
- def before_features(features)
73
- @step_count = features && features.step_count || 0 #TODO: Make this work with core!
74
-
75
- builder.build_document!
76
- builder.format_features! features
77
- end
78
-
79
- def after_features(features)
80
- print_stats(features)
81
- builder << '</div>'
82
- builder << '</body>'
83
- builder << '</html>'
84
- end
85
-
86
- def before_feature(_feature)
87
- @exceptions = []
88
- builder << '<div class="feature">'
89
- end
90
-
91
- def after_feature(_feature)
92
- builder << '</div>'
93
- end
94
-
95
- def before_comment(_comment)
96
- builder << '<pre class="comment">'
97
- end
98
-
99
- def after_comment(_comment)
100
- builder << '</pre>'
101
- end
102
-
103
- def comment_line(comment_line)
104
- builder.text!(comment_line)
105
- builder.br
106
- end
107
-
108
- def after_tags(_tags)
109
- @tag_spacer = nil
110
- end
111
-
112
- def tag_name(tag_name)
113
- builder.text!(@tag_spacer) if @tag_spacer
114
- @tag_spacer = ' '
115
- builder.span(tag_name, :class => 'tag')
116
- end
117
-
118
- def feature_name(keyword, name)
119
- lines = name.split(/\r?\n/)
120
- return if lines.empty?
121
- builder.h2 do |h2|
122
- builder.span(keyword + ': ' + lines[0], :class => 'val')
123
- end
124
- builder.p(:class => 'narrative') do
125
- lines[1..-1].each do |line|
126
- builder.text!(line.strip)
127
- builder.br
128
- end
129
- end
130
- end
131
-
132
- def before_test_case(_test_case)
133
- @previous_step_keyword = nil
134
- end
135
-
136
- def before_background(_background)
137
- @in_background = true
138
- builder << '<div class="background">'
139
- end
140
-
141
- def after_background(_background)
142
- @in_background = nil
143
- builder << '</div>'
144
- end
145
-
146
- def background_name(keyword, name, _file_colon_line, _source_indent)
147
- @listing_background = true
148
- builder.h3(:id => "background_#{@scenario_number}") do |h3|
149
- builder.span(keyword, :class => 'keyword')
150
- builder.text!(' ')
151
- builder.span(name, :class => 'val')
152
- end
153
- end
154
-
155
- def before_feature_element(feature_element)
156
- @scenario_number+=1
157
- @scenario_red = false
158
- css_class = AST_CLASSES[feature_element.class]
159
- builder << "<div class='#{css_class}'>"
160
- @in_scenario_outline = feature_element.class == Cucumber::Core::Ast::ScenarioOutline
161
- end
162
-
163
- def after_feature_element(_feature_element)
164
- unless @in_scenario_outline
165
- print_messages
166
- builder << '</ol>'
167
- end
168
- builder << '</div>'
169
- @in_scenario_outline = nil
170
- end
171
-
172
- def scenario_name(keyword, name, file_colon_line, _source_indent)
173
- builder.span(:class => 'scenario_file') do
174
- builder << file_colon_line
175
- end
176
- @listing_background = false
177
- scenario_id = "scenario_#{@scenario_number}"
178
- if @inside_outline
179
- @outline_row += 1
180
- scenario_id += "_#{@outline_row}"
181
- @scenario_red = false
182
- end
183
- builder.h3(:id => scenario_id) do
184
- builder.span(keyword + ':', :class => 'keyword')
185
- builder.text!(' ')
186
- builder.span(name, :class => 'val')
187
- end
188
- end
189
-
190
- def before_outline_table(_outline_table)
191
- @inside_outline = true
192
- @outline_row = 0
193
- builder << '<table>'
194
- end
195
-
196
- def after_outline_table(_outline_table)
197
- builder << '</table>'
198
- @outline_row = nil
199
- @inside_outline = false
200
- end
201
-
202
- def before_examples(_examples)
203
- builder << '<div class="examples">'
204
- end
205
-
206
- def after_examples(_examples)
207
- builder << '</div>'
208
- end
209
-
210
- def examples_name(keyword, name)
211
- builder.h4 do
212
- builder.span(keyword, :class => 'keyword')
213
- builder.text!(' ')
214
- builder.span(name, :class => 'val')
215
- end
216
- end
217
-
218
- def before_steps(_steps)
219
- builder << '<ol>'
220
- end
221
-
222
- def after_steps(_steps)
223
- print_messages
224
- builder << '</ol>' if @in_background || @in_scenario_outline
225
- end
226
-
227
- def before_step(step)
228
- print_messages
229
- @step_id = step.dom_id
230
- @step_number += 1
231
- @step = step
232
- end
233
-
234
- def after_step(_step)
235
- move_progress
236
- end
237
-
238
- def before_step_result(_keyword, step_match, _multiline_arg, status, exception, _source_indent, background, _file_colon_line)
239
- @step_match = step_match
240
- @hide_this_step = false
241
- if exception
242
- if @exceptions.include?(exception)
243
- @hide_this_step = true
244
- return
245
- end
246
- @exceptions << exception
247
- end
248
- if status != :failed && @in_background ^ background
249
- @hide_this_step = true
250
- return
251
- end
252
- @status = status
253
- return if @hide_this_step
254
- set_scenario_color(status)
255
- builder << "<li id='#{@step_id}' class='step #{status}'>"
256
- end
257
-
258
- def after_step_result(keyword, step_match, _multiline_arg, status, _exception, _source_indent, _background, _file_colon_line)
259
- return if @hide_this_step
260
- # print snippet for undefined steps
261
- unless outline_step?(@step)
262
- keyword = @step.actual_keyword(@previous_step_keyword)
263
- @previous_step_keyword = keyword
264
- end
265
- if status == :undefined
266
- builder.pre do |pre|
267
- # TODO: snippet text should be an event sent to the formatter so we don't
268
- # have this couping to the runtime.
269
- pre << @runtime.snippet_text(keyword,step_match.instance_variable_get('@name') || '', @step.multiline_arg)
270
- end
271
- end
272
- builder << '</li>'
273
- print_messages
274
- end
275
-
276
- def step_name(keyword, step_match, status, _source_indent, background, _file_colon_line)
277
- background_in_scenario = background && !@listing_background
278
- @skip_step = background_in_scenario
279
-
280
- unless @skip_step
281
- build_step(keyword, step_match, status)
282
- end
283
- end
284
-
285
- def exception(exception, _status)
286
- return if @hide_this_step
287
- print_messages
288
- build_exception_detail(exception)
289
- end
290
-
291
- def extra_failure_content(file_colon_line)
292
- @snippet_extractor ||= SnippetExtractor.new
293
- "<pre class=\"ruby\"><code>#{@snippet_extractor.snippet(file_colon_line)}</code></pre>"
294
- end
295
-
296
- def before_multiline_arg(multiline_arg)
297
- return if @hide_this_step || @skip_step
298
- if AST_DATA_TABLE === multiline_arg
299
- builder << '<table>'
300
- end
301
- end
302
-
303
- def after_multiline_arg(multiline_arg)
304
- return if @hide_this_step || @skip_step
305
- if AST_DATA_TABLE === multiline_arg
306
- builder << '</table>'
307
- end
308
- end
309
-
310
- def doc_string(string)
311
- return if @hide_this_step
312
- builder.pre(:class => 'val') do |pre|
313
- builder << h(string).gsub("\n", '&#x000A;')
314
- end
315
- end
316
-
317
- def before_table_row(table_row)
318
- @row_id = table_row.dom_id
319
- @col_index = 0
320
- return if @hide_this_step
321
- builder << "<tr class='step' id='#{@row_id}'>"
322
- end
323
-
324
- def after_table_row(table_row)
325
- return if @hide_this_step
326
- print_table_row_messages
327
- builder << '</tr>'
328
- if table_row.exception
329
- builder.tr do
330
- builder.td(:colspan => @col_index.to_s, :class => 'failed') do
331
- builder.pre do |pre|
332
- pre << h(format_exception(table_row.exception))
333
- end
334
- end
335
- end
336
- if table_row.exception.is_a? ::Cucumber::Pending
337
- set_scenario_color_pending
338
- else
339
- set_scenario_color_failed
340
- end
341
- end
342
- if @outline_row
343
- @outline_row += 1
344
- end
345
- @step_number += 1
346
- move_progress
347
- end
348
-
349
- def table_cell_value(value, status)
350
- return if @hide_this_step
351
-
352
- @cell_type = @outline_row == 0 ? :th : :td
353
- attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'step'}
354
- attributes[:class] += " #{status}" if status
355
- build_cell(@cell_type, value, attributes)
356
- set_scenario_color(status) if @inside_outline
357
- @col_index += 1
358
- end
359
-
360
- def puts(message)
361
- @delayed_messages << message
362
- #builder.pre(message, :class => 'message')
363
- end
364
-
365
- def print_messages
366
- return if @delayed_messages.empty?
367
-
368
- #builder.ol do
369
- @delayed_messages.each do |ann|
370
- builder.li(:class => 'step message') do
371
- builder << ann
372
- end
373
- end
374
- #end
375
- empty_messages
376
- end
377
-
378
- def print_table_row_messages
379
- return if @delayed_messages.empty?
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
380
14
 
381
- builder.td(:class => 'message') do
382
- builder << @delayed_messages.join(', ')
383
- end
384
- empty_messages
15
+ super(config)
385
16
  end
386
17
 
387
- def empty_messages
388
- @delayed_messages = []
389
- end
390
-
391
- def after_test_case(_test_case, result)
392
- if result.failed? && !@scenario_red
393
- set_scenario_color_failed
394
- end
395
- end
396
-
397
- protected
398
-
399
- def next_id(type)
400
- @indices ||= Hash.new { 0 }
401
- @indices[type] += 1
402
- "#{type}_#{@indices[type]}"
403
- end
404
-
405
- def build_exception_detail(exception)
406
- backtrace = Array.new
407
-
408
- builder.div(:class => 'message') do
409
- message = exception.message
410
-
411
- if defined?(RAILS_ROOT) && message.include?('Exception caught')
412
- matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
413
- backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
414
- matches = message.match(/<code>([^(\/)]+)<\//m)
415
- message = matches ? matches[1] : ''
416
- end
417
-
418
- unless exception.instance_of?(RuntimeError)
419
- message = "#{message} (#{exception.class})"
420
- end
421
-
422
- builder.pre do
423
- builder.text!(message)
424
- end
425
- end
426
-
427
- builder.div(:class => 'backtrace') do
428
- builder.pre do
429
- backtrace = exception.backtrace
430
- backtrace.delete_if { |x| x =~ /\/gems\/(cucumber|rspec)/ }
431
- builder << backtrace_line(backtrace.join("\n"))
432
- end
433
- end
434
-
435
- extra = extra_failure_content(backtrace)
436
- builder << extra unless extra == ''
437
- end
438
-
439
- def set_scenario_color(status)
440
- if status.nil? || status == :undefined || status == :pending
441
- set_scenario_color_pending
442
- end
443
- if status == :failed
444
- set_scenario_color_failed
445
- end
446
- end
447
-
448
- def set_scenario_color_failed
449
- builder.script do
450
- builder.text!("makeRed('cucumber-header');") unless @header_red
451
- @header_red = true
452
- scenario_or_background = @in_background ? 'background' : 'scenario'
453
- builder.text!("makeRed('#{scenario_or_background}_#{@scenario_number}');") unless @scenario_red
454
- @scenario_red = true
455
- if @options[:expand] && @inside_outline
456
- builder.text!("makeRed('#{scenario_or_background}_#{@scenario_number}_#{@outline_row}');")
457
- end
458
- end
459
- end
460
-
461
- def set_scenario_color_pending
462
- builder.script do
463
- builder.text!("makeYellow('cucumber-header');") unless @header_red
464
- scenario_or_background = @in_background ? 'background' : 'scenario'
465
- builder.text!("makeYellow('#{scenario_or_background}_#{@scenario_number}');") unless @scenario_red
466
- end
467
- end
468
-
469
- def build_step(keyword, step_match, _status)
470
- step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
471
- builder.div(:class => 'step_name') do |div|
472
- builder.span(keyword, :class => 'keyword')
473
- builder.span(:class => 'step val') do |name|
474
- name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
475
- end
476
- end
477
-
478
- step_file = step_match.file_colon_line
479
- step_file.gsub(/^([^:]*\.rb):(\d*)/) do
480
- if ENV['TM_PROJECT_DIRECTORY']
481
- step_file = "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
482
- end
483
- end
484
-
485
- builder.div(:class => 'step_file') do |div|
486
- builder.span do
487
- builder << step_file
488
- end
489
- end
490
- end
491
-
492
- def build_cell(cell_type, value, attributes)
493
- builder.__send__(cell_type, attributes) do
494
- builder.div do
495
- builder.span(value,:class => 'step param')
496
- end
497
- end
498
- end
499
-
500
- def move_progress
501
- builder << " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
502
- end
503
-
504
- def percent_done
505
- result = 100.0
506
- if @step_count != 0
507
- result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
508
- end
509
- result
510
- end
511
-
512
- def format_exception(exception)
513
- ([exception.message.to_s] + exception.backtrace).join("\n")
514
- end
515
-
516
- def backtrace_line(line)
517
- if ENV['TM_PROJECT_DIRECTORY']
518
- line.gsub(/^([^:]*\.(?:rb|feature|haml)):(\d*).*$/) do
519
- "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
520
- end
521
- else
522
- line
523
- end
524
- end
525
-
526
- def print_stats(features)
527
- builder << "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
528
- builder << "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
529
- end
530
-
531
- def print_stat_string(_features)
532
- string = String.new
533
- string << dump_count(@runtime.scenarios.length, 'scenario')
534
- scenario_count = print_status_counts{|status| @runtime.scenarios(status)}
535
- string << scenario_count if scenario_count
536
- string << '<br />'
537
- string << dump_count(@runtime.steps.length, 'step')
538
- step_count = print_status_counts{|status| @runtime.steps(status)}
539
- string << step_count if step_count
540
- end
541
-
542
- def print_status_counts
543
- counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
544
- elements = yield status
545
- elements.any? ? "#{elements.length} #{status}" : nil
546
- end.compact
547
- return " (#{counts.join(', ')})" if counts.any?
548
- end
549
-
550
- def dump_count(count, what, state=nil)
551
- [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(' ')
552
- end
553
-
554
- def outline_step?(_step)
555
- not @step.step.respond_to?(:actual_keyword)
556
- end
557
-
558
- class SnippetExtractor #:nodoc:
559
- class NullConverter; def convert(code, _pre); code; end; end #:nodoc:
560
-
561
- begin
562
- require 'syntax/convertors/html'
563
- @@converter = Syntax::Convertors::HTML.for_syntax 'ruby'
564
- rescue LoadError
565
- @@converter = NullConverter.new
566
- end
567
-
568
- def snippet(error)
569
- raw_code, line = snippet_for(error[0])
570
- highlighted = @@converter.convert(raw_code, false)
571
- highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
572
- post_process(highlighted, line)
573
- end
574
-
575
- def snippet_for(error_line)
576
- if error_line =~ /(.*):(\d+)/
577
- file = $1
578
- line = $2.to_i
579
- [lines_around(file, line), line]
580
- else
581
- ["# Couldn't get snippet for #{error_line}", 1]
582
- end
583
- end
584
-
585
- def lines_around(file, line)
586
- if File.file?(file)
587
- begin
588
- lines = File.open(file).read.split("\n")
589
- rescue ArgumentError
590
- return "# Couldn't get snippet for #{file}"
591
- end
592
- min = [0, line-3].max
593
- max = [line+1, lines.length-1].min
594
- selected_lines = []
595
- selected_lines.join("\n")
596
- lines[min..max].join("\n")
597
- else
598
- "# Couldn't get snippet for #{file}"
599
- end
600
- end
601
-
602
- def post_process(highlighted, offending_line)
603
- new_lines = []
604
- highlighted.split("\n").each_with_index do |line, i|
605
- new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
606
- new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
607
- new_lines << new_line
608
- end
609
- new_lines.join("\n")
610
- end
611
-
18
+ def output_envelope(envelope)
19
+ @html_formatter.write_message(envelope)
20
+ @html_formatter.write_post_message if envelope.test_run_finished
612
21
  end
613
22
  end
614
23
  end