katsuya-rcov 0.9.7.1

Sign up to get free protection for your applications and to get access to all the features.
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