daddy 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,70 @@
1
+ #menu {
2
+ width: 120px;
3
+ float: left;
4
+ background: #FFFFFF;
5
+ color: #000000;
6
+ }
7
+
8
+ #menu ol {
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+
13
+ #menu ol li.sprint {
14
+ margin: 0;
15
+ padding: 0 5px;
16
+ border-left: 5px solid #65c400;
17
+ border-bottom: 1px solid #65c400;
18
+ line-height: 40px;
19
+ vertical-align: middle;
20
+ font-size: 1.2em;
21
+ font-weight: bold;
22
+ }
23
+
24
+ #menu ol li.diary {
25
+ margin: 0;
26
+ padding: 0 20px 0 40px;
27
+ border-left: 5px solid #65c400;
28
+ border-bottom: 1px solid #65c400;
29
+ line-height: 40px;
30
+ vertical-align: middle;
31
+ font-size: 1.2em;
32
+ font-weight: bold;
33
+ }
34
+
35
+ .contents {
36
+ margin-left: 120px;
37
+ }
38
+
39
+ .cucumber .scenario h3, td .scenario h3, th .scenario h3, .background h3 {
40
+ font-size: 11px;
41
+ padding: 3px;
42
+ margin: 0;
43
+ background: #FFFFFF;
44
+ color: #000000;
45
+ font-weight: bold;
46
+ }
47
+
48
+ .cucumber ol li.passed, td ol li.passed, th ol li.passed {
49
+ border-left: 5px solid #65c400;
50
+ border-bottom: 1px solid #65c400;
51
+ background: #FFFFFF;
52
+ color: #000000;
53
+ }
54
+
55
+ .step_contents {
56
+ display: none;
57
+ background: #FFFFFF;
58
+ border: solid 1px #999999;
59
+ margin-left: 10px;
60
+ padding: 10px;
61
+ }
62
+
63
+ div.scenario table {
64
+ margin: 10px 0;
65
+ }
66
+
67
+ div.scenario td {
68
+ border: solid 1px #999999;
69
+ padding: 5px;
70
+ }
@@ -0,0 +1,745 @@
1
+ # coding: UTF-8
2
+
3
+ require 'erb'
4
+ require 'cucumber/formatter/ordered_xml_markup'
5
+ require 'cucumber/formatter/duration'
6
+ require 'cucumber/formatter/io'
7
+
8
+ module Daddy
9
+ module Formatter
10
+ class Html
11
+ include ERB::Util # for the #h method
12
+ include ::Cucumber::Formatter::Duration
13
+ include ::Cucumber::Formatter::Io
14
+
15
+ def initialize(runtime, path_or_io, options)
16
+ @path_or_io = path_or_io
17
+ @io = ensure_io(path_or_io, "html")
18
+ @runtime = runtime
19
+ @options = options
20
+ @buffer = {}
21
+ @builder = create_builder(@io)
22
+ @feature_number = 0
23
+ @scenario_number = 0
24
+ @step_number = 0
25
+ @header_red = nil
26
+ @delayed_messages = []
27
+ @img_id = 0
28
+ end
29
+
30
+ def embed(src, mime_type, label)
31
+ case(mime_type)
32
+ when /^image\/(png|gif|jpg|jpeg)/
33
+ embed_image(src, label)
34
+ end
35
+ end
36
+
37
+ def embed_image(src, label)
38
+ id = "img_#{@img_id}"
39
+ @img_id += 1
40
+ @builder.span(:class => 'embed') do |pre|
41
+ pre << %{<a href="" onclick="img=document.getElementById('#{id}'); img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false">#{label}</a><br>&nbsp;
42
+ <img id="#{id}" style="display: none" src="#{src}"/>}
43
+ end
44
+ end
45
+
46
+ def before_features(features)
47
+ @step_count = get_step_count(features)
48
+
49
+ @builder.declare!(:DOCTYPE, :html)
50
+ @builder << '<html>'
51
+ @builder << '<head>'
52
+ @builder.meta('http-equiv' => 'Content-Type', :content => 'text/html;charset=utf-8')
53
+ @builder.title 'Daddy'
54
+ inline_css
55
+ inline_js
56
+ @builder << '</head>'
57
+ @builder << '<body>'
58
+ @builder << "<!-- Step count #{@step_count}-->"
59
+ @builder << '<div class="cucumber">'
60
+ @builder.div(:id => 'cucumber-header') do
61
+ @builder.div(:id => 'label') do
62
+ @builder.h1('Daddy')
63
+ end
64
+ @builder.div(:id => 'summary') do
65
+ @builder.p('',:id => 'totals')
66
+ @builder.p('',:id => 'duration')
67
+ @builder.div(:id => 'expand-collapse') do
68
+ @builder.p('すべて開く', :id => 'expander')
69
+ @builder.p('すべて閉じる', :id => 'collapser')
70
+ end
71
+ end
72
+ end
73
+
74
+ before_menu
75
+ end
76
+
77
+ def before_menu
78
+ if ENV['PUBLISH']
79
+ @builder << "<div>"
80
+
81
+ @builder.div(:id => 'menu') do
82
+ @builder << make_menu_for_publish
83
+ end
84
+
85
+ @builder << "<div class='contents'>"
86
+ end
87
+ end
88
+
89
+ def after_menu
90
+ if ENV['PUBLISH']
91
+ @builder << '</div>'
92
+ @builder << '</div>'
93
+ end
94
+ end
95
+
96
+ def make_menu_for_publish
97
+ menu = 'tmp/menu.html'
98
+ system("erb -T - #{File.dirname(__FILE__)}/menu.html.erb > #{menu}")
99
+ File.readlines(menu).join
100
+ end
101
+
102
+ def after_features(features)
103
+ after_menu
104
+ print_stats(features)
105
+ @builder << '</div>'
106
+ @builder << '</body>'
107
+ @builder << '</html>'
108
+ end
109
+
110
+ def before_feature(feature)
111
+ @feature = feature
112
+ @exceptions = []
113
+ @builder << '<div class="feature">'
114
+ end
115
+
116
+ def after_feature(feature)
117
+ @builder << '</div>'
118
+ end
119
+
120
+ def before_comment(comment)
121
+ return if comment == '# language: ja'
122
+ @builder << '<pre class="comment">'
123
+ end
124
+
125
+ def after_comment(comment)
126
+ return if comment == '# language: ja'
127
+ @builder << '</pre>'
128
+ end
129
+
130
+ def comment_line(comment_line)
131
+ return if comment_line == '# language: ja'
132
+ @builder.text!(comment_line)
133
+ @builder.br
134
+ end
135
+
136
+ def after_tags(tags)
137
+ @tag_spacer = nil
138
+ end
139
+
140
+ def tag_name(tag_name)
141
+ @builder.text!(@tag_spacer) if @tag_spacer
142
+ @tag_spacer = ' '
143
+ @builder.span(tag_name, :class => 'tag')
144
+ end
145
+
146
+ def feature_name(keyword, name)
147
+ title = @feature.file.split('/').last.gsub(/\.feature/, '')
148
+
149
+ @builder.h2 do |h2|
150
+ @builder.span(title, :class => 'val')
151
+ end
152
+
153
+ lines = name.split(/\r?\n/)
154
+ unless lines.empty?
155
+ @builder.p(:class => 'narrative') do
156
+ lines.each do |line|
157
+ @builder.text!(line.strip)
158
+ @builder.br
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ def before_background(background)
165
+ @in_background = true
166
+ @builder << '<div class="background">'
167
+ end
168
+
169
+ def after_background(background)
170
+ @in_background = nil
171
+ @builder << '</div>'
172
+ end
173
+
174
+ def background_name(keyword, name, file_colon_line, source_indent)
175
+ @listing_background = true
176
+ @builder.h3(:id => "background_#{@scenario_number}") do |h3|
177
+ @builder.span(keyword, :class => 'keyword')
178
+ @builder.text!(' ')
179
+ @builder.span(name, :class => 'val')
180
+ end
181
+ end
182
+
183
+ def before_feature_element(feature_element)
184
+ @scenario_number+=1
185
+ @scenario_red = false
186
+ css_class = {
187
+ ::Cucumber::Ast::Scenario => 'scenario',
188
+ ::Cucumber::Ast::ScenarioOutline => 'scenario outline'
189
+ }[feature_element.class]
190
+ @builder << "<div class='#{css_class}'>"
191
+ end
192
+
193
+ def after_feature_element(feature_element)
194
+ @builder << '</div>'
195
+ @open_step_list = true
196
+ end
197
+
198
+ def scenario_name(keyword, name, file_colon_line, source_indent)
199
+ @step_number_in_scenario = 0
200
+
201
+ @builder.span(:class => 'scenario_file') do
202
+ @builder << file_colon_line
203
+ end
204
+ @listing_background = false
205
+ @builder.h3(:id => "scenario_#{@scenario_number}") do
206
+ @builder.span(keyword + ':', :class => 'keyword')
207
+ @builder.text!(' ')
208
+ @builder.span(name, :class => 'val')
209
+ end
210
+ end
211
+
212
+ def before_outline_table(outline_table)
213
+ @outline_row = 0
214
+ @builder << '<table>'
215
+ end
216
+
217
+ def after_outline_table(outline_table)
218
+ @builder << '</table>'
219
+ @outline_row = nil
220
+ end
221
+
222
+ def before_examples(examples)
223
+ @builder << '<div class="examples">'
224
+ end
225
+
226
+ def after_examples(examples)
227
+ @builder << '</div>'
228
+ end
229
+
230
+ def examples_name(keyword, name)
231
+ @builder.h4 do
232
+ @builder.span(keyword, :class => 'keyword')
233
+ @builder.text!(' ')
234
+ @builder.span(name, :class => 'val')
235
+ end
236
+ end
237
+
238
+ def before_steps(steps)
239
+ @step_count_in_scenario = steps.count
240
+ @builder << '<ol>'
241
+ end
242
+
243
+ def after_steps(steps)
244
+ @builder << '</ol>'
245
+ end
246
+
247
+ def before_step(step)
248
+ @step_id = step.dom_id
249
+ @step_number += 1
250
+ @step = step
251
+ end
252
+
253
+ def after_step(step)
254
+ move_progress
255
+ end
256
+
257
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
258
+ @step_match = step_match
259
+ @hide_this_step = false
260
+ if exception
261
+ if @exceptions.include?(exception)
262
+ @hide_this_step = true
263
+ return
264
+ end
265
+ @exceptions << exception
266
+ end
267
+ if status != :failed && @in_background ^ background
268
+ @hide_this_step = true
269
+ return
270
+ end
271
+ @status = status
272
+ return if @hide_this_step
273
+ set_scenario_color(status)
274
+ @builder << "<li id='#{@step_id}' class='step #{status}'>"
275
+ end
276
+
277
+ def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
278
+ return if @hide_this_step
279
+ # print snippet for undefined steps
280
+ if status == :undefined
281
+ keyword = @step.actual_keyword if @step.respond_to?(:actual_keyword)
282
+ step_multiline_class = @step.multiline_arg ? @step.multiline_arg.class : nil
283
+ @builder.pre do |pre|
284
+ pre << @runtime.snippet_text(keyword,step_match.instance_variable_get("@name") || '',step_multiline_class)
285
+ end
286
+ end
287
+ @builder << '</li>'
288
+
289
+ unless status == :undefined
290
+ step_file = step_match.file_colon_line
291
+ step_contents = "<div class=\"step_contents\"><pre>"
292
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
293
+ line_index = $2.to_i - 1
294
+ File.readlines(File.expand_path($1.force_encoding('UTF-8')))[line_index..-1].each do |line|
295
+ step_contents << line
296
+ break if line.chop == 'end' or line.chop.start_with?('end ')
297
+ end
298
+ end
299
+ step_contents << "</pre></div>"
300
+ @builder << step_contents
301
+ end
302
+
303
+ print_messages
304
+ end
305
+
306
+ def step_name(keyword, step_match, status, source_indent, background, file_colon_line)
307
+ background_in_scenario = background && !@listing_background
308
+ @skip_step = background_in_scenario
309
+
310
+ unless @skip_step
311
+ build_step(keyword, step_match, status)
312
+ end
313
+ end
314
+
315
+ def exception(exception, status)
316
+ build_exception_detail(exception)
317
+ end
318
+
319
+ def extra_failure_content(file_colon_line)
320
+ @snippet_extractor ||= SnippetExtractor.new
321
+ "<pre class=\"ruby\"><code>#{@snippet_extractor.snippet(file_colon_line)}</code></pre>"
322
+ end
323
+
324
+ def before_multiline_arg(multiline_arg)
325
+ return if @hide_this_step || @skip_step
326
+ if ::Cucumber::Ast::Table === multiline_arg
327
+ @builder << '<table>'
328
+ end
329
+ end
330
+
331
+ def after_multiline_arg(multiline_arg)
332
+ return if @hide_this_step || @skip_step
333
+ if ::Cucumber::Ast::Table === multiline_arg
334
+ @builder << '</table>'
335
+ end
336
+ end
337
+
338
+ def doc_string(string)
339
+ return if @hide_this_step
340
+ @builder.pre(:class => 'val') do |pre|
341
+ @builder << h(string).gsub("\n", '&#x000A;')
342
+ end
343
+ end
344
+
345
+
346
+ def before_table_row(table_row)
347
+ @row_id = table_row.dom_id
348
+ @col_index = 0
349
+ return if @hide_this_step
350
+ @builder << "<tr class='step' id='#{@row_id}'>"
351
+ end
352
+
353
+ def after_table_row(table_row)
354
+ return if @hide_this_step
355
+ print_table_row_messages
356
+ @builder << '</tr>'
357
+ if table_row.exception
358
+ @builder.tr do
359
+ @builder.td(:colspan => @col_index.to_s, :class => 'failed') do
360
+ @builder.pre do |pre|
361
+ pre << h(format_exception(table_row.exception))
362
+ end
363
+ end
364
+ end
365
+ set_scenario_color_failed
366
+ end
367
+ if @outline_row
368
+ @outline_row += 1
369
+ end
370
+ @step_number += 1
371
+ move_progress
372
+ end
373
+
374
+ def table_cell_value(value, status)
375
+ return if @hide_this_step
376
+
377
+ @cell_type = @outline_row == 0 ? :th : :td
378
+ attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'step'}
379
+ attributes[:class] += " #{status}" if status
380
+ build_cell(@cell_type, value, attributes)
381
+ set_scenario_color(status)
382
+ @col_index += 1
383
+ end
384
+
385
+ def puts(message)
386
+ @delayed_messages << message
387
+ #@builder.pre(message, :class => 'message')
388
+ end
389
+
390
+ def print_messages
391
+ return if @delayed_messages.empty?
392
+
393
+ #@builder.ol do
394
+ @delayed_messages.each_with_index do |ann, i|
395
+ @builder.li(:id => "#{@step_number}_#{i}", :class => 'step message') do
396
+ @builder << ann
397
+ end
398
+ end
399
+ @builder.script do |script|
400
+ script << "$(function() {"
401
+ script << " $('##{@step_id}').css('cursor', 'pointer').click(function() {"
402
+ script << " $(this).nextAll('li[id^=\"#{@step_number}_\"]').toggle(250);"
403
+ script << " });"
404
+ script << "});"
405
+ end
406
+ #end
407
+ empty_messages
408
+ end
409
+
410
+ def print_table_row_messages
411
+ end
412
+
413
+ def empty_messages
414
+ @delayed_messages = []
415
+ end
416
+
417
+ protected
418
+
419
+ def build_exception_detail(exception)
420
+ backtrace = Array.new
421
+ @builder.div(:class => 'message') do
422
+ message = exception.message
423
+ if defined?(RAILS_ROOT) && message.include?('Exception caught')
424
+ matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
425
+ backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
426
+ matches = message.match(/<code>([^(\/)]+)<\//m)
427
+ message = matches ? matches[1] : ""
428
+ end
429
+
430
+ unless exception.instance_of?(RuntimeError)
431
+ message = "#{message} (#{exception.class})"
432
+ end
433
+
434
+ @builder.pre do
435
+ @builder.text!(message)
436
+ end
437
+ end
438
+ @builder.div(:class => 'backtrace') do
439
+ @builder.pre do
440
+ backtrace = exception.backtrace
441
+ backtrace.delete_if { |x| x =~ /\/gems\/(cucumber|rspec)/ }
442
+ @builder << backtrace_line(backtrace.join("\n"))
443
+ end
444
+ end
445
+ extra = extra_failure_content(backtrace)
446
+ @builder << extra unless extra == ""
447
+ end
448
+
449
+ def set_scenario_color(status)
450
+ if status == :undefined or status == :pending
451
+ set_scenario_color_pending
452
+ end
453
+ if status == :failed
454
+ set_scenario_color_failed
455
+ end
456
+ end
457
+
458
+ def set_scenario_color_failed
459
+ @builder.script do
460
+ unless @header_red
461
+ @builder.text!("makeRed('cucumber-header');")
462
+ @builder.text!("makeMenuRed();")
463
+ @header_red = true
464
+ end
465
+ @builder.text!("makeRed('scenario_#{@scenario_number}');") unless @scenario_red
466
+ @scenario_red = true
467
+ end
468
+ end
469
+
470
+ def set_scenario_color_pending
471
+ @builder.script do
472
+ unless @header_red
473
+ @builder.text!("makeYellow('cucumber-header');")
474
+ @builder.text!("makeMenuYellow();")
475
+ end
476
+ @builder.text!("makeYellow('scenario_#{@scenario_number}');") unless @scenario_red
477
+ end
478
+ end
479
+
480
+ def get_step_count(features)
481
+ count = 0
482
+ features = features.instance_variable_get("@features")
483
+ features.each do |feature|
484
+ #get background steps
485
+ if feature.instance_variable_get("@background")
486
+ background = feature.instance_variable_get("@background")
487
+ background.init
488
+ background_steps = background.instance_variable_get("@steps").instance_variable_get("@steps")
489
+ count += background_steps.size
490
+ end
491
+ #get scenarios
492
+ feature.instance_variable_get("@feature_elements").each do |scenario|
493
+ scenario.init
494
+ #get steps
495
+ steps = scenario.instance_variable_get("@steps").instance_variable_get("@steps")
496
+ count += steps.size
497
+
498
+ #get example table
499
+ examples = scenario.instance_variable_get("@examples_array")
500
+ unless examples.nil?
501
+ examples.each do |example|
502
+ example_matrix = example.instance_variable_get("@outline_table").instance_variable_get("@cell_matrix")
503
+ count += example_matrix.size
504
+ end
505
+ end
506
+
507
+ #get multiline step tables
508
+ steps.each do |step|
509
+ multi_arg = step.instance_variable_get("@multiline_arg")
510
+ next if multi_arg.nil?
511
+ matrix = multi_arg.instance_variable_get("@cell_matrix")
512
+ count += matrix.size unless matrix.nil?
513
+ end
514
+ end
515
+ end
516
+ return count
517
+ end
518
+
519
+ def build_step(keyword, step_match, status)
520
+ @step_number_in_scenario += 1
521
+ formatted_step_number = sprintf("%0#{@step_count_in_scenario.to_s.size}d", @step_number_in_scenario)
522
+
523
+ step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
524
+ @builder.div(:class => 'step_name') do |div|
525
+ @builder.span("#{formatted_step_number}. ", :class => 'keyword')
526
+ @builder.span(:class => 'step val') do |name|
527
+ name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
528
+ end
529
+ end
530
+
531
+ step_file = step_match.file_colon_line
532
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
533
+ if index = $1.index('lib/daddy/cucumber/step_definitions/')
534
+ step_file = "daddy: " + $1[index..-1]
535
+ end
536
+
537
+ step_file = "<span id=\"step_file_#{@step_number}\">#{step_file}:#{$2}</span>"
538
+ end
539
+
540
+ @builder.div(:class => 'step_file') do |div|
541
+ @builder.span do
542
+ @builder << step_file
543
+ @builder.script do |script|
544
+ script << "$(function() {"
545
+ script << " $('#step_file_#{@step_number}').css('cursor', 'pointer').click(function(event) {"
546
+ script << " $(this).closest('li').next('.step_contents').toggle(250);"
547
+ script << " event.stopImmediatePropagation();"
548
+ script << " });"
549
+ script << "});"
550
+ end
551
+ end
552
+ end
553
+ end
554
+
555
+ def build_cell(cell_type, value, attributes)
556
+ @builder.__send__(cell_type, attributes) do
557
+ @builder.div do
558
+ @builder.span(value,:class => 'step param')
559
+ end
560
+ end
561
+ end
562
+
563
+ def inline_css
564
+ @builder.style do
565
+ @builder << File.read(File.dirname(__FILE__) + '/cucumber.css')
566
+ @builder << File.read(File.dirname(__FILE__) + '/daddy.css')
567
+ end
568
+ end
569
+
570
+ def inline_js
571
+ @builder.script do
572
+ @builder << inline_jquery
573
+ @builder << inline_js_content
574
+ if ENV['EXPAND']
575
+ @builder << %w{
576
+ $(document).ready(function() {
577
+ $(SCENARIOS).siblings().show();
578
+ $('li.message').show();
579
+ });
580
+ }.join
581
+ else
582
+ @builder << %w{
583
+ $(document).ready(function() {
584
+ $(SCENARIOS).siblings().show();
585
+ $('li.message').hide();
586
+ });
587
+ }.join
588
+ end
589
+ end
590
+ end
591
+
592
+ def inline_jquery
593
+ File.read(File.dirname(__FILE__) + '/jquery-min.js')
594
+ end
595
+
596
+ def inline_js_content
597
+ <<-EOF
598
+
599
+ SCENARIOS = "h3[id^='scenario_'],h3[id^=background_]";
600
+
601
+ $(document).ready(function() {
602
+ $(SCENARIOS).css('cursor', 'pointer');
603
+ $(SCENARIOS).click(function() {
604
+ $(this).siblings().toggle(250);
605
+ });
606
+
607
+ $("#collapser").css('cursor', 'pointer');
608
+ $("#collapser").click(function() {
609
+ $(SCENARIOS).siblings().hide();
610
+ $('li.message').hide();
611
+ });
612
+
613
+ $("#expander").css('cursor', 'pointer');
614
+ $("#expander").click(function() {
615
+ $(SCENARIOS).siblings().show();
616
+ });
617
+ })
618
+
619
+ function moveProgressBar(percentDone) {
620
+ $("cucumber-header").css('width', percentDone +"%");
621
+ }
622
+ function makeRed(element_id) {
623
+ $('#'+element_id).css('background', '#C40D0D');
624
+ $('#'+element_id).css('color', '#FFFFFF');
625
+ }
626
+ function makeMenuRed() {
627
+ $('#menu .sprint').css('border-color', '#C40D0D');
628
+ }
629
+ function makeYellow(element_id) {
630
+ $('#'+element_id).css('background', '#FAF834');
631
+ $('#'+element_id).css('color', '#000000');
632
+ }
633
+ function makeMenuYellow() {
634
+ $('#menu .sprint').css('border-color', '#FAF834');
635
+ }
636
+
637
+ EOF
638
+ end
639
+
640
+ def move_progress
641
+ @builder << " <script>moveProgressBar('#{percent_done}');</script>"
642
+ end
643
+
644
+ def percent_done
645
+ result = 100.0
646
+ if @step_count != 0
647
+ result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
648
+ end
649
+ result
650
+ end
651
+
652
+ def format_exception(exception)
653
+ (["#{exception.message}"] + exception.backtrace).join("\n")
654
+ end
655
+
656
+ def backtrace_line(line)
657
+ line.gsub(/\A([^:]*\.(?:rb|feature|haml)):(\d*).*\z/) do
658
+ if ENV['TM_PROJECT_DIRECTORY']
659
+ "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
660
+ else
661
+ line
662
+ end
663
+ end
664
+ end
665
+
666
+ def print_stats(features)
667
+ @builder << "<script>document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
668
+ @builder << "<script>document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
669
+ end
670
+
671
+ def print_stat_string(features)
672
+ string = String.new
673
+ string << dump_count(@runtime.scenarios.length, "scenario")
674
+ scenario_count = print_status_counts{|status| @runtime.scenarios(status)}
675
+ string << scenario_count if scenario_count
676
+ string << "<br />"
677
+ string << dump_count(@runtime.steps.length, "step")
678
+ step_count = print_status_counts{|status| @runtime.steps(status)}
679
+ string << step_count if step_count
680
+ end
681
+
682
+ def print_status_counts
683
+ counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
684
+ elements = yield status
685
+ elements.any? ? "#{elements.length} #{status.to_s}" : nil
686
+ end.compact
687
+ return " (#{counts.join(', ')})" if counts.any?
688
+ end
689
+
690
+ def dump_count(count, what, state=nil)
691
+ [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(" ")
692
+ end
693
+
694
+ def create_builder(io)
695
+ ::Cucumber::Formatter::OrderedXmlMarkup.new(:target => io, :indent => 0)
696
+ end
697
+
698
+ class SnippetExtractor #:nodoc:
699
+ class NullConverter; def convert(code, pre); code; end; end #:nodoc:
700
+ begin; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end
701
+
702
+ def snippet(error)
703
+ raw_code, line = snippet_for(error[0])
704
+ highlighted = @@converter.convert(raw_code, false)
705
+ highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
706
+ post_process(highlighted, line)
707
+ end
708
+
709
+ def snippet_for(error_line)
710
+ if error_line =~ /(.*):(\d+)/
711
+ file = $1
712
+ line = $2.to_i
713
+ [lines_around(file, line), line]
714
+ else
715
+ ["# Couldn't get snippet for #{error_line}", 1]
716
+ end
717
+ end
718
+
719
+ def lines_around(file, line)
720
+ if File.file?(file)
721
+ lines = File.open(file).read.split("\n")
722
+ min = [0, line-3].max
723
+ max = [line+1, lines.length-1].min
724
+ selected_lines = []
725
+ selected_lines.join("\n")
726
+ lines[min..max].join("\n")
727
+ else
728
+ "# Couldn't get snippet for #{file}"
729
+ end
730
+ end
731
+
732
+ def post_process(highlighted, offending_line)
733
+ new_lines = []
734
+ highlighted.split("\n").each_with_index do |line, i|
735
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
736
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
737
+ new_lines << new_line
738
+ end
739
+ new_lines.join("\n")
740
+ end
741
+
742
+ end
743
+ end
744
+ end
745
+ end