jruby-rcov 0.8.2.1-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/BLURB +149 -0
- data/CHANGES +177 -0
- data/LEGAL +36 -0
- data/LICENSE +56 -0
- data/Rakefile +100 -0
- data/THANKS +96 -0
- data/bin/rcov +560 -0
- data/doc/readme_for_api +41 -0
- data/doc/readme_for_emacs +64 -0
- data/doc/readme_for_rake +62 -0
- data/doc/readme_for_vim +47 -0
- data/editor-extensions/rcov.el +131 -0
- data/editor-extensions/rcov.vim +38 -0
- data/lib/rcov.rb +991 -0
- data/lib/rcov/formatters.rb +14 -0
- data/lib/rcov/formatters/base_formatter.rb +199 -0
- data/lib/rcov/formatters/full_text_report.rb +55 -0
- data/lib/rcov/formatters/html_coverage.rb +735 -0
- data/lib/rcov/formatters/text_coverage_diff.rb +199 -0
- data/lib/rcov/formatters/text_report.rb +36 -0
- data/lib/rcov/formatters/text_summary.rb +15 -0
- data/lib/rcov/lowlevel.rb +145 -0
- data/lib/rcov/rcovtask.rb +156 -0
- data/lib/rcov/report.rb +71 -0
- data/lib/rcov/rexml_extensions.rb +44 -0
- data/lib/rcov/version.rb +11 -0
- data/lib/rcov/xx.rb +754 -0
- data/lib/rcovrt.jar +0 -0
- data/setup.rb +1588 -0
- data/test/assets/sample_01.rb +7 -0
- data/test/assets/sample_02.rb +5 -0
- data/test/assets/sample_03.rb +20 -0
- data/test/assets/sample_04.rb +10 -0
- data/test/assets/sample_05-new.rb +17 -0
- data/test/assets/sample_05-old.rb +13 -0
- data/test/assets/sample_05.rb +17 -0
- data/test/call_site_analyzer_test.rb +171 -0
- data/test/code_coverage_analyzer_test.rb +188 -0
- data/test/file_statistics_test.rb +471 -0
- data/test/functional_test.rb +89 -0
- data/test/turn_off_rcovrt.rb +4 -0
- metadata +102 -0
data/doc/readme_for_api
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
= +rcov+
|
|
3
|
+
|
|
4
|
+
+rcov+ is a:
|
|
5
|
+
1. tool for code coverage analysis for Ruby
|
|
6
|
+
2. library for collecting code coverage and execution count information
|
|
7
|
+
introspectively
|
|
8
|
+
|
|
9
|
+
If you want to use the command line tool, the output from
|
|
10
|
+
rcov -h
|
|
11
|
+
is self-explicative.
|
|
12
|
+
|
|
13
|
+
If you want to automate the execution of +rcov+ via Rake take a look at
|
|
14
|
+
readme_for_rake[link:files/README_rake.html].
|
|
15
|
+
|
|
16
|
+
If you want to use the associated library, read on.
|
|
17
|
+
|
|
18
|
+
== Usage of the +rcov+ runtime/library
|
|
19
|
+
|
|
20
|
+
+rcov+ is primarily a tool for code coverage analysis, but since 0.4.0 it
|
|
21
|
+
exposes some of its code so that you can build on top of its heuristics for
|
|
22
|
+
code analysis and its capabilities for coverage information and execution
|
|
23
|
+
count gathering.
|
|
24
|
+
|
|
25
|
+
The main classes of interest are Rcov::FileStatistics,
|
|
26
|
+
Rcov::CodeCoverageAnalyzer and Rcov::CallSiteAnalyzer.
|
|
27
|
+
|
|
28
|
+
Rcov::FileStatistics can use some heuristics to determine
|
|
29
|
+
which parts of the file are executable and which are mere comments.
|
|
30
|
+
|
|
31
|
+
Rcov::CodeCoverageAnalyzer is used to gather code coverage and execution
|
|
32
|
+
count information inside a running Ruby program.
|
|
33
|
+
|
|
34
|
+
Rcov::CallSiteAnalyzer is used to obtain information about where methods are
|
|
35
|
+
defined and who calls them.
|
|
36
|
+
|
|
37
|
+
The parts of +rcov+'s runtime meant to be reused (i.e. the external API) are
|
|
38
|
+
documented with RDoc. Those not meant to be used are clearly marked as so or
|
|
39
|
+
were deliberately removed from the present documentation.
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
<tt>rcov.el</tt> allows you to use rcov from Emacs conveniently.
|
|
3
|
+
* Run unit tests and jump to uncovered code by <tt>C-x `</tt>.
|
|
4
|
+
* Run unit tests and save the current coverage status.
|
|
5
|
+
* Run unit tests and jump to uncovered code introduced since the last run.
|
|
6
|
+
* View cross-reference annotated code.
|
|
7
|
+
|
|
8
|
+
== Installation
|
|
9
|
+
|
|
10
|
+
Copy <tt>rcov.el</tt> to the appropriate directory, which is in load-path.
|
|
11
|
+
Then require it.
|
|
12
|
+
(require 'rcov)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
== Usage
|
|
16
|
+
|
|
17
|
+
There are some commands to run rcov in Emacs.
|
|
18
|
+
All of them displays +rcov+ window, whose major-mode is compilation-mode.
|
|
19
|
+
Therefore you can jump to uncovered code by <tt>C-x `</tt>.
|
|
20
|
+
|
|
21
|
+
+rcov-command-line+, +rcovsave-command-line+, and +rcovdiff-command-line+ define
|
|
22
|
+
command line to run rcov.
|
|
23
|
+
If you do not use +rcov+ from Rake, you must modify them.
|
|
24
|
+
|
|
25
|
+
=== Finding uncovered code
|
|
26
|
+
|
|
27
|
+
Type the following while editing your program:
|
|
28
|
+
M-x rcov
|
|
29
|
+
|
|
30
|
+
=== Setting the reference point
|
|
31
|
+
|
|
32
|
+
+rcov+'s <tt>--text-coverage-diff</tt> mode compares the current coverage status against
|
|
33
|
+
the saved one. It therefore needs that information to be recorded
|
|
34
|
+
before you write new code (typically right after you perform a commit) in
|
|
35
|
+
order to have something to compare against.
|
|
36
|
+
|
|
37
|
+
You can save the current status with the <tt>--save</tt> option.
|
|
38
|
+
|
|
39
|
+
Type the following to save the current status in Emacs:
|
|
40
|
+
M-x rcovsave
|
|
41
|
+
If you do not use +rcov+ from Rake, you must modify +rcovsave-command-line+ variable.
|
|
42
|
+
|
|
43
|
+
=== Finding new uncovered code
|
|
44
|
+
|
|
45
|
+
Type the following to save the current status in Emacs:
|
|
46
|
+
M-x rcovdiff
|
|
47
|
+
|
|
48
|
+
=== Viewing cross-reference annotated code
|
|
49
|
+
|
|
50
|
+
If you read cross-reference annotated code, issue
|
|
51
|
+
rake rcov RCOVOPTS='-a'
|
|
52
|
+
at the beginning.
|
|
53
|
+
This command creates +coverage+ directory and many *.rb files in it.
|
|
54
|
+
Filenames of these Ruby scripts are converted from original path.
|
|
55
|
+
You can browse them by normally <tt>C-x C-f</tt>.
|
|
56
|
+
You can think of <tt>-a</tt> option as <tt>--xrefs</tt> option and output format is Ruby script.
|
|
57
|
+
|
|
58
|
+
After find-file-ed annotated script, the major-mode is rcov-xref-mode,
|
|
59
|
+
which is derived from ruby-mode and specializes navigation.
|
|
60
|
+
|
|
61
|
+
<tt>Tab</tt> and <tt>M-Tab</tt> goes forward/backward links.
|
|
62
|
+
<tt>Ret</tt> follows selected link.
|
|
63
|
+
|
|
64
|
+
This feature is useful to read third-party code or to follow control flow.
|
data/doc/readme_for_rake
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
== Code coverage analysis automation with Rake
|
|
3
|
+
|
|
4
|
+
Since 0.4.0, <tt>rcov</tt> features a <tt>Rcov::RcovTask</tt> task for rake
|
|
5
|
+
which can be used to automate test coverage analysis. Basic usage is as
|
|
6
|
+
follows:
|
|
7
|
+
|
|
8
|
+
require 'rcov/rcovtask'
|
|
9
|
+
Rcov::RcovTask.new do |t|
|
|
10
|
+
t.test_files = FileList['test/test*.rb']
|
|
11
|
+
# t.verbose = true # uncomment to see the executed command
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
This will create by default a task named <tt>rcov</tt>, and also a task to
|
|
15
|
+
remove the output directory where the XHTML report is generated.
|
|
16
|
+
The latter will be named <tt>clobber_rcob</tt>, and will be added to the main
|
|
17
|
+
<tt>clobber</tt> target.
|
|
18
|
+
|
|
19
|
+
=== Passing command line options to <tt>rcov</tt>
|
|
20
|
+
|
|
21
|
+
You can provide a description, change the name of the generated tasks (the
|
|
22
|
+
one used to generate the report(s) and the clobber_ one) and pass options to
|
|
23
|
+
<tt>rcov</tt>:
|
|
24
|
+
|
|
25
|
+
desc "Analyze code coverage of the unit tests."
|
|
26
|
+
Rcov::RcovTask.new(:coverage) do |t|
|
|
27
|
+
t.test_files = FileList['test/test*.rb']
|
|
28
|
+
t.verbose = true
|
|
29
|
+
## get a text report on stdout when rake is run:
|
|
30
|
+
t.rcov_opts << "--text-report"
|
|
31
|
+
## only report files under 80% coverage
|
|
32
|
+
t.rcov_opts << "--threshold 80"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
That will generate a <tt>coverage</tt> task and the associated
|
|
36
|
+
<tt>clobber_coverage</tt> task to remove the directory the report is dumped
|
|
37
|
+
to ("<tt>coverage</tt>" by default).
|
|
38
|
+
|
|
39
|
+
You can specify a different destination directory, which comes handy if you
|
|
40
|
+
have several <tt>RcovTask</tt>s; the <tt>clobber_*</tt> will take care of
|
|
41
|
+
removing that directory:
|
|
42
|
+
|
|
43
|
+
desc "Analyze code coverage for the FileStatistics class."
|
|
44
|
+
Rcov::RcovTask.new(:rcov_sourcefile) do |t|
|
|
45
|
+
t.test_files = FileList['test/test_FileStatistics.rb']
|
|
46
|
+
t.verbose = true
|
|
47
|
+
t.rcov_opts << "--test-unit-only"
|
|
48
|
+
t.output_dir = "coverage.sourcefile"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Rcov::RcovTask.new(:rcov_ccanalyzer) do |t|
|
|
52
|
+
t.test_files = FileList['test/test_CodeCoverageAnalyzer.rb']
|
|
53
|
+
t.verbose = true
|
|
54
|
+
t.rcov_opts << "--test-unit-only"
|
|
55
|
+
t.output_dir = "coverage.ccanalyzer"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
=== Options passed through the <tt>rake</tt> command line
|
|
59
|
+
|
|
60
|
+
You can override the options defined in the RcovTask by passing the new
|
|
61
|
+
options at the time you invoke rake.
|
|
62
|
+
The documentation for the Rcov::RcovTask explains how this can be done.
|
data/doc/readme_for_vim
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
<tt>rcov.vim</tt> allows you to run unit tests from vim and enter quickfix mode in
|
|
3
|
+
order to jump to uncovered code introduced since the last run.
|
|
4
|
+
|
|
5
|
+
== Installation
|
|
6
|
+
Copy <tt>rcov.vim</tt> to the appropriate "compiler" directory (typically
|
|
7
|
+
<tt>$HOME/.vim/compiler</tt>).
|
|
8
|
+
|
|
9
|
+
== Usage
|
|
10
|
+
|
|
11
|
+
=== Setting the reference point
|
|
12
|
+
|
|
13
|
+
+rcov+'s <tt>--text-coverage-diff</tt> mode compares the current coverage status against
|
|
14
|
+
the saved one. It therefore needs that information to be recorded
|
|
15
|
+
before you write new code (typically right after you perform a commit) in
|
|
16
|
+
order to have something to compare against.
|
|
17
|
+
|
|
18
|
+
You can save the current status with the <tt>--save</tt> option.
|
|
19
|
+
If you're running +rcov+ from Rake, you can do something like
|
|
20
|
+
rake rcov_units RCOVOPTS="-T --save --rails"
|
|
21
|
+
in order to take the current status as the reference point.
|
|
22
|
+
|
|
23
|
+
=== Finding new uncovered code
|
|
24
|
+
|
|
25
|
+
Type the following in command mode while editing your program:
|
|
26
|
+
:compiler rcov
|
|
27
|
+
|
|
28
|
+
rcov.vim assumes +rcov+ can be invoked with a rake task (see
|
|
29
|
+
readme_for_rake[link:files/README_rake.html] for
|
|
30
|
+
information on how to create it).
|
|
31
|
+
|
|
32
|
+
You can then execute +rcov+ and enter quickfix mode by typing
|
|
33
|
+
|
|
34
|
+
:make <taskname>
|
|
35
|
+
|
|
36
|
+
where taskname is the +rcov+ task you want to use; if you didn't override the
|
|
37
|
+
default name in the Rakefile, just
|
|
38
|
+
|
|
39
|
+
:make rcov
|
|
40
|
+
|
|
41
|
+
will do.
|
|
42
|
+
|
|
43
|
+
vim will then enter quickfix mode, allowing you to jump to the areas that were
|
|
44
|
+
not covered since the last time you saved the coverage data.
|
|
45
|
+
|
|
46
|
+
--------
|
|
47
|
+
# vim: ft=text :
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
;;; rcov.el -- Ruby Coverage Analysis Tool
|
|
2
|
+
|
|
3
|
+
;;; Copyright (c) 2006 rubikitch <rubikitch@ruby-lang.org>
|
|
4
|
+
;;;
|
|
5
|
+
;;; Use and distribution subject to the terms of the rcov license.
|
|
6
|
+
|
|
7
|
+
(defvar rcov-xref-before-visit-source-hook nil
|
|
8
|
+
"Hook executed before jump.")
|
|
9
|
+
(defvar rcov-xref-after-visit-source-hook nil
|
|
10
|
+
"Hook executed after jump.")
|
|
11
|
+
(defvar rcov-command-line "rake rcov RCOVOPTS='--gcc --no-html'"
|
|
12
|
+
"Rcov command line to find uncovered code.
|
|
13
|
+
It is good to use rcov with Rake because it `cd's appropriate directory.
|
|
14
|
+
`--gcc' option is strongly recommended because `rcov' uses compilation-mode.")
|
|
15
|
+
(defvar rcovsave-command-line "rake rcov RCOVOPTS='--gcc --no-html --save=coverage.info'"
|
|
16
|
+
"Rcov command line to save coverage status. See also `rcov-command-line'.")
|
|
17
|
+
(defvar rcovdiff-command-line "rake rcov RCOVOPTS='-D --gcc --no-html'"
|
|
18
|
+
"Rcov command line to find new uncovered code. See also `rcov-command-line'.")
|
|
19
|
+
|
|
20
|
+
;;;; rcov-xref-mode
|
|
21
|
+
(define-derived-mode rcov-xref-mode ruby-mode "Rxref"
|
|
22
|
+
"Major mode for annotated Ruby scripts (coverage/*.rb) by rcov."
|
|
23
|
+
(setq truncate-lines t)
|
|
24
|
+
;; ruby-electric-mode / pabbrev-mode hijacks TAB binding.
|
|
25
|
+
(and ruby-electric-mode (ruby-electric-mode -1))
|
|
26
|
+
(and (boundp 'pabbrev-mode) pabbrev-mode (pabbrev-mode -1))
|
|
27
|
+
(suppress-keymap rcov-xref-mode-map)
|
|
28
|
+
(define-key rcov-xref-mode-map "\C-i" 'rcov-xref-next-tag)
|
|
29
|
+
(define-key rcov-xref-mode-map "\M-\C-i" 'rcov-xref-previous-tag)
|
|
30
|
+
(define-key rcov-xref-mode-map "\C-m" 'rcov-xref-visit-source)
|
|
31
|
+
(set (make-local-variable 'automatic-hscrolling) nil)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
(defvar rcov-xref-tag-regexp "\\[\\[\\(.*?\\)\\]\\]")
|
|
35
|
+
|
|
36
|
+
(defun rcov-xref-next-tag (n)
|
|
37
|
+
"Go to next LINK."
|
|
38
|
+
(interactive "p")
|
|
39
|
+
(when (looking-at rcov-xref-tag-regexp)
|
|
40
|
+
(goto-char (match-end 0)))
|
|
41
|
+
(when (re-search-forward rcov-xref-tag-regexp nil t n)
|
|
42
|
+
(goto-char (match-beginning 0)))
|
|
43
|
+
(rcov-xref-show-link))
|
|
44
|
+
|
|
45
|
+
(defun rcov-xref-previous-tag (n)
|
|
46
|
+
"Go to previous LINK."
|
|
47
|
+
(interactive "p")
|
|
48
|
+
(re-search-backward rcov-xref-tag-regexp nil t n)
|
|
49
|
+
(rcov-xref-show-link))
|
|
50
|
+
|
|
51
|
+
(defvar rcov-xref-link-tempbuffer " *rcov-link*")
|
|
52
|
+
(defun rcov-xref-show-link ()
|
|
53
|
+
"Follow current LINK."
|
|
54
|
+
(let ((link (match-string 1))
|
|
55
|
+
(eol (point-at-eol)))
|
|
56
|
+
(save-excursion
|
|
57
|
+
(when (and link
|
|
58
|
+
(re-search-backward "# \\(>>\\|<<\\) " (point-at-bol) t))
|
|
59
|
+
(while (re-search-forward rcov-xref-tag-regexp eol t)
|
|
60
|
+
(let ((matched (match-string 1)))
|
|
61
|
+
(when (string= link matched)
|
|
62
|
+
(add-text-properties 0 (length matched) '(face highlight) matched))
|
|
63
|
+
(with-current-buffer (get-buffer-create rcov-xref-link-tempbuffer)
|
|
64
|
+
(insert matched "\n"))))
|
|
65
|
+
(let (message-log-max) ; inhibit *Messages*
|
|
66
|
+
(message "%s" (with-current-buffer rcov-xref-link-tempbuffer
|
|
67
|
+
(substring (buffer-string) 0 -1)))) ; chomp
|
|
68
|
+
(kill-buffer rcov-xref-link-tempbuffer)))))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
;; copied from jw-visit-source
|
|
72
|
+
(defun rcov-xref-extract-file-lines (line)
|
|
73
|
+
"Extract a list of file/line pairs from the given line of text."
|
|
74
|
+
(let*
|
|
75
|
+
((unix_fn "[^ \t\n\r\"'([<{]+")
|
|
76
|
+
(dos_fn "[a-zA-Z]:[^ \t\n\r\"'([<{]+")
|
|
77
|
+
(flre (concat "\\(" unix_fn "\\|" dos_fn "\\):\\([0-9]+\\)"))
|
|
78
|
+
(start nil)
|
|
79
|
+
(result nil))
|
|
80
|
+
(while (string-match flre line start)
|
|
81
|
+
(setq start (match-end 0))
|
|
82
|
+
(setq result
|
|
83
|
+
(cons (list
|
|
84
|
+
(substring line (match-beginning 1) (match-end 1))
|
|
85
|
+
(string-to-int (substring line (match-beginning 2) (match-end 2))))
|
|
86
|
+
result)))
|
|
87
|
+
result))
|
|
88
|
+
|
|
89
|
+
(defun rcov-xref-select-file-line (candidates)
|
|
90
|
+
"Select a file/line candidate that references an existing file."
|
|
91
|
+
(cond ((null candidates) nil)
|
|
92
|
+
((file-readable-p (caar candidates)) (car candidates))
|
|
93
|
+
(t (rcov-xref-select-file-line (cdr candidates))) ))
|
|
94
|
+
|
|
95
|
+
(defun rcov-xref-visit-source ()
|
|
96
|
+
"If the current line contains text like '../src/program.rb:34', visit
|
|
97
|
+
that file in the other window and position point on that line."
|
|
98
|
+
(interactive)
|
|
99
|
+
(let* ((line (progn (looking-at rcov-xref-tag-regexp) (match-string 1)))
|
|
100
|
+
(candidates (rcov-xref-extract-file-lines line))
|
|
101
|
+
(file-line (rcov-xref-select-file-line candidates)))
|
|
102
|
+
(cond (file-line
|
|
103
|
+
(run-hooks 'rcov-xref-before-visit-source-hook)
|
|
104
|
+
(find-file (car file-line))
|
|
105
|
+
(goto-line (cadr file-line))
|
|
106
|
+
(run-hooks 'rcov-xref-after-visit-source-hook))
|
|
107
|
+
(t
|
|
108
|
+
(error "No source location on line.")) )))
|
|
109
|
+
|
|
110
|
+
;;;; Running rcov with various options.
|
|
111
|
+
(defun rcov-internal (cmdline)
|
|
112
|
+
"Run rcov with various options."
|
|
113
|
+
(compile-internal cmdline ""
|
|
114
|
+
nil nil nil (lambda (x) "*rcov*")))
|
|
115
|
+
|
|
116
|
+
(defun rcov ()
|
|
117
|
+
"Run rcov to find uncovered code."
|
|
118
|
+
(interactive)
|
|
119
|
+
(rcov-internal rcov-command-line))
|
|
120
|
+
|
|
121
|
+
(defun rcovsave ()
|
|
122
|
+
"Run rcov to save coverage status."
|
|
123
|
+
(interactive)
|
|
124
|
+
(rcov-internal rcovsave-command-line))
|
|
125
|
+
|
|
126
|
+
(defun rcovdiff ()
|
|
127
|
+
"Run rcov to find new uncovered code."
|
|
128
|
+
(interactive)
|
|
129
|
+
(rcov-internal rcovdiff-command-line))
|
|
130
|
+
|
|
131
|
+
(provide 'rcov)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
" Vim compiler file
|
|
2
|
+
" Language: Ruby
|
|
3
|
+
" Function: Code coverage information with rcov
|
|
4
|
+
" Maintainer: Mauricio Fernandez <mfp at acm dot org>
|
|
5
|
+
" Info:
|
|
6
|
+
" URL: http://eigenclass.org/hiki.rb?rcov
|
|
7
|
+
" ----------------------------------------------------------------------------
|
|
8
|
+
"
|
|
9
|
+
" Changelog:
|
|
10
|
+
" 0.1: initial version, shipped with rcov 0.6.0
|
|
11
|
+
"
|
|
12
|
+
" Comments:
|
|
13
|
+
" Initial attempt.
|
|
14
|
+
" ----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
if exists("current_compiler")
|
|
17
|
+
finish
|
|
18
|
+
endif
|
|
19
|
+
let current_compiler = "rcov"
|
|
20
|
+
|
|
21
|
+
if exists(":CompilerSet") != 2 " older Vim always used :setlocal
|
|
22
|
+
command -nargs=* CompilerSet setlocal <args>
|
|
23
|
+
endif
|
|
24
|
+
|
|
25
|
+
let s:cpo_save = &cpo
|
|
26
|
+
set cpo-=C
|
|
27
|
+
|
|
28
|
+
CompilerSet makeprg=rake\ $*\ RCOVOPTS=\"-D\ --no-html\ --no-color\"\ $*
|
|
29
|
+
|
|
30
|
+
CompilerSet errorformat=
|
|
31
|
+
\%+W\#\#\#\ %f:%l\,
|
|
32
|
+
\%-C\ \ \ ,
|
|
33
|
+
\%-C!!\
|
|
34
|
+
|
|
35
|
+
let &cpo = s:cpo_save
|
|
36
|
+
unlet s:cpo_save
|
|
37
|
+
|
|
38
|
+
" vim: nowrap sw=2 sts=2 ts=8 ff=unix :
|
data/lib/rcov.rb
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
# rcov Copyright (c) 2004-2006 Mauricio Fernandez <mfp@acm.org>
|
|
2
|
+
#
|
|
3
|
+
# See LEGAL and LICENSE for licensing information.
|
|
4
|
+
|
|
5
|
+
# NOTE: if you're reading this in the XHTML code coverage report generated by
|
|
6
|
+
# rcov, you'll notice that only code inside methods is reported as covered,
|
|
7
|
+
# very much like what happens when you run it with --test-unit-only.
|
|
8
|
+
# This is due to the fact that we're running rcov on itself: the code below is
|
|
9
|
+
# already loaded before coverage tracing is activated, so only code inside
|
|
10
|
+
# methods is actually executed under rcov's inspection.
|
|
11
|
+
|
|
12
|
+
require 'rcov/version'
|
|
13
|
+
require 'rcov/formatters'
|
|
14
|
+
|
|
15
|
+
SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
|
|
16
|
+
|
|
17
|
+
module Rcov
|
|
18
|
+
|
|
19
|
+
# Rcov::CoverageInfo is but a wrapper for an array, with some additional
|
|
20
|
+
# checks. It is returned by FileStatistics#coverage.
|
|
21
|
+
class CoverageInfo
|
|
22
|
+
def initialize(coverage_array)
|
|
23
|
+
@cover = coverage_array.clone
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Return the coverage status for the requested line. There are four possible
|
|
27
|
+
# return values:
|
|
28
|
+
# * nil if there's no information for the requested line (i.e. it doesn't exist)
|
|
29
|
+
# * true if the line was reported by Ruby as executed
|
|
30
|
+
# * :inferred if rcov inferred it was executed, despite not being reported
|
|
31
|
+
# by Ruby.
|
|
32
|
+
# * false otherwise, i.e. if it was not reported by Ruby and rcov's
|
|
33
|
+
# heuristics indicated that it was not executed
|
|
34
|
+
def [](line)
|
|
35
|
+
@cover[line]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def []=(line, val) # :nodoc:
|
|
39
|
+
unless [true, false, :inferred].include? val
|
|
40
|
+
raise RuntimeError, "What does #{val} mean?"
|
|
41
|
+
end
|
|
42
|
+
return if line < 0 || line >= @cover.size
|
|
43
|
+
@cover[line] = val
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return an Array holding the code coverage information.
|
|
47
|
+
def to_a
|
|
48
|
+
@cover.clone
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def method_missing(meth, *a, &b) # :nodoc:
|
|
52
|
+
@cover.send(meth, *a, &b)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A FileStatistics object associates a filename to:
|
|
57
|
+
# 1. its source code
|
|
58
|
+
# 2. the per-line coverage information after correction using rcov's heuristics
|
|
59
|
+
# 3. the per-line execution counts
|
|
60
|
+
#
|
|
61
|
+
# A FileStatistics object can be therefore be built given the filename, the
|
|
62
|
+
# associated source code, and an array holding execution counts (i.e. how many
|
|
63
|
+
# times each line has been executed).
|
|
64
|
+
#
|
|
65
|
+
# FileStatistics is relatively intelligent: it handles normal comments,
|
|
66
|
+
# <tt>=begin/=end</tt>, heredocs, many multiline-expressions... It uses a
|
|
67
|
+
# number of heuristics to determine what is code and what is a comment, and to
|
|
68
|
+
# refine the initial (incomplete) coverage information.
|
|
69
|
+
#
|
|
70
|
+
# Basic usage is as follows:
|
|
71
|
+
# sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", " false",
|
|
72
|
+
# "puts 2", "end"], [1, 1, 0, 0, 0])
|
|
73
|
+
# sf.num_lines # => 5
|
|
74
|
+
# sf.num_code_lines # => 5
|
|
75
|
+
# sf.coverage[2] # => true
|
|
76
|
+
# sf.coverage[3] # => :inferred
|
|
77
|
+
# sf.code_coverage # => 0.6
|
|
78
|
+
#
|
|
79
|
+
# The array of strings representing the source code and the array of execution
|
|
80
|
+
# counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
|
|
81
|
+
class FileStatistics
|
|
82
|
+
attr_reader :name, :lines, :coverage, :counts
|
|
83
|
+
def initialize(name, lines, counts, comments_run_by_default = false)
|
|
84
|
+
@name = name
|
|
85
|
+
@lines = lines
|
|
86
|
+
initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
|
|
87
|
+
@coverage = CoverageInfo.new initial_coverage
|
|
88
|
+
@counts = counts
|
|
89
|
+
@is_begin_comment = nil
|
|
90
|
+
# points to the line defining the heredoc identifier
|
|
91
|
+
# but only if it was marked (we don't care otherwise)
|
|
92
|
+
@heredoc_start = Array.new(lines.size, false)
|
|
93
|
+
@multiline_string_start = Array.new(lines.size, false)
|
|
94
|
+
extend_heredocs
|
|
95
|
+
find_multiline_strings
|
|
96
|
+
precompute_coverage comments_run_by_default
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Merge code coverage and execution count information.
|
|
100
|
+
# As for code coverage, a line will be considered
|
|
101
|
+
# * covered for sure (true) if it is covered in either +self+ or in the
|
|
102
|
+
# +coverage+ array
|
|
103
|
+
# * considered <tt>:inferred</tt> if the neither +self+ nor the +coverage+ array
|
|
104
|
+
# indicate that it was definitely executed, but it was <tt>inferred</tt>
|
|
105
|
+
# in either one
|
|
106
|
+
# * not covered (<tt>false</tt>) if it was uncovered in both
|
|
107
|
+
#
|
|
108
|
+
# Execution counts are just summated on a per-line basis.
|
|
109
|
+
def merge(lines, coverage, counts)
|
|
110
|
+
coverage.each_with_index do |v, idx|
|
|
111
|
+
case @coverage[idx]
|
|
112
|
+
when :inferred
|
|
113
|
+
@coverage[idx] = v || @coverage[idx]
|
|
114
|
+
when false
|
|
115
|
+
@coverage[idx] ||= v
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
counts.each_with_index{|v, idx| @counts[idx] += v }
|
|
119
|
+
precompute_coverage false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Total coverage rate if comments are also considered "executable", given as
|
|
123
|
+
# a fraction, i.e. from 0 to 1.0.
|
|
124
|
+
# A comment is attached to the code following it (RDoc-style): it will be
|
|
125
|
+
# considered executed if the the next statement was executed.
|
|
126
|
+
def total_coverage
|
|
127
|
+
return 0 if @coverage.size == 0
|
|
128
|
+
@coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Code coverage rate: fraction of lines of code executed, relative to the
|
|
132
|
+
# total amount of lines of code (loc). Returns a float from 0 to 1.0.
|
|
133
|
+
def code_coverage
|
|
134
|
+
indices = (0...@lines.size).select{|i| is_code? i }
|
|
135
|
+
return 0 if indices.size == 0
|
|
136
|
+
count = 0
|
|
137
|
+
indices.each {|i| count += 1 if @coverage[i] }
|
|
138
|
+
1.0 * count / indices.size
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Number of lines of code (loc).
|
|
142
|
+
def num_code_lines
|
|
143
|
+
(0...@lines.size).select{|i| is_code? i}.size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Total number of lines.
|
|
147
|
+
def num_lines
|
|
148
|
+
@lines.size
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns true if the given line number corresponds to code, as opposed to a
|
|
152
|
+
# comment (either # or =begin/=end blocks).
|
|
153
|
+
def is_code?(lineno)
|
|
154
|
+
unless @is_begin_comment
|
|
155
|
+
@is_begin_comment = Array.new(@lines.size, false)
|
|
156
|
+
pending = []
|
|
157
|
+
state = :code
|
|
158
|
+
@lines.each_with_index do |line, index|
|
|
159
|
+
case state
|
|
160
|
+
when :code
|
|
161
|
+
if /^=begin\b/ =~ line
|
|
162
|
+
state = :comment
|
|
163
|
+
pending << index
|
|
164
|
+
end
|
|
165
|
+
when :comment
|
|
166
|
+
pending << index
|
|
167
|
+
if /^=end\b/ =~ line
|
|
168
|
+
state = :code
|
|
169
|
+
pending.each{|idx| @is_begin_comment[idx] = true}
|
|
170
|
+
pending.clear
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
@lines[lineno] && !@is_begin_comment[lineno] &&
|
|
176
|
+
@lines[lineno] !~ /^\s*(#|$)/
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def find_multiline_strings
|
|
182
|
+
state = :awaiting_string
|
|
183
|
+
wanted_delimiter = nil
|
|
184
|
+
string_begin_line = 0
|
|
185
|
+
@lines.each_with_index do |line, i|
|
|
186
|
+
matching_delimiters = Hash.new{|h,k| k}
|
|
187
|
+
matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
|
|
188
|
+
case state
|
|
189
|
+
when :awaiting_string
|
|
190
|
+
# very conservative, doesn't consider the last delimited string but
|
|
191
|
+
# only the very first one
|
|
192
|
+
if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
|
|
193
|
+
wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
|
|
194
|
+
# check if closed on the very same line
|
|
195
|
+
# conservative again, we might have several quoted strings with the
|
|
196
|
+
# same delimiter on the same line, leaving the last one open
|
|
197
|
+
unless wanted_delimiter.match(md.post_match)
|
|
198
|
+
state = :want_end_delimiter
|
|
199
|
+
string_begin_line = i
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
when :want_end_delimiter
|
|
203
|
+
@multiline_string_start[i] = string_begin_line
|
|
204
|
+
if wanted_delimiter.match(line)
|
|
205
|
+
state = :awaiting_string
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def precompute_coverage(comments_run_by_default = true)
|
|
212
|
+
changed = false
|
|
213
|
+
lastidx = lines.size - 1
|
|
214
|
+
if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
|
|
215
|
+
# mark the last block of comments
|
|
216
|
+
@coverage[lastidx] ||= :inferred
|
|
217
|
+
(lastidx-1).downto(0) do |i|
|
|
218
|
+
break if is_code?(i)
|
|
219
|
+
@coverage[i] ||= :inferred
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
(0...lines.size).each do |i|
|
|
223
|
+
next if @coverage[i]
|
|
224
|
+
line = @lines[i]
|
|
225
|
+
if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
|
|
226
|
+
/^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
|
|
227
|
+
/^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
|
|
228
|
+
/^\s*rescue\b/ =~ line && next_expr_marked?(i) or
|
|
229
|
+
/(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
|
|
230
|
+
prev_expr_continued?(i) && prev_expr_marked?(i) or
|
|
231
|
+
comments_run_by_default && !is_code?(i) or
|
|
232
|
+
/^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
|
|
233
|
+
prev_expr_continued?(i+1) && next_expr_marked?(i)
|
|
234
|
+
@coverage[i] ||= :inferred
|
|
235
|
+
changed = true
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
(@lines.size-1).downto(0) do |i|
|
|
239
|
+
next if @coverage[i]
|
|
240
|
+
if !is_code?(i) and @coverage[i+1]
|
|
241
|
+
@coverage[i] = :inferred
|
|
242
|
+
changed = true
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
extend_heredocs if changed
|
|
247
|
+
|
|
248
|
+
# if there was any change, we have to recompute; we'll eventually
|
|
249
|
+
# reach a fixed point and stop there
|
|
250
|
+
precompute_coverage(comments_run_by_default) if changed
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
require 'strscan'
|
|
254
|
+
def extend_heredocs
|
|
255
|
+
i = 0
|
|
256
|
+
while i < @lines.size
|
|
257
|
+
unless is_code? i
|
|
258
|
+
i += 1
|
|
259
|
+
next
|
|
260
|
+
end
|
|
261
|
+
#FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
|
|
262
|
+
# matches when unquoted, so as to avoid problems with 1<<2
|
|
263
|
+
# (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
|
|
264
|
+
# parse error, but a = 1<<2 is of course fine)
|
|
265
|
+
scanner = StringScanner.new(@lines[i])
|
|
266
|
+
j = k = i
|
|
267
|
+
loop do
|
|
268
|
+
scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/,
|
|
269
|
+
true, true)
|
|
270
|
+
# k is the first line after the end delimiter for the last heredoc
|
|
271
|
+
# scanned so far
|
|
272
|
+
unless scanner.matched?
|
|
273
|
+
i = k
|
|
274
|
+
break
|
|
275
|
+
end
|
|
276
|
+
term = scanner[3] || scanner[4]
|
|
277
|
+
# try to ignore symbolic bitshifts like 1<<LSHIFT
|
|
278
|
+
ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
|
|
279
|
+
if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
|
|
280
|
+
# it was preceded by a number, ignore
|
|
281
|
+
i = k
|
|
282
|
+
break
|
|
283
|
+
end
|
|
284
|
+
must_mark = []
|
|
285
|
+
end_of_heredoc = (scanner[1] == "-") ?
|
|
286
|
+
/^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
|
|
287
|
+
loop do
|
|
288
|
+
break if j == @lines.size
|
|
289
|
+
must_mark << j
|
|
290
|
+
if end_of_heredoc =~ @lines[j]
|
|
291
|
+
must_mark.each do |n|
|
|
292
|
+
@heredoc_start[n] = i
|
|
293
|
+
end
|
|
294
|
+
if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
|
|
295
|
+
@coverage[i] ||= :inferred
|
|
296
|
+
must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
|
|
297
|
+
end
|
|
298
|
+
# move the "first line after heredocs" index
|
|
299
|
+
k = (j += 1)
|
|
300
|
+
break
|
|
301
|
+
end
|
|
302
|
+
j += 1
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
i += 1
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def next_expr_marked?(lineno)
|
|
311
|
+
return false if lineno >= @lines.size
|
|
312
|
+
found = false
|
|
313
|
+
idx = (lineno+1).upto(@lines.size-1) do |i|
|
|
314
|
+
next unless is_code? i
|
|
315
|
+
found = true
|
|
316
|
+
break i
|
|
317
|
+
end
|
|
318
|
+
return false unless found
|
|
319
|
+
@coverage[idx]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def prev_expr_marked?(lineno)
|
|
323
|
+
return false if lineno <= 0
|
|
324
|
+
found = false
|
|
325
|
+
idx = (lineno-1).downto(0) do |i|
|
|
326
|
+
next unless is_code? i
|
|
327
|
+
found = true
|
|
328
|
+
break i
|
|
329
|
+
end
|
|
330
|
+
return false unless found
|
|
331
|
+
@coverage[idx]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def prev_expr_continued?(lineno)
|
|
335
|
+
return false if lineno <= 0
|
|
336
|
+
return false if lineno >= @lines.size
|
|
337
|
+
found = false
|
|
338
|
+
if @multiline_string_start[lineno] &&
|
|
339
|
+
@multiline_string_start[lineno] < lineno
|
|
340
|
+
return true
|
|
341
|
+
end
|
|
342
|
+
# find index of previous code line
|
|
343
|
+
idx = (lineno-1).downto(0) do |i|
|
|
344
|
+
if @heredoc_start[i]
|
|
345
|
+
found = true
|
|
346
|
+
break @heredoc_start[i]
|
|
347
|
+
end
|
|
348
|
+
next unless is_code? i
|
|
349
|
+
found = true
|
|
350
|
+
break i
|
|
351
|
+
end
|
|
352
|
+
return false unless found
|
|
353
|
+
#TODO: write a comprehensive list
|
|
354
|
+
if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
|
|
355
|
+
return true
|
|
356
|
+
end
|
|
357
|
+
#FIXME: / matches regexps too
|
|
358
|
+
# the following regexp tries to reject #{interpolation}
|
|
359
|
+
r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
|
|
360
|
+
# try to see if a multi-line expression with opening, closing delimiters
|
|
361
|
+
# started on that line
|
|
362
|
+
[%w!( )!].each do |opening_str, closing_str|
|
|
363
|
+
# conservative: only consider nesting levels opened in that line, not
|
|
364
|
+
# previous ones too.
|
|
365
|
+
# next regexp considers interpolation too
|
|
366
|
+
line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
|
|
367
|
+
opened = line.scan(/#{Regexp.escape(opening_str)}/).size
|
|
368
|
+
closed = line.scan(/#{Regexp.escape(closing_str)}/).size
|
|
369
|
+
return true if opened - closed > 0
|
|
370
|
+
end
|
|
371
|
+
if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
|
|
372
|
+
return false
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
r
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
autoload :RCOV__, "rcov/lowlevel.rb"
|
|
381
|
+
|
|
382
|
+
class DifferentialAnalyzer
|
|
383
|
+
require 'thread'
|
|
384
|
+
@@mutex = Mutex.new
|
|
385
|
+
|
|
386
|
+
def initialize(install_hook_meth, remove_hook_meth, reset_meth)
|
|
387
|
+
@cache_state = :wait
|
|
388
|
+
@start_raw_data = data_default
|
|
389
|
+
@end_raw_data = data_default
|
|
390
|
+
@aggregated_data = data_default
|
|
391
|
+
@install_hook_meth = install_hook_meth
|
|
392
|
+
@remove_hook_meth= remove_hook_meth
|
|
393
|
+
@reset_meth= reset_meth
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Execute the code in the given block, monitoring it in order to gather
|
|
397
|
+
# information about which code was executed.
|
|
398
|
+
def run_hooked
|
|
399
|
+
install_hook
|
|
400
|
+
yield
|
|
401
|
+
ensure
|
|
402
|
+
remove_hook
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Start monitoring execution to gather information. Such data will be
|
|
406
|
+
# collected until #remove_hook is called.
|
|
407
|
+
#
|
|
408
|
+
# Use #run_hooked instead if possible.
|
|
409
|
+
def install_hook
|
|
410
|
+
@start_raw_data = raw_data_absolute
|
|
411
|
+
Rcov::RCOV__.send(@install_hook_meth)
|
|
412
|
+
@cache_state = :hooked
|
|
413
|
+
@@mutex.synchronize{ self.class.hook_level += 1 }
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Stop collecting information.
|
|
417
|
+
# #remove_hook will also stop collecting info if it is run inside a
|
|
418
|
+
# #run_hooked block.
|
|
419
|
+
def remove_hook
|
|
420
|
+
@@mutex.synchronize do
|
|
421
|
+
self.class.hook_level -= 1
|
|
422
|
+
Rcov::RCOV__.send(@remove_hook_meth) if self.class.hook_level == 0
|
|
423
|
+
end
|
|
424
|
+
@end_raw_data = raw_data_absolute
|
|
425
|
+
@cache_state = :done
|
|
426
|
+
# force computation of the stats for the traced code in this run;
|
|
427
|
+
# we cannot simply let it be if self.class.hook_level == 0 because
|
|
428
|
+
# some other analyzer could install a hook, causing the raw_data_absolute
|
|
429
|
+
# to change again.
|
|
430
|
+
# TODO: lazy computation of raw_data_relative, only when the hook gets
|
|
431
|
+
# activated again.
|
|
432
|
+
raw_data_relative
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Remove the data collected so far. Further collection will start from
|
|
436
|
+
# scratch.
|
|
437
|
+
def reset
|
|
438
|
+
@@mutex.synchronize do
|
|
439
|
+
if self.class.hook_level == 0
|
|
440
|
+
# Unfortunately there's no way to report this as covered with rcov:
|
|
441
|
+
# if we run the tests under rcov self.class.hook_level will be >= 1 !
|
|
442
|
+
# It is however executed when we run the tests normally.
|
|
443
|
+
Rcov::RCOV__.send(@reset_meth)
|
|
444
|
+
@start_raw_data = data_default
|
|
445
|
+
@end_raw_data = data_default
|
|
446
|
+
else
|
|
447
|
+
@start_raw_data = @end_raw_data = raw_data_absolute
|
|
448
|
+
end
|
|
449
|
+
@raw_data_relative = data_default
|
|
450
|
+
@aggregated_data = data_default
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
protected
|
|
455
|
+
|
|
456
|
+
def data_default
|
|
457
|
+
raise "must be implemented by the subclass"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def self.hook_level
|
|
461
|
+
raise "must be implemented by the subclass"
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def raw_data_absolute
|
|
465
|
+
raise "must be implemented by the subclass"
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def aggregate_data(aggregated_data, delta)
|
|
469
|
+
raise "must be implemented by the subclass"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def compute_raw_data_difference(first, last)
|
|
473
|
+
raise "must be implemented by the subclass"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
private
|
|
477
|
+
def raw_data_relative
|
|
478
|
+
case @cache_state
|
|
479
|
+
when :wait
|
|
480
|
+
return @aggregated_data
|
|
481
|
+
when :hooked
|
|
482
|
+
new_start = raw_data_absolute
|
|
483
|
+
new_diff = compute_raw_data_difference(@start_raw_data, new_start)
|
|
484
|
+
@start_raw_data = new_start
|
|
485
|
+
when :done
|
|
486
|
+
@cache_state = :wait
|
|
487
|
+
new_diff = compute_raw_data_difference(@start_raw_data,
|
|
488
|
+
@end_raw_data)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
aggregate_data(@aggregated_data, new_diff)
|
|
492
|
+
|
|
493
|
+
@aggregated_data
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# A CodeCoverageAnalyzer is responsible for tracing code execution and
|
|
499
|
+
# returning code coverage and execution count information.
|
|
500
|
+
#
|
|
501
|
+
# Note that you must <tt>require 'rcov'</tt> before the code you want to
|
|
502
|
+
# analyze is parsed (i.e. before it gets loaded or required). You can do that
|
|
503
|
+
# by either invoking ruby with the <tt>-rrcov</tt> command-line option or
|
|
504
|
+
# just:
|
|
505
|
+
# require 'rcov'
|
|
506
|
+
# require 'mycode'
|
|
507
|
+
# # ....
|
|
508
|
+
#
|
|
509
|
+
# == Example
|
|
510
|
+
#
|
|
511
|
+
# analyzer = Rcov::CodeCoverageAnalyzer.new
|
|
512
|
+
# analyzer.run_hooked do
|
|
513
|
+
# do_foo
|
|
514
|
+
# # all the code executed as a result of this method call is traced
|
|
515
|
+
# end
|
|
516
|
+
# # ....
|
|
517
|
+
#
|
|
518
|
+
# analyzer.run_hooked do
|
|
519
|
+
# do_bar
|
|
520
|
+
# # the code coverage information generated in this run is aggregated
|
|
521
|
+
# # to the previously recorded one
|
|
522
|
+
# end
|
|
523
|
+
#
|
|
524
|
+
# analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
|
|
525
|
+
# lines, marked_info, count_info = analyzer.data("foo.rb")
|
|
526
|
+
#
|
|
527
|
+
# In this example, two pieces of code are monitored, and the data generated in
|
|
528
|
+
# both runs are aggregated. +lines+ is an array of strings representing the
|
|
529
|
+
# source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
|
|
530
|
+
# true values indicating whether the corresponding lines of code were reported
|
|
531
|
+
# as executed by Ruby. +count_info+ is an array of integers representing how
|
|
532
|
+
# many times each line of code has been executed (more precisely, how many
|
|
533
|
+
# events where reported by Ruby --- a single line might correspond to several
|
|
534
|
+
# events, e.g. many method calls).
|
|
535
|
+
#
|
|
536
|
+
# You can have several CodeCoverageAnalyzer objects at a time, and it is
|
|
537
|
+
# possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
|
|
538
|
+
# analyzer will manage its data separately. Note however that no special
|
|
539
|
+
# provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
|
|
540
|
+
# class. At any rate this will not pose a problem since it's easy to ignore it
|
|
541
|
+
# manually: just don't do
|
|
542
|
+
# lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
|
|
543
|
+
# if you're not interested in that information.
|
|
544
|
+
class CodeCoverageAnalyzer < DifferentialAnalyzer
|
|
545
|
+
@hook_level = 0
|
|
546
|
+
# defined this way instead of attr_accessor so that it's covered
|
|
547
|
+
def self.hook_level # :nodoc:
|
|
548
|
+
@hook_level
|
|
549
|
+
end
|
|
550
|
+
def self.hook_level=(x) # :nodoc:
|
|
551
|
+
@hook_level = x
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def initialize
|
|
555
|
+
@script_lines__ = SCRIPT_LINES__
|
|
556
|
+
super(:install_coverage_hook, :remove_coverage_hook,
|
|
557
|
+
:reset_coverage)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Return an array with the names of the files whose code was executed inside
|
|
561
|
+
# the block given to #run_hooked or between #install_hook and #remove_hook.
|
|
562
|
+
def analyzed_files
|
|
563
|
+
update_script_lines__
|
|
564
|
+
raw_data_relative.select do |file, lines|
|
|
565
|
+
@script_lines__.has_key?(file)
|
|
566
|
+
end.map{|fname,| fname}
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Return the available data about the requested file, or nil if none of its
|
|
570
|
+
# code was executed or it cannot be found.
|
|
571
|
+
# The return value is an array with three elements:
|
|
572
|
+
# lines, marked_info, count_info = analyzer.data("foo.rb")
|
|
573
|
+
# +lines+ is an array of strings representing the
|
|
574
|
+
# source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
|
|
575
|
+
# true values indicating whether the corresponding lines of code were reported
|
|
576
|
+
# as executed by Ruby. +count_info+ is an array of integers representing how
|
|
577
|
+
# many times each line of code has been executed (more precisely, how many
|
|
578
|
+
# events where reported by Ruby --- a single line might correspond to several
|
|
579
|
+
# events, e.g. many method calls).
|
|
580
|
+
#
|
|
581
|
+
# The returned data corresponds to the aggregation of all the statistics
|
|
582
|
+
# collected in each #run_hooked or #install_hook/#remove_hook runs. You can
|
|
583
|
+
# reset the data at any time with #reset to start from scratch.
|
|
584
|
+
def data(filename)
|
|
585
|
+
raw_data = raw_data_relative
|
|
586
|
+
update_script_lines__
|
|
587
|
+
unless @script_lines__.has_key?(filename) &&
|
|
588
|
+
raw_data.has_key?(filename)
|
|
589
|
+
return nil
|
|
590
|
+
end
|
|
591
|
+
refine_coverage_info(@script_lines__[filename], raw_data[filename])
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Data for the first file matching the given regexp.
|
|
595
|
+
# See #data.
|
|
596
|
+
def data_matching(filename_re)
|
|
597
|
+
raw_data = raw_data_relative
|
|
598
|
+
update_script_lines__
|
|
599
|
+
|
|
600
|
+
match = raw_data.keys.sort.grep(filename_re).first
|
|
601
|
+
return nil unless match
|
|
602
|
+
|
|
603
|
+
refine_coverage_info(@script_lines__[match], raw_data[match])
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Execute the code in the given block, monitoring it in order to gather
|
|
607
|
+
# information about which code was executed.
|
|
608
|
+
def run_hooked; super end
|
|
609
|
+
|
|
610
|
+
# Start monitoring execution to gather code coverage and execution count
|
|
611
|
+
# information. Such data will be collected until #remove_hook is called.
|
|
612
|
+
#
|
|
613
|
+
# Use #run_hooked instead if possible.
|
|
614
|
+
def install_hook; super end
|
|
615
|
+
|
|
616
|
+
# Stop collecting code coverage and execution count information.
|
|
617
|
+
# #remove_hook will also stop collecting info if it is run inside a
|
|
618
|
+
# #run_hooked block.
|
|
619
|
+
def remove_hook; super end
|
|
620
|
+
|
|
621
|
+
# Remove the data collected so far. The coverage and execution count
|
|
622
|
+
# "history" will be erased, and further collection will start from scratch:
|
|
623
|
+
# no code is considered executed, and therefore all execution counts are 0.
|
|
624
|
+
# Right after #reset, #analyzed_files will return an empty array, and
|
|
625
|
+
# #data(filename) will return nil.
|
|
626
|
+
def reset; super end
|
|
627
|
+
|
|
628
|
+
def dump_coverage_info(formatters) # :nodoc:
|
|
629
|
+
update_script_lines__
|
|
630
|
+
raw_data_relative.each do |file, lines|
|
|
631
|
+
next if @script_lines__.has_key?(file) == false
|
|
632
|
+
lines = @script_lines__[file]
|
|
633
|
+
raw_coverage_array = raw_data_relative[file]
|
|
634
|
+
|
|
635
|
+
line_info, marked_info,
|
|
636
|
+
count_info = refine_coverage_info(lines, raw_coverage_array)
|
|
637
|
+
formatters.each do |formatter|
|
|
638
|
+
formatter.add_file(file, line_info, marked_info, count_info)
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
formatters.each{|formatter| formatter.execute}
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
private
|
|
645
|
+
|
|
646
|
+
def data_default; {} end
|
|
647
|
+
|
|
648
|
+
def raw_data_absolute
|
|
649
|
+
Rcov::RCOV__.generate_coverage_info
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def aggregate_data(aggregated_data, delta)
|
|
653
|
+
delta.each_pair do |file, cov_arr|
|
|
654
|
+
dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
|
|
655
|
+
cov_arr.each_with_index{|x,i| dest[i] += x}
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def compute_raw_data_difference(first, last)
|
|
660
|
+
difference = {}
|
|
661
|
+
last.each_pair do |fname, cov_arr|
|
|
662
|
+
unless first.has_key?(fname)
|
|
663
|
+
difference[fname] = cov_arr.clone
|
|
664
|
+
else
|
|
665
|
+
orig_arr = first[fname]
|
|
666
|
+
diff_arr = Array.new(cov_arr.size, 0)
|
|
667
|
+
changed = false
|
|
668
|
+
cov_arr.each_with_index do |x, i|
|
|
669
|
+
diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
|
|
670
|
+
changed = true if diff != 0
|
|
671
|
+
end
|
|
672
|
+
difference[fname] = diff_arr if changed
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
difference
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def refine_coverage_info(lines, covers)
|
|
680
|
+
marked_info = []
|
|
681
|
+
count_info = []
|
|
682
|
+
lines.size.times do |i|
|
|
683
|
+
c = covers[i]
|
|
684
|
+
marked_info << ((c && c > 0) ? true : false)
|
|
685
|
+
count_info << (c || 0)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
script_lines_workaround(lines, marked_info, count_info)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Try to detect repeated data, based on observed repetitions in line_info:
|
|
692
|
+
# this is a workaround for SCRIPT_LINES__[filename] including as many copies
|
|
693
|
+
# of the file as the number of times it was parsed.
|
|
694
|
+
def script_lines_workaround(line_info, coverage_info, count_info)
|
|
695
|
+
is_repeated = lambda do |div|
|
|
696
|
+
n = line_info.size / div
|
|
697
|
+
break false unless line_info.size % div == 0 && n > 1
|
|
698
|
+
different = false
|
|
699
|
+
n.times do |i|
|
|
700
|
+
|
|
701
|
+
things = (0...div).map { |j| line_info[i + j * n] }
|
|
702
|
+
if things.uniq.size != 1
|
|
703
|
+
different = true
|
|
704
|
+
break
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
! different
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
factors = braindead_factorize(line_info.size)
|
|
712
|
+
factors.each do |n|
|
|
713
|
+
if is_repeated[n]
|
|
714
|
+
line_info = line_info[0, line_info.size / n]
|
|
715
|
+
coverage_info = coverage_info[0, coverage_info.size / n]
|
|
716
|
+
count_info = count_info[0, count_info.size / n]
|
|
717
|
+
end
|
|
718
|
+
end if factors.size > 1 # don't even try if it's prime
|
|
719
|
+
|
|
720
|
+
[line_info, coverage_info, count_info]
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def braindead_factorize(num)
|
|
724
|
+
return [0] if num == 0
|
|
725
|
+
return [-1] + braindead_factorize(-num) if num < 0
|
|
726
|
+
factors = []
|
|
727
|
+
while num % 2 == 0
|
|
728
|
+
factors << 2
|
|
729
|
+
num /= 2
|
|
730
|
+
end
|
|
731
|
+
size = num
|
|
732
|
+
n = 3
|
|
733
|
+
max = Math.sqrt(num)
|
|
734
|
+
while n <= max && n <= size
|
|
735
|
+
while size % n == 0
|
|
736
|
+
size /= n
|
|
737
|
+
factors << n
|
|
738
|
+
end
|
|
739
|
+
n += 2
|
|
740
|
+
end
|
|
741
|
+
factors << size if size != 1
|
|
742
|
+
factors
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def update_script_lines__
|
|
746
|
+
@script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
public
|
|
750
|
+
def marshal_dump # :nodoc:
|
|
751
|
+
# @script_lines__ is updated just before serialization so as to avoid
|
|
752
|
+
# missing files in SCRIPT_LINES__
|
|
753
|
+
ivs = {}
|
|
754
|
+
update_script_lines__
|
|
755
|
+
instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
|
|
756
|
+
ivs
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def marshal_load(ivs) # :nodoc:
|
|
760
|
+
ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
end # CodeCoverageAnalyzer
|
|
764
|
+
|
|
765
|
+
# A CallSiteAnalyzer can be used to obtain information about:
|
|
766
|
+
# * where a method is defined ("+defsite+")
|
|
767
|
+
# * where a method was called from ("+callsite+")
|
|
768
|
+
#
|
|
769
|
+
# == Example
|
|
770
|
+
# <tt>example.rb</tt>:
|
|
771
|
+
# class X
|
|
772
|
+
# def f1; f2 end
|
|
773
|
+
# def f2; 1 + 1 end
|
|
774
|
+
# def f3; f1 end
|
|
775
|
+
# end
|
|
776
|
+
#
|
|
777
|
+
# analyzer = Rcov::CallSiteAnalyzer.new
|
|
778
|
+
# x = X.new
|
|
779
|
+
# analyzer.run_hooked do
|
|
780
|
+
# x.f1
|
|
781
|
+
# end
|
|
782
|
+
# # ....
|
|
783
|
+
#
|
|
784
|
+
# analyzer.run_hooked do
|
|
785
|
+
# x.f3
|
|
786
|
+
# # the information generated in this run is aggregated
|
|
787
|
+
# # to the previously recorded one
|
|
788
|
+
# end
|
|
789
|
+
#
|
|
790
|
+
# analyzer.analyzed_classes # => ["X", ... ]
|
|
791
|
+
# analyzer.methods_for_class("X") # => ["f1", "f2", "f3"]
|
|
792
|
+
# analyzer.defsite("X#f1") # => DefSite object
|
|
793
|
+
# analyzer.callsites("X#f2") # => hash with CallSite => count
|
|
794
|
+
# # associations
|
|
795
|
+
# defsite = analyzer.defsite("X#f1")
|
|
796
|
+
# defsite.file # => "example.rb"
|
|
797
|
+
# defsite.line # => 2
|
|
798
|
+
#
|
|
799
|
+
# You can have several CallSiteAnalyzer objects at a time, and it is
|
|
800
|
+
# possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
|
|
801
|
+
# analyzer will manage its data separately. Note however that no special
|
|
802
|
+
# provision is taken to ignore code executed "inside" the CallSiteAnalyzer
|
|
803
|
+
# class.
|
|
804
|
+
#
|
|
805
|
+
# +defsite+ information is only available for methods that were called under
|
|
806
|
+
# the inspection of the CallSiteAnalyzer, i.o.w. you will only have +defsite+
|
|
807
|
+
# information for those methods for which callsite information is
|
|
808
|
+
# available.
|
|
809
|
+
class CallSiteAnalyzer < DifferentialAnalyzer
|
|
810
|
+
# A method definition site.
|
|
811
|
+
class DefSite < Struct.new(:file, :line)
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Object representing a method call site.
|
|
815
|
+
# It corresponds to a part of the callstack starting from the context that
|
|
816
|
+
# called the method.
|
|
817
|
+
class CallSite < Struct.new(:backtrace)
|
|
818
|
+
# The depth of a CallSite is the number of stack frames
|
|
819
|
+
# whose information is included in the CallSite object.
|
|
820
|
+
def depth
|
|
821
|
+
backtrace.size
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# File where the method call originated.
|
|
825
|
+
# Might return +nil+ or "" if it is not meaningful (C extensions, etc).
|
|
826
|
+
def file(level = 0)
|
|
827
|
+
stack_frame = backtrace[level]
|
|
828
|
+
stack_frame ? stack_frame[2] : nil
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Line where the method call originated.
|
|
832
|
+
# Might return +nil+ or 0 if it is not meaningful (C extensions, etc).
|
|
833
|
+
def line(level = 0)
|
|
834
|
+
stack_frame = backtrace[level]
|
|
835
|
+
stack_frame ? stack_frame[3] : nil
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Name of the method where the call originated.
|
|
839
|
+
# Returns +nil+ if the call originated in +toplevel+.
|
|
840
|
+
# Might return +nil+ if it could not be determined.
|
|
841
|
+
def calling_method(level = 0)
|
|
842
|
+
stack_frame = backtrace[level]
|
|
843
|
+
stack_frame ? stack_frame[1] : nil
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Name of the class holding the method where the call originated.
|
|
847
|
+
# Might return +nil+ if it could not be determined.
|
|
848
|
+
def calling_class(level = 0)
|
|
849
|
+
stack_frame = backtrace[level]
|
|
850
|
+
stack_frame ? stack_frame[0] : nil
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
@hook_level = 0
|
|
855
|
+
# defined this way instead of attr_accessor so that it's covered
|
|
856
|
+
def self.hook_level # :nodoc:
|
|
857
|
+
@hook_level
|
|
858
|
+
end
|
|
859
|
+
def self.hook_level=(x) # :nodoc:
|
|
860
|
+
@hook_level = x
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def initialize
|
|
864
|
+
super(:install_callsite_hook, :remove_callsite_hook,
|
|
865
|
+
:reset_callsite)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
# Classes whose methods have been called.
|
|
869
|
+
# Returns an array of strings describing the classes (just klass.to_s for
|
|
870
|
+
# each of them). Singleton classes are rendered as:
|
|
871
|
+
# #<Class:MyNamespace::MyClass>
|
|
872
|
+
def analyzed_classes
|
|
873
|
+
raw_data_relative.first.keys.map{|klass, meth| klass}.uniq.sort
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# Methods that were called for the given class. See #analyzed_classes for
|
|
877
|
+
# the notation used for singleton classes.
|
|
878
|
+
# Returns an array of strings or +nil+
|
|
879
|
+
def methods_for_class(classname)
|
|
880
|
+
a = raw_data_relative.first.keys.select{|kl,_| kl == classname}.map{|_,meth| meth}.sort
|
|
881
|
+
a.empty? ? nil : a
|
|
882
|
+
end
|
|
883
|
+
alias_method :analyzed_methods, :methods_for_class
|
|
884
|
+
|
|
885
|
+
# Returns a hash with <tt>CallSite => call count</tt> associations or +nil+
|
|
886
|
+
# Can be called in two ways:
|
|
887
|
+
# analyzer.callsites("Foo#f1") # instance method
|
|
888
|
+
# analyzer.callsites("Foo.g1") # singleton method of the class
|
|
889
|
+
# or
|
|
890
|
+
# analyzer.callsites("Foo", "f1")
|
|
891
|
+
# analyzer.callsites("#<class:Foo>", "g1")
|
|
892
|
+
def callsites(classname_or_fullname, methodname = nil)
|
|
893
|
+
rawsites = raw_data_relative.first[expand_name(classname_or_fullname, methodname)]
|
|
894
|
+
return nil unless rawsites
|
|
895
|
+
ret = {}
|
|
896
|
+
# could be a job for inject but it's slow and I don't mind the extra loc
|
|
897
|
+
rawsites.each_pair do |backtrace, count|
|
|
898
|
+
ret[CallSite.new(backtrace)] = count
|
|
899
|
+
end
|
|
900
|
+
ret
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# Returns a DefSite object corresponding to the given method
|
|
904
|
+
# Can be called in two ways:
|
|
905
|
+
# analyzer.defsite("Foo#f1") # instance method
|
|
906
|
+
# analyzer.defsite("Foo.g1") # singleton method of the class
|
|
907
|
+
# or
|
|
908
|
+
# analyzer.defsite("Foo", "f1")
|
|
909
|
+
# analyzer.defsite("#<class:Foo>", "g1")
|
|
910
|
+
def defsite(classname_or_fullname, methodname = nil)
|
|
911
|
+
file, line = raw_data_relative[1][expand_name(classname_or_fullname, methodname)]
|
|
912
|
+
return nil unless file && line
|
|
913
|
+
DefSite.new(file, line)
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
private
|
|
917
|
+
|
|
918
|
+
def expand_name(classname_or_fullname, methodname = nil)
|
|
919
|
+
if methodname.nil?
|
|
920
|
+
case classname_or_fullname
|
|
921
|
+
when /(.*)#(.*)/ then classname, methodname = $1, $2
|
|
922
|
+
when /(.*)\.(.*)/ then classname, methodname = "#<Class:#{$1}>", $2
|
|
923
|
+
else
|
|
924
|
+
raise ArgumentError, "Incorrect method name"
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
return [classname, methodname]
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
[classname_or_fullname, methodname]
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def data_default; [{}, {}] end
|
|
934
|
+
|
|
935
|
+
def raw_data_absolute
|
|
936
|
+
raw, method_def_site = RCOV__.generate_callsite_info
|
|
937
|
+
ret1 = {}
|
|
938
|
+
ret2 = {}
|
|
939
|
+
raw.each_pair do |(klass, method), hash|
|
|
940
|
+
begin
|
|
941
|
+
key = [klass.to_s, method.to_s]
|
|
942
|
+
ret1[key] = hash.clone #Marshal.load(Marshal.dump(hash))
|
|
943
|
+
ret2[key] = method_def_site[[klass, method]]
|
|
944
|
+
#rescue Exception
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
[ret1, ret2]
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def aggregate_data(aggregated_data, delta)
|
|
952
|
+
callsites1, defsites1 = aggregated_data
|
|
953
|
+
callsites2, defsites2 = delta
|
|
954
|
+
|
|
955
|
+
callsites2.each_pair do |(klass, method), hash|
|
|
956
|
+
dest_hash = (callsites1[[klass, method]] ||= {})
|
|
957
|
+
hash.each_pair do |callsite, count|
|
|
958
|
+
dest_hash[callsite] ||= 0
|
|
959
|
+
dest_hash[callsite] += count
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
defsites1.update(defsites2)
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
def compute_raw_data_difference(first, last)
|
|
967
|
+
difference = {}
|
|
968
|
+
default = Hash.new(0)
|
|
969
|
+
|
|
970
|
+
callsites1, defsites1 = *first
|
|
971
|
+
callsites2, defsites2 = *last
|
|
972
|
+
|
|
973
|
+
callsites2.each_pair do |(klass, method), hash|
|
|
974
|
+
old_hash = callsites1[[klass, method]] || default
|
|
975
|
+
hash.each_pair do |callsite, count|
|
|
976
|
+
diff = hash[callsite] - (old_hash[callsite] || 0)
|
|
977
|
+
if diff > 0
|
|
978
|
+
difference[[klass, method]] ||= {}
|
|
979
|
+
difference[[klass, method]][callsite] = diff
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
[difference, defsites1.update(defsites2)]
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
end # Rcov
|
|
990
|
+
|
|
991
|
+
# vi: set sw=2:
|