cucumber 3.1.2 → 5.2.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 (115) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +287 -14
  3. data/CONTRIBUTING.md +11 -25
  4. data/README.md +4 -5
  5. data/bin/cucumber +1 -1
  6. data/lib/autotest/cucumber_mixin.rb +46 -53
  7. data/lib/cucumber.rb +1 -1
  8. data/lib/cucumber/cli/configuration.rb +5 -5
  9. data/lib/cucumber/cli/main.rb +12 -12
  10. data/lib/cucumber/cli/options.rb +97 -76
  11. data/lib/cucumber/cli/profile_loader.rb +49 -26
  12. data/lib/cucumber/configuration.rb +44 -29
  13. data/lib/cucumber/constantize.rb +2 -5
  14. data/lib/cucumber/deprecate.rb +31 -7
  15. data/lib/cucumber/errors.rb +5 -7
  16. data/lib/cucumber/events.rb +13 -6
  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 +13 -0
  20. data/lib/cucumber/events/step_activated.rb +2 -1
  21. data/lib/cucumber/events/test_case_created.rb +13 -0
  22. data/lib/cucumber/events/test_case_ready.rb +12 -0
  23. data/lib/cucumber/events/test_step_created.rb +13 -0
  24. data/lib/cucumber/events/undefined_parameter_type.rb +10 -0
  25. data/lib/cucumber/file_specs.rb +6 -6
  26. data/lib/cucumber/filters.rb +1 -0
  27. data/lib/cucumber/filters/activate_steps.rb +5 -3
  28. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  29. data/lib/cucumber/filters/prepare_world.rb +5 -9
  30. data/lib/cucumber/filters/quit.rb +1 -3
  31. data/lib/cucumber/filters/tag_limits/verifier.rb +2 -4
  32. data/lib/cucumber/formatter/ansicolor.rb +40 -45
  33. data/lib/cucumber/formatter/ast_lookup.rb +163 -0
  34. data/lib/cucumber/formatter/backtrace_filter.rb +9 -8
  35. data/lib/cucumber/formatter/console.rb +58 -66
  36. data/lib/cucumber/formatter/console_counts.rb +4 -9
  37. data/lib/cucumber/formatter/console_issues.rb +6 -3
  38. data/lib/cucumber/formatter/duration.rb +1 -1
  39. data/lib/cucumber/formatter/duration_extractor.rb +3 -1
  40. data/lib/cucumber/formatter/errors.rb +6 -0
  41. data/lib/cucumber/formatter/fanout.rb +2 -0
  42. data/lib/cucumber/formatter/html.rb +11 -598
  43. data/lib/cucumber/formatter/http_io.rb +147 -0
  44. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  45. data/lib/cucumber/formatter/interceptor.rb +11 -30
  46. data/lib/cucumber/formatter/io.rb +55 -13
  47. data/lib/cucumber/formatter/json.rb +102 -110
  48. data/lib/cucumber/formatter/junit.rb +55 -55
  49. data/lib/cucumber/formatter/message.rb +22 -0
  50. data/lib/cucumber/formatter/message_builder.rb +255 -0
  51. data/lib/cucumber/formatter/pretty.rb +359 -153
  52. data/lib/cucumber/formatter/progress.rb +30 -32
  53. data/lib/cucumber/formatter/publish_banner_printer.rb +77 -0
  54. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  55. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  56. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  57. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  58. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  59. data/lib/cucumber/formatter/rerun.rb +22 -4
  60. data/lib/cucumber/formatter/stepdefs.rb +1 -2
  61. data/lib/cucumber/formatter/steps.rb +3 -4
  62. data/lib/cucumber/formatter/summary.rb +16 -8
  63. data/lib/cucumber/formatter/unicode.rb +15 -17
  64. data/lib/cucumber/formatter/url_reporter.rb +17 -0
  65. data/lib/cucumber/formatter/usage.rb +11 -10
  66. data/lib/cucumber/gherkin/data_table_parser.rb +17 -6
  67. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +13 -17
  68. data/lib/cucumber/gherkin/formatter/escaping.rb +2 -2
  69. data/lib/cucumber/gherkin/steps_parser.rb +17 -8
  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 +42 -33
  73. data/lib/cucumber/glue/registry_and_more.rb +42 -12
  74. data/lib/cucumber/glue/snippet.rb +23 -22
  75. data/lib/cucumber/glue/step_definition.rb +42 -19
  76. data/lib/cucumber/glue/world_factory.rb +1 -1
  77. data/lib/cucumber/hooks.rb +11 -11
  78. data/lib/cucumber/multiline_argument.rb +4 -6
  79. data/lib/cucumber/multiline_argument/data_table.rb +97 -64
  80. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +2 -2
  81. data/lib/cucumber/multiline_argument/doc_string.rb +1 -1
  82. data/lib/cucumber/platform.rb +3 -3
  83. data/lib/cucumber/rake/task.rb +16 -18
  84. data/lib/cucumber/rspec/disable_option_parser.rb +9 -8
  85. data/lib/cucumber/rspec/doubles.rb +3 -5
  86. data/lib/cucumber/running_test_case.rb +2 -53
  87. data/lib/cucumber/runtime.rb +41 -58
  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/step_definition_light.rb +4 -3
  95. data/lib/cucumber/step_definitions.rb +2 -2
  96. data/lib/cucumber/step_match.rb +12 -11
  97. data/lib/cucumber/step_match_search.rb +2 -1
  98. data/lib/cucumber/term/ansicolor.rb +9 -9
  99. data/lib/cucumber/term/banner.rb +56 -0
  100. data/lib/cucumber/version +1 -1
  101. metadata +254 -83
  102. data/lib/cucumber/events/gherkin_source_parsed.rb~ +0 -14
  103. data/lib/cucumber/formatter/ast_lookup.rb~ +0 -9
  104. data/lib/cucumber/formatter/cucumber.css +0 -286
  105. data/lib/cucumber/formatter/cucumber.sass +0 -247
  106. data/lib/cucumber/formatter/hook_query_visitor.rb +0 -42
  107. data/lib/cucumber/formatter/html_builder.rb +0 -121
  108. data/lib/cucumber/formatter/inline-js.js +0 -30
  109. data/lib/cucumber/formatter/jquery-min.js +0 -154
  110. data/lib/cucumber/formatter/json_pretty.rb +0 -11
  111. data/lib/cucumber/formatter/legacy_api/adapter.rb +0 -1028
  112. data/lib/cucumber/formatter/legacy_api/ast.rb +0 -394
  113. data/lib/cucumber/formatter/legacy_api/results.rb +0 -50
  114. data/lib/cucumber/formatter/legacy_api/runtime_facade.rb +0 -32
  115. data/lib/cucumber/step_argument.rb +0 -25
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/ModuleLength
4
+
3
5
  require 'cucumber/formatter/ansicolor'
