rcov 0.5.0.1 → 0.6.0.1

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