pretty_face 0.1

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 (46) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +5 -0
  4. data/ChangeLog +2 -0
  5. data/Gemfile +12 -0
  6. data/Guardfile +17 -0
  7. data/README.md +21 -0
  8. data/Rakefile +18 -0
  9. data/cucumber.yml +3 -0
  10. data/features/pretty_face_report.feature +63 -0
  11. data/features/support/env.rb +9 -0
  12. data/features/support/hooks.rb +6 -0
  13. data/fixtures/advanced.feature +14 -0
  14. data/fixtures/basic.feature +17 -0
  15. data/fixtures/step_definitions/advanced_steps.rb +12 -0
  16. data/fixtures/step_definitions/basic_steps.rb +19 -0
  17. data/fixtures/support/env.rb +3 -0
  18. data/lib/pretty_face.rb +6 -0
  19. data/lib/pretty_face/formatter/html.rb +140 -0
  20. data/lib/pretty_face/formatter/report.rb +68 -0
  21. data/lib/pretty_face/formatter/view_helper.rb +60 -0
  22. data/lib/pretty_face/templates/face.jpg +0 -0
  23. data/lib/pretty_face/templates/failed.jpg +0 -0
  24. data/lib/pretty_face/templates/main.erb +194 -0
  25. data/lib/pretty_face/templates/passed.jpg +0 -0
  26. data/lib/pretty_face/templates/pending.jpg +0 -0
  27. data/lib/pretty_face/templates/skipped.jpg +0 -0
  28. data/lib/pretty_face/templates/undefined.jpg +0 -0
  29. data/lib/pretty_face/version.rb +3 -0
  30. data/pretty_face.gemspec +26 -0
  31. data/sample_report/LisaCrispin1.png +0 -0
  32. data/sample_report/LisaCrispin2.png +0 -0
  33. data/sample_report/blue_s.jpeg +0 -0
  34. data/sample_report/green_bar.jpeg +0 -0
  35. data/sample_report/green_check.jpg +0 -0
  36. data/sample_report/red_bar.gif +0 -0
  37. data/sample_report/red_x.jpg +0 -0
  38. data/sample_report/sample_report.html +167 -0
  39. data/sample_report/some_code/gherkin_formatter_adapter.rb +87 -0
  40. data/sample_report/some_code/html.rb +655 -0
  41. data/sample_report/some_code/ordered_xml_markup.rb +24 -0
  42. data/sample_report/some_code/summary.rb +35 -0
  43. data/sample_report/yellow_o.jpg +0 -0
  44. data/spec/lib/html_formatter_spec.rb +111 -0
  45. data/spec/spec_helper.rb +5 -0
  46. metadata +168 -0
@@ -0,0 +1,87 @@
1
+ require 'cucumber/formatter/io'
2
+ require 'gherkin/formatter/argument'
3
+
4
+ module Cucumber
5
+ module Formatter
6
+ # Adapts Cucumber formatter events to Gherkin formatter events
7
+ # This class will disappear when Cucumber is based on Gherkin's model.
8
+ class GherkinFormatterAdapter
9
+ def initialize(gherkin_formatter, print_emtpy_match)
10
+ @gf = gherkin_formatter
11
+ @print_emtpy_match = print_emtpy_match
12
+ end
13
+
14
+ def before_feature(feature)
15
+ @gf.uri(feature.file)
16
+ @gf.feature(feature.gherkin_statement)
17
+ end
18
+
19
+ def before_background(background)
20
+ @outline = false
21
+ @gf.background(background.gherkin_statement)
22
+ end
23
+
24
+ def before_feature_element(feature_element)
25
+ case(feature_element)
26
+ when Ast::Scenario
27
+ @outline = false
28
+ @gf.scenario(feature_element.gherkin_statement)
29
+ when Ast::ScenarioOutline
30
+ @outline = true
31
+ @gf.scenario_outline(feature_element.gherkin_statement)
32
+ else
33
+ raise "Bad type: #{feature_element.class}"
34
+ end
35
+ end
36
+
37
+ def before_step(step)
38
+ @gf.step(step.gherkin_statement)
39
+ if @print_emtpy_match
40
+ if(@outline)
41
+ match = Gherkin::Formatter::Model::Match.new(step.gherkin_statement.outline_args, nil)
42
+ else
43
+ match = Gherkin::Formatter::Model::Match.new([], nil)
44
+ end
45
+ @gf.match(match)
46
+ end
47
+ end
48
+
49
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
50
+ arguments = step_match.step_arguments.map{|a| Gherkin::Formatter::Argument.new(a.offset, a.val)}
51
+ location = step_match.file_colon_line
52
+ match = Gherkin::Formatter::Model::Match.new(arguments, location)
53
+ if @print_emtpy_match
54
+ # Trick the formatter to believe that's what was printed previously so we get arg highlights on #result
55
+ @gf.instance_variable_set('@match', match)
56
+ else
57
+ @gf.match(match)
58
+ end
59
+
60
+ error_message = exception ? "#{exception.message} (#{exception.class})\n#{exception.backtrace.join("\n")}" : nil
61
+ unless @outline
62
+ @gf.result(Gherkin::Formatter::Model::Result.new(status, nil, error_message))
63
+ end
64
+ end
65
+
66
+ def before_examples(examples)
67
+ @gf.examples(examples.gherkin_statement)
68
+ end
69
+
70
+ def after_feature(feature)
71
+ @gf.eof
72
+ end
73
+
74
+ def after_features(features)
75
+ @gf.done
76
+ end
77
+
78
+ def embed(file, mime_type, label)
79
+ data = File.read(file)
80
+ if defined?(JRUBY_VERSION)
81
+ data = data.to_java_bytes
82
+ end
83
+ @gf.embedding(mime_type, data)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,655 @@
1
+ require 'erb'
2
+ require 'cucumber/formatter/ordered_xml_markup'
3
+ require 'cucumber/formatter/duration'
4
+ require 'cucumber/formatter/io'
5
+
6
+ module Cucumber
7
+ module Formatter
8
+ class Html
9
+ include ERB::Util # for the #h method
10
+ include Duration
11
+ include Io
12
+
13
+ def initialize(step_mother, path_or_io, options)
14
+ @io = ensure_io(path_or_io, "html")
15
+ @step_mother = step_mother
16
+ @options = options
17
+ @buffer = {}
18
+ @builder = create_builder(@io)
19
+ @feature_number = 0
20
+ @scenario_number = 0
21
+ @step_number = 0
22
+ @header_red = nil
23
+ @delayed_messages = []
24
+ @img_id = 0
25
+ end
26
+
27
+ def embed(src, mime_type, label)
28
+ case(mime_type)
29
+ when /^image\/(png|gif|jpg|jpeg)/
30
+ embed_image(src, label)
31
+ end
32
+ end
33
+
34
+ def embed_image(src, label)
35
+ id = "img_#{@img_id}"
36
+ @img_id += 1
37
+ @builder.span(:class => 'embed') do |pre|
38
+ pre << %{<a href="" onclick="img=document.getElementById('#{id}'); img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false">#{label}</a><br>&nbsp;
39
+ <img id="#{id}" style="display: none" src="#{src}"/>}
40
+ end
41
+ end
42
+
43
+
44
+ def before_features(features)
45
+ @step_count = get_step_count(features)
46
+
47
+ # <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
48
+ @builder.declare!(
49
+ :DOCTYPE,
50
+ :html,
51
+ :PUBLIC,
52
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
53
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
54
+ )
55
+
56
+ @builder << '<html xmlns ="http://www.w3.org/1999/xhtml">'
57
+ @builder.head do
58
+ @builder.meta(:content => 'text/html;charset=utf-8')
59
+ @builder.title 'Cucumber'
60
+ inline_css
61
+ inline_js
62
+ end
63
+ @builder << '<body>'
64
+ @builder << "<!-- Step count #{@step_count}-->"
65
+ @builder << '<div class="cucumber">'
66
+ @builder.div(:id => 'cucumber-header') do
67
+ @builder.div(:id => 'label') do
68
+ @builder.h1('Cucumber Features')
69
+ end
70
+ @builder.div(:id => 'summary') do
71
+ @builder.p('',:id => 'totals')
72
+ @builder.p('',:id => 'duration')
73
+ @builder.div(:id => 'expand-collapse') do
74
+ @builder.p('Expand All', :id => 'expander')
75
+ @builder.p('Collapse All', :id => 'collapser')
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def after_features(features)
82
+ print_stats(features)
83
+ @builder << '</div>'
84
+ @builder << '</body>'
85
+ @builder << '</html>'
86
+ end
87
+
88
+ def before_feature(feature)
89
+ @exceptions = []
90
+ @builder << '<div class="feature">'
91
+ end
92
+
93
+ def after_feature(feature)
94
+ @builder << '</div>'
95
+ end
96
+
97
+ def before_comment(comment)
98
+ @builder << '<pre class="comment">'
99
+ end
100
+
101
+ def after_comment(comment)
102
+ @builder << '</pre>'
103
+ end
104
+
105
+ def comment_line(comment_line)
106
+ @builder.text!(comment_line)
107
+ @builder.br
108
+ end
109
+
110
+ def after_tags(tags)
111
+ @tag_spacer = nil
112
+ end
113
+
114
+ def tag_name(tag_name)
115
+ @builder.text!(@tag_spacer) if @tag_spacer
116
+ @tag_spacer = ' '
117
+ @builder.span(tag_name, :class => 'tag')
118
+ end
119
+
120
+ def feature_name(keyword, name)
121
+ lines = name.split(/\r?\n/)
122
+ return if lines.empty?
123
+ @builder.h2 do |h2|
124
+ @builder.span(keyword + ': ' + lines[0], :class => 'val')
125
+ end
126
+ @builder.p(:class => 'narrative') do
127
+ lines[1..-1].each do |line|
128
+ @builder.text!(line.strip)
129
+ @builder.br
130
+ end
131
+ end
132
+ end
133
+
134
+ def before_background(background)
135
+ @in_background = true
136
+ @builder << '<div class="background">'
137
+ end
138
+
139
+ def after_background(background)
140
+ @in_background = nil
141
+ @builder << '</div>'
142
+ end
143
+
144
+ def background_name(keyword, name, file_colon_line, source_indent)
145
+ @listing_background = true
146
+ @builder.h3(:id => "background_#{@scenario_number}") do |h3|
147
+ @builder.span(keyword, :class => 'keyword')
148
+ @builder.text!(' ')
149
+ @builder.span(name, :class => 'val')
150
+ end
151
+ end
152
+
153
+ def before_feature_element(feature_element)
154
+ @scenario_number+=1
155
+ @scenario_red = false
156
+ css_class = {
157
+ Ast::Scenario => 'scenario',
158
+ Ast::ScenarioOutline => 'scenario outline'
159
+ }[feature_element.class]
160
+ @builder << "<div class='#{css_class}'>"
161
+ end
162
+
163
+ def after_feature_element(feature_element)
164
+ @builder << '</div>'
165
+ @open_step_list = true
166
+ end
167
+
168
+ def scenario_name(keyword, name, file_colon_line, source_indent)
169
+ @builder.span(:class => 'scenario_file') do
170
+ @builder << file_colon_line
171
+ end
172
+ @listing_background = false
173
+ @builder.h3(:id => "scenario_#{@scenario_number}") do
174
+ @builder.span(keyword + ':', :class => 'keyword')
175
+ @builder.text!(' ')
176
+ @builder.span(name, :class => 'val')
177
+ end
178
+ end
179
+
180
+ def before_outline_table(outline_table)
181
+ @outline_row = 0
182
+ @builder << '<table>'
183
+ end
184
+
185
+ def after_outline_table(outline_table)
186
+ @builder << '</table>'
187
+ @outline_row = nil
188
+ end
189
+
190
+ def before_examples(examples)
191
+ @builder << '<div class="examples">'
192
+ end
193
+
194
+ def after_examples(examples)
195
+ @builder << '</div>'
196
+ end
197
+
198
+ def examples_name(keyword, name)
199
+ @builder.h4 do
200
+ @builder.span(keyword, :class => 'keyword')
201
+ @builder.text!(' ')
202
+ @builder.span(name, :class => 'val')
203
+ end
204
+ end
205
+
206
+ def before_steps(steps)
207
+ @builder << '<ol>'
208
+ end
209
+
210
+ def after_steps(steps)
211
+ @builder << '</ol>'
212
+ end
213
+
214
+ def before_step(step)
215
+ @step_id = step.dom_id
216
+ @step_number += 1
217
+ @step = step
218
+ end
219
+
220
+ def after_step(step)
221
+ move_progress
222
+ end
223
+
224
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
225
+ @step_match = step_match
226
+ @hide_this_step = false
227
+ if exception
228
+ if @exceptions.include?(exception)
229
+ @hide_this_step = true
230
+ return
231
+ end
232
+ @exceptions << exception
233
+ end
234
+ if status != :failed && @in_background ^ background
235
+ @hide_this_step = true
236
+ return
237
+ end
238
+ @status = status
239
+ return if @hide_this_step
240
+ set_scenario_color(status)
241
+ @builder << "<li id='#{@step_id}' class='step #{status}'>"
242
+ end
243
+
244
+ def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
245
+ return if @hide_this_step
246
+ # print snippet for undefined steps
247
+ if status == :undefined
248
+ step_multiline_class = @step.multiline_arg ? @step.multiline_arg.class : nil
249
+ @builder.pre do |pre|
250
+ pre << @step_mother.snippet_text(@step.actual_keyword,step_match.instance_variable_get("@name") || '',step_multiline_class)
251
+ end
252
+ end
253
+ @builder << '</li>'
254
+ print_messages
255
+ end
256
+
257
+ def step_name(keyword, step_match, status, source_indent, background, file_colon_line)
258
+ @step_matches ||= []
259
+ background_in_scenario = background && !@listing_background
260
+ @skip_step = @step_matches.index(step_match) || background_in_scenario
261
+ @step_matches << step_match
262
+
263
+ unless @skip_step
264
+ build_step(keyword, step_match, status)
265
+ end
266
+ end
267
+
268
+ def exception(exception, status)
269
+ build_exception_detail(exception)
270
+ end
271
+
272
+ def extra_failure_content(file_colon_line)
273
+ @snippet_extractor ||= SnippetExtractor.new
274
+ "<pre class=\"ruby\"><code>#{@snippet_extractor.snippet(file_colon_line)}</code></pre>"
275
+ end
276
+
277
+ def before_multiline_arg(multiline_arg)
278
+ return if @hide_this_step || @skip_step
279
+ if Ast::Table === multiline_arg
280
+ @builder << '<table>'
281
+ end
282
+ end
283
+
284
+ def after_multiline_arg(multiline_arg)
285
+ return if @hide_this_step || @skip_step
286
+ if Ast::Table === multiline_arg
287
+ @builder << '</table>'
288
+ end
289
+ end
290
+
291
+ def doc_string(string)
292
+ return if @hide_this_step
293
+ @builder.pre(:class => 'val') do |pre|
294
+ @builder << h(string).gsub("\n", '&#x000A;')
295
+ end
296
+ end
297
+
298
+
299
+ def before_table_row(table_row)
300
+ @row_id = table_row.dom_id
301
+ @col_index = 0
302
+ return if @hide_this_step
303
+ @builder << "<tr class='step' id='#{@row_id}'>"
304
+ end
305
+
306
+ def after_table_row(table_row)
307
+ return if @hide_this_step
308
+ print_table_row_messages
309
+ @builder << '</tr>'
310
+ if table_row.exception
311
+ @builder.tr do
312
+ @builder.td(:colspan => @col_index.to_s, :class => 'failed') do
313
+ @builder.pre do |pre|
314
+ pre << h(format_exception(table_row.exception))
315
+ end
316
+ end
317
+ end
318
+ set_scenario_color_failed
319
+ end
320
+ if @outline_row
321
+ @outline_row += 1
322
+ end
323
+ @step_number += 1
324
+ move_progress
325
+ end
326
+
327
+ def table_cell_value(value, status)
328
+ return if @hide_this_step
329
+
330
+ @cell_type = @outline_row == 0 ? :th : :td
331
+ attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'step'}
332
+ attributes[:class] += " #{status}" if status
333
+ build_cell(@cell_type, value, attributes)
334
+ set_scenario_color(status)
335
+ @col_index += 1
336
+ end
337
+
338
+ def puts(message)
339
+ @delayed_messages << message
340
+ #@builder.pre(message, :class => 'message')
341
+ end
342
+
343
+ def print_messages
344
+ return if @delayed_messages.empty?
345
+
346
+ #@builder.ol do
347
+ @delayed_messages.each do |ann|
348
+ @builder.li(:class => 'step message') do
349
+ @builder << ann
350
+ end
351
+ end
352
+ #end
353
+ empty_messages
354
+ end
355
+
356
+ def print_table_row_messages
357
+ return if @delayed_messages.empty?
358
+
359
+ @builder.td(:class => 'message') do
360
+ @builder << @delayed_messages.join(", ")
361
+ end
362
+ empty_messages
363
+ end
364
+
365
+ def empty_messages
366
+ @delayed_messages = []
367
+ end
368
+
369
+ protected
370
+
371
+ def build_exception_detail(exception)
372
+ backtrace = Array.new
373
+ @builder.div(:class => 'message') do
374
+ message = exception.message
375
+ if defined?(RAILS_ROOT) && message.include?('Exception caught')
376
+ matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
377
+ backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
378
+ matches = message.match(/<code>([^(\/)]+)<\//m)
379
+ message = matches ? matches[1] : ""
380
+ end
381
+
382
+ unless exception.instance_of?(RuntimeError)
383
+ message = "#{message} (#{exception.class})"
384
+ end
385
+
386
+ @builder.pre do
387
+ @builder.text!(message)
388
+ end
389
+ end
390
+ @builder.div(:class => 'backtrace') do
391
+ @builder.pre do
392
+ backtrace = exception.backtrace
393
+ backtrace.delete_if { |x| x =~ /\/gems\/(cucumber|rspec)/ }
394
+ @builder << backtrace_line(backtrace.join("\n"))
395
+ end
396
+ end
397
+ extra = extra_failure_content(backtrace)
398
+ @builder << extra unless extra == ""
399
+ end
400
+
401
+ def set_scenario_color(status)
402
+ if status == :undefined or status == :pending
403
+ set_scenario_color_pending
404
+ end
405
+ if status == :failed
406
+ set_scenario_color_failed
407
+ end
408
+ end
409
+
410
+ def set_scenario_color_failed
411
+ @builder.script do
412
+ @builder.text!("makeRed('cucumber-header');") unless @header_red
413
+ @header_red = true
414
+ @builder.text!("makeRed('scenario_#{@scenario_number}');") unless @scenario_red
415
+ @scenario_red = true
416
+ end
417
+ end
418
+
419
+ def set_scenario_color_pending
420
+ @builder.script do
421
+ @builder.text!("makeYellow('cucumber-header');") unless @header_red
422
+ @builder.text!("makeYellow('scenario_#{@scenario_number}');") unless @scenario_red
423
+ end
424
+ end
425
+
426
+ def get_step_count(features)
427
+ count = 0
428
+ features = features.instance_variable_get("@features")
429
+ features.each do |feature|
430
+ #get background steps
431
+ if feature.instance_variable_get("@background")
432
+ background = feature.instance_variable_get("@background")
433
+ background.init
434
+ background_steps = background.instance_variable_get("@steps").instance_variable_get("@steps")
435
+ count += background_steps.size
436
+ end
437
+ #get scenarios
438
+ feature.instance_variable_get("@feature_elements").each do |scenario|
439
+ scenario.init
440
+ #get steps
441
+ steps = scenario.instance_variable_get("@steps").instance_variable_get("@steps")
442
+ count += steps.size
443
+
444
+ #get example table
445
+ examples = scenario.instance_variable_get("@examples_array")
446
+ unless examples.nil?
447
+ examples.each do |example|
448
+ example_matrix = example.instance_variable_get("@outline_table").instance_variable_get("@cell_matrix")
449
+ count += example_matrix.size
450
+ end
451
+ end
452
+
453
+ #get multiline step tables
454
+ steps.each do |step|
455
+ multi_arg = step.instance_variable_get("@multiline_arg")
456
+ next if multi_arg.nil?
457
+ matrix = multi_arg.instance_variable_get("@cell_matrix")
458
+ count += matrix.size unless matrix.nil?
459
+ end
460
+ end
461
+ end
462
+ return count
463
+ end
464
+
465
+ def build_step(keyword, step_match, status)
466
+ step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
467
+ @builder.div(:class => 'step_name') do |div|
468
+ @builder.span(keyword, :class => 'keyword')
469
+ @builder.span(:class => 'step val') do |name|
470
+ name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
471
+ end
472
+ end
473
+
474
+ step_file = step_match.file_colon_line
475
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
476
+ if ENV['TM_PROJECT_DIRECTORY']
477
+ step_file = "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
478
+ end
479
+ end
480
+
481
+ @builder.div(:class => 'step_file') do |div|
482
+ @builder.span do
483
+ @builder << step_file
484
+ end
485
+ end
486
+ end
487
+
488
+ def build_cell(cell_type, value, attributes)
489
+ @builder.__send__(cell_type, attributes) do
490
+ @builder.div do
491
+ @builder.span(value,:class => 'step param')
492
+ end
493
+ end
494
+ end
495
+
496
+ def inline_css
497
+ @builder.style(:type => 'text/css') do
498
+ @builder << File.read(File.dirname(__FILE__) + '/cucumber.css')
499
+ end
500
+ end
501
+
502
+ def inline_js
503
+ @builder.script(:type => 'text/javascript') do
504
+ @builder << inline_jquery
505
+ @builder << inline_js_content
506
+ end
507
+ end
508
+
509
+ def inline_jquery
510
+ File.read(File.dirname(__FILE__) + '/jquery-min.js')
511
+ end
512
+
513
+ def inline_js_content
514
+ <<-EOF
515
+
516
+ SCENARIOS = "h3[id^='scenario_'],h3[id^=background_]";
517
+
518
+ $(document).ready(function() {
519
+ $(SCENARIOS).css('cursor', 'pointer');
520
+ $(SCENARIOS).click(function() {
521
+ $(this).siblings().toggle(250);
522
+ });
523
+
524
+ $("#collapser").css('cursor', 'pointer');
525
+ $("#collapser").click(function() {
526
+ $(SCENARIOS).siblings().hide();
527
+ });
528
+
529
+ $("#expander").css('cursor', 'pointer');
530
+ $("#expander").click(function() {
531
+ $(SCENARIOS).siblings().show();
532
+ });
533
+ })
534
+
535
+ function moveProgressBar(percentDone) {
536
+ $("cucumber-header").css('width', percentDone +"%");
537
+ }
538
+ function makeRed(element_id) {
539
+ $('#'+element_id).css('background', '#C40D0D');
540
+ $('#'+element_id).css('color', '#FFFFFF');
541
+ }
542
+ function makeYellow(element_id) {
543
+ $('#'+element_id).css('background', '#FAF834');
544
+ $('#'+element_id).css('color', '#000000');
545
+ }
546
+
547
+ EOF
548
+ end
549
+
550
+ def move_progress
551
+ @builder << " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
552
+ end
553
+
554
+ def percent_done
555
+ result = 100.0
556
+ if @step_count != 0
557
+ result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
558
+ end
559
+ result
560
+ end
561
+
562
+ def format_exception(exception)
563
+ (["#{exception.message}"] + exception.backtrace).join("\n")
564
+ end
565
+
566
+ def backtrace_line(line)
567
+ line.gsub(/\A([^:]*\.(?:rb|feature|haml)):(\d*).*\z/) do
568
+ if ENV['TM_PROJECT_DIRECTORY']
569
+ "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
570
+ else
571
+ line
572
+ end
573
+ end
574
+ end
575
+
576
+ def print_stats(features)
577
+ @builder << "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
578
+ @builder << "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
579
+ end
580
+
581
+ def print_stat_string(features)
582
+ string = String.new
583
+ string << dump_count(@step_mother.scenarios.length, "scenario")
584
+ scenario_count = print_status_counts{|status| @step_mother.scenarios(status)}
585
+ string << scenario_count if scenario_count
586
+ string << "<br />"
587
+ string << dump_count(@step_mother.steps.length, "step")
588
+ step_count = print_status_counts{|status| @step_mother.steps(status)}
589
+ string << step_count if step_count
590
+ end
591
+
592
+ def print_status_counts
593
+ counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
594
+ elements = yield status
595
+ elements.any? ? "#{elements.length} #{status.to_s}" : nil
596
+ end.compact
597
+ return " (#{counts.join(', ')})" if counts.any?
598
+ end
599
+
600
+ def dump_count(count, what, state=nil)
601
+ [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(" ")
602
+ end
603
+
604
+ def create_builder(io)
605
+ OrderedXmlMarkup.new(:target => io, :indent => 0)
606
+ end
607
+
608
+ class SnippetExtractor #:nodoc:
609
+ class NullConverter; def convert(code, pre); code; end; end #:nodoc:
610
+ begin; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end
611
+
612
+ def snippet(error)
613
+ raw_code, line = snippet_for(error[0])
614
+ highlighted = @@converter.convert(raw_code, false)
615
+ highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
616
+ post_process(highlighted, line)
617
+ end
618
+
619
+ def snippet_for(error_line)
620
+ if error_line =~ /(.*):(\d+)/
621
+ file = $1
622
+ line = $2.to_i
623
+ [lines_around(file, line), line]
624
+ else
625
+ ["# Couldn't get snippet for #{error_line}", 1]
626
+ end
627
+ end
628
+
629
+ def lines_around(file, line)
630
+ if File.file?(file)
631
+ lines = File.open(file).read.split("\n")
632
+ min = [0, line-3].max
633
+ max = [line+1, lines.length-1].min
634
+ selected_lines = []
635
+ selected_lines.join("\n")
636
+ lines[min..max].join("\n")
637
+ else
638
+ "# Couldn't get snippet for #{file}"
639
+ end
640
+ end
641
+
642
+ def post_process(highlighted, offending_line)
643
+ new_lines = []
644
+ highlighted.split("\n").each_with_index do |line, i|
645
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
646
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
647
+ new_lines << new_line
648
+ end
649
+ new_lines.join("\n")
650
+ end
651
+
652
+ end
653
+ end
654
+ end
655
+ end