Module: SimpleCov::Formatter::CoverageJSONWriter

Defined in:
lib/simplecov/formatter/coverage_json_writer.rb,
sig/simplecov.rbs

Overview

Shared writer for the coverage.json artifact (JSONFormatter's report and HTMLFormatter's side file): one pretty-printed binary write plus the issue-#1171 concurrent-overwrite warning.

Constant Summary collapse

FILENAME =

Returns:

  • ("coverage.json")
"coverage.json"
META_SCAN_BYTES =

The previous report can embed the project's entire source text, so the overwrite check bounds how much of it is read looking for the meta object before falling back to a full parse.

Returns:

  • (Integer)
64 * 1024

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.existing_meta(path) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 57

def existing_meta(path)
  return nil unless File.exist?(path)

  meta = parse_meta(path) or return nil
  timestamp = meta[:timestamp] or return nil

  {timestamp: Time.iso8601(timestamp), command_name: meta[:command_name]}
rescue ArgumentError
  # An unparseable timestamp disables the check for this file.
  nil
end

.parse_meta(path) ⇒ Object



69
70
71
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 69

def parse_meta(path)
  parse_meta_head(path) || parse_meta_full(path)
end

.parse_meta_full(path) ⇒ Object



89
90
91
92
93
94
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 89

def parse_meta_full(path)
  parsed = JSON.parse(File.read(path), symbolize_names: true)
  parsed.is_a?(Hash) ? parsed[:meta] : nil
rescue JSON::ParserError
  nil
end

.parse_meta_head(path) ⇒ Object

The meta object is flat and sits at the head of every file this module writes, so the common case parses just that slice instead of a multi-megabyte report. A miss (foreign key order, a brace inside a meta string) falls back to the full parse.



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 77

def parse_meta_head(path)
  head = File.read(path, META_SCAN_BYTES).to_s
  slice = head[/"meta"\s*:\s*(\{.*?\})/m, 1]
  return nil unless slice

  # The captured slice is always a JSON object, so a successful parse
  # is always a Hash.
  JSON.parse(slice, symbolize_names: true)
rescue JSON::ParserError
  nil
end

.warn_if_concurrent_overwrite(path, result) ⇒ Object

Warns when the existing coverage.json has a timestamp newer than this process's start time — a strong signal that a sibling test process (e.g., parallel_tests) wrote it while we were running, and that our write is about to clobber their data.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 38

def warn_if_concurrent_overwrite(path, result)
  start_time = SimpleCov.process_start_time or return
  existing = existing_meta(path) or return
  return unless existing[:timestamp] > start_time

  # Both formatters write coverage.json through this method, so when
  # they are configured together the file found here was just written
  # by our own run, not a concurrent one. A matching command_name
  # means the same merged result, so there's nothing to lose by
  # overwriting. See issue #1171.
  return if existing[:command_name] == result.command_name

  warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
       "this process started at #{start_time.iso8601}. Overwriting " \
       "likely loses coverage data from a concurrent test run. For " \
       "parallel test setups, use SimpleCov::ResultMerger or run a single " \
       "collation step after all workers finish."
end

.write(output_path, hash, result) ⇒ Object



27
28
29
30
31
32
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 27

def write(output_path, hash, result)
  path = File.join(output_path, FILENAME)
  warn_if_concurrent_overwrite(path, result)
  AtomicFile.write(path, JSON.pretty_generate(hash), binary: true)
  path
end

Instance Method Details

#self?.existing_meta{ timestamp: untyped, command_name: untyped }?

timestamp stays untyped (not Time) so the comparison sites don't require the stdlib time extension sigs; the value is produced by Time.iso8601 at runtime.

Parameters:

  • path (String)

Returns:

  • ({ timestamp: untyped, command_name: untyped }, nil)


1472
# File 'sig/simplecov.rbs', line 1472

def self?.existing_meta: (String path) -> ({ timestamp: untyped, command_name: untyped })?

#self?.parse_metaHash[Symbol, untyped]?

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


1474
# File 'sig/simplecov.rbs', line 1474

def self?.parse_meta: (String path) -> Hash[Symbol, untyped]?

#self?.parse_meta_fullHash[Symbol, untyped]?

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


1481
# File 'sig/simplecov.rbs', line 1481

def self?.parse_meta_full: (String path) -> Hash[Symbol, untyped]?

#self?.parse_meta_headHash[Symbol, untyped]?

Bounded head read that parses just the flat meta object of files this module wrote; nil on any miss so parse_meta falls back to a full parse.

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


1479
# File 'sig/simplecov.rbs', line 1479

def self?.parse_meta_head: (String path) -> Hash[Symbol, untyped]?

#self?.warn_if_concurrent_overwritevoid

This method returns an undefined value.

Warns when an existing coverage.json is newer than this process's start time (a concurrent sibling writer). See issue #1171.

Parameters:



1467
# File 'sig/simplecov.rbs', line 1467

def self?.warn_if_concurrent_overwrite: (String path, SimpleCov::Result result) -> void

#self?.writeString

Parameters:

Returns:

  • (String)


1463
# File 'sig/simplecov.rbs', line 1463

def self?.write: (untyped output_path, Hash[Symbol, untyped] hash, SimpleCov::Result result) -> String