Class: SimpleCov::Baseline
- Inherits:
-
Object
- Object
- SimpleCov::Baseline
- Defined in:
- lib/simplecov/baseline.rb,
lib/simplecov/baseline/parser.rb,
sig/simplecov.rbs
Overview
The checked-in per-file coverage floor behind simplecov ratchet (#1268):
.rubocop_todo.yml applied to coverage. Each entry records, per criterion,
the percent a file has already reached and the number of misses it had when
that percent was recorded.
A floor carries both numbers because each covers for the other's blind spot. The percent is the policy, but a percent moves when a file is edited without any coverage change at all, so the missed count acts as the dampener. A violation therefore requires both a lower percent and more misses, which is the honest reading of "this file got worse".
Defined Under Namespace
Modules: Parser Classes: Floor, Outcome
Constant Summary collapse
- DEFAULT_FILENAME =
".simplecov_baseline.yml"
Instance Attribute Summary collapse
-
#entries ⇒ Hash[String, Hash[Symbol, Floor]]
readonly
Returns the value of attribute entries.
Class Method Summary collapse
- .generate(current) ⇒ Baseline
-
.read(path) ⇒ Baseline?
currentis a{project_filename => {criterion => {percent:, missed:}}}Hash of the measured state. -
.read_if_exists(path) ⇒ Baseline?
A file that exists but cannot be read as a baseline raises ConfigurationError: a malformed policy must fail loudly rather than silently un-enforce every floor it carried.
Instance Method Summary collapse
-
#bucket_for(entry, merged, current_entry) ⇒ Symbol
The regressed bucket mirrors the violation rule exactly, so "below its floor" in the ratchet summary always means the exit check fails the file, never a percent drift the dampener tolerates.
-
#covers?(project_filename, criterion) ⇒ Boolean
Which is also what exempts the pair from the per-file thresholds.
- #dump_floor(floor) ⇒ Hash[String, untyped]
- #entry_for(project_filename) ⇒ Hash[Symbol, Floor]?
- #floor_for(project_filename, criterion) ⇒ Floor?
-
#initialize(entries) ⇒ Baseline
constructor
A new instance of Baseline.
-
#ratchet(current) ⇒ Outcome
Floors only move in the tightening direction, each axis independently: the percent keeps its best, the missed count its lowest.
-
#ratchet_entry(entry, current_entry) ⇒ {bucket: Symbol, ?entry: Hash[Symbol, Floor]}
A nil current prunes the entry, which carries no replacement and so answers with the bucket alone.
-
#tighten(floor, current) ⇒ Floor
Either side may be absent: a floor with no current measurement stays, a measurement with no floor becomes one.
-
#to_yaml ⇒ String
Sorted by path so a ratchet rewrite diffs as the set of floors that actually moved.
Constructor Details
#initialize(entries) ⇒ Baseline
Returns a new instance of Baseline.
67 68 69 |
# File 'lib/simplecov/baseline.rb', line 67 def initialize(entries) @entries = entries end |
Instance Attribute Details
#entries ⇒ Hash[String, Hash[Symbol, Floor]] (readonly)
Returns the value of attribute entries.
65 66 67 |
# File 'lib/simplecov/baseline.rb', line 65 def entries @entries end |
Class Method Details
.generate(current) ⇒ Baseline
59 60 61 62 63 |
# File 'lib/simplecov/baseline.rb', line 59 def self.generate(current) new(current.transform_values do |criteria| criteria.transform_values { |floor| Floor.new(percent: floor.fetch(:percent), missed: floor.fetch(:missed)) } end) end |
.read(path) ⇒ Baseline?
current is a {project_filename => {criterion => {percent:, missed:}}}
Hash of the measured state. Everything the report carries gets a floor at
its current coverage.
54 55 56 57 |
# File 'lib/simplecov/baseline.rb', line 54 def self.read(path) Deprecation.warn("`SimpleCov::Baseline.read` is deprecated. Replace with `read_if_exists`.") read_if_exists(path) end |
.read_if_exists(path) ⇒ Baseline?
A file that exists but cannot be read as a baseline raises ConfigurationError: a malformed policy must fail loudly rather than silently un-enforce every floor it carried.
42 43 44 45 46 47 48 49 |
# File 'lib/simplecov/baseline.rb', line 42 def self.read_if_exists(path) return nil unless File.exist?(path) new(Parser.call(YAML.safe_load_file(path), path)) rescue Psych::Exception => e # Interpolating the exception renders the message Psych objected with. raise ConfigurationError, "baseline file #{path} is not valid YAML: #{e}" end |
Instance Method Details
#bucket_for(entry, merged, current_entry) ⇒ Symbol
The regressed bucket mirrors the violation rule exactly, so "below its floor" in the ratchet summary always means the exit check fails the file, never a percent drift the dampener tolerates.
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/simplecov/baseline.rb', line 132 def bucket_for(entry, merged, current_entry) # Exact equality rather than numeric: tightening keeps the entry's own # floor on a tie, so an entry that did not move is the very values it # came in with. return :tightened unless merged.eql?(entry) regressed = entry.any? do |criterion, floor| current = current_entry[criterion] current && current.fetch(:percent) < floor.percent && (floor.missed.nil? || current.fetch(:missed) > floor.missed) end regressed ? :regressed : :unchanged end |
#covers?(project_filename, criterion) ⇒ Boolean
Which is also what exempts the pair from the per-file thresholds.
80 81 82 |
# File 'lib/simplecov/baseline.rb', line 80 def covers?(project_filename, criterion) !floor_for(project_filename, criterion).nil? end |
#dump_floor(floor) ⇒ Hash[String, untyped]
162 163 164 165 166 |
# File 'lib/simplecov/baseline.rb', line 162 def dump_floor(floor) dumped = {"percent" => floor.percent} #: Hash[String, untyped] dumped["missed"] = floor.missed unless floor.missed.nil? dumped end |
#entry_for(project_filename) ⇒ Hash[Symbol, Floor]?
71 72 73 |
# File 'lib/simplecov/baseline.rb', line 71 def entry_for(project_filename) entries[project_filename] end |
#floor_for(project_filename, criterion) ⇒ Floor?
75 76 77 |
# File 'lib/simplecov/baseline.rb', line 75 def floor_for(project_filename, criterion) entries.dig(project_filename, criterion) end |
#ratchet(current) ⇒ Outcome
Floors only move in the tightening direction, each axis independently:
the percent keeps its best, the missed count its lowest. Entries for
files current does not carry are pruned; files current carries
without an entry stay uncovered, so new code answers to the global
standard rather than to a floor cut at whatever it launched with. A
criterion the current run did not measure keeps its floor, and a newly
measured criterion joins the entry, so a later enable_coverage :branch
grandfathers the legacy files' branch state.
92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/simplecov/baseline.rb', line 92 def ratchet(current) buckets = {tightened: [], pruned: [], regressed: [], unchanged: []} #: Hash[Symbol, Array[String]] ratcheted = entries.filter_map do |file, entry| step = ratchet_entry(entry, current[file]) buckets.fetch(step.fetch(:bucket)) << file merged = step[:entry] [file, merged] if merged end.to_h Outcome.new(baseline: Baseline.new(ratcheted), tightened: buckets.fetch(:tightened), pruned: buckets.fetch(:pruned), regressed: buckets.fetch(:regressed), unchanged: buckets.fetch(:unchanged)) end |
#ratchet_entry(entry, current_entry) ⇒ {bucket: Symbol, ?entry: Hash[Symbol, Floor]}
A nil current prunes the entry, which carries no replacement and so answers with the bucket alone. Otherwise every measured criterion tightens, keeping the ones this run did not measure.
120 121 122 123 124 125 126 127 |
# File 'lib/simplecov/baseline.rb', line 120 def ratchet_entry(entry, current_entry) return {bucket: :pruned} unless current_entry merged = (entry.keys | current_entry.keys).to_h do |criterion| [criterion, tighten(entry[criterion], current_entry[criterion])] end {bucket: bucket_for(entry, merged, current_entry), entry: merged} end |
#tighten(floor, current) ⇒ Floor
Either side may be absent: a floor with no current measurement stays, a measurement with no floor becomes one.
148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/simplecov/baseline.rb', line 148 def tighten(floor, current) # The keys union in ratchet_entry guarantees at least one side is present, # which the cast restates for the type checker. return (_ = floor) unless current current_floor = Floor.new(percent: current.fetch(:percent), missed: current.fetch(:missed)) return current_floor unless floor Floor.new( percent: [floor.percent, current_floor.percent].max, missed: [floor.missed, current_floor.missed].compact.min ) end |
#to_yaml ⇒ String
Sorted by path so a ratchet rewrite diffs as the set of floors that actually moved.
108 109 110 111 112 113 |
# File 'lib/simplecov/baseline.rb', line 108 def to_yaml document = entries.sort.to_h do |file, entry| [file, entry.to_h { |criterion, floor| [CRITERIA.key(criterion), dump_floor(floor)] }] end HEADER + YAML.dump(document).delete_prefix("---\n") end |