spicycode-rcov 0.8.1.3.0

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