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.
%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
- .combined_content_sha(paths) ⇒ Object
-
.dependency_files(root = Dir.pwd) ⇒ Object
Existing dependency files resolved against
root, sorted. -
.dependency_fingerprint(root = Dir.pwd) ⇒ Object
Run-level combined SHA over the dependency file set.
- .pruned_tree_files(dir) ⇒ Object
-
.subject_source_hash(mutant) ⇒ Object
SHA256 of the subject's source lines.
-
.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.
- .survivor_fingerprint_matches?(fingerprint, live_paths, dependency_sha) ⇒ Boolean
-
.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.
-
.tests_fingerprint(test_files) ⇒ Object
JSON fingerprint of the covering test files: their sorted paths plus a combined content hash.
-
.tests_fingerprint_current?(fingerprint_json) ⇒ Boolean
True when every recorded test file still exists with identical content.
Instance Method Summary collapse
- #self?.combined_content_sha ⇒ String?
- #self?.dependency_fingerprint ⇒ String?
- #self?.subject_source_hash ⇒ String?
- #self?.survivor_fingerprint_current? ⇒ Boolean
- #self?.survivor_fingerprint_matches? ⇒ Boolean
- #self?.survivor_tests_fingerprint ⇒ String?
- #self?.tests_fingerprint ⇒ String?
- #self?.tests_fingerprint_current? ⇒ Boolean
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.
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
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.
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_sha ⇒ String?
667 |
# File 'sig/henitai.rbs', line 667
def self?.combined_content_sha: (untyped) -> String?
|
#self?.dependency_fingerprint ⇒ String?
663 |
# File 'sig/henitai.rbs', line 663
def self?.dependency_fingerprint: (?String) -> String?
|
#self?.subject_source_hash ⇒ String?
660 |
# File 'sig/henitai.rbs', line 660
def self?.subject_source_hash: (untyped) -> String?
|
#self?.survivor_fingerprint_current? ⇒ 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
666 |
# File 'sig/henitai.rbs', line 666
def self?.survivor_fingerprint_matches?: (untyped, Array[String], String?) -> bool
|
#self?.survivor_tests_fingerprint ⇒ String?
664 |
# File 'sig/henitai.rbs', line 664
def self?.survivor_tests_fingerprint: (untyped, dependency_sha: String?) -> String?
|
#self?.tests_fingerprint ⇒ String?
661 |
# File 'sig/henitai.rbs', line 661
def self?.tests_fingerprint: (untyped) -> String?
|
#self?.tests_fingerprint_current? ⇒ Boolean
662 |
# File 'sig/henitai.rbs', line 662
def self?.tests_fingerprint_current?: (String?) -> bool
|