katsuya-rcov 0.9.7.1

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.
Files changed (66) hide show
  1. data/BLURB +111 -0
  2. data/LICENSE +53 -0
  3. data/Rakefile +103 -0
  4. data/THANKS +110 -0
  5. data/bin/rcov +514 -0
  6. data/doc/readme_for_api.markdown +22 -0
  7. data/doc/readme_for_emacs.markdown +52 -0
  8. data/doc/readme_for_rake.markdown +51 -0
  9. data/doc/readme_for_vim.markdown +34 -0
  10. data/editor-extensions/rcov.el +131 -0
  11. data/editor-extensions/rcov.vim +38 -0
  12. data/ext/rcovrt/1.8/callsite.c +216 -0
  13. data/ext/rcovrt/1.8/rcovrt.c +294 -0
  14. data/ext/rcovrt/1.9/callsite.c +234 -0
  15. data/ext/rcovrt/1.9/rcovrt.c +264 -0
  16. data/ext/rcovrt/extconf.rb +21 -0
  17. data/lib/rcov.rb +33 -0
  18. data/lib/rcov/call_site_analyzer.rb +225 -0
  19. data/lib/rcov/code_coverage_analyzer.rb +271 -0
  20. data/lib/rcov/coverage_info.rb +36 -0
  21. data/lib/rcov/differential_analyzer.rb +116 -0
  22. data/lib/rcov/file_statistics.rb +355 -0
  23. data/lib/rcov/formatters.rb +13 -0
  24. data/lib/rcov/formatters/base_formatter.rb +174 -0
  25. data/lib/rcov/formatters/failure_report.rb +15 -0
  26. data/lib/rcov/formatters/full_text_report.rb +48 -0
  27. data/lib/rcov/formatters/html_coverage.rb +274 -0
  28. data/lib/rcov/formatters/html_erb_template.rb +62 -0
  29. data/lib/rcov/formatters/text_coverage_diff.rb +193 -0
  30. data/lib/rcov/formatters/text_report.rb +32 -0
  31. data/lib/rcov/formatters/text_summary.rb +11 -0
  32. data/lib/rcov/lowlevel.rb +146 -0
  33. data/lib/rcov/rcovtask.rb +155 -0
  34. data/lib/rcov/templates/detail.html.erb +64 -0
  35. data/lib/rcov/templates/index.html.erb +93 -0
  36. data/lib/rcov/templates/jquery-1.3.2.min.js +19 -0
  37. data/lib/rcov/templates/jquery.tablesorter.min.js +15 -0
  38. data/lib/rcov/templates/print.css +12 -0
  39. data/lib/rcov/templates/rcov.js +42 -0
  40. data/lib/rcov/templates/screen.css +270 -0
  41. data/lib/rcov/version.rb +10 -0
  42. data/setup.rb +1588 -0
  43. data/test/assets/sample_01.rb +7 -0
  44. data/test/assets/sample_02.rb +5 -0
  45. data/test/assets/sample_03.rb +20 -0
  46. data/test/assets/sample_04.rb +10 -0
  47. data/test/assets/sample_05-new.rb +17 -0
  48. data/test/assets/sample_05-old.rb +13 -0
  49. data/test/assets/sample_05.rb +17 -0
  50. data/test/assets/sample_06.rb +8 -0
  51. data/test/call_site_analyzer_test.rb +171 -0
  52. data/test/code_coverage_analyzer_test.rb +219 -0
  53. data/test/expected_coverage/diff-gcc-all.out +7 -0
  54. data/test/expected_coverage/diff-gcc-diff.out +11 -0
  55. data/test/expected_coverage/diff-gcc-original.out +5 -0
  56. data/test/expected_coverage/diff-no-color.out +12 -0
  57. data/test/expected_coverage/diff.out +12 -0
  58. data/test/expected_coverage/gcc-text.out +10 -0
  59. data/test/expected_coverage/sample_03_rb.html +651 -0
  60. data/test/expected_coverage/sample_03_rb.rb +28 -0
  61. data/test/expected_coverage/sample_04_rb.html +641 -0
  62. data/test/file_statistics_test.rb +471 -0
  63. data/test/functional_test.rb +91 -0
  64. data/test/test_helper.rb +4 -0
  65. data/test/turn_off_rcovrt.rb +4 -0
  66. metadata +126 -0
