Module: Henitai::VerdictFingerprint

Defined in:
lib/henitai/verdict_fingerprint.rb,
sig/henitai.rbs

Overview

Content fingerprints that decide whether a stored verdict is still trustworthy: the subject's source (method body) and the covering test files must both be byte-identical to what was recorded. Survived verdicts additionally record the full-map covering-set membership plus a run-level dependency fingerprint (ADR-11). Any read failure yields nil / a mismatch — conservative, never reuse on doubt.

Constant Summary collapse

DEPENDENCY_GLOBS =

Files that influence test behavior but never appear in any covering set: helpers, support code, fixtures/factories, lockfile and tool config. One combined hash over all of them gates survivor reuse.

Returns:

  • (Array[String])
%w[
  spec/spec_helper.rb
  spec/rails_helper.rb
  spec/support/**/*
  spec/fixtures/**/*
  spec/factories/**/*
  test/test_helper.rb
  test/support/**/*
  test/fixtures/**/*
  test/factories/**/*
  Gemfile.lock
  .henitai.yml
  .rspec
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.combined_content_sha(paths) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/henitai/verdict_fingerprint.rb', line 139

def combined_content_sha(paths)
  digest = Digest::SHA256.new
  paths.each do |path|
    content = File.read(path)
    digest.update(path.bytesize.to_s)
    digest.update(":")
    digest.update(path)
    digest.update(content.bytesize.to_s)
    digest.update(":")
    digest.update(content)
  end
  digest.hexdigest
rescue StandardError
  nil
end

.dependency_files(root = Dir.pwd) ⇒ Object

Existing dependency files resolved against root, sorted. Shared by the fingerprint below and by the coverage freshness watch — a stale per-test map after a dependency edit would let the test selector omit a newly covering test. Uses Find with pruning instead of Dir.glob so generated subtrees (thousands of mutation-log files under fixture projects) are never walked at all.



80
81
82
83
84
85
86
# File 'lib/henitai/verdict_fingerprint.rb', line 80

def dependency_files(root = Dir.pwd)
  files = DEPENDENCY_GLOBS.flat_map do |pattern|
    base = File.join(root, pattern)
    pattern.include?("*") ? pruned_tree_files(File.dirname(base.sub("/**/*", "/x"))) : [base]
  end
  files.select { |path| File.file?(path) }.uniq.sort
end

.dependency_fingerprint(root = Dir.pwd) ⇒ Object

Run-level combined SHA over the dependency file set. Missing files are simply excluded (not an error); a read failure yields nil — no reuse.



101
# File 'lib/henitai/verdict_fingerprint.rb', line 101

def dependency_fingerprint(root = Dir.pwd) = combined_content_sha(dependency_files(root))

.pruned_tree_files(dir) ⇒ Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/henitai/verdict_fingerprint.rb', line 88

def pruned_tree_files(dir)
  return [] unless File.directory?(dir)

  collected = [] # : Array[String]
  Find.find(dir) do |path|
    Find.prune if File.directory?(path) && GeneratedArtifacts.generated_dir?(path)
    collected << path if File.file?(path)
  end
  collected
end

.subject_source_hash(mutant) ⇒ Object

SHA256 of the subject's source lines. nil when the subject has no source range or the file cannot be read.



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/henitai/verdict_fingerprint.rb', line 39

def subject_source_hash(mutant)
  subject = mutant.subject
  return nil unless subject.respond_to?(:source_file) && subject.respond_to?(:source_range)

  range = subject.source_range
  return nil unless range

  lines = File.readlines(subject.source_file)
  Digest::SHA256.hexdigest(lines[(range.begin - 1)..(range.end - 1)].to_a.join)
rescue SystemCallError, IOError
  nil
end

.survivor_fingerprint_current?(fingerprint_json, live_paths:, dependency_sha:) ⇒ Boolean

True when the recorded covering set still equals the live one in membership AND content, and the dependency fingerprint is unchanged. Any parse/read failure or missing field resolves to stale.

Returns:

  • (Boolean)


122
123
124
125
126
127
128
# File 'lib/henitai/verdict_fingerprint.rb', line 122

def survivor_fingerprint_current?(fingerprint_json, live_paths:, dependency_sha:)
  return false if fingerprint_json.nil? || dependency_sha.nil?

  survivor_fingerprint_matches?(JSON.parse(fingerprint_json), live_paths, dependency_sha)
rescue StandardError
  false
end

.survivor_fingerprint_matches?(fingerprint, live_paths, dependency_sha) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
# File 'lib/henitai/verdict_fingerprint.rb', line 130

def survivor_fingerprint_matches?(fingerprint, live_paths, dependency_sha)
  recorded_paths = fingerprint.fetch("paths")
  return false if recorded_paths.empty?

  recorded_paths == Array(live_paths).map(&:to_s).sort &&
    fingerprint.fetch("dependencies") == dependency_sha &&
    combined_content_sha(recorded_paths) == fingerprint.fetch("sha")
end

.survivor_tests_fingerprint(test_files, dependency_sha:) ⇒ Object

Fingerprint recorded for a Survived verdict: the sorted full-map intersection set (paths + combined content sha) plus the run-level dependency sha. nil when any input is unavailable — the verdict is then never reusable.



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/henitai/verdict_fingerprint.rb', line 107

def survivor_tests_fingerprint(test_files, dependency_sha:)
  return nil if dependency_sha.nil?

  paths = Array(test_files).map(&:to_s).sort
  return nil if paths.empty?

  sha = combined_content_sha(paths)
  return nil unless sha

  JSON.generate("paths" => paths, "sha" => sha, "dependencies" => dependency_sha)
end

.tests_fingerprint(test_files) ⇒ Object

JSON fingerprint of the covering test files: their sorted paths plus a combined content hash. nil when no tests are known or any is unreadable.



54
55
56
57
58
59
60
61
62
# File 'lib/henitai/verdict_fingerprint.rb', line 54

def tests_fingerprint(test_files)
  paths = Array(test_files).map(&:to_s).sort
  return nil if paths.empty?

  sha = combined_content_sha(paths)
  return nil unless sha

  JSON.generate("paths" => paths, "sha" => sha)
end

.tests_fingerprint_current?(fingerprint_json) ⇒ Boolean

True when every recorded test file still exists with identical content.

Returns:

  • (Boolean)


65
66
67
68
69
70
71
72
# File 'lib/henitai/verdict_fingerprint.rb', line 65

def tests_fingerprint_current?(fingerprint_json)
  return false if fingerprint_json.nil?

  fingerprint = JSON.parse(fingerprint_json)
  combined_content_sha(fingerprint.fetch("paths")) == fingerprint.fetch("sha")
rescue StandardError
  false
end

Instance Method Details

#self?.combined_content_shaString?

Parameters:

  • (Object)

Returns:

  • (String, nil)


667
# File 'sig/henitai.rbs', line 667

def self?.combined_content_sha: (untyped) -> String?

#self?.dependency_fingerprintString?

Parameters:

  • (String)

Returns:

  • (String, nil)


663
# File 'sig/henitai.rbs', line 663

def self?.dependency_fingerprint: (?String) -> String?

#self?.subject_source_hashString?

Parameters:

  • (Object)

Returns:

  • (String, nil)


660
# File 'sig/henitai.rbs', line 660

def self?.subject_source_hash: (untyped) -> String?

#self?.survivor_fingerprint_current?Boolean

Parameters:

  • (String, nil)
  • live_paths: (Array[String])
  • dependency_sha: (String, nil)

Returns:

  • (Boolean)


665
# File 'sig/henitai.rbs', line 665

def self?.survivor_fingerprint_current?: (String?, live_paths: Array[String], dependency_sha: String?) -> bool

#self?.survivor_fingerprint_matches?Boolean

Parameters:

  • (Object)
  • (Array[String])
  • (String, nil)

Returns:

  • (Boolean)


666
# File 'sig/henitai.rbs', line 666

def self?.survivor_fingerprint_matches?: (untyped, Array[String], String?) -> bool

#self?.survivor_tests_fingerprintString?

Parameters:

  • (Object)
  • dependency_sha: (String, nil)

Returns:

  • (String, nil)


664
# File 'sig/henitai.rbs', line 664

def self?.survivor_tests_fingerprint: (untyped, dependency_sha: String?) -> String?

#self?.tests_fingerprintString?

Parameters:

  • (Object)

Returns:

  • (String, nil)


661
# File 'sig/henitai.rbs', line 661

def self?.tests_fingerprint: (untyped) -> String?

#self?.tests_fingerprint_current?Boolean

Parameters:

  • (String, nil)

Returns:

  • (Boolean)


662
# File 'sig/henitai.rbs', line 662

def self?.tests_fingerprint_current?: (String?) -> bool