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.
@@ -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.
@@ -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.
@@ -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: