pretty_face 0.1

Sign up to get free protection for your applications and to get access to all the features.
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