@@ -0,0 +1,271 @@
1
+ module Rcov
2
+ # A CodeCoverageAnalyzer is responsible for tracing code execution and
3
+ # returning code coverage and execution count information.
4
+ #
5
+ # Note that you must <tt>require 'rcov'</tt> before the code you want to
6
+ # analyze is parsed (i.e. before it gets loaded or required). You can do that
7
+ # by either invoking ruby with the <tt>-rrcov</tt> command-line option or
8
+ # just:
9
+ # require 'rcov'
10
+ # require 'mycode'
11
+ # # ....
12
+ #
13
+ # == Example
14
+ #
15
+ # analyzer = Rcov::CodeCoverageAnalyzer.new
16
+ # analyzer.run_hooked do
17
+ # do_foo
18
+ # # all the code executed as a result of this method call is traced
19
+ # end
20
+ # # ....
21
+ #
22
+ # analyzer.run_hooked do
23
+ # do_bar
24
+ # # the code coverage information generated in this run is aggregated
25
+ # # to the previously recorded one
26
+ # end
27
+ #
28
+ # analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
29
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
30
+ #
31
+ # In this example, two pieces of code are monitored, and the data generated in
32
+ # both runs are aggregated. +lines+ is an array of strings representing the
33
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
34
+ # true values indicating whether the corresponding lines of code were reported
35
+ # as executed by Ruby. +count_info+ is an array of integers representing how
36
+ # many times each line of code has been executed (more precisely, how many
37
+ # events where reported by Ruby --- a single line might correspond to several
38
+ # events, e.g. many method calls).
39
+ #
40
+ # You can have several CodeCoverageAnalyzer objects at a time, and it is
41
+ # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
42
+ # analyzer will manage its data separately. Note however that no special
43
+ # provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
44
+ # class. At any rate this will not pose a problem since it's easy to ignore it
45
+ # manually: just don't do
46
+ # lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
47
+ # if you're not interested in that information.
48
+ class CodeCoverageAnalyzer < DifferentialAnalyzer
49
+ @hook_level = 0
50
+ # defined this way instead of attr_accessor so that it's covered
51
+ def self.hook_level # :nodoc:
52
+ @hook_level
53
+ end
54
+
55
+ def self.hook_level=(x) # :nodoc:
56
+ @hook_level = x
57
+ end
58
+
59
+ def initialize
60
+ @script_lines__ = SCRIPT_LINES__
61
+ super(:install_coverage_hook, :remove_coverage_hook,
62
+ :reset_coverage)
63
+ end
64
+
65
+ # Return an array with the names of the files whose code was executed inside
66
+ # the block given to #run_hooked or between #install_hook and #remove_hook.
67
+ def analyzed_files
68
+ update_script_lines__
69
+ raw_data_relative.select do |file, lines|
70
+ @script_lines__.has_key?(file)
71
+ end.map{|fname,| fname}
72
+ end
73
+
74
+ # Return the available data about the requested file, or nil if none of its
75
+ # code was executed or it cannot be found.
76
+ # The return value is an array with three elements:
77
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
78
+ # +lines+ is an array of strings representing the
79
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
80
+ # true values indicating whether the corresponding lines of code were reported
81
+ # as executed by Ruby. +count_info+ is an array of integers representing how
82
+ # many times each line of code has been executed (more precisely, how many
83
+ # events where reported by Ruby --- a single line might correspond to several
84
+ # events, e.g. many method calls).
85
+ #
86
+ # The returned data corresponds to the aggregation of all the statistics
87
+ # collected in each #run_hooked or #install_hook/#remove_hook runs. You can
88
+ # reset the data at any time with #reset to start from scratch.
89
+ def data(filename)
90
+ raw_data = raw_data_relative
91
+ update_script_lines__
92
+ unless @script_lines__.has_key?(filename) &&
93
+ raw_data.has_key?(filename)
94
+ return nil
95
+ end
96
+ refine_coverage_info(@script_lines__[filename], raw_data[filename])
97
+ end
98
+
99
+ # Data for the first file matching the given regexp.
100
+ # See #data.
101
+ def data_matching(filename_re)
102
+ raw_data = raw_data_relative
103
+ update_script_lines__
104
+
105
+ match = raw_data.keys.sort.grep(filename_re).first
106
+ return nil unless match
107
+
108
+ refine_coverage_info(@script_lines__[match], raw_data[match])
109
+ end
110
+
111
+ # Execute the code in the given block, monitoring it in order to gather
112
+ # information about which code was executed.
113
+ def run_hooked; super end
114
+
115
+ # Start monitoring execution to gather code coverage and execution count
116
+ # information. Such data will be collected until #remove_hook is called.
117
+ #
118
+ # Use #run_hooked instead if possible.
119
+ def install_hook; super end
120
+
121
+ # Stop collecting code coverage and execution count information.
122
+ # #remove_hook will also stop collecting info if it is run inside a
123
+ # #run_hooked block.
124
+ def remove_hook; super end
125
+
126
+ # Remove the data collected so far. The coverage and execution count
127
+ # "history" will be erased, and further collection will start from scratch:
128
+ # no code is considered executed, and therefore all execution counts are 0.
129
+ # Right after #reset, #analyzed_files will return an empty array, and
130
+ # #data(filename) will return nil.
131
+ def reset; super end
132
+
133
+ def dump_coverage_info(formatters) # :nodoc:
134
+ update_script_lines__
135
+ raw_data_relative.each do |file, lines|
136
+ next if @script_lines__.has_key?(file) == false
137
+ lines = @script_lines__[file]
138
+ raw_coverage_array = raw_data_relative[file]
139
+
140
+ line_info, marked_info,
141
+ count_info = refine_coverage_info(lines, raw_coverage_array)
142
+ formatters.each do |formatter|
143
+ formatter.add_file(file, line_info, marked_info, count_info)
144
+ end
145
+ end
146
+ formatters.each{|formatter| formatter.execute}
147
+ end
148
+
149
+ private
150
+
151
+ def data_default; {} end
152
+
153
+ def raw_data_absolute
154
+ Rcov::RCOV__.generate_coverage_info
155
+ end
156
+
157
+ def aggregate_data(aggregated_data, delta)
158
+ delta.each_pair do |file, cov_arr|
159
+ dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
160
+ cov_arr.each_with_index do |x,i|
161
+ dest[i] ||= 0
162
+ dest[i] += x.to_i
163
+ end
164
+ end
165
+ end
166
+
167
+ def compute_raw_data_difference(first, last)
168
+ difference = {}
169
+ last.each_pair do |fname, cov_arr|
170
+ unless first.has_key?(fname)
171
+ difference[fname] = cov_arr.clone
172
+ else
173
+ orig_arr = first[fname]
174
+ diff_arr = Array.new(cov_arr.size, 0)
175
+ changed = false
176
+ cov_arr.each_with_index do |x, i|
177
+ diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
178
+ changed = true if diff != 0
179
+ end
180
+ difference[fname] = diff_arr if changed
181
+ end
182
+ end
183
+ difference
184
+ end
185
+
186
+ def refine_coverage_info(lines, covers)
187
+ marked_info = []
188
+ count_info = []
189
+ lines.size.times do |i|
190
+ c = covers[i]
191
+ marked_info << ((c && c > 0) ? true : false)
192
+ count_info << (c || 0)
193
+ end
194
+
195
+ script_lines_workaround(lines, marked_info, count_info)
196
+ end
197
+
198
+ # Try to detect repeated data, based on observed repetitions in line_info:
199
+ # this is a workaround for SCRIPT_LINES__[filename] including as many copies
200
+ # of the file as the number of times it was parsed.
201
+ def script_lines_workaround(line_info, coverage_info, count_info)
202
+ is_repeated = lambda do |div|
203
+ n = line_info.size / div
204
+ break false unless line_info.size % div == 0 && n > 1
205
+ different = false
206
+ n.times do |i|
207
+
208
+ things = (0...div).map { |j| line_info[i + j * n] }
209
+ if things.uniq.size != 1
210
+ different = true
211
+ break
212
+ end
213
+ end
214
+
215
+ ! different
216
+ end
217
+
218
+ factors = braindead_factorize(line_info.size)
219
+ factors.each do |n|
220
+ if is_repeated[n]
221
+ line_info = line_info[0, line_info.size / n]
222
+ coverage_info = coverage_info[0, coverage_info.size / n]
223
+ count_info = count_info[0, count_info.size / n]
224
+ end
225
+ end if factors.size > 1 # don't even try if it's prime
226
+
227
+ [line_info, coverage_info, count_info]
228
+ end
229
+
230
+ def braindead_factorize(num)
231
+ return [0] if num == 0
232
+ return [-1] + braindead_factorize(-num) if num < 0
233
+ factors = []
234
+ while num % 2 == 0
235
+ factors << 2
236
+ num /= 2
237
+ end
238
+ size = num
239
+ n = 3
240
+ max = Math.sqrt(num)
241
+ while n <= max && n <= size
242
+ while size % n == 0
243
+ size /= n
244
+ factors << n
245
+ end
246
+ n += 2
247
+ end
248
+ factors << size if size != 1
249
+ factors
250
+ end
251
+
252
+ def update_script_lines__
253
+ @script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
254
+ end
255
+
256
+ public
257
+
258
+ def marshal_dump # :nodoc:
259
+ # @script_lines__ is updated just before serialization so as to avoid
260
+ # missing files in SCRIPT_LINES__
261
+ ivs = {}
262
+ update_script_lines__
263
+ instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
264
+ ivs
265
+ end
266
+
267
+ def marshal_load(ivs) # :nodoc:
268
+ ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
269
+ end
270
+ end # CodeCoverageAnalyzer
271
+ end
@@ -0,0 +1,36 @@
1
+ # Rcov::CoverageInfo is but a wrapper for an array, with some additional
2
+ # checks. It is returned by FileStatistics#coverage.
3
+ class CoverageInfo
4
+ def initialize(coverage_array)
5
+ @cover = coverage_array.clone
6
+ end
7
+
8
+ # Return the coverage status for the requested line. There are four possible
9
+ # return values:
10
+ # * nil if there's no information for the requested line (i.e. it doesn't exist)
11
+ # * true if the line was reported by Ruby as executed
12
+ # * :inferred if rcov inferred it was executed, despite not being reported
13
+ # by Ruby.
14
+ # * false otherwise, i.e. if it was not reported by Ruby and rcov's
15
+ # heuristics indicated that it was not executed
16
+ def [](line)
17
+ @cover[line]
18
+ end
19
+
20
+ def []=(line, val) # :nodoc:
21
+ unless [true, false, :inferred].include? val
22
+ raise RuntimeError, "What does #{val} mean?"
23
+ end
24
+ return if line < 0 || line >= @cover.size
25
+ @cover[line] = val
26
+ end
27
+
28
+ # Return an Array holding the code coverage information.
29
+ def to_a
30
+ @cover.clone
31
+ end
32
+
33
+ def method_missing(meth, *a, &b) # :nodoc:
34
+ @cover.send(meth, *a, &b)
35
+ end
36
+ end
@@ -0,0 +1,116 @@
1
+ module Rcov
2
+ class DifferentialAnalyzer
3
+ require 'thread'
4
+ @@mutex = Mutex.new
5
+
6
+ def initialize(install_hook_meth, remove_hook_meth, reset_meth)
7
+ @cache_state = :wait
8
+ @start_raw_data = data_default
9
+ @end_raw_data = data_default
10
+ @aggregated_data = data_default
11
+ @install_hook_meth = install_hook_meth
12
+ @remove_hook_meth= remove_hook_meth
13
+ @reset_meth= reset_meth
14
+ end
15
+
16
+ # Execute the code in the given block, monitoring it in order to gather
17
+ # information about which code was executed.
18
+ def run_hooked
19
+ install_hook
20
+ yield
21
+ ensure
22
+ remove_hook
23
+ end
24
+
25
+ # Start monitoring execution to gather information. Such data will be
26
+ # collected until #remove_hook is called.
27
+ #
28
+ # Use #run_hooked instead if possible.
29
+ def install_hook
30
+ @start_raw_data = raw_data_absolute
31
+ Rcov::RCOV__.send(@install_hook_meth)
32
+ @cache_state = :hooked
33
+ @@mutex.synchronize{ self.class.hook_level += 1 }
34
+ end
35
+
36
+ # Stop collecting information.
37
+ # #remove_hook will also stop collecting info if it is run inside a
38
+ # #run_hooked block.
39
+ def remove_hook
40
+ @@mutex.synchronize do
41
+ self.class.hook_level -= 1
42
+ Rcov::RCOV__.send(@remove_hook_meth) if self.class.hook_level == 0
43
+ end
44
+ @end_raw_data = raw_data_absolute
45
+ @cache_state = :done
46
+ # force computation of the stats for the traced code in this run;
47
+ # we cannot simply let it be if self.class.hook_level == 0 because
48
+ # some other analyzer could install a hook, causing the raw_data_absolute
49
+ # to change again.
50
+ # TODO: lazy computation of raw_data_relative, only when the hook gets
51
+ # activated again.
52
+ raw_data_relative
53
+ end
54
+
55
+ # Remove the data collected so far. Further collection will start from
56
+ # scratch.
57
+ def reset
58
+ @@mutex.synchronize do
59
+ if self.class.hook_level == 0
60
+ # Unfortunately there's no way to report this as covered with rcov:
61
+ # if we run the tests under rcov self.class.hook_level will be >= 1 !
62
+ # It is however executed when we run the tests normally.
63
+ Rcov::RCOV__.send(@reset_meth)
64
+ @start_raw_data = data_default
65
+ @end_raw_data = data_default
66
+ else
67
+ @start_raw_data = @end_raw_data = raw_data_absolute
68
+ end
69
+ @raw_data_relative = data_default
70
+ @aggregated_data = data_default
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def data_default
77
+ raise "must be implemented by the subclass"
78
+ end
79
+
80
+ def self.hook_level
81
+ raise "must be implemented by the subclass"
82
+ end
83
+
84
+ def raw_data_absolute
85
+ raise "must be implemented by the subclass"
86
+ end
87
+
88
+ def aggregate_data(aggregated_data, delta)
89
+ raise "must be implemented by the subclass"
90
+ end
91
+
92
+ def compute_raw_data_difference(first, last)
93
+ raise "must be implemented by the subclass"
94
+ end
95
+
96
+ private
97
+
98
+ def raw_data_relative
99
+ case @cache_state
100
+ when :wait
101
+ return @aggregated_data
102
+ when :hooked
103
+ new_start = raw_data_absolute
104
+ new_diff = compute_raw_data_difference(@start_raw_data, new_start)
105
+ @start_raw_data = new_start
106
+ when :done
107
+ @cache_state = :wait
108
+ new_diff = compute_raw_data_difference(@start_raw_data,
109
+ @end_raw_data)
110
+ end
111
+
112
+ aggregate_data(@aggregated_data, new_diff)
113
+ @aggregated_data
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,355 @@
1
+ module Rcov
2
+ # A FileStatistics object associates a filename to:
3
+ # 1. its source code
4
+ # 2. the per-line coverage information after correction using rcov's heuristics
5
+ # 3. the per-line execution counts
6
+ #
7
+ # A FileStatistics object can be therefore be built given the filename, the
8
+ # associated source code, and an array holding execution counts (i.e. how many
9
+ # times each line has been executed).
10
+ #
11
+ # FileStatistics is relatively intelligent: it handles normal comments,
12
+ # <tt>=begin/=end</tt>, heredocs, many multiline-expressions... It uses a
13
+ # number of heuristics to determine what is code and what is a comment, and to
14
+ # refine the initial (incomplete) coverage information.
15
+ #
16
+ # Basic usage is as follows:
17
+ # sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", " false",
18
+ # "puts 2", "end"], [1, 1, 0, 0, 0])
19
+ # sf.num_lines # => 5
20
+ # sf.num_code_lines # => 5
21
+ # sf.coverage[2] # => true
22
+ # sf.coverage[3] # => :inferred
23
+ # sf.code_coverage # => 0.6
24
+ #
25
+ # The array of strings representing the source code and the array of execution
26
+ # counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
27
+ class FileStatistics
28
+ attr_reader :name, :lines, :coverage, :counts
29
+ def initialize(name, lines, counts, comments_run_by_default = false)
30
+ @name = name
31
+ @lines = lines
32
+ initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
33
+ @coverage = CoverageInfo.new initial_coverage
34
+ @counts = counts
35
+ @is_begin_comment = nil
36
+ # points to the line defining the heredoc identifier
37
+ # but only if it was marked (we don't care otherwise)
38
+ @heredoc_start = Array.new(lines.size, false)
39
+ @multiline_string_start = Array.new(lines.size, false)
40
+ extend_heredocs
41
+ find_multiline_strings
42
+ precompute_coverage comments_run_by_default
43
+ end
44
+
45
+ # Merge code coverage and execution count information.
46
+ # As for code coverage, a line will be considered
47
+ # * covered for sure (true) if it is covered in either +self+ or in the
48
+ # +coverage+ array
49
+ # * considered <tt>:inferred</tt> if the neither +self+ nor the +coverage+ array
50
+ # indicate that it was definitely executed, but it was <tt>inferred</tt>
51
+ # in either one
52
+ # * not covered (<tt>false</tt>) if it was uncovered in both
53
+ #
54
+ # Execution counts are just summated on a per-line basis.
55
+ def merge(lines, coverage, counts)
56
+ coverage.each_with_index do |v, idx|
57
+ case @coverage[idx]
58
+ when :inferred
59
+ @coverage[idx] = v || @coverage[idx]
60
+ when false
61
+ @coverage[idx] ||= v
62
+ end
63
+ end
64
+ counts.each_with_index{|v, idx| @counts[idx] += v }
65
+ precompute_coverage false
66
+ end
67
+
68
+ # Total coverage rate if comments are also considered "executable", given as
69
+ # a fraction, i.e. from 0 to 1.0.
70
+ # A comment is attached to the code following it (RDoc-style): it will be
71
+ # considered executed if the the next statement was executed.
72
+ def total_coverage
73
+ return 0 if @coverage.size == 0
74
+ @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
75
+ end
76
+
77
+ # Code coverage rate: fraction of lines of code executed, relative to the
78
+ # total amount of lines of code (loc). Returns a float from 0 to 1.0.
79
+ def code_coverage
80
+ indices = (0...@lines.size).select{|i| is_code? i }
81
+ return 0 if indices.size == 0
82
+ count = 0
83
+ indices.each {|i| count += 1 if @coverage[i] }
84
+ 1.0 * count / indices.size
85
+ end
86
+
87
+ def code_coverage_for_report
88
+ code_coverage * 100
89
+ end
90
+
91
+ def total_coverage_for_report
92
+ total_coverage * 100
93
+ end
94
+
95
+ # Number of lines of code (loc).
96
+ def num_code_lines
97
+ (0...@lines.size).select{|i| is_code? i}.size
98
+ end
99
+
100
+ # Total number of lines.
101
+ def num_lines
102
+ @lines.size
103
+ end
104
+
105
+ # Returns true if the given line number corresponds to code, as opposed to a
106
+ # comment (either # or =begin/=end blocks).
107
+ def is_code?(lineno)
108
+ unless @is_begin_comment
109
+ @is_begin_comment = Array.new(@lines.size, false)
110
+ pending = []
111
+ state = :code
112
+ @lines.each_with_index do |line, index|
113
+ case state
114
+ when :code
115
+ if /^=begin\b/ =~ line
116
+ state = :comment
117
+ pending << index
118
+ end
119
+ when :comment
120
+ pending << index
121
+ if /^=end\b/ =~ line
122
+ state = :code
123
+ pending.each{|idx| @is_begin_comment[idx] = true}
124
+ pending.clear
125
+ end
126
+ end
127
+ end
128
+ end
129
+ @lines[lineno] && !@is_begin_comment[lineno] && @lines[lineno] !~ /^\s*(#|$)/
130
+ end
131
+
132
+ private
133
+
134
+ def find_multiline_strings
135
+ state = :awaiting_string
136
+ wanted_delimiter = nil
137
+ string_begin_line = 0
138
+ @lines.each_with_index do |line, i|
139
+ matching_delimiters = Hash.new{|h,k| k}
140
+ matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
141
+ case state
142
+ when :awaiting_string
143
+ # very conservative, doesn't consider the last delimited string but
144
+ # only the very first one
145
+ if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
146
+ if !/"%"/.match(line)
147
+ wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
148
+ # check if closed on the very same line
149
+ # conservative again, we might have several quoted strings with the
150
+ # same delimiter on the same line, leaving the last one open
151
+ unless wanted_delimiter.match(md.post_match)
152
+ state = :want_end_delimiter
153
+ string_begin_line = i
154
+ end
155
+ end
156
+ end
157
+ when :want_end_delimiter
158
+ @multiline_string_start[i] = string_begin_line
159
+ if wanted_delimiter.match(line)
160
+ state = :awaiting_string
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def is_nocov?(line)
167
+ line =~ /#:nocov:/
168
+ end
169
+
170
+ def mark_nocov_regions(nocov_line_numbers, coverage)
171
+ while nocov_line_numbers.size > 0
172
+ begin_line, end_line = nocov_line_numbers.shift, nocov_line_numbers.shift
173
+ next unless begin_line && end_line
174
+ (begin_line..end_line).each do |line_num|
175
+ coverage[line_num] ||= :inferred
176
+ end
177
+ end
178
+ end
179
+
180
+ def precompute_coverage(comments_run_by_default = true)
181
+ changed = false
182
+ lastidx = lines.size - 1
183
+ if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
184
+ # mark the last block of comments
185
+ @coverage[lastidx] ||= :inferred
186
+ (lastidx-1).downto(0) do |i|
187
+ break if is_code?(i)
188
+ @coverage[i] ||= :inferred
189
+ end
190
+ end
191
+ nocov_line_numbers = []
192
+
193
+ (0...lines.size).each do |i|
194
+ nocov_line_numbers << i if is_nocov?(@lines[i])
195
+
196
+ next if @coverage[i]
197
+ line = @lines[i]
198
+ if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
199
+ /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
200
+ /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
201
+ /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
202
+ /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
203
+ prev_expr_continued?(i) && prev_expr_marked?(i) or
204
+ comments_run_by_default && !is_code?(i) or
205
+ /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
206
+ prev_expr_continued?(i+1) && next_expr_marked?(i)
207
+ @coverage[i] ||= :inferred
208
+ changed = true
209
+ end
210
+
211
+ end
212
+
213
+ mark_nocov_regions(nocov_line_numbers, @coverage)
214
+
215
+ (@lines.size-1).downto(0) do |i|
216
+ next if @coverage[i]
217
+ if !is_code?(i) and @coverage[i+1]
218
+ @coverage[i] = :inferred
219
+ changed = true
220
+ end
221
+ end
222
+
223
+ extend_heredocs if changed
224
+
225
+ # if there was any change, we have to recompute; we'll eventually
226
+ # reach a fixed point and stop there
227
+ precompute_coverage(comments_run_by_default) if changed
228
+ end
229
+
230
+ require 'strscan'
231
+ def extend_heredocs
232
+ i = 0
233
+ while i < @lines.size
234
+ unless is_code? i
235
+ i += 1
236
+ next
237
+ end
238
+ #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
239
+ # matches when unquoted, so as to avoid problems with 1<<2
240
+ # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
241
+ # parse error, but a = 1<<2 is of course fine)
242
+ scanner = StringScanner.new(@lines[i])
243
+ j = k = i
244
+ loop do
245
+ scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/, true, true)
246
+ # k is the first line after the end delimiter for the last heredoc
247
+ # scanned so far
248
+ unless scanner.matched?
249
+ i = k
250
+ break
251
+ end
252
+ term = scanner[3] || scanner[4]
253
+ # try to ignore symbolic bitshifts like 1<<LSHIFT
254
+ ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
255
+ if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
256
+ # it was preceded by a number, ignore
257
+ i = k
258
+ break
259
+ end
260
+ must_mark = []
261
+ end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
262
+ loop do
263
+ break if j == @lines.size
264
+ must_mark << j
265
+ if end_of_heredoc =~ @lines[j]
266
+ must_mark.each do |n|
267
+ @heredoc_start[n] = i
268
+ end
269
+ if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
270
+ @coverage[i] ||= :inferred
271
+ must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
272
+ end
273
+ # move the "first line after heredocs" index
274
+ if @lines[j+=1] =~ /^\s*\n$/
275
+ k = j
276
+ end
277
+ break
278
+ end
279
+ j += 1
280
+ end
281
+ end
282
+
283
+ i += 1
284
+ end
285
+ end
286
+
287
+ def next_expr_marked?(lineno)
288
+ return false if lineno >= @lines.size
289
+ found = false
290
+ idx = (lineno+1).upto(@lines.size-1) do |i|
291
+ next unless is_code? i
292
+ found = true
293
+ break i
294
+ end
295
+ return false unless found
296
+ @coverage[idx]
297
+ end
298
+
299
+ def prev_expr_marked?(lineno)
300
+ return false if lineno <= 0
301
+ found = false
302
+ idx = (lineno-1).downto(0) do |i|
303
+ next unless is_code? i
304
+ found = true
305
+ break i
306
+ end
307
+ return false unless found
308
+ @coverage[idx]
309
+ end
310
+
311
+ def prev_expr_continued?(lineno)
312
+ return false if lineno <= 0
313
+ return false if lineno >= @lines.size
314
+ found = false
315
+ if @multiline_string_start[lineno] &&
316
+ @multiline_string_start[lineno] < lineno
317
+ return true
318
+ end
319
+ # find index of previous code line
320
+ idx = (lineno-1).downto(0) do |i|
321
+ if @heredoc_start[i]
322
+ found = true
323
+ break @heredoc_start[i]
324
+ end
325
+ next unless is_code? i
326
+ found = true
327
+ break i
328
+ end
329
+ return false unless found
330
+ #TODO: write a comprehensive list
331
+ if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
332
+ return true
333
+ end
334
+ #FIXME: / matches regexps too
335
+ # the following regexp tries to reject #{interpolation}
336
+ r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
337
+ # try to see if a multi-line expression with opening, closing delimiters
338
+ # started on that line
339
+ [%w!( )!].each do |opening_str, closing_str|
340
+ # conservative: only consider nesting levels opened in that line, not
341
+ # previous ones too.
342
+ # next regexp considers interpolation too
343
+ line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
344
+ opened = line.scan(/#{Regexp.escape(opening_str)}/).size
345
+ closed = line.scan(/#{Regexp.escape(closing_str)}/).size
346
+ return true if opened - closed > 0
347
+ end
348
+ if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
349
+ return false
350
+ end
351
+
352
+ r
353
+ end
354
+ end
355
+ end