module Rcov
# A CodeCoverageAnalyzer is responsible for tracing code execution and
# returning code coverage and execution count information.
#
# Note that you must require 'rcov' before the code you want to
# analyze is parsed (i.e. before it gets loaded or required). You can do that
# by either invoking ruby with the -rrcov command-line option or
# just:
# require 'rcov'
# require 'mycode'
# # ....
#
# == Example
#
# analyzer = Rcov::CodeCoverageAnalyzer.new
# analyzer.run_hooked do
# do_foo
# # all the code executed as a result of this method call is traced
# end
# # ....
#
# analyzer.run_hooked do
# do_bar
# # the code coverage information generated in this run is aggregated
# # to the previously recorded one
# end
#
# analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
# lines, marked_info, count_info = analyzer.data("foo.rb")
#
# In this example, two pieces of code are monitored, and the data generated in
# both runs are aggregated. +lines+ is an array of strings representing the
# source code of foo.rb. +marked_info+ is an array holding false,
# true values indicating whether the corresponding lines of code were reported
# as executed by Ruby. +count_info+ is an array of integers representing how
# many times each line of code has been executed (more precisely, how many
# events where reported by Ruby --- a single line might correspond to several
# events, e.g. many method calls).
#
# You can have several CodeCoverageAnalyzer objects at a time, and it is
# possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
# analyzer will manage its data separately. Note however that no special
# provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
# class. At any rate this will not pose a problem since it's easy to ignore it
# manually: just don't do
# lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
# if you're not interested in that information.
class CodeCoverageAnalyzer < DifferentialAnalyzer
@hook_level = 0
# defined this way instead of attr_accessor so that it's covered
def self.hook_level # :nodoc:
@hook_level
end
def self.hook_level=(x) # :nodoc:
@hook_level = x
end
def initialize
@script_lines__ = SCRIPT_LINES__
super(:install_coverage_hook, :remove_coverage_hook,
:reset_coverage)
end
# Return an array with the names of the files whose code was executed inside
# the block given to #run_hooked or between #install_hook and #remove_hook.
def analyzed_files
update_script_lines__
raw_data_relative.select do |file, lines|
@script_lines__.has_key?(file)
end.map{|fname,| fname}
end
# Return the available data about the requested file, or nil if none of its
# code was executed or it cannot be found.
# The return value is an array with three elements:
# lines, marked_info, count_info = analyzer.data("foo.rb")
# +lines+ is an array of strings representing the
# source code of foo.rb. +marked_info+ is an array holding false,
# true values indicating whether the corresponding lines of code were reported
# as executed by Ruby. +count_info+ is an array of integers representing how
# many times each line of code has been executed (more precisely, how many
# events where reported by Ruby --- a single line might correspond to several
# events, e.g. many method calls).
#
# The returned data corresponds to the aggregation of all the statistics
# collected in each #run_hooked or #install_hook/#remove_hook runs. You can
# reset the data at any time with #reset to start from scratch.
def data(filename)
raw_data = raw_data_relative
update_script_lines__
unless @script_lines__.has_key?(filename) &&
raw_data.has_key?(filename)
return nil
end
refine_coverage_info(@script_lines__[filename], raw_data[filename])
end
# Data for the first file matching the given regexp.
# See #data.
def data_matching(filename_re)
raw_data = raw_data_relative
update_script_lines__
match = raw_data.keys.sort.grep(filename_re).first
return nil unless match
refine_coverage_info(@script_lines__[match], raw_data[match])
end
# Execute the code in the given block, monitoring it in order to gather
# information about which code was executed.
def run_hooked; super end
# Start monitoring execution to gather code coverage and execution count
# information. Such data will be collected until #remove_hook is called.
#
# Use #run_hooked instead if possible.
def install_hook; super end
# Stop collecting code coverage and execution count information.
# #remove_hook will also stop collecting info if it is run inside a
# #run_hooked block.
def remove_hook; super end
# Remove the data collected so far. The coverage and execution count
# "history" will be erased, and further collection will start from scratch:
# no code is considered executed, and therefore all execution counts are 0.
# Right after #reset, #analyzed_files will return an empty array, and
# #data(filename) will return nil.
def reset; super end
def dump_coverage_info(formatters) # :nodoc:
update_script_lines__
raw_data_relative.each do |file, lines|
next if @script_lines__.has_key?(file) == false
lines = @script_lines__[file]
raw_coverage_array = raw_data_relative[file]
line_info, marked_info,
count_info = refine_coverage_info(lines, raw_coverage_array)
formatters.each do |formatter|
formatter.add_file(file, line_info, marked_info, count_info)
end
end
formatters.each{|formatter| formatter.execute}
end
private
def data_default; {} end
def raw_data_absolute
Rcov::RCOV__.generate_coverage_info
end
def aggregate_data(aggregated_data, delta)
delta.each_pair do |file, cov_arr|
dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
cov_arr.each_with_index{|x,i| dest[i] += x.to_i}
end
end
def compute_raw_data_difference(first, last)
difference = {}
last.each_pair do |fname, cov_arr|
unless first.has_key?(fname)
difference[fname] = cov_arr.clone
else
orig_arr = first[fname]
diff_arr = Array.new(cov_arr.size, 0)
changed = false
cov_arr.each_with_index do |x, i|
diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
changed = true if diff != 0
end
difference[fname] = diff_arr if changed
end
end
difference
end
def refine_coverage_info(lines, covers)
marked_info = []
count_info = []
lines.size.times do |i|
c = covers[i]
marked_info << ((c && c > 0) ? true : false)
count_info << (c || 0)
end
script_lines_workaround(lines, marked_info, count_info)
end
# Try to detect repeated data, based on observed repetitions in line_info:
# this is a workaround for SCRIPT_LINES__[filename] including as many copies
# of the file as the number of times it was parsed.
def script_lines_workaround(line_info, coverage_info, count_info)
is_repeated = lambda do |div|
n = line_info.size / div
break false unless line_info.size % div == 0 && n > 1
different = false
n.times do |i|
things = (0...div).map { |j| line_info[i + j * n] }
if things.uniq.size != 1
different = true
break
end
end
! different
end
factors = braindead_factorize(line_info.size)
factors.each do |n|
if is_repeated[n]
line_info = line_info[0, line_info.size / n]
coverage_info = coverage_info[0, coverage_info.size / n]
count_info = count_info[0, count_info.size / n]
end
end if factors.size > 1 # don't even try if it's prime
[line_info, coverage_info, count_info]
end
def braindead_factorize(num)
return [0] if num == 0
return [-1] + braindead_factorize(-num) if num < 0
factors = []
while num % 2 == 0
factors << 2
num /= 2
end
size = num
n = 3
max = Math.sqrt(num)
while n <= max && n <= size
while size % n == 0
size /= n
factors << n
end
n += 2
end
factors << size if size != 1
factors
end
def update_script_lines__
@script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
end
public
def marshal_dump # :nodoc:
# @script_lines__ is updated just before serialization so as to avoid
# missing files in SCRIPT_LINES__
ivs = {}
update_script_lines__
instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
ivs
end
def marshal_load(ivs) # :nodoc:
ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
end
end # CodeCoverageAnalyzer
end