Module: SimpleCov::ResultMerger

Defined in:
lib/simplecov/result_merger.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.

Class Method Summary collapse

Class Method Details

.adapt_pre_simplecov_0_18_result(result) ⇒ Object


187
188
189
190
191
# File 'lib/simplecov/result_merger.rb', line 187

def adapt_pre_simplecov_0_18_result(result)
  result.transform_values do |line_coverage_data|
    {"lines" => line_coverage_data}
  end
end

.adapt_result(result) ⇒ Object

We changed the format of the raw result data in simplecov, as people are likely to have “old” resultsets lying around (but not too old so that they're still considered we can adapt them). See github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747


172
173
174
175
176
177
178
# File 'lib/simplecov/result_merger.rb', line 172

def adapt_result(result)
  if pre_simplecov_0_18_result?(result)
    adapt_pre_simplecov_0_18_result(result)
  else
    result
  end
end

.create_result(command_names, coverage) ⇒ Object


93
94
95
96
97
98
# File 'lib/simplecov/result_merger.rb', line 93

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

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


22
23
24
25
26
# File 'lib/simplecov/result_merger.rb', line 22

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


100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/simplecov/result_merger.rb', line 100

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)
    merged_command = memo_command + command

    [merged_command, merged_coverage]
  end
end

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


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

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


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

def merge_valid_results(results, ignore_timeout: false)
  results = results.select { |_command_name, data| within_merge_timeout?(data) } unless ignore_timeout

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

  # one file itself _might_ include multiple test runs
  merge_coverage(*command_plus_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


117
118
119
120
121
122
123
124
# File 'lib/simplecov/result_merger.rb', line 117

def merged_result
  # conceptually this is just doing `merge_results(resultset_path)`
  # it's more involved to make syre `synchronize_resultset` is only used around reading
  resultset_hash = read_resultset
  command_names, coverage = merge_valid_results(resultset_hash)

  create_result(command_names, coverage)
end

.parse_file(path) ⇒ Object


51
52
53
54
# File 'lib/simplecov/result_merger.rb', line 51

def parse_file(path)
  data = read_file(path)
  parse_json(data)
end

.parse_json(content) ⇒ Object


65
66
67
68
69
70
71
72
# File 'lib/simplecov/result_merger.rb', line 65

def parse_json(content)
  return {} unless content

  JSON.parse(content) || {}
rescue StandardError
  warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
  {}
end

.pre_simplecov_0_18_result?(result) ⇒ Boolean

pre 0.18 coverage data pointed from file directly to an array of line coverage

Returns:

  • (Boolean)

181
182
183
184
185
# File 'lib/simplecov/result_merger.rb', line 181

def pre_simplecov_0_18_result?(result)
  _key, data = result.first

  data.is_a?(Array)
end

.read_file(path) ⇒ Object


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

def read_file(path)
  return unless File.exist?(path)

  data = File.read(path)
  return if data.nil? || data.length < 2

  data
end

.read_resultsetObject


126
127
128
129
130
131
132
133
# File 'lib/simplecov/result_merger.rb', line 126

def read_resultset
  resultset_content =
    synchronize_resultset do
      read_file(resultset_path)
    end

  parse_json(resultset_content)
end

.resultset_pathObject

The path to the .resultset.json cache file


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

def resultset_path
  File.join(SimpleCov.coverage_path, ".resultset.json")
end

.resultset_writelockObject


18
19
20
# File 'lib/simplecov/result_merger.rb', line 18

def resultset_writelock
  File.join(SimpleCov.coverage_path, ".resultset.json.lock")
end

.store_result(result) ⇒ Object

Saves the given SimpleCov::Result in the resultset cache


136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/simplecov/result_merger.rb', line 136

def store_result(result)
  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] = data
    File.open(resultset_path, "w+") do |f_|
      f_.puts JSON.pretty_generate(new_resultset)
    end
  end
  true
end

.synchronize_resultsetObject

Ensure only one process is reading or writing the resultset at any given time


153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/simplecov/result_merger.rb', line 153

def synchronize_resultset
  # make it reentrant
  return yield if defined?(@resultset_locked) && @resultset_locked

  begin
    @resultset_locked = true
    File.open(resultset_writelock, "w+") do |f|
      f.flock(File::LOCK_EX)
      yield
    end
  ensure
    @resultset_locked = false
  end
end

.time_since_result_creation(data) ⇒ Object


89
90
91
# File 'lib/simplecov/result_merger.rb', line 89

def time_since_result_creation(data)
  Time.now - Time.at(data.fetch("timestamp"))
end

.valid_results(file_path, ignore_timeout: false) ⇒ Object


46
47
48
49
# File 'lib/simplecov/result_merger.rb', line 46

def valid_results(file_path, ignore_timeout: false)
  results = parse_file(file_path)
  merge_valid_results(results, ignore_timeout: ignore_timeout)
end

.within_merge_timeout?(data) ⇒ Boolean

Returns:

  • (Boolean)

85
86
87
# File 'lib/simplecov/result_merger.rb', line 85

def within_merge_timeout?(data)
  time_since_result_creation(data) < SimpleCov.merge_timeout
end