4
6
  require 'cucumber/formatter/duration'
5
7
  require 'cucumber/gherkin/i18n'
@@ -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
128
  snippet_text_proc = lambda do |step_keyword, step_name, multiline_arg|
131
- runtime.snippet_text(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)
132
135
  end
133
- do_print_snippets(snippet_text_proc)
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,59 +167,47 @@ 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
213
189
 
214
190
  @io.puts "Using the #{profiles_sentence} profile#{'s' if profiles.size > 1}..."
215
191
  end
216
192
 
193
+ def do_print_undefined_parameter_type_snippet(type_name)
194
+ camelized = type_name.split(/_|-/).collect(&:capitalize).join
195
+
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")
209
+ end
210
+
217
211
  private
218
212
 
219
213
  FORMATS = Hash.new { |hash, format| hash[format] = method(format).to_proc }
@@ -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
@@ -249,9 +239,11 @@ module Cucumber
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
246
  end
256
247
  end
257
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 do |status|
33
- count = summary.total(status)
34
- [status, count]
35
- end.select do |status, count|
36
- count > 0
37
- end.map do |status, count|
38
- format_string("#{count} #{status}", status)
39
- end
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
@@ -18,6 +18,7 @@ module Cucumber
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
@@ -35,8 +36,10 @@ module Cucumber
35
36
  def scenario_listing(type, test_cases)
36
37
  return [] if test_cases.empty?
37
38
  [format_string("#{type_heading(type)} Scenarios:", type)] + test_cases.map do |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
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
40
43
  end
41
44
  end
42
45
 
@@ -8,7 +8,7 @@ module Cucumber
8
8
  # <tt>time</tt> format.
9
9
  def format_duration(seconds)
10
10
  m, s = seconds.divmod(60)
11
- "#{m}m#{format('%.3f', s)}s"
11
+ "#{m}m#{format('%<seconds>.3f', seconds: s)}s"
12
12
  end
13
13
  end
14
14
  end
@@ -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
@@ -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
@@ -1,610 +1,23 @@
1
- # frozen_string_literal: true
2
-
3
- require 'erb'
4
- require 'cucumber/formatter/duration'
5
1
  require 'cucumber/formatter/io'
6
- require 'cucumber/formatter/html_builder'
7
- require 'pathname'
2
+ require 'cucumber/html_formatter'
3
+ require 'cucumber/formatter/message_builder'
8
4
 
9
5
  module Cucumber
10
6
  module Formatter
11
- class Html
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: 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 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
10
+ def initialize(config)
11
+ @io = ensure_io(config.out_stream, config.error_stream)
12
+ @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io)
13
+ @html_formatter.write_pre_message
102
14
 
103
- def comment_line(comment_line)
104
- builder.text!(comment_line)
105
- builder.br
15
+ super(config)
106
16
  end
107
17
 
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
- 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
- scenario_color(status) if @inside_outline
357
- @col_index += 1
358
- end
359
-
360
- def puts(message)
361
- @delayed_messages << message
362
- end
363
-
364
- def print_messages
365
- return if @delayed_messages.empty?
366
-
367
- @delayed_messages.each do |ann|
368
- builder.li(:class => 'step message') do
369
- builder << ann
370
- end
371
- end
372
- empty_messages
373
- end
374
-
375
- def print_table_row_messages
376
- return if @delayed_messages.empty?
377
-
378
- builder.td(:class => 'message') do
379
- builder << @delayed_messages.join(', ')
380
- end
381
- empty_messages
382
- end
383
-
384
- def empty_messages
385
- @delayed_messages = []
386
- end
387
-
388
- def after_test_case(_test_case, result)
389
- if result.failed? && !@scenario_red
390
- set_scenario_color_failed
391
- end
392
- end
393
-
394
- protected
395
-
396
- def next_id(type)
397
- @indices ||= Hash.new { 0 }
398
- @indices[type] += 1
399
- "#{type}_#{@indices[type]}"
400
- end
401
-
402
- def build_exception_detail(exception)
403
- backtrace = Array.new
404
-
405
- builder.div(:class => 'message') do
406
- message = exception.message
407
-
408
- if defined?(RAILS_ROOT) && message.include?('Exception caught')
409
- matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
410
- backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
411
- matches = message.match(/<code>([^(\/)]+)<\//m)
412
- message = matches ? matches[1] : ''
413
- end
414
-
415
- unless exception.instance_of?(RuntimeError)
416
- message = "#{message} (#{exception.class})"
417
- end
418
-
419
- builder.pre do
420
- builder.text!(message)
421
- end
422
- end
423
-
424
- builder.div(:class => 'backtrace') do
425
- builder.pre do
426
- backtrace = exception.backtrace
427
- backtrace.delete_if { |x| x =~ /\/gems\/(cucumber|rspec)/ }
428
- builder << backtrace_line(backtrace.join("\n"))
429
- end
430
- end
431
-
432
- extra = extra_failure_content(backtrace)
433
- builder << extra unless extra == ''
434
- end
435
-
436
- def scenario_color(status)
437
- if status.nil? || status == :undefined || status == :pending
438
- set_scenario_color_pending
439
- end
440
- if status == :failed
441
- set_scenario_color_failed
442
- end
443
- end
444
-
445
- def set_scenario_color_failed
446
- builder.script do
447
- builder.text!("makeRed('cucumber-header');") unless @header_red
448
- @header_red = true
449
- scenario_or_background = @in_background ? 'background' : 'scenario'
450
- builder.text!("makeRed('#{scenario_or_background}_#{@scenario_number}');") unless @scenario_red
451
- @scenario_red = true
452
- if @options[:expand] && @inside_outline
453
- builder.text!("makeRed('#{scenario_or_background}_#{@scenario_number}_#{@outline_row}');")
454
- end
455
- end
456
- end
457
-
458
- def set_scenario_color_pending
459
- builder.script do
460
- builder.text!("makeYellow('cucumber-header');") unless @header_red
461
- scenario_or_background = @in_background ? 'background' : 'scenario'
462
- builder.text!("makeYellow('#{scenario_or_background}_#{@scenario_number}');") unless @scenario_red
463
- end
464
- end
465
-
466
- def build_step(keyword, step_match, _status)
467
- step_name = step_match.format_args(lambda { |param| %{<span class="param">#{param}</span>} })
468
- builder.div(:class => 'step_name') do |div|
469
- builder.span(keyword, :class => 'keyword')
470
- builder.span(:class => 'step val') do |name|
471
- name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
472
- end
473
- end
474
-
475
- step_file = step_match.file_colon_line
476
- step_file.gsub(/^([^:]*\.rb):(\d*)/) do
477
- if ENV['TM_PROJECT_DIRECTORY']
478
- step_file = "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
479
- end
480
- end
481
-
482
- builder.div(:class => 'step_file') do |div|
483
- builder.span do
484
- builder << step_file
485
- end
486
- end
487
- end
488
-
489
- def build_cell(cell_type, value, attributes)
490
- builder.__send__(cell_type, attributes) do
491
- builder.div do
492
- builder.span(value, :class => 'step param')
493
- end
494
- end
495
- end
496
-
497
- def move_progress
498
- builder << " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
499
- end
500
-
501
- def percent_done
502
- result = 100.0
503
- if @step_count != 0
504
- result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
505
- end
506
- result
507
- end
508
-
509
- def format_exception(exception)
510
- ([exception.message.to_s] + exception.backtrace).join("\n")
511
- end
512
-
513
- def backtrace_line(line)
514
- if ENV['TM_PROJECT_DIRECTORY']
515
- line.gsub(/^([^:]*\.(?:rb|feature|haml)):(\d*).*$/) do
516
- "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
517
- end
518
- else
519
- line
520
- end
521
- end
522
-
523
- def print_stats(features)
524
- builder << "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
525
- builder << "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
526
- end
527
-
528
- def print_stat_string(_features)
529
- string = String.new
530
- string << dump_count(@runtime.scenarios.length, 'scenario')
531
- scenario_count = print_status_counts { |status| @runtime.scenarios(status) }
532
- string << scenario_count if scenario_count
533
- string << '<br />'
534
- string << dump_count(@runtime.steps.length, 'step')
535
- step_count = print_status_counts { |status| @runtime.steps(status) }
536
- string << step_count if step_count
537
- end
538
-
539
- def print_status_counts
540
- counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
541
- elements = yield status
542
- elements.any? ? "#{elements.length} #{status}" : nil
543
- end.compact
544
- return " (#{counts.join(', ')})" if counts.any?
545
- end
546
-
547
- def dump_count(count, what, state = nil)
548
- [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(' ')
549
- end
550
-
551
- def outline_step?(_step)
552
- not @step.step.respond_to?(:actual_keyword)
553
- end
554
-
555
- class SnippetExtractor #:nodoc:
556
- class NullConverter; def convert(code, _pre); code; end; end #:nodoc:
557
-
558
- begin
559
- require 'syntax/convertors/html'
560
- @@converter = Syntax::Convertors::HTML.for_syntax 'ruby'
561
- rescue LoadError
562
- @@converter = NullConverter.new
563
- end
564
-
565
- def snippet(error)
566
- raw_code, line = snippet_for(error[0])
567
- highlighted = @@converter.convert(raw_code, false)
568
- highlighted += "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
569
- post_process(highlighted, line)
570
- end
571
-
572
- def snippet_for(error_line)
573
- if error_line =~ /(.*):(\d+)/
574
- file = $1
575
- line = $2.to_i
576
- [lines_around(file, line), line]
577
- else
578
- ["# Couldn't get snippet for #{error_line}", 1]
579
- end
580
- end
581
-
582
- def lines_around(file, line)
583
- if File.file?(file)
584
- begin
585
- lines = File.open(file).read.split("\n")
586
- rescue ArgumentError
587
- return "# Couldn't get snippet for #{file}"
588
- end
589
- min = [0, line - 3].max
590
- max = [line + 1, lines.length - 1].min
591
- selected_lines = []
592
- selected_lines.join("\n")
593
- lines[min..max].join("\n")
594
- else
595
- "# Couldn't get snippet for #{file}"
596
- end
597
- end
598
-
599
- def post_process(highlighted, offending_line)
600
- new_lines = []
601
- highlighted.split("\n").each_with_index do |line, i|
602
- new_line = "<span class=\"linenum\">#{offending_line + i - 2}</span>#{line}"
603
- new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
604
- new_lines << new_line
605
- end
606
- new_lines.join("\n")
607
- end
18
+ def output_envelope(envelope)
19
+ @html_formatter.write_message(envelope)
20
+ @html_formatter.write_post_message if envelope.test_run_finished
608
21
  end
609
22
  end
610
23
  end