Class: Mutineer::Baseline

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

Overview

CI baseline/delta gating. A baseline is a prior mutineer run --format json document (no bespoke format to version). Diff the current run against it by the stable survivor id: 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 is 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.



51
52
53
54
# File 'lib/mutineer/baseline.rb', line 51

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

Instance Attribute Details

#scoreObject (readonly)

Returns the value of attribute score.



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

def score
  @score
end

Class Method Details

.load(path) ⇒ Mutineer::Baseline

Load a prior --format json run. Raises ConfigError (NOT exit: a data class must never kill the host) on a missing/unreadable file, unparseable JSON, or a doc that is not 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:



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

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:



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

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 cannot 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