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

Builds a baseline from a JSON document.

The baseline retains the survivor document and score from the JSON report.

Parameters:

  • doc (Hash)

    parsed JSON document.



53
54
55
56
# File 'lib/mutineer/baseline.rb', line 53

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

Instance Attribute Details

#scoreObject (readonly)

Returns the value of attribute score.



46
47
48
# File 'lib/mutineer/baseline.rb', line 46

def score
  @score
end

Class Method Details

.load(path) ⇒ Mutineer::Baseline

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.

Parameters:

  • path (String)

    baseline JSON file path.

Returns:

Raises:



35
36
37
38
39
40
41
42
43
44
# File 'lib/mutineer/baseline.rb', line 35

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) ⇒ Mutineer::Baseline::Delta

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

Parameters:

Returns:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/mutineer/baseline.rb', line 65

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