Module: Evilution::Compare::Categorizer

Defined in:
lib/evilution/compare/categorizer.rb

Constant Summary collapse

ALIVE =
%i[survived].freeze
DEAD =
%i[killed timeout error].freeze

Class Method Summary collapse

Class Method Details

.bucket_single_sided(against_record, current_record, a_kind, c_kind, buckets) ⇒ Object



77
78
79
80
81
82
83
84
# File 'lib/evilution/compare/categorizer.rb', line 77

def bucket_single_sided(against_record, current_record, a_kind, c_kind, buckets)
  # peer_status is the peer record's status symbol, or nil if peer absent.
  # When the peer is excluded, its status symbol (e.g. :neutral) flows through.
  a_peer = current_record && current_record.status
  c_peer = against_record && against_record.status
  buckets[:alive_only_against] << { record: against_record, peer_status: a_peer } if a_kind == :alive
  buckets[:alive_only_current] << { record: current_record, peer_status: c_peer } if c_kind == :alive
end

.call(against, current) ⇒ Hash

Returns bucketed comparison result with keys:

  • ‘:alive_only_against` => `Array<Record, peer_status: Symbol|nil>` records that survived in against but not in current (or absent in current). `peer_status` is the current-side record’s status symbol, or ‘nil` when no current-side record exists for that fingerprint.

  • ‘:alive_only_current` => `Array<Record, peer_status: Symbol|nil>` mirror of the above from the current side.

  • ‘:shared_alive` => `Array<Record, current: Record>` mutations that survived in both runs.

  • ‘:shared_dead` => `Array<Record, current: Record>` mutations killed/timed-out/errored in both runs.

  • ‘:excluded_against` => `Integer` count of against records with non-actionable statuses (neutral, equivalent, unresolved, unparseable).

  • ‘:excluded_current` => `Integer` mirror for the current side.

Parameters:

  • against (Array<Record>)

    prior run (baseline)

  • current (Array<Record>)

    current run

Returns:

  • (Hash)

    bucketed comparison result with keys:

    • ‘:alive_only_against` => `Array<Record, peer_status: Symbol|nil>` records that survived in against but not in current (or absent in current). `peer_status` is the current-side record’s status symbol, or ‘nil` when no current-side record exists for that fingerprint.

    • ‘:alive_only_current` => `Array<Record, peer_status: Symbol|nil>` mirror of the above from the current side.

    • ‘:shared_alive` => `Array<Record, current: Record>` mutations that survived in both runs.

    • ‘:shared_dead` => `Array<Record, current: Record>` mutations killed/timed-out/errored in both runs.

    • ‘:excluded_against` => `Integer` count of against records with non-actionable statuses (neutral, equivalent, unresolved, unparseable).

    • ‘:excluded_current` => `Integer` mirror for the current side.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/evilution/compare/categorizer.rb', line 31

def call(against, current)
  # Duplicate fingerprints within one side should not happen (Normalizer
  # invariant). If they do, last write wins — we do not dedupe proactively.
  against_by_fp = index_by_fingerprint(against)
  current_by_fp = index_by_fingerprint(current)

  buckets = {
    alive_only_against: [],
    alive_only_current: [],
    shared_alive: [],
    shared_dead: [],
    excluded_against: 0,
    excluded_current: 0
  }

  (against_by_fp.keys | current_by_fp.keys).each do |fp|
    classify(against_by_fp[fp], current_by_fp[fp], buckets)
  end

  sort_buckets!(buckets)
  buckets
end

.classify(against_record, current_record, buckets) ⇒ Object

Dispatches one fingerprint pair into buckets. Either record may be nil (fingerprint present on only one side).



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/evilution/compare/categorizer.rb', line 56

def classify(against_record, current_record, buckets)
  count_excluded(against_record, current_record, buckets)
  a_kind = kind_of(against_record)
  c_kind = kind_of(current_record)

  if a_kind == :alive && c_kind == :alive
    buckets[:shared_alive] << { against: against_record, current: current_record }
  elsif a_kind == :dead && c_kind == :dead
    buckets[:shared_dead] << { against: against_record, current: current_record }
  else
    bucket_single_sided(against_record, current_record, a_kind, c_kind, buckets)
  end
  # A dead-only fingerprint (dead on one side, absent on the other) is
  # intentionally not bucketed and not counted as excluded.
end

.count_excluded(against_record, current_record, buckets) ⇒ Object



72
73
74
75
# File 'lib/evilution/compare/categorizer.rb', line 72

def count_excluded(against_record, current_record, buckets)
  buckets[:excluded_against] += 1 if against_record && kind_of(against_record) == :excluded
  buckets[:excluded_current] += 1 if current_record && kind_of(current_record) == :excluded
end

.index_by_fingerprint(records) ⇒ Object



106
107
108
# File 'lib/evilution/compare/categorizer.rb', line 106

def index_by_fingerprint(records)
  records.to_h { |r| [r.fingerprint, r] }
end

.kind_of(record) ⇒ Object

Returns :alive, :dead, :excluded, or nil (for nil records).



87
88
89
90
91
92
93
# File 'lib/evilution/compare/categorizer.rb', line 87

def kind_of(record)
  return nil if record.nil?
  return :alive if ALIVE.include?(record.status)
  return :dead  if DEAD.include?(record.status)

  :excluded
end

.sort_buckets!(buckets) ⇒ Object



95
96
97
98
99
100
# File 'lib/evilution/compare/categorizer.rb', line 95

def sort_buckets!(buckets)
  buckets[:alive_only_against].sort_by! { |e| sort_key(e[:record]) }
  buckets[:alive_only_current].sort_by! { |e| sort_key(e[:record]) }
  buckets[:shared_alive].sort_by!       { |e| sort_key(e[:against]) }
  buckets[:shared_dead].sort_by!        { |e| sort_key(e[:against]) }
end

.sort_key(record) ⇒ Object



102
103
104
# File 'lib/evilution/compare/categorizer.rb', line 102

def sort_key(record)
  [record.file_path, record.line, record.fingerprint]
end