jruby-rcov 0.8.2.2-java → 0.8.2.3-java

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