class SimpleCov::ResultAdapter
Responsible for adapting the format of the coverage result whether itβs default or with statistics
def self.call(*)
def self.call(*) new(*).adapt end
def adapt
def adapt return unless result result.to_h do |file_name, cover_statistic| [file_name, adapt_one(file_name, cover_statistic)] end end
def adapt_one(file_name, cover_statistic)
shape. Newer entries also need their methods and branches tables
array; everything since uses the `{lines:, branches:, methods:}`
Pre-0.18 resultsets pointed each filename straight at a line-coverage
def adapt_one(file_name, cover_statistic) return {"lines" => cover_statistic} if cover_statistic.is_a?(Array) adapt_oneshot_lines_if_needed(file_name, cover_statistic) normalize_method_keys(cover_statistic) aggregate_duplicated_branches(cover_statistic) cover_statistic end
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
def adapt_oneshot_lines_if_needed(file_name, cover_statistic) return unless cover_statistic.key?(:oneshot_lines) oneshot_lines = cover_statistic.delete(:oneshot_lines) line_stub = build_line_stub(file_name, oneshot_lines) oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 } cover_statistic[:lines] = line_stub end
def aggregate_duplicated_branches(cover_statistic)
on location identity. Regular (non-eval) source can never produce two
with an empty one dedups within it, since BranchesCombiner keys arms
#1235). Aggregate them by (type, location) β combining a branches hash
side covered under a different compile into a phantom miss (issue
compile. Reported as-is they inflate the branch denominator and turn a
the same file, each counting only the renders that flowed through that
several `[:if, id, location]` conditions at identical coordinates in
classes (e.g. hanami-view compiles each template once per view) yields
COMPILE of an eval'd string: a template rendered through multiple view
Ruby's eval coverage records a fresh set of branch entries for every
def aggregate_duplicated_branches(cover_statistic) branches = cover_statistic[:branches] return unless branches cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {}) end
def build_line_stub(file_name, oneshot_lines)
def build_line_stub(file_name, oneshot_lines) Coverage.line_stub(file_name) rescue Errno::ENOENT, SyntaxError Array.new(oneshot_lines.max || 0, nil) end
def class_display_name(klass)
be shadowed), falling back to the address form, which
singleton wrapper from `Module#name` via bound methods (which cannot
must never crash the host suite over that, so on failure rebuild the
`#
`inspect(value, max_depth = 2)` as a module_function, so rendering
shadow with an incompatible signature (Liquid::Utils defines
`to_s` renders its attached object via `#inspect`, which a module can
Rendering a class name can execute user code: a singleton class's
def class_display_name(klass) klass.to_s rescue StandardError singleton_wrapper_name(klass) || Object.instance_method(:to_s).bind_call(klass) end
def initialize(result)
def initialize(result) @result = result end
def normalize_method_key(key)
def normalize_method_key(key) normalized_key = key.dup normalized_key[0] = class_display_name(key[0]) .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER) .sub(SINGLETON_WRAPPER_PATTERN, '\1') normalized_key end
def normalize_method_keys(cover_statistic)
they are unaffected. The first entry's (normalized) key is kept for
coverage is 100%. Regular `def`s map one location to one name, so
ran shows as a phantom uncovered method on a line whose line
hits β otherwise each receiver or name whose generated copy never
executed", so entries are aggregated by location alone, summing
report can only express "was the method at this location ever
over a container), all pointing at the same source. A file-based
every name it's defined under (a builder looping `define_method key`
(a module's `included` hook defining onto each descendant) AND for
`[receiver, name, location]` entry for every class it's defined on
`define_singleton_method` from a shared code path yields a separate
source location: a block handed to `define_method` /
Ruby's method coverage records one entry per DEFINED METHOD, not per
def normalize_method_keys(cover_statistic) methods = cover_statistic[:methods] return unless methods aggregated = {} #: Hash[untyped, [untyped, Integer]] methods.each_with_object(aggregated) do |(key, count), memo| location = key[2..] #: Array[untyped] retained_key, existing = memo[location] || [normalize_method_key(key), 0] memo[location] = [retained_key, existing + count] end cover_statistic[:methods] = aggregated.values.to_h end
def singleton_wrapper_name(klass)
def singleton_wrapper_name(klass) return nil unless klass.is_a?(Class) && klass.singleton_class? attached = klass.attached_object # simplecov:disable branch β CRuby only reaches the rescue via a # Module/Class attached object (instance singletons render from the # class name chain without calling user inspect), so the non-Module # arm is defensive. name = Module.instance_method(:name).bind_call(attached) if attached.is_a?(Module) # simplecov:enable branch name && "#<Class:#{name}>" end