Module: SimpleCov::Formatter::CoverageJSONWriter

Extended by:
CoverageJSONWriter
Included in:
CoverageJSONWriter
Defined in:
lib/simplecov/formatter/coverage_json_writer.rb,
sig/simplecov.rbs

Overview

Shared writer for the coverage.json artifact, used by JSONFormatter as its report and by HTMLFormatter as the side file feeding simplecov serve and external tools. Centralizing the write keeps the two copies byte-identical and gives both formatters the concurrent-overwrite warning of #1171.

Constant Summary collapse

FILENAME =

Returns:

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

Returns:

  • (Integer)
64 * 1024

Instance Method Summary collapse

Instance Method Details

#existing_meta(path) ⇒ Object



47
48
49
50
51
52
53
54
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 47

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

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

  {timestamp: timestamp, command_name: meta[:command_name]}
end

#parse_meta(path) ⇒ Object



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

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

#parse_meta_full(path) ⇒ Object



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

def parse_meta_full(path)
  parsed = JSON.parse(File.read(path), symbolize_names: true)
  parsed[:meta] if parsed.is_a?(Hash)
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 falls back to the full parse.



77
78
79
80
81
82
83
84
85
# 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

  JSON.parse(slice, symbolize_names: true)
rescue JSON::ParserError
  nil
end

#parse_time(value) ⇒ Object

Our own writer emits an ISO 8601 string, but coverage.json is whatever wrote it last: third-party formatters rewrite it in their own shape with an epoch integer, and Time.iso8601 raised TypeError on that, taking the whole report down on the next run. Anything but the two spellings disables the concurrency check for this file instead of failing the write (#1285).



61
62
63
64
65
66
67
68
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 61

def parse_time(value)
  case value
  when String then Time.iso8601(value)
  when Numeric then Time.at(value)
  end
rescue ArgumentError
  nil
end

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

Parameters:

  • path (String)

Returns:

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


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

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

#self?.parse_metaHash[Symbol, untyped]?

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


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

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

#self?.parse_meta_fullHash[Symbol, untyped]?

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


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

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

#self?.parse_meta_headHash[Symbol, untyped]?

Parameters:

  • path (String)

Returns:

  • (Hash[Symbol, untyped], nil)


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

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

#self?.parse_timeObject

Parameters:

  • value (Object)

Returns:

  • (Object)


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

def self?.parse_time: (untyped value) -> untyped

#self?.warn_if_concurrent_overwritevoid

This method returns an undefined value.

Parameters:



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

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

#self?.writeString

Parameters:

Returns:

  • (String)


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

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

#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 wrote it while we were running. A matching command_name means the same merged result, which is what both formatters configured together produce, so there is nothing to lose by overwriting (#1171).



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 33

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

  return if existing.fetch(:command_name).eql?(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



21
22
23
24
25
26
# File 'lib/simplecov/formatter/coverage_json_writer.rb', line 21

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