Class: SimpleCov::Combine::CoverageAccumulator

Inherits:
Object
  • Object
show all
Defined in:
lib/simplecov/combine/coverage_accumulator.rb

Overview

Folds any number of resultsets' coverage into one, absorbing them one at a time.

This replaces a pairwise reduce over the resultsets, which rebuilt the entire accumulated structure on every one of its N-1 steps: a fresh outer file hash, a fresh lines array for every file, and a fresh branch / method table for every file whose keys were re-interned from their tuples each time. On a 160-worker run over ~1,800 files that is ~290,000 whole-file rebuilds to produce ~1,800 files of output.

The accumulated side is private to the fold, so it can be updated in place instead, and the interned tables only have to be turned back into tuple-keyed hashes once, at the end.

Resultsets are absorbed one at a time rather than taken as a list, so ResultMerger.merge_results can keep reading and discarding them one file at a time — reading 100s of CI jobs' worth of coverage into memory at once is what that method is careful not to do.

Defined Under Namespace

Classes: MergedFile

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCoverageAccumulator

Returns a new instance of CoverageAccumulator.



69
70
71
72
73
74
75
76
77
78
# File 'lib/simplecov/combine/coverage_accumulator.rb', line 69

def initialize
  @files = {} #: Hash[String, untyped]
  @absorbed = false
  # Whether a criterion is enabled can't change mid-fold, so read it once
  # here rather than once per file. `branch_coverage?` reaches
  # `Coverage.supported?`, and a large parallel run merges thousands of
  # files.
  @branch_coverage = SimpleCov.branch_coverage?
  @method_coverage = SimpleCov.method_coverage?
end

Class Method Details

.executed?(lines) ⇒ Boolean

A file some process actually loaded has at least one executed line; a simulated (never-loaded) file's lines are all nil or 0. This is the signal the merge reconciles synthesized tuples on, and the one ResultMerger re-derives a merged result's not-loaded set from, so it lives here rather than being spelled out at each site.

Array() plus the Numeric test rather than a bare any?(&:positive?) because this reads straight off a parsed resultset, which is external input: a file written by another SimpleCov version, or hand-edited, can carry anything under "lines" — a Hash coerces to pairs, a String to itself. Keeping a malformed entry to a wrong answer instead of a NoMethodError out of the middle of a merge is the point.

Returns:

  • (Boolean)


43
44
45
46
# File 'lib/simplecov/combine/coverage_accumulator.rb', line 43

def self.executed?(lines)
  counts = Array(lines) #: Array[untyped]
  counts.any? { |count| count.is_a?(Numeric) && count.positive? }
end

.fold(pairs) ⇒ Array

Folds [command_names, coverage] pairs into one merged coverage.

pairs is only ever iterated, so a caller that reads resultsets off disk can hand in a lazy enumerable and never hold more than one in memory — which is what ResultMerger.merge_results is careful about.

Returns:

  • (Array)

    the concatenated command names and the merged coverage



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/simplecov/combine/coverage_accumulator.rb', line 57

def self.fold(pairs)
  accumulator = new
  command_names = [] #: Array[String]

  pairs.each do |names, coverage|
    command_names.concat(names)
    accumulator.absorb(coverage)
  end

  [command_names, accumulator.result]
end

Instance Method Details

#absorb(coverage) ⇒ CoverageAccumulator

Folds one resultset's coverage (filename => per-file coverage) into the accumulator. A nil coverage is ignored — a resultset that carried nothing contributes nothing.

Returns:



87
88
89
90
91
92
93
94
95
96
# File 'lib/simplecov/combine/coverage_accumulator.rb', line 87

def absorb(coverage)
  return self unless coverage

  @absorbed = true
  coverage.each do |filename, file_coverage|
    @files[filename] = merge_file(@files[filename], file_coverage)
  end

  self
end

#resultHash?

The merged coverage, or nil when nothing was absorbed at all — the caller needs to tell "no results" apart from "results that cover nothing", and only the former means there is no report to build.

A file only one resultset carried is returned exactly as it came in, untouched: with nothing to merge it into, copying it would only cost memory.

Returns:

  • (Hash, nil)


109
110
111
112
113
114
115
# File 'lib/simplecov/combine/coverage_accumulator.rb', line 109

def result
  return nil unless @absorbed

  @files.transform_values do |entry|
    entry.is_a?(MergedFile) ? entry.to_h : entry
  end
end