Class: Mutineer::Baseline

Inherits:
Object
  • Object
show all
Defined in:
lib/mutineer/baseline.rb

Overview

#13: CI baseline/delta gating. A baseline is literally a prior mutineer run --format json document (KTD-1) — no bespoke format to version. We diff the current run against it by the #10 stable survivor id (KTD-2): a NEW survivor (id present now, absent in the baseline) OR a score drop is a regression the CLI turns into exit 1. Pure data — stdlib json only, no fork, no Rails — so it's testable in isolation from a canned JSON + a hand-built AggregateResult.

Defined Under Namespace

Classes: Delta

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(doc) ⇒ Baseline

Returns a new instance of Baseline.



43
44
45
46
# File 'lib/mutineer/baseline.rb', line 43

def initialize(doc)
  @survivors = doc["survivors"] || []
  @score = doc.dig("summary", "score")
end

Instance Attribute Details

#scoreObject (readonly)

Returns the value of attribute score.



41
42
43
# File 'lib/mutineer/baseline.rb', line 41

def score
  @score
end

Class Method Details

.load(path) ⇒ Object

Load a prior --format json run. Raises ConfigError (NOT exit — R8: a data class must never kill the host) on a missing/unreadable file, unparseable JSON, or a doc that isn't a baseline shape, so the CLI maps it to exit 2 (usage) like every other bad-path flag.



30
31
32
33
34
35
36
37
38
39
# File 'lib/mutineer/baseline.rb', line 30

def self.load(path)
  doc = JSON.parse(File.read(path))
  unless doc.is_a?(Hash) && doc["schema_version"] && doc["survivors"].is_a?(Array)
    raise ConfigError, "not a Mutineer JSON report: #{path}"
  end

  new(doc)
rescue JSON::ParserError => e
  raise ConfigError, "invalid baseline JSON in #{path}: #{e.message}"
end

Instance Method Details

#diff(aggregate, epsilon: 0.0) ⇒ Object

Diff a current AggregateResult against this baseline by stable survivor id. epsilon tolerates float jitter on the score (default 0.0 = any drop gates).



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/mutineer/baseline.rb', line 50

def diff(aggregate, epsilon: 0.0)
  current = aggregate.surviving_mutants
  current_ids = current.map(&:id)
  baseline_ids = @survivors.map { |h| h["id"] }

  new_survivors = current.reject { |r| baseline_ids.include?(r.id) }
  fixed = @survivors.reject { |h| current_ids.include?(h["id"]) }

  current_score = aggregate.mutation_score
  # nil-score discipline (mirrors Reporter#exit_code): a score absent on either
  # side can't be compared — skip the drop check, keep the new-survivor check.
  score_drop = !@score.nil? && !current_score.nil? &&
               current_score < @score - epsilon

  Delta.new(new_survivors: new_survivors, fixed_survivors: fixed,
            score_before: @score, score_after: current_score,
            score_drop: score_drop,
            regressed: !new_survivors.empty? || score_drop)
end