Class: Henitai::PerTestCoverageCollector
- Inherits:
-
Object
- Object
- Henitai::PerTestCoverageCollector
- Defined in:
- lib/henitai/per_test_coverage_collector.rb,
sig/henitai.rbs
Overview
Accumulates per-test line coverage deltas across test examples.
Framework-agnostic core used by both the RSpec and Minitest adapters. Callers invoke record_test after each test completes and write_report once the full suite has finished.
Constant Summary collapse
- REPORT_DIR_ENV =
"HENITAI_REPORTS_DIR"- REPORT_FILE_NAME =
"henitai_per_test.json"
Instance Method Summary collapse
- #current_snapshot ⇒ Object
-
#initialize ⇒ PerTestCoverageCollector
constructor
A new instance of PerTestCoverageCollector.
- #line_counts_for(file_coverage) ⇒ Array[untyped]
-
#new_line_numbers(file_coverage, previous_counts) ⇒ Array[Integer]
Count-delta attribution: a line belongs to every test whose run incremented its cumulative count, not just the first toucher.
- #new_lines(snapshot) ⇒ Hash[String, Array[Integer]]
- #previous_line_counts(source_file) ⇒ Array[untyped]
- #record_test(test_file, duration: nil) ⇒ void
- #report_path ⇒ String
- #reports_dir ⇒ String
- #serializable_report ⇒ Hash[String, Hash[String, Array[Integer]]]
- #source_file?(path) ⇒ Boolean
- #warn_missing_coverage ⇒ void
- #write_report ⇒ void
Constructor Details
#initialize ⇒ PerTestCoverageCollector
Returns a new instance of PerTestCoverageCollector.
17 18 19 20 21 22 23 24 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 17 def initialize @coverage_by_test = Hash.new do |hash, test_file| hash[test_file] = Hash.new { |nested, source_file| nested[source_file] = [] } end @duration_by_test = Hash.new(0.0) @previous_snapshot = {} @warned_missing_coverage = false end |
Instance Method Details
#current_snapshot ⇒ Object
56 57 58 59 60 61 62 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 56 def current_snapshot Coverage.peek_result rescue RuntimeError # Raised when coverage measurement is not running; caller warns and skips. # Other errors are genuine bugs and must surface. nil end |
#line_counts_for(file_coverage) ⇒ Array[untyped]
108 109 110 111 112 113 114 115 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 108 def line_counts_for(file_coverage) case file_coverage when Hash Array(file_coverage[:lines] || file_coverage["lines"]) else Array(file_coverage) end end |
#new_line_numbers(file_coverage, previous_counts) ⇒ Array[Integer]
Count-delta attribution: a line belongs to every test whose run incremented its cumulative count, not just the first toucher. This keeps the per-test map a complete over-approximation of reachability — the soundness precondition for survivor verdict reuse (ADR-11).
96 97 98 99 100 101 102 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 96 def new_line_numbers(file_coverage, previous_counts) line_counts_for(file_coverage).each_with_index.filter_map do |count, index| next unless count.to_i > previous_counts.fetch(index, 0).to_i index + 1 end end |
#new_lines(snapshot) ⇒ Hash[String, Array[Integer]]
80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 80 def new_lines(snapshot) snapshot.each_with_object({}) do |(source_file, file_coverage), result| next unless source_file?(source_file) lines = new_line_numbers( file_coverage, previous_line_counts(source_file) ) result[source_file] = lines unless lines.empty? end end |
#previous_line_counts(source_file) ⇒ Array[untyped]
104 105 106 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 104 def previous_line_counts(source_file) line_counts_for(@previous_snapshot.fetch(source_file, [])) end |
#record_test(test_file, duration: nil) ⇒ void
This method returns an undefined value.
26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 26 def record_test(test_file, duration: nil) snapshot = current_snapshot return warn_missing_coverage unless snapshot new_lines(snapshot).each do |source_file, lines| @coverage_by_test[test_file][source_file].concat(lines) @coverage_by_test[test_file][source_file].uniq! @coverage_by_test[test_file][source_file].sort! end @duration_by_test[test_file] += duration.to_f @previous_snapshot = snapshot end |
#report_path ⇒ String
48 49 50 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 48 def report_path File.join(reports_dir, REPORT_FILE_NAME) end |
#reports_dir ⇒ String
52 53 54 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 52 def reports_dir ENV.fetch(REPORT_DIR_ENV, "coverage") end |
#serializable_report ⇒ Hash[String, Hash[String, Array[Integer]]]
127 128 129 130 131 132 133 134 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 127 def serializable_report @coverage_by_test.to_h do |test_file, source_map| coverage = source_map.to_h do |source_file, lines| [File.(source_file), lines.uniq.sort] end [test_file, { "coverage" => coverage, "duration" => @duration_by_test[test_file] }] end end |
#source_file?(path) ⇒ Boolean
117 118 119 120 121 122 123 124 125 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 117 def source_file?(path) = File.(path) prefix = "#{Dir.pwd}#{File::SEPARATOR}" return false unless .start_with?(prefix) relative = .sub(prefix, "") !relative.start_with?("spec#{File::SEPARATOR}") && !relative.start_with?("test#{File::SEPARATOR}") end |
#warn_missing_coverage ⇒ void
This method returns an undefined value.
64 65 66 67 68 69 70 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 64 def warn_missing_coverage return if @warned_missing_coverage return if coverage_suppressed? warn "Per-test coverage unavailable; skipping coverage formatter output" @warned_missing_coverage = true end |
#write_report ⇒ void
This method returns an undefined value.
39 40 41 42 43 44 |
# File 'lib/henitai/per_test_coverage_collector.rb', line 39 def write_report return if @coverage_by_test.empty? FileUtils.mkdir_p(File.dirname(report_path)) File.write(report_path, JSON.pretty_generate(serializable_report)) end |