Class: SimpleCov::Production::FileSink
- Inherits:
-
Object
- Object
- SimpleCov::Production::FileSink
- Defined in:
- lib/simplecov/production/file_sink.rb,
sig/simplecov.rbs
Overview
The bundled sink: a single JSON file, union-merged under an exclusive lock so any number of processes on the same host can share it. This is the reference for the sink contract:
store(coverage) # {"lib/foo.rb" => [1, 3, 12], ...} of
# root-relative paths to sorted line numbers
A sink must merge (never replace: many processes each hold only a slice), must be idempotent (the same lines may arrive twice), and signals failure by raising, which makes the runtime keep the delta and retry next interval.
last_seen stamps each file with the last store that carried it.
Oneshot clears on drain, so still-running code re-reports every interval
and the stamp tracks real recency, not first sighting. The field is
optional on read: a v1 store written before it existed simply has no
recency evidence to offer.
Constant Summary collapse
- ENVELOPE =
The envelope key, which is also how
readtells a production coverage file from an arbitrary JSON document it must not clobber or misread. "simplecov_production"- FORMAT_VERSION =
1
Instance Attribute Summary collapse
-
#path ⇒ String
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.envelope_of(document, path) ⇒ Hash[String, untyped]
The refusal is what keeps an arbitrary JSON file from being misread as empty coverage.
-
.parse(content, path) ⇒ Hash[String, untyped]
empty or missing file is a fresh store.
-
.read(path) ⇒ Hash[String, untyped]
Raises
Errorfor anything that is not a production coverage file, naming the path: misreading an arbitrary JSON file as empty coverage would quietly report every line dead.
Instance Method Summary collapse
- #envelope(existing, incoming) ⇒ Hash[String, untyped]
-
#initialize(path:) ⇒ FileSink
constructor
A new instance of FileSink.
- #merge(existing, incoming) ⇒ Hash[String, untyped]
-
#open_file(name, mode, perm) ⇒ void
mutant:disable — Ruby 4.0 removed Kernel#open's leading-pipe command mode, so no test can tell
File.openfromopenhere any more. -
#rewrite(file, payload) ⇒ void
Truncates any leftover tail from a previously larger document.
- #store(coverage) ⇒ true
-
#with_exclusive_lock ⇒ void
Both halves of the read-modify-write happen through the one handle, so processes sharing the file take turns rather than overwriting each other.
Constructor Details
#initialize(path:) ⇒ FileSink
Returns a new instance of FileSink.
35 36 37 |
# File 'lib/simplecov/production/file_sink.rb', line 35 def initialize(path:) @path = File.(path) end |
Instance Attribute Details
#path ⇒ String (readonly)
Returns the value of attribute path.
33 34 35 |
# File 'lib/simplecov/production/file_sink.rb', line 33 def path @path end |
Class Method Details
.envelope_of(document, path) ⇒ Hash[String, untyped]
The refusal is what keeps an arbitrary JSON file from being misread as empty coverage.
81 82 83 84 85 86 |
# File 'lib/simplecov/production/file_sink.rb', line 81 def self.envelope_of(document, path) inner = document[ENVELOPE] if document.instance_of?(Hash) raise Error, "#{path} is not a SimpleCov production coverage file" unless inner.instance_of?(Hash) inner end |
.parse(content, path) ⇒ Hash[String, untyped]
empty or missing file is a fresh store. JSON answers plain hashes, arrays, and scalars, never a subclass of one, so the shape checks ask about the class itself.
66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/simplecov/production/file_sink.rb', line 66 def self.parse(content, path) return {"coverage" => {}, "last_seen" => {}, "started_at" => nil} if content.match?(/\A\s*\z/) inner = envelope_of(JSON.parse(content), path) inner["coverage"] = {} unless inner["coverage"].instance_of?(Hash) inner["last_seen"] = {} unless inner["last_seen"].instance_of?(Hash) inner rescue JSON::ParserError => e # One line of the parser's complaint, which for some inputs quotes the # document back and runs long. raise Error, "#{path} is not valid JSON (#{e.message.lines.first.to_s.rstrip})" end |
.read(path) ⇒ Hash[String, untyped]
Raises Error for anything that is not a production coverage file,
naming the path: misreading an arbitrary JSON file as empty coverage
would quietly report every line dead.
58 59 60 |
# File 'lib/simplecov/production/file_sink.rb', line 58 def self.read(path) parse(File.read(path), path) end |
Instance Method Details
#envelope(existing, incoming) ⇒ Hash[String, untyped]
116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/simplecov/production/file_sink.rb', line 116 def envelope(existing, incoming) now = Time.now.utc.iso8601 { ENVELOPE => { "format_version" => FORMAT_VERSION, "started_at" => existing["started_at"] || now, "updated_at" => now, "coverage" => merge(existing.fetch("coverage"), incoming).sort.to_h, "last_seen" => existing.fetch("last_seen").merge(incoming.transform_values { now }).sort.to_h } } end |
#merge(existing, incoming) ⇒ Hash[String, untyped]
110 111 112 113 114 |
# File 'lib/simplecov/production/file_sink.rb', line 110 def merge(existing, incoming) incoming.each_with_object(existing) do |(file, lines), merged| merged[file] = ((merged[file] || []) | lines).sort end end |
#open_file(name, mode, perm) ⇒ void
This method returns an undefined value.
mutant:disable — Ruby 4.0 removed Kernel#open's leading-pipe
command mode, so no test can tell File.open from open here
any more. The explicit receiver is kept: on older rubies it is
what refuses to run a store path as a command.
106 107 108 |
# File 'lib/simplecov/production/file_sink.rb', line 106 def open_file(name, mode, perm, &) File.open(name, mode, perm, &) end |
#rewrite(file, payload) ⇒ void
This method returns an undefined value.
Truncates any leftover tail from a previously larger document.
49 50 51 52 53 |
# File 'lib/simplecov/production/file_sink.rb', line 49 def rewrite(file, payload) file.rewind file.write(JSON.generate(payload)) file.truncate(file.pos) end |
#store(coverage) ⇒ true
39 40 41 42 43 44 45 46 |
# File 'lib/simplecov/production/file_sink.rb', line 39 def store(coverage) FileUtils.mkdir_p(File.dirname(path)) with_exclusive_lock do |file| existing = self.class.parse(file.read, path) rewrite(file, envelope(existing, coverage)) end true end |
#with_exclusive_lock ⇒ void
This method returns an undefined value.
Both halves of the read-modify-write happen through the one handle, so processes sharing the file take turns rather than overwriting each other. The open flags are summed, which for disjoint bits is the same number OR would build and leaves no spelling of the combination without a witness.
95 96 97 98 99 100 |
# File 'lib/simplecov/production/file_sink.rb', line 95 def with_exclusive_lock open_file(path, File::RDWR + File::CREAT, 0o644) do |file| file.flock(File::LOCK_EX) yield file end end |