Module: SimpleCov::ResultMerger

Defined in:
lib/simplecov/result_merger.rb,
lib/simplecov/result_merger/resultset_file.rb,
lib/simplecov/result_merger/resultset_store.rb,
lib/simplecov/result_merger/legacy_format_adapter.rb

Overview

Singleton that is responsible for caching, loading and merging SimpleCov::Results into a single result for coverage analysis based upon multiple test suites.

Defined Under Namespace

Modules: LegacyFormatAdapter, ResultsetFile, ResultsetStore

Class Method Summary collapse

Class Method Details

.concurrent_runner_entry?(entry) ⇒ Boolean

Returns:

  • (Boolean)


149
150
151
152
153
154
155
# File 'lib/simplecov/result_merger.rb', line 149

def concurrent_runner_entry?(entry)
  return false unless entry.is_a?(Hash)

  timestamp = entry["timestamp"]
  process_start = SimpleCov.process_start_time
  timestamp && process_start && timestamp.to_i >= process_start.to_i
end

.create_result(command_names, coverage) ⇒ Object



85
86
87
88
89
90
91
92
93
94
# File 'lib/simplecov/result_merger.rb', line 85

def create_result(command_names, coverage)
  return nil unless coverage

  command_name = command_names.reject(&:empty?).sort.join(", ")
  # The merged result is the authoritative one users actually see, so
  # it's the one that warns about source files dropped because they no
  # longer exist on disk (issue #980). The per-process slices built in
  # `process_coverage_result` stay quiet to avoid one warning per worker.
  SimpleCov::Result.new(coverage, command_name: command_name, report: true)
end

.drop_expired_results(results) ⇒ Object



58
59
60
61
62
63
64
# File 'lib/simplecov/result_merger.rb', line 58

def drop_expired_results(results)
  fresh, expired = results.partition { |_command_name, data| within_merge_timeout?(data) }
  return results if expired.empty?

  warn_about_expired_results(expired.map(&:first))
  fresh.to_h
end

.merge_and_store(*file_paths, ignore_timeout: false) ⇒ Object



19
20
21
22
23
# File 'lib/simplecov/result_merger.rb', line 19

def merge_and_store(*file_paths, ignore_timeout: false)
  result = merge_results(*file_paths, ignore_timeout: ignore_timeout)
  store_result(result) if result
  result
end

.merge_coverage(*results) ⇒ Object



96
97
98
99
100
101
102
103
104
105
# File 'lib/simplecov/result_merger.rb', line 96

def merge_coverage(*results)
  return [[""], nil] if results.empty?
  return results.first if results.size == 1

  results.reduce do |(memo_command, memo_coverage), (command, coverage)|
    # timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
    merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
    [memo_command + command, merged_coverage]
  end
end

.merge_results(*file_paths, ignore_timeout: false) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/simplecov/result_merger.rb', line 25

def merge_results(*file_paths, ignore_timeout: false)
  # It is intentional here that files are only read in and parsed one at a time.
  #
  # In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
  # of data. Reading them all in easily produces Gigabytes of memory consumption which
  # we want to avoid.
  #
  # For similar reasons a SimpleCov::Result is only created in the end as that'd create
  # even more data especially when it also reads in all source files.
  initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)

  command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
    merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
  end

  create_result(command_names, coverage)
end

.merge_valid_results(results, ignore_timeout: false) ⇒ Object



47
48
49
50
51
52
53
54
55
56
# File 'lib/simplecov/result_merger.rb', line 47

def merge_valid_results(results, ignore_timeout: false)
  results = drop_expired_results(results) unless ignore_timeout

  command_plus_coverage = results.map do |command_name, data|
    [[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
  end

  # one file itself _might_ include multiple test runs
  merge_coverage(*command_plus_coverage)
end

.merged_entry(existing, incoming) ⇒ Object

If an entry with the same command_name was written AFTER our process started, a sibling test runner (typically a subprocess our parent process shelled out to) wrote it. Combine coverage data rather than overwriting, so an empty parent-process result doesn’t clobber the subprocess’s real data. See github.com/simplecov-ruby/simplecov/issues/581.



141
142
143
144
145
146
147
# File 'lib/simplecov/result_merger.rb', line 141

def merged_entry(existing, incoming)
  return incoming unless concurrent_runner_entry?(existing)

  incoming.merge(
    "coverage" => Combine.combine(Combine::ResultsCombiner, existing["coverage"], incoming["coverage"])
  )
end

.merged_resultObject

Gets all SimpleCov::Results stored in resultset, merges them and produces a new SimpleCov::Result with merged coverage data and the command_name for the result consisting of a join on all source result’s names



111
112
113
114
# File 'lib/simplecov/result_merger.rb', line 111

def merged_result
  command_names, coverage = merge_valid_results(read_resultset)
  create_result(command_names, coverage)
end

.read_resultsetObject



116
117
118
119
# File 'lib/simplecov/result_merger.rb', line 116

def read_resultset
  content = synchronize_resultset { ResultsetFile.read(resultset_path) }
  ResultsetFile.decode(content)
end

.resultset_pathObject



15
16
17
# File 'lib/simplecov/result_merger.rb', line 15

def resultset_path
  ResultsetStore.resultset_path
end

.store_result(result) ⇒ Object

Saves the given SimpleCov::Result in the resultset cache



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/simplecov/result_merger.rb', line 122

def store_result(result) # rubocop:disable Naming/PredicateMethod
  synchronize_resultset do
    # Ensure we have the latest, in case it was already cached
    new_resultset = read_resultset

    # A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
    command_name, data = result.to_hash.first
    new_resultset[command_name] = merged_entry(new_resultset[command_name], data)

    ResultsetStore.write(new_resultset)
  end
  true
end

.synchronize_resultsetObject



157
158
159
# File 'lib/simplecov/result_merger.rb', line 157

def synchronize_resultset(&)
  ResultsetStore.synchronize(&)
end

.valid_results(file_path, ignore_timeout: false) ⇒ Object



43
44
45
# File 'lib/simplecov/result_merger.rb', line 43

def valid_results(file_path, ignore_timeout: false)
  merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout)
end

.warn_about_expired_results(expired_command_names) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/simplecov/result_merger.rb', line 70

def warn_about_expired_results(expired_command_names)
  # Subprocesses merge the resultset too (each forked worker calls
  # `SimpleCov.result` to store its slice), and the default `at_fork`
  # sets `print_errors false` for them. Without this guard the warning
  # is emitted once per worker — N copies of the same message for an
  # N-worker run. Gate on `print_errors` like every other SimpleCov
  # warning so only the reporting process speaks up.
  return unless SimpleCov.print_errors

  warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
       "merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
       "#{expired_command_names.sort.join(', ')}. " \
       "Increase SimpleCov.merge_timeout to include them."
end

.within_merge_timeout?(data) ⇒ Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/simplecov/result_merger.rb', line 66

def within_merge_timeout?(data)
  (Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout
end