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)


137
138
139
140
141
142
143
# File 'lib/simplecov/result_merger.rb', line 137

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



77
78
79
80
81
82
# File 'lib/simplecov/result_merger.rb', line 77

def create_result(command_names, coverage)
  return nil unless coverage

  command_name = command_names.reject(&:empty?).sort.join(", ")
  SimpleCov::Result.new(coverage, command_name: command_name)
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



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

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.



129
130
131
132
133
134
135
# File 'lib/simplecov/result_merger.rb', line 129

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



99
100
101
102
# File 'lib/simplecov/result_merger.rb', line 99

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

.read_resultsetObject



104
105
106
107
# File 'lib/simplecov/result_merger.rb', line 104

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



110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/simplecov/result_merger.rb', line 110

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



145
146
147
# File 'lib/simplecov/result_merger.rb', line 145

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
# File 'lib/simplecov/result_merger.rb', line 70

def warn_about_expired_results(expired_command_names)
  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