Class: SimpleCov::Baseline

Inherits:
Object
  • Object
show all
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 =

Returns:

  • (String)
".simplecov_baseline.yml"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries) ⇒ Baseline

Returns a new instance of Baseline.

Parameters:

  • entries (Hash[String, Hash[Symbol, Floor]])


67
68
69
# File 'lib/simplecov/baseline.rb', line 67

def initialize(entries)
  @entries = entries
end

Instance Attribute Details

#entriesHash[String, Hash[Symbol, Floor]] (readonly)

Returns the value of attribute entries.

Returns:

  • (Hash[String, Hash[Symbol, Floor]])


65
66
67
# File 'lib/simplecov/baseline.rb', line 65

def entries
  @entries
end

Class Method Details

.generate(current) ⇒ Baseline

Parameters:

  • current (current_floors)

Returns:



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.

Parameters:

  • path (String)

Returns:



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.

Parameters:

  • path (String)

Returns:



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.

Parameters:

  • entry (Hash[Symbol, Floor])
  • merged (Hash[Symbol, Floor])
  • current_entry (Hash[Symbol, {percent: Numeric, missed: Integer}])

Returns:

  • (Symbol)


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.

Parameters:

  • project_filename (String)
  • criterion (Symbol)

Returns:

  • (Boolean)


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]

Parameters:

Returns:

  • (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]?

Parameters:

  • project_filename (String)

Returns:

  • (Hash[Symbol, Floor], nil)


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?

Parameters:

  • project_filename (String)
  • criterion (Symbol)

Returns:



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.

Parameters:

  • current (current_floors)

Returns:



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.

Parameters:

  • entry (Hash[Symbol, Floor])
  • current_entry (Hash[Symbol, {percent: Numeric, missed: Integer}], nil)

Returns:

  • ({bucket: Symbol, ?entry: Hash[Symbol, Floor]})


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.

Parameters:

  • floor (Floor, nil)
  • current ({percent: Numeric, missed: Integer}, nil)

Returns:



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_yamlString

Sorted by path so a ratchet rewrite diffs as the set of floors that actually moved.

Returns:

  • (String)


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