module SimpleCov::ResultMerger
def concurrent_runner_entry?(entry)
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
def create_result(command_names, coverage)
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 drop_expired_results(results)
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
def merge_and_store(*file_paths, ignore_timeout: false)
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_coverage(*results)
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
def merge_results(*file_paths, ignore_timeout: false)
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 merge_valid_results(results, ignore_timeout: false)
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
def merged_entry(existing, incoming)
overwriting, so an empty parent-process result doesn't clobber the
process shelled out to) wrote it. Combine coverage data rather than
started, a sibling test runner (typically a subprocess our parent
If an entry with the same command_name was written AFTER our process
def merged_entry(existing, incoming) return incoming unless concurrent_runner_entry?(existing) incoming.merge( "coverage" => Combine.combine(Combine::ResultsCombiner, existing["coverage"], incoming["coverage"]) ) end
def merged_result
SimpleCov::Result with merged coverage data and the command_name
Gets all SimpleCov::Results stored in resultset, merges them and produces a new
def merged_result command_names, coverage = merge_valid_results(read_resultset) create_result(command_names, coverage) end
def read_resultset
def read_resultset content = synchronize_resultset { ResultsetFile.read(resultset_path) } ResultsetFile.decode(content) end
def resultset_path
def resultset_path ResultsetStore.resultset_path end
def store_result(result) # rubocop:disable Naming/PredicateMethod
Saves the given SimpleCov::Result in the resultset cache
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
def synchronize_resultset(&)
def synchronize_resultset(&) ResultsetStore.synchronize(&) end
def valid_results(file_path, ignore_timeout: false)
def valid_results(file_path, ignore_timeout: false) merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout) end
def warn_about_expired_results(expired_command_names)
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
def within_merge_timeout?(data)
def within_merge_timeout?(data) (Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout end