1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
# frozen_string_literal: true
require "json"
module SimpleCov
#
# Singleton that is responsible for caching, loading and merging
# SimpleCov::Results into a single result for coverage analysis based
# upon multiple test suites.
#
module ResultMerger
class << self
# The path to the .resultset.json cache file
def resultset_path
File.join(SimpleCov.coverage_path, ".resultset.json")
end
def resultset_writelock
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
end
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
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
def valid_results(file_path, ignore_timeout: false)
results = parse_file(file_path)
merge_valid_results(results, ignore_timeout: ignore_timeout)
end
def parse_file(path)
data = read_file(path)
parse_json(data)
end
def read_file(path)
return unless File.exist?(path)
data = File.read(path)
return if data.nil? || data.length < 2
data
end
def parse_json(content)
return {} unless content
JSON.parse(content) || {}
rescue StandardError
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
{}
end
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
def within_merge_timeout?(data)
time_since_result_creation(data) < SimpleCov.merge_timeout
end
def time_since_result_creation(data)
Time.now - Time.at(data.fetch("timestamp"))
end
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
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
#
# 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
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
def read_resultset
resultset_content =
synchronize_resultset do
read_file(resultset_path)
end
parse_json(resultset_content)
end
# Saves the given SimpleCov::Result in the resultset cache
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
# Ensure only one process is reading or writing the resultset at any
# given time
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
# 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 https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
def adapt_result(result)
if pre_simplecov_0_18_result?(result)
adapt_pre_simplecov_0_18_result(result)
else
result
end
end
# pre 0.18 coverage data pointed from file directly to an array of line coverage
def pre_simplecov_0_18_result?(result)
_key, data = result.first
data.is_a?(Array)
end
def adapt_pre_simplecov_0_18_result(result)
result.transform_values do |line_coverage_data|
{"lines" => line_coverage_data}
end
end
end
end
end
|