spicycode-rcov 0.8.1.3.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.
@@ -0,0 +1,1249 @@
1
+ # rcov Copyright (c) 2004-2006 Mauricio Fernandez <mfp@acm.org>
2
+ # See LEGAL and LICENSE for additional licensing information.
3
+
4
+ require 'pathname'
5
+ require 'rcov/xx'
6
+
7
+ # extend XX
8
+ module XX
9
+ module XMLish
10
+ include Markup
11
+
12
+ def xmlish_ *a, &b
13
+ xx_which(XMLish){ xx_with_doc_in_effect(*a, &b)}
14
+ end
15
+ end
16
+ end
17
+
18
+ module Rcov
19
+
20
+ # Try to fix bugs in the REXML shipped with Ruby 1.8.6
21
+ # They affect Mac OSX 10.5.1 users and motivates endless bug reports.
22
+ begin
23
+ require 'rexml/formatters/transitive'
24
+ require 'rexml/formatter/pretty'
25
+ rescue LoadError
26
+ end
27
+
28
+ if (RUBY_VERSION == "1.8.6" || RUBY_VERSION == "1.8.7") && defined? REXML::Formatters::Transitive
29
+ class REXML::Document
30
+ remove_method :write rescue nil
31
+ def write( output=$stdout, indent=-1, trans=false, ie_hack=false )
32
+ if xml_decl.encoding != "UTF-8" && !output.kind_of?(Output)
33
+ output = Output.new( output, xml_decl.encoding )
34
+ end
35
+ formatter = if indent > -1
36
+ #if trans
37
+ REXML::Formatters::Transitive.new( indent )
38
+ #else
39
+ # REXML::Formatters::Pretty.new( indent, ie_hack )
40
+ #end
41
+ else
42
+ REXML::Formatters::Default.new( ie_hack )
43
+ end
44
+ formatter.write( self, output )
45
+ end
46
+ end
47
+
48
+ class REXML::Formatters::Transitive
49
+ remove_method :write_element rescue nil
50
+ def write_element( node, output )
51
+ output << "<#{node.expanded_name}"
52
+
53
+ node.attributes.each_attribute do |attr|
54
+ output << " "
55
+ attr.write( output )
56
+ end unless node.attributes.empty?
57
+
58
+ if node.children.empty?
59
+ output << "/>"
60
+ else
61
+ output << ">"
62
+ # If compact and all children are text, and if the formatted output
63
+ # is less than the specified width, then try to print everything on
64
+ # one line
65
+ skip = false
66
+ @level += @indentation
67
+ node.children.each { |child|
68
+ write( child, output )
69
+ }
70
+ @level -= @indentation
71
+ output << "</#{node.expanded_name}>"
72
+ end
73
+ output << "\n"
74
+ output << ' '*@level
75
+ end
76
+ end
77
+ end
78
+
79
+ class Formatter # :nodoc:
80
+ require 'pathname'
81
+ ignore_files = [
82
+ /\A#{Regexp.escape(Pathname.new(Config::CONFIG["libdir"]).cleanpath.to_s)}/,
83
+ /\btc_[^.]*.rb/,
84
+ /_test\.rb\z/,
85
+ /\btest\//,
86
+ /\bvendor\//,
87
+ /\A#{Regexp.escape(__FILE__)}\z/]
88
+ DEFAULT_OPTS = {:ignore => ignore_files, :sort => :name, :sort_reverse => false,
89
+ :output_threshold => 101, :dont_ignore => [],
90
+ :callsite_analyzer => nil, :comments_run_by_default => false}
91
+ def initialize(opts = {})
92
+ options = DEFAULT_OPTS.clone.update(opts)
93
+ @files = {}
94
+ @ignore_files = options[:ignore]
95
+ @dont_ignore_files = options[:dont_ignore]
96
+ @sort_criterium = case options[:sort]
97
+ when :loc : lambda{|fname, finfo| finfo.num_code_lines}
98
+ when :coverage : lambda{|fname, finfo| finfo.code_coverage}
99
+ else lambda{|fname, finfo| fname}
100
+ end
101
+ @sort_reverse = options[:sort_reverse]
102
+ @output_threshold = options[:output_threshold]
103
+ @callsite_analyzer = options[:callsite_analyzer]
104
+ @comments_run_by_default = options[:comments_run_by_default]
105
+ @callsite_index = nil
106
+
107
+ @mangle_filename = Hash.new{|h,base|
108
+ h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".html"
109
+ }
110
+ end
111
+
112
+ def add_file(filename, lines, coverage, counts)
113
+ old_filename = filename
114
+ filename = normalize_filename(filename)
115
+ SCRIPT_LINES__[filename] = SCRIPT_LINES__[old_filename]
116
+ if @ignore_files.any?{|x| x === filename} &&
117
+ !@dont_ignore_files.any?{|x| x === filename}
118
+ return nil
119
+ end
120
+ if @files[filename]
121
+ @files[filename].merge(lines, coverage, counts)
122
+ else
123
+ @files[filename] = FileStatistics.new(filename, lines, counts,
124
+ @comments_run_by_default)
125
+ end
126
+ end
127
+
128
+ def normalize_filename(filename)
129
+ File.expand_path(filename).gsub(/^#{Regexp.escape(Dir.getwd)}\//, '')
130
+ end
131
+
132
+ def mangle_filename(base)
133
+ @mangle_filename[base]
134
+ end
135
+
136
+ def each_file_pair_sorted(&b)
137
+ return sorted_file_pairs unless block_given?
138
+ sorted_file_pairs.each(&b)
139
+ end
140
+
141
+ def sorted_file_pairs
142
+ pairs = @files.sort_by do |fname, finfo|
143
+ @sort_criterium.call(fname, finfo)
144
+ end.select{|_, finfo| 100 * finfo.code_coverage < @output_threshold}
145
+ @sort_reverse ? pairs.reverse : pairs
146
+ end
147
+
148
+ def total_coverage
149
+ lines = 0
150
+ total = 0.0
151
+ @files.each do |k,f|
152
+ total += f.num_lines * f.total_coverage
153
+ lines += f.num_lines
154
+ end
155
+ return 0 if lines == 0
156
+ total / lines
157
+ end
158
+
159
+ def code_coverage
160
+ lines = 0
161
+ total = 0.0
162
+ @files.each do |k,f|
163
+ total += f.num_code_lines * f.code_coverage
164
+ lines += f.num_code_lines
165
+ end
166
+ return 0 if lines == 0
167
+ total / lines
168
+ end
169
+
170
+ def num_code_lines
171
+ lines = 0
172
+ @files.each{|k, f| lines += f.num_code_lines }
173
+ lines
174
+ end
175
+
176
+ def num_lines
177
+ lines = 0
178
+ @files.each{|k, f| lines += f.num_lines }
179
+ lines
180
+ end
181
+
182
+ private
183
+ def cross_references_for(filename, lineno)
184
+ return nil unless @callsite_analyzer
185
+ @callsite_index ||= build_callsite_index
186
+ @callsite_index[normalize_filename(filename)][lineno]
187
+ end
188
+
189
+ def reverse_cross_references_for(filename, lineno)
190
+ return nil unless @callsite_analyzer
191
+ @callsite_reverse_index ||= build_reverse_callsite_index
192
+ @callsite_reverse_index[normalize_filename(filename)][lineno]
193
+ end
194
+
195
+ def build_callsite_index
196
+ index = Hash.new{|h,k| h[k] = {}}
197
+ @callsite_analyzer.analyzed_classes.each do |classname|
198
+ @callsite_analyzer.analyzed_methods(classname).each do |methname|
199
+ defsite = @callsite_analyzer.defsite(classname, methname)
200
+ index[normalize_filename(defsite.file)][defsite.line] =
201
+ @callsite_analyzer.callsites(classname, methname)
202
+ end
203
+ end
204
+ index
205
+ end
206
+
207
+ def build_reverse_callsite_index
208
+ index = Hash.new{|h,k| h[k] = {}}
209
+ @callsite_analyzer.analyzed_classes.each do |classname|
210
+ @callsite_analyzer.analyzed_methods(classname).each do |methname|
211
+ callsites = @callsite_analyzer.callsites(classname, methname)
212
+ defsite = @callsite_analyzer.defsite(classname, methname)
213
+ callsites.each_pair do |callsite, count|
214
+ next unless callsite.file
215
+ fname = normalize_filename(callsite.file)
216
+ (index[fname][callsite.line] ||= []) << [classname, methname, defsite, count]
217
+ end
218
+ end
219
+ end
220
+ index
221
+ end
222
+
223
+ class XRefHelper < Struct.new(:file, :line, :klass, :mid, :count) # :nodoc:
224
+ end
225
+
226
+ def _get_defsites(ref_blocks, filename, lineno, linetext, label, &format_call_ref)
227
+ if @do_cross_references and
228
+ (rev_xref = reverse_cross_references_for(filename, lineno))
229
+ refs = rev_xref.map do |classname, methodname, defsite, count|
230
+ XRefHelper.new(defsite.file, defsite.line, classname, methodname, count)
231
+ end.sort_by{|r| r.count}.reverse
232
+ ref_blocks << [refs, label, format_call_ref]
233
+ end
234
+ end
235
+
236
+ def _get_callsites(ref_blocks, filename, lineno, linetext, label, &format_called_ref)
237
+ if @do_callsites and
238
+ (refs = cross_references_for(filename, lineno))
239
+ refs = refs.sort_by{|k,count| count}.map do |ref, count|
240
+ XRefHelper.new(ref.file, ref.line, ref.calling_class, ref.calling_method, count)
241
+ end.reverse
242
+ ref_blocks << [refs, label, format_called_ref]
243
+ end
244
+ end
245
+ end
246
+
247
+ class TextSummary < Formatter # :nodoc:
248
+ def execute
249
+ puts summary
250
+ end
251
+
252
+ def summary
253
+ "%.1f%% %d file(s) %d Lines %d LOC" % [code_coverage * 100,
254
+ @files.size, num_lines, num_code_lines]
255
+ end
256
+ end
257
+
258
+ class TextReport < TextSummary # :nodoc:
259
+ def execute
260
+ print_lines
261
+ print_header
262
+ print_lines
263
+ each_file_pair_sorted do |fname, finfo|
264
+ name = fname.size < 52 ? fname : "..." + fname[-48..-1]
265
+ print_info(name, finfo.num_lines, finfo.num_code_lines,
266
+ finfo.code_coverage)
267
+ end
268
+ print_lines
269
+ print_info("Total", num_lines, num_code_lines, code_coverage)
270
+ print_lines
271
+ puts summary
272
+ end
273
+
274
+ def print_info(name, lines, loc, coverage)
275
+ puts "|%-51s | %5d | %5d | %5.1f%% |" % [name, lines, loc, 100 * coverage]
276
+ end
277
+
278
+ def print_lines
279
+ puts "+----------------------------------------------------+-------+-------+--------+"
280
+ end
281
+
282
+ def print_header
283
+ puts "| File | Lines | LOC | COV |"
284
+ end
285
+ end
286
+
287
+ class FullTextReport < Formatter # :nodoc:
288
+ DEFAULT_OPTS = {:textmode => :coverage}
289
+ def initialize(opts = {})
290
+ options = DEFAULT_OPTS.clone.update(opts)
291
+ @textmode = options[:textmode]
292
+ @color = options[:color]
293
+ super(options)
294
+ end
295
+
296
+ def execute
297
+ each_file_pair_sorted do |filename, fileinfo|
298
+ puts "=" * 80
299
+ puts filename
300
+ puts "=" * 80
301
+ lines = SCRIPT_LINES__[filename]
302
+ unless lines
303
+ # try to get the source code from the global code coverage
304
+ # analyzer
305
+ re = /#{Regexp.escape(filename)}\z/
306
+ if $rcov_code_coverage_analyzer and
307
+ (data = $rcov_code_coverage_analyzer.data_matching(re))
308
+ lines = data[0]
309
+ end
310
+ end
311
+ (lines || []).each_with_index do |line, i|
312
+ case @textmode
313
+ when :counts
314
+ puts "%-70s| %6d" % [line.chomp[0,70], fileinfo.counts[i]]
315
+ when :gcc
316
+ puts "%s:%d:%s" % [filename, i+1, line.chomp] unless fileinfo.coverage[i]
317
+ when :coverage
318
+ if @color
319
+ prefix = fileinfo.coverage[i] ? "\e[32;40m" : "\e[31;40m"
320
+ puts "#{prefix}%s\e[37;40m" % line.chomp
321
+ else
322
+ prefix = fileinfo.coverage[i] ? " " : "!! "
323
+ puts "#{prefix}#{line}"
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ class TextCoverageDiff < Formatter # :nodoc:
332
+ FORMAT_VERSION = [0, 1, 0]
333
+ DEFAULT_OPTS = {:textmode => :coverage_diff,
334
+ :coverage_diff_mode => :record,
335
+ :coverage_diff_file => "coverage.info",
336
+ :diff_cmd => "diff", :comments_run_by_default => true}
337
+ def SERIALIZER
338
+ # mfp> this was going to be YAML but I caught it failing at basic
339
+ # round-tripping, turning "\n" into "" and corrupting the data, so
340
+ # it must be Marshal for now
341
+ Marshal
342
+ end
343
+
344
+ def initialize(opts = {})
345
+ options = DEFAULT_OPTS.clone.update(opts)
346
+ @textmode = options[:textmode]
347
+ @color = options[:color]
348
+ @mode = options[:coverage_diff_mode]
349
+ @state_file = options[:coverage_diff_file]
350
+ @diff_cmd = options[:diff_cmd]
351
+ @gcc_output = options[:gcc_output]
352
+ super(options)
353
+ end
354
+
355
+ def execute
356
+ case @mode
357
+ when :record
358
+ record_state
359
+ when :compare
360
+ compare_state
361
+ else
362
+ raise "Unknown TextCoverageDiff mode: #{mode.inspect}."
363
+ end
364
+ end
365
+
366
+ def record_state
367
+ state = {}
368
+ each_file_pair_sorted do |filename, fileinfo|
369
+ state[filename] = {:lines => SCRIPT_LINES__[filename],
370
+ :coverage => fileinfo.coverage.to_a,
371
+ :counts => fileinfo.counts}
372
+ end
373
+ File.open(@state_file, "w") do |f|
374
+ self.SERIALIZER.dump([FORMAT_VERSION, state], f)
375
+ end
376
+ rescue
377
+ $stderr.puts <<-EOF
378
+ Couldn't save coverage data to #{@state_file}.
379
+ EOF
380
+ end # '
381
+
382
+ require 'tempfile'
383
+ def compare_state
384
+ return unless verify_diff_available
385
+ begin
386
+ format, prev_state = File.open(@state_file){|f| self.SERIALIZER.load(f) }
387
+ rescue
388
+ $stderr.puts <<-EOF
389
+ Couldn't load coverage data from #{@state_file}.
390
+ EOF
391
+ return # '
392
+ end
393
+ if !(Array === format) or
394
+ FORMAT_VERSION[0] != format[0] || FORMAT_VERSION[1] < format[1]
395
+ $stderr.puts <<-EOF
396
+ Couldn't load coverage data from #{@state_file}.
397
+ The file is saved in the format #{format.inspect[0..20]}.
398
+ This rcov executable understands #{FORMAT_VERSION.inspect}.
399
+ EOF
400
+ return # '
401
+ end
402
+ each_file_pair_sorted do |filename, fileinfo|
403
+ old_data = Tempfile.new("#{mangle_filename(filename)}-old")
404
+ new_data = Tempfile.new("#{mangle_filename(filename)}-new")
405
+ if prev_state.has_key? filename
406
+ old_code, old_cov = prev_state[filename].values_at(:lines, :coverage)
407
+ old_code.each_with_index do |line, i|
408
+ prefix = old_cov[i] ? " " : "!! "
409
+ old_data.write "#{prefix}#{line}"
410
+ end
411
+ else
412
+ old_data.write ""
413
+ end
414
+ old_data.close
415
+ SCRIPT_LINES__[filename].each_with_index do |line, i|
416
+ prefix = fileinfo.coverage[i] ? " " : "!! "
417
+ new_data.write "#{prefix}#{line}"
418
+ end
419
+ new_data.close
420
+
421
+ diff = `#{@diff_cmd} -u "#{old_data.path}" "#{new_data.path}"`
422
+ new_uncovered_hunks = process_unified_diff(filename, diff)
423
+ old_data.close!
424
+ new_data.close!
425
+ display_hunks(filename, new_uncovered_hunks)
426
+ end
427
+ end
428
+
429
+ def display_hunks(filename, hunks)
430
+ return if hunks.empty?
431
+ puts
432
+ puts "=" * 80
433
+ puts <<EOF
434
+ !!!!! Uncovered code introduced in #{filename}
435
+
436
+ EOF
437
+ hunks.each do |offset, lines|
438
+ if @gcc_output
439
+ lines.each_with_index do |line,i|
440
+ lineno = offset + i
441
+ flag = (/^!! / !~ line) ? "-" : ":"
442
+ prefix = "#{filename}#{flag}#{lineno}#{flag}"
443
+ puts "#{prefix}#{line[3..-1]}"
444
+ end
445
+ elsif @color
446
+ puts "### #{filename}:#{offset}"
447
+ lines.each do |line|
448
+ prefix = (/^!! / !~ line) ? "\e[32;40m" : "\e[31;40m"
449
+ puts "#{prefix}#{line[3..-1].chomp}\e[37;40m"
450
+ end
451
+ else
452
+ puts "### #{filename}:#{offset}"
453
+ puts lines
454
+ end
455
+ end
456
+ end
457
+
458
+ def verify_diff_available
459
+ old_stderr = STDERR.dup
460
+ old_stdout = STDOUT.dup
461
+ # TODO: should use /dev/null or NUL(?), but I don't want to add the
462
+ # win32 check right now
463
+ new_stderr = Tempfile.new("rcov_check_diff")
464
+ STDERR.reopen new_stderr.path
465
+ STDOUT.reopen new_stderr.path
466
+
467
+ retval = system "#{@diff_cmd} --version"
468
+ unless retval
469
+ old_stderr.puts <<EOF
470
+
471
+ The '#{@diff_cmd}' executable seems not to be available.
472
+ You can specify which diff executable should be used with --diff-cmd.
473
+ If your system doesn't have one, you might want to use Diff::LCS's:
474
+ gem install diff-lcs
475
+ and use --diff-cmd=ldiff.
476
+ EOF
477
+ return false
478
+ end
479
+ true
480
+ ensure
481
+ STDOUT.reopen old_stdout
482
+ STDERR.reopen old_stderr
483
+ new_stderr.close!
484
+ end
485
+
486
+ HUNK_HEADER = /@@ -\d+,\d+ \+(\d+),(\d+) @@/
487
+ def process_unified_diff(filename, diff)
488
+ current_hunk = []
489
+ current_hunk_start = 0
490
+ keep_current_hunk = false
491
+ state = :init
492
+ interesting_hunks = []
493
+ diff.each_with_index do |line, i|
494
+ #puts "#{state} %5d #{line}" % i
495
+ case state
496
+ when :init
497
+ if md = HUNK_HEADER.match(line)
498
+ current_hunk = []
499
+ current_hunk_start = md[1].to_i
500
+ state = :body
501
+ end
502
+ when :body
503
+ case line
504
+ when HUNK_HEADER
505
+ new_start = $1.to_i
506
+ if keep_current_hunk
507
+ interesting_hunks << [current_hunk_start, current_hunk]
508
+ end
509
+ current_hunk_start = new_start
510
+ current_hunk = []
511
+ keep_current_hunk = false
512
+ when /^-/
513
+ # ignore
514
+ when /^\+!! /
515
+ keep_current_hunk = true
516
+ current_hunk << line[1..-1]
517
+ else
518
+ current_hunk << line[1..-1]
519
+ end
520
+ end
521
+ end
522
+ if keep_current_hunk
523
+ interesting_hunks << [current_hunk_start, current_hunk]
524
+ end
525
+
526
+ interesting_hunks
527
+ end
528
+ end
529
+
530
+
531
+ class HTMLCoverage < Formatter # :nodoc:
532
+ include XX::XHTML
533
+ include XX::XMLish
534
+ require 'fileutils'
535
+ JAVASCRIPT_PROLOG = <<-EOS
536
+
537
+ // <![CDATA[
538
+ function toggleCode( id ) {
539
+ if ( document.getElementById )
540
+ elem = document.getElementById( id );
541
+ else if ( document.all )
542
+ elem = eval( "document.all." + id );
543
+ else
544
+ return false;
545
+
546
+ elemStyle = elem.style;
547
+
548
+ if ( elemStyle.display != "block" ) {
549
+ elemStyle.display = "block"
550
+ } else {
551
+ elemStyle.display = "none"
552
+ }
553
+
554
+ return true;
555
+ }
556
+
557
+ // Make cross-references hidden by default
558
+ document.writeln( "<style type=\\"text/css\\">span.cross-ref { display: none }</style>" )
559
+ // ]]>
560
+ EOS
561
+
562
+ CSS_PROLOG = <<-EOS
563
+ span.cross-ref-title {
564
+ font-size: 140%;
565
+ }
566
+ span.cross-ref a {
567
+ text-decoration: none;
568
+ }
569
+ span.cross-ref {
570
+ background-color:#f3f7fa;
571
+ border: 1px dashed #333;
572
+ margin: 1em;
573
+ padding: 0.5em;
574
+ overflow: hidden;
575
+ }
576
+ a.crossref-toggle {
577
+ text-decoration: none;
578
+ }
579
+ span.marked0 {
580
+ background-color: rgb(185, 210, 200);
581
+ display: block;
582
+ }
583
+ span.marked1 {
584
+ background-color: rgb(190, 215, 205);
585
+ display: block;
586
+ }
587
+ span.inferred0 {
588
+ background-color: rgb(255, 255, 240);
589
+ display: block;
590
+ }
591
+ span.inferred1 {
592
+ background-color: rgb(255, 255, 240);
593
+ display: block;
594
+ }
595
+ span.uncovered0 {
596
+ background-color: rgb(225, 110, 110);
597
+ display: block;
598
+ }
599
+ span.uncovered1 {
600
+ background-color: rgb(235, 120, 120);
601
+ display: block;
602
+ }
603
+ span.overview {
604
+ border-bottom: 8px solid black;
605
+ }
606
+ div.overview {
607
+ border-bottom: 8px solid black;
608
+ }
609
+ body {
610
+ font-family: verdana, arial, helvetica;
611
+ }
612
+
613
+ div.footer {
614
+ font-size: 68%;
615
+ margin-top: 1.5em;
616
+ }
617
+
618
+ h1, h2, h3, h4, h5, h6 {
619
+ margin-bottom: 0.5em;
620
+ }
621
+
622
+ h5 {
623
+ margin-top: 0.5em;
624
+ }
625
+
626
+ .hidden {
627
+ display: none;
628
+ }
629
+
630
+ div.separator {
631
+ height: 10px;
632
+ }
633
+ /* Commented out for better readability, esp. on IE */
634
+ /*
635
+ table tr td, table tr th {
636
+ font-size: 68%;
637
+ }
638
+
639
+ td.value table tr td {
640
+ font-size: 11px;
641
+ }
642
+ */
643
+
644
+ table.percent_graph {
645
+ height: 12px;
646
+ border: #808080 1px solid;
647
+ empty-cells: show;
648
+ }
649
+
650
+ table.percent_graph td.covered {
651
+ height: 10px;
652
+ background: #00f000;
653
+ }
654
+
655
+ table.percent_graph td.uncovered {
656
+ height: 10px;
657
+ background: #e00000;
658
+ }
659
+
660
+ table.percent_graph td.NA {
661
+ height: 10px;
662
+ background: #eaeaea;
663
+ }
664
+
665
+ table.report {
666
+ border-collapse: collapse;
667
+ width: 100%;
668
+ }
669
+
670
+ table.report td.heading {
671
+ background: #dcecff;
672
+ border: #d0d0d0 1px solid;
673
+ font-weight: bold;
674
+ text-align: center;
675
+ }
676
+
677
+ table.report td.heading:hover {
678
+ background: #c0ffc0;
679
+ }
680
+
681
+ table.report td.text {
682
+ border: #d0d0d0 1px solid;
683
+ }
684
+
685
+ table.report td.value,
686
+ table.report td.lines_total,
687
+ table.report td.lines_code {
688
+ text-align: right;
689
+ border: #d0d0d0 1px solid;
690
+ }
691
+ table.report tr.light {
692
+ background-color: rgb(240, 240, 245);
693
+ }
694
+ table.report tr.dark {
695
+ background-color: rgb(230, 230, 235);
696
+ }
697
+ EOS
698
+
699
+ DEFAULT_OPTS = {:color => false, :fsr => 30, :destdir => "coverage",
700
+ :callsites => false, :cross_references => false,
701
+ :validator_links => true, :charset => nil
702
+ }
703
+ def initialize(opts = {})
704
+ options = DEFAULT_OPTS.clone.update(opts)
705
+ super(options)
706
+ @dest = options[:destdir]
707
+ @color = options[:color]
708
+ @fsr = options[:fsr]
709
+ @do_callsites = options[:callsites]
710
+ @do_cross_references = options[:cross_references]
711
+ @span_class_index = 0
712
+ @show_validator_links = options[:validator_links]
713
+ @charset = options[:charset]
714
+ end
715
+
716
+ def execute
717
+ return if @files.empty?
718
+ FileUtils.mkdir_p @dest
719
+ create_index(File.join(@dest, "index.html"))
720
+ each_file_pair_sorted do |filename, fileinfo|
721
+ create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
722
+ end
723
+ end
724
+
725
+ private
726
+
727
+ def blurb
728
+ xmlish_ {
729
+ p_ {
730
+ t_{ "Generated using the " }
731
+ a_(:href => "http://eigenclass.org/hiki.rb?rcov") {
732
+ t_{ "rcov code coverage analysis tool for Ruby" }
733
+ }
734
+ t_{ " version #{Rcov::VERSION}." }
735
+ }
736
+ }.pretty
737
+ end
738
+
739
+ def output_color_table?
740
+ true
741
+ end
742
+
743
+ def default_color
744
+ "rgb(240, 240, 245)"
745
+ end
746
+
747
+ def default_title
748
+ "C0 code coverage information"
749
+ end
750
+
751
+ def format_overview(*file_infos)
752
+ table_text = xmlish_ {
753
+ table_(:class => "report") {
754
+ thead_ {
755
+ tr_ {
756
+ ["Name", "Total lines", "Lines of code", "Total coverage",
757
+ "Code coverage"].each do |heading|
758
+ td_(:class => "heading") { heading }
759
+ end
760
+ }
761
+ }
762
+ tbody_ {
763
+ color_class_index = 1
764
+ color_classes = %w[light dark]
765
+ file_infos.each do |f|
766
+ color_class_index += 1
767
+ color_class_index %= color_classes.size
768
+ tr_(:class => color_classes[color_class_index]) {
769
+ td_ {
770
+ case f.name
771
+ when "TOTAL":
772
+ t_ { "TOTAL" }
773
+ else
774
+ a_(:href => mangle_filename(f.name)){ t_ { f.name } }
775
+ end
776
+ }
777
+ [[f.num_lines, "lines_total"],
778
+ [f.num_code_lines, "lines_code"]].each do |value, css_class|
779
+ td_(:class => css_class) { tt_{ value } }
780
+ end
781
+ [[f.total_coverage, "coverage_total"],
782
+ [f.code_coverage, "coverage_code"]].each do |value, css_class|
783
+ value *= 100
784
+ td_ {
785
+ table_(:cellpadding => "0", :cellspacing => "0", :align => "right") {
786
+ tr_ {
787
+ td_ {
788
+ tt_(:class => css_class) { "%3.1f%%" % value }
789
+ x_ "&nbsp;"
790
+ }
791
+ ivalue = value.round
792
+ td_ {
793
+ table_(:class => "percent_graph", :cellpadding => "0",
794
+ :cellspacing => "0", :width => "100") {
795
+ tr_ {
796
+ td_(:class => "covered", :width => ivalue.to_s)
797
+ td_(:class => "uncovered", :width => (100-ivalue).to_s)
798
+ }
799
+ }
800
+ }
801
+ }
802
+ }
803
+ }
804
+ end
805
+ }
806
+ end
807
+ }
808
+ }
809
+ }
810
+ table_text.pretty
811
+ end
812
+
813
+ class SummaryFileInfo # :nodoc:
814
+ def initialize(obj); @o = obj end
815
+ %w[num_lines num_code_lines code_coverage total_coverage].each do |m|
816
+ define_method(m){ @o.send(m) }
817
+ end
818
+ def name; "TOTAL" end
819
+ end
820
+
821
+ def create_index(destname)
822
+ files = [SummaryFileInfo.new(self)] + each_file_pair_sorted.map{|k,v| v}
823
+ title = default_title
824
+ output = xhtml_ { html_ {
825
+ head_ {
826
+ if @charset
827
+ meta_("http-equiv".to_sym => "Content-Type",
828
+ :content => "text/html;charset=#{@charset}")
829
+ end
830
+ title_{ title }
831
+ style_(:type => "text/css") { t_{ "body { background-color: #{default_color}; }" } }
832
+ style_(:type => "text/css") { CSS_PROLOG }
833
+ script_(:type => "text/javascript") { h_{ JAVASCRIPT_PROLOG } }
834
+ }
835
+ body_ {
836
+ h3_{
837
+ t_{ title }
838
+ }
839
+ p_ {
840
+ t_{ "Generated on #{Time.new.to_s} with " }
841
+ a_(:href => Rcov::UPSTREAM_URL){ "rcov #{Rcov::VERSION}" }
842
+ }
843
+ p_ { "Threshold: #{@output_threshold}%" } if @output_threshold != 101
844
+ hr_
845
+ x_{ format_overview(*files) }
846
+ hr_
847
+ x_{ blurb }
848
+
849
+ if @show_validator_links
850
+ p_ {
851
+ a_(:href => "http://validator.w3.org/check/referer") {
852
+ img_(:src => "http://www.w3.org/Icons/valid-xhtml11",
853
+ :alt => "Valid XHTML 1.1!", :height => "31", :width => "88")
854
+ }
855
+ a_(:href => "http://jigsaw.w3.org/css-validator/check/referer") {
856
+ img_(:style => "border:0;width:88px;height:31px",
857
+ :src => "http://jigsaw.w3.org/css-validator/images/vcss",
858
+ :alt => "Valid CSS!")
859
+ }
860
+ }
861
+ end
862
+ }
863
+ } }
864
+ lines = output.pretty.to_a
865
+ lines.unshift lines.pop if /DOCTYPE/ =~ lines[-1]
866
+ File.open(destname, "w") do |f|
867
+ f.puts lines
868
+ end
869
+ end
870
+
871
+ def format_lines(file)
872
+ result = ""
873
+ last = nil
874
+ end_of_span = ""
875
+ format_line = "%#{file.num_lines.to_s.size}d"
876
+ file.num_lines.times do |i|
877
+ line = file.lines[i].chomp
878
+ marked = file.coverage[i]
879
+ count = file.counts[i]
880
+ spanclass = span_class(file, marked, count)
881
+ if spanclass != last
882
+ result += end_of_span
883
+ case spanclass
884
+ when nil
885
+ end_of_span = ""
886
+ else
887
+ result += %[<span class="#{spanclass}">]
888
+ end_of_span = "</span>"
889
+ end
890
+ end
891
+ result += %[<a name="line#{i+1}"></a>] + (format_line % (i+1)) +
892
+ " " + create_cross_refs(file.name, i+1, CGI.escapeHTML(line)) + "\n"
893
+ last = spanclass
894
+ end
895
+ result += end_of_span
896
+ "<pre>#{result}</pre>"
897
+ end
898
+
899
+ def create_cross_refs(filename, lineno, linetext)
900
+ return linetext unless @callsite_analyzer && @do_callsites
901
+ ref_blocks = []
902
+ _get_defsites(ref_blocks, filename, lineno, "Calls", linetext) do |ref|
903
+ if ref.file
904
+ where = "at #{normalize_filename(ref.file)}:#{ref.line}"
905
+ else
906
+ where = "(C extension/core)"
907
+ end
908
+ CGI.escapeHTML("%7d %s" %
909
+ [ref.count, "#{ref.klass}##{ref.mid} " + where])
910
+ end
911
+ _get_callsites(ref_blocks, filename, lineno, "Called by", linetext) do |ref|
912
+ r = "%7d %s" % [ref.count,
913
+ "#{normalize_filename(ref.file||'C code')}:#{ref.line} " +
914
+ "in '#{ref.klass}##{ref.mid}'"]
915
+ CGI.escapeHTML(r)
916
+ end
917
+
918
+ create_cross_reference_block(linetext, ref_blocks)
919
+ end
920
+
921
+ def create_cross_reference_block(linetext, ref_blocks)
922
+ return linetext if ref_blocks.empty?
923
+ ret = ""
924
+ @cross_ref_idx ||= 0
925
+ @known_files ||= sorted_file_pairs.map{|fname, finfo| normalize_filename(fname)}
926
+ ret << %[<a class="crossref-toggle" href="#" onclick="toggleCode('XREF-#{@cross_ref_idx+=1}'); return false;">#{linetext}</a>]
927
+ ret << %[<span class="cross-ref" id="XREF-#{@cross_ref_idx}">]
928
+ ret << "\n"
929
+ ref_blocks.each do |refs, toplabel, label_proc|
930
+ unless !toplabel || toplabel.empty?
931
+ ret << %!<span class="cross-ref-title">#{toplabel}</span>\n!
932
+ end
933
+ refs.each do |dst|
934
+ dstfile = normalize_filename(dst.file) if dst.file
935
+ dstline = dst.line
936
+ label = label_proc.call(dst)
937
+ if dst.file && @known_files.include?(dstfile)
938
+ ret << %[<a href="#{mangle_filename(dstfile)}#line#{dstline}">#{label}</a>]
939
+ else
940
+ ret << label
941
+ end
942
+ ret << "\n"
943
+ end
944
+ end
945
+ ret << "</span>"
946
+ end
947
+
948
+ def span_class(sourceinfo, marked, count)
949
+ @span_class_index ^= 1
950
+ case marked
951
+ when true
952
+ "marked#{@span_class_index}"
953
+ when :inferred
954
+ "inferred#{@span_class_index}"
955
+ else
956
+ "uncovered#{@span_class_index}"
957
+ end
958
+ end
959
+
960
+ def create_file(destfile, fileinfo)
961
+ #$stderr.puts "Generating #{destfile.inspect}"
962
+ body = format_overview(fileinfo) + format_lines(fileinfo)
963
+ title = fileinfo.name + " - #{default_title}"
964
+ do_ctable = output_color_table?
965
+ output = xhtml_ { html_ {
966
+ head_ {
967
+ if @charset
968
+ meta_("http-equiv".to_sym => "Content-Type",
969
+ :content => "text/html;charset=#{@charset}")
970
+ end
971
+ title_{ title }
972
+ style_(:type => "text/css") { t_{ "body { background-color: #{default_color}; }" } }
973
+ style_(:type => "text/css") { CSS_PROLOG }
974
+ script_(:type => "text/javascript") { h_ { JAVASCRIPT_PROLOG } }
975
+ style_(:type => "text/css") { h_ { colorscale } }
976
+ }
977
+ body_ {
978
+ h3_{ t_{ default_title } }
979
+ p_ {
980
+ t_{ "Generated on #{Time.new.to_s} with " }
981
+ a_(:href => Rcov::UPSTREAM_URL){ "rcov #{Rcov::VERSION}" }
982
+ }
983
+ hr_
984
+ if do_ctable
985
+ # this kludge needed to ensure .pretty doesn't mangle it
986
+ x_ { <<EOS
987
+ <pre><span class='marked0'>Code reported as executed by Ruby looks like this...
988
+ </span><span class='marked1'>and this: this line is also marked as covered.
989
+ </span><span class='inferred0'>Lines considered as run by rcov, but not reported by Ruby, look like this,
990
+ </span><span class='inferred1'>and this: these lines were inferred by rcov (using simple heuristics).
991
+ </span><span class='uncovered0'>Finally, here&apos;s a line marked as not executed.
992
+ </span></pre>
993
+ EOS
994
+ }
995
+ end
996
+ x_{ body }
997
+ hr_
998
+ x_ { blurb }
999
+
1000
+ if @show_validator_links
1001
+ p_ {
1002
+ a_(:href => "http://validator.w3.org/check/referer") {
1003
+ img_(:src => "http://www.w3.org/Icons/valid-xhtml10",
1004
+ :alt => "Valid XHTML 1.0!", :height => "31", :width => "88")
1005
+ }
1006
+ a_(:href => "http://jigsaw.w3.org/css-validator/check/referer") {
1007
+ img_(:style => "border:0;width:88px;height:31px",
1008
+ :src => "http://jigsaw.w3.org/css-validator/images/vcss",
1009
+ :alt => "Valid CSS!")
1010
+ }
1011
+ }
1012
+ end
1013
+ }
1014
+ } }
1015
+ # .pretty needed to make sure DOCTYPE is in a separate line
1016
+ lines = output.pretty.to_a
1017
+ lines.unshift lines.pop if /DOCTYPE/ =~ lines[-1]
1018
+ File.open(destfile, "w") do |f|
1019
+ f.puts lines
1020
+ end
1021
+ end
1022
+
1023
+ def colorscale
1024
+ colorscalebase =<<EOF
1025
+ span.run%d {
1026
+ background-color: rgb(%d, %d, %d);
1027
+ display: block;
1028
+ }
1029
+ EOF
1030
+ cscale = ""
1031
+ 101.times do |i|
1032
+ if @color
1033
+ r, g, b = hsv2rgb(220-(2.2*i).to_i, 0.3, 1)
1034
+ r = (r * 255).to_i
1035
+ g = (g * 255).to_i
1036
+ b = (b * 255).to_i
1037
+ else
1038
+ r = g = b = 255 - i
1039
+ end
1040
+ cscale << colorscalebase % [i, r, g, b]
1041
+ end
1042
+ cscale
1043
+ end
1044
+
1045
+ # thanks to kig @ #ruby-lang for this one
1046
+ def hsv2rgb(h,s,v)
1047
+ return [v,v,v] if s == 0
1048
+ h = h/60.0
1049
+ i = h.floor
1050
+ f = h-i
1051
+ p = v * (1-s)
1052
+ q = v * (1-s*f)
1053
+ t = v * (1-s*(1-f))
1054
+ case i
1055
+ when 0
1056
+ r = v
1057
+ g = t
1058
+ b = p
1059
+ when 1
1060
+ r = q
1061
+ g = v
1062
+ b = p
1063
+ when 2
1064
+ r = p
1065
+ g = v
1066
+ b = t
1067
+ when 3
1068
+ r = p
1069
+ g = q
1070
+ b = v
1071
+ when 4
1072
+ r = t
1073
+ g = p
1074
+ b = v
1075
+ when 5
1076
+ r = v
1077
+ g = p
1078
+ b = q
1079
+ end
1080
+ [r,g,b]
1081
+ end
1082
+ end
1083
+
1084
+ class HTMLProfiling < HTMLCoverage # :nodoc:
1085
+
1086
+ DEFAULT_OPTS = {:destdir => "profiling"}
1087
+ def initialize(opts = {})
1088
+ options = DEFAULT_OPTS.clone.update(opts)
1089
+ super(options)
1090
+ @max_cache = {}
1091
+ @median_cache = {}
1092
+ end
1093
+
1094
+ def default_title
1095
+ "Bogo-profile information"
1096
+ end
1097
+
1098
+ def default_color
1099
+ if @color
1100
+ "rgb(179,205,255)"
1101
+ else
1102
+ "rgb(255, 255, 255)"
1103
+ end
1104
+ end
1105
+
1106
+ def output_color_table?
1107
+ false
1108
+ end
1109
+
1110
+ def span_class(sourceinfo, marked, count)
1111
+ full_scale_range = @fsr # dB
1112
+ nz_count = sourceinfo.counts.select{|x| x && x != 0}
1113
+ nz_count << 1 # avoid div by 0
1114
+ max = @max_cache[sourceinfo] ||= nz_count.max
1115
+ #avg = @median_cache[sourceinfo] ||= 1.0 *
1116
+ # nz_count.inject{|a,b| a+b} / nz_count.size
1117
+ median = @median_cache[sourceinfo] ||= 1.0 * nz_count.sort[nz_count.size/2]
1118
+ max ||= 2
1119
+ max = 2 if max == 1
1120
+ if marked == true
1121
+ count = 1 if !count || count == 0
1122
+ idx = 50 + 1.0 * (500/full_scale_range) * Math.log(count/median) /
1123
+ Math.log(10)
1124
+ idx = idx.to_i
1125
+ idx = 0 if idx < 0
1126
+ idx = 100 if idx > 100
1127
+ "run#{idx}"
1128
+ else
1129
+ nil
1130
+ end
1131
+ end
1132
+ end
1133
+
1134
+ class RubyAnnotation < Formatter # :nodoc:
1135
+ DEFAULT_OPTS = { :destdir => "coverage" }
1136
+ def initialize(opts = {})
1137
+ options = DEFAULT_OPTS.clone.update(opts)
1138
+ super(options)
1139
+ @dest = options[:destdir]
1140
+ @do_callsites = true
1141
+ @do_cross_references = true
1142
+
1143
+ @mangle_filename = Hash.new{|h,base|
1144
+ h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".rb"
1145
+ }
1146
+ end
1147
+
1148
+ def execute
1149
+ return if @files.empty?
1150
+ FileUtils.mkdir_p @dest
1151
+ each_file_pair_sorted do |filename, fileinfo|
1152
+ create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
1153
+ end
1154
+ end
1155
+
1156
+ private
1157
+
1158
+ def format_lines(file)
1159
+ result = ""
1160
+ format_line = "%#{file.num_lines.to_s.size}d"
1161
+ file.num_lines.times do |i|
1162
+ line = file.lines[i].chomp
1163
+ marked = file.coverage[i]
1164
+ count = file.counts[i]
1165
+ result << create_cross_refs(file.name, i+1, line, marked) + "\n"
1166
+ end
1167
+ result
1168
+ end
1169
+
1170
+ def create_cross_refs(filename, lineno, linetext, marked)
1171
+ return linetext unless @callsite_analyzer && @do_callsites
1172
+ ref_blocks = []
1173
+ _get_defsites(ref_blocks, filename, lineno, linetext, ">>") do |ref|
1174
+ if ref.file
1175
+ ref.file.sub!(%r!^./!, '')
1176
+ where = "at #{mangle_filename(ref.file)}:#{ref.line}"
1177
+ else
1178
+ where = "(C extension/core)"
1179
+ end
1180
+ "#{ref.klass}##{ref.mid} " + where + ""
1181
+ end
1182
+ _get_callsites(ref_blocks, filename, lineno, linetext, "<<") do |ref| # "
1183
+ ref.file.sub!(%r!^./!, '')
1184
+ "#{mangle_filename(ref.file||'C code')}:#{ref.line} " +
1185
+ "in #{ref.klass}##{ref.mid}"
1186
+ end
1187
+
1188
+ create_cross_reference_block(linetext, ref_blocks, marked)
1189
+ end
1190
+
1191
+ def create_cross_reference_block(linetext, ref_blocks, marked)
1192
+ codelen = 75
1193
+ if ref_blocks.empty?
1194
+ if marked
1195
+ return "%-#{codelen}s #o" % linetext
1196
+ else
1197
+ return linetext
1198
+ end
1199
+ end
1200
+ ret = ""
1201
+ @cross_ref_idx ||= 0
1202
+ @known_files ||= sorted_file_pairs.map{|fname, finfo| normalize_filename(fname)}
1203
+ ret << "%-#{codelen}s # " % linetext
1204
+ ref_blocks.each do |refs, toplabel, label_proc|
1205
+ unless !toplabel || toplabel.empty?
1206
+ ret << toplabel << " "
1207
+ end
1208
+ refs.each do |dst|
1209
+ dstfile = normalize_filename(dst.file) if dst.file
1210
+ dstline = dst.line
1211
+ label = label_proc.call(dst)
1212
+ if dst.file && @known_files.include?(dstfile)
1213
+ ret << "[[" << label << "]], "
1214
+ else
1215
+ ret << label << ", "
1216
+ end
1217
+ end
1218
+ end
1219
+
1220
+ ret
1221
+ end
1222
+
1223
+ def create_file(destfile, fileinfo)
1224
+ #$stderr.puts "Generating #{destfile.inspect}"
1225
+ body = format_lines(fileinfo)
1226
+ File.open(destfile, "w") do |f|
1227
+ f.puts body
1228
+ f.puts footer(fileinfo)
1229
+ end
1230
+ end
1231
+
1232
+ def footer(fileinfo)
1233
+ s = "# Total lines : %d\n" % fileinfo.num_lines
1234
+ s << "# Lines of code : %d\n" % fileinfo.num_code_lines
1235
+ s << "# Total coverage : %3.1f%%\n" % [ fileinfo.total_coverage*100 ]
1236
+ s << "# Code coverage : %3.1f%%\n\n" % [ fileinfo.code_coverage*100 ]
1237
+ # prevents false positives on Emacs
1238
+ s << "# Local " "Variables:\n" "# mode: " "rcov-xref\n" "# End:\n"
1239
+ end
1240
+ end
1241
+
1242
+
1243
+ end # Rcov
1244
+
1245
+ # vi: set sw=4:
1246
+ # Here is Emacs setting. DO NOT REMOVE!
1247
+ # Local Variables:
1248
+ # ruby-indent-level: 4
1249
+ # End: