123456789_123456789_123456789_123456789_123456789_

Class: SimpleCov::ResultAdapter

Relationships & Source Files
Inherits: Object
Defined in: lib/simplecov/result_adapter.rb

Overview

Responsible for adapting the format of the coverage result whether it's default or with statistics

Constant Summary

  • ADDRESS_PATTERN = private

    Normalize memory addresses in method coverage keys so that results from different processes can be merged. Anonymous class names like "#Class:0x00007ff19ab24790" get inconsistent addresses across runs. Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit CRuby: 16; some JVM/TruffleRuby formats may differ), so match any length of hex digits and collapse to a single placeholder.

    # File 'lib/simplecov/result_adapter.rb', line 47
    /0x\h+/
  • ADDRESS_PLACEHOLDER = private
    # File 'lib/simplecov/result_adapter.rb', line 50
    "0x0"
  • SINGLETON_WRAPPER_PATTERN = private

    Strip the # wrapper Ruby's Coverage adds to singleton-class method keys. module_function and class methods get recorded both as singleton ([#<Class:Foo>, <code>:m</code>, …]) and instance/module ([Foo, <code>:m</code>, …]) entries pointing at the same source location; only one of the two is ever reachable at runtime, so we merge them. Only applies to named constants — anonymous-class addresses like # are left alone (handled by ADDRESS_PATTERN above).

    # File 'lib/simplecov/result_adapter.rb', line 60
    /\A#<Class:([A-Z_][\w:]*)>\z/

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(result) ⇒ ResultAdapter

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 10

def initialize(result)
  @result = result
end

Class Method Details

.call

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 14

def self.call(*)
  new(*).adapt
end

Instance Attribute Details

#result (readonly)

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 8

attr_reader :result

Instance Method Details

#adapt

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 18

def adapt
  return unless result

  result.to_h do |file_name, cover_statistic|
    [file_name, adapt_one(file_name, cover_statistic)]
  end
end

#adapt_one(file_name, cover_statistic) (private)

Pre-0.18 resultsets pointed each filename straight at a line-coverage array; everything since uses the {lines:, branches:, methods:} shape. Newer entries also need their methods and branches tables massaged before downstream code reports or merges them.

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 32

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

#adapt_oneshot_lines_if_needed(file_name, cover_statistic) (private)

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 144

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

#aggregate_duplicated_branches(cover_statistic) (private)

Ruby's eval coverage records a fresh set of branch entries for every COMPILE of an eval'd string: a template rendered through multiple view classes (e.g. hanami-view compiles each template once per view) yields several [:if, id, location] conditions at identical coordinates in the same file, each counting only the renders that flowed through that compile. Reported as-is they inflate the branch denominator and turn a side covered under a different compile into a phantom miss (issue #1235). Aggregate them by (type, location) — combining a branches hash with an empty one dedups within it, since BranchesCombiner keys arms on location identity. Regular (non-eval) source can never produce two conditions at the same location, so this is a no-op outside eval.

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 137

def aggregate_duplicated_branches(cover_statistic)
  branches = cover_statistic[:branches]
  return unless branches

  cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {})
end

#build_line_stub(file_name, oneshot_lines) (private)

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 153

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

#class_display_name(klass) (private)

Rendering a class name can execute user code: a singleton class's to_s renders its attached object via #inspect, which a module can shadow with an incompatible signature (Liquid::Utils defines inspect(value, max_depth = 2) as a module_function, so rendering # raises ArgumentError). A coverage report must never crash the host suite over that, so on failure rebuild the singleton wrapper from Module#name via bound methods (which cannot be shadowed), falling back to the address form, which ADDRESS_PATTERN then normalizes. See issue #1236.

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 107

def class_display_name(klass)
  klass.to_s
rescue StandardError
  singleton_wrapper_name(klass) || Object.instance_method(:to_s).bind_call(klass)
end

#normalize_method_key(key) (private)

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 90

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

#normalize_method_keys(cover_statistic) (private)

Ruby's method coverage records one entry per DEFINED METHOD, not per source location: a block handed to define_method / define_singleton_method from a shared code path yields a separate [receiver, name, location] entry for every class it's defined on (a module's included hook defining onto each descendant) AND for every name it's defined under (a builder looping define_method key over a container), all pointing at the same source. A file-based report can only express "was the method at this location ever executed", so entries are aggregated by location alone, summing hits — otherwise each receiver or name whose generated copy never ran shows as a phantom uncovered method on a line whose line coverage is 100%. Regular defs map one location to one name, so they are unaffected. The first entry's (normalized) key is kept for display. See issue #1234.

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 77

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

#singleton_wrapper_name(klass) (private)

[ GitHub ]

  
# File 'lib/simplecov/result_adapter.rb', line 113

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