henitai 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Henitai
6
+ # Reads a Stryker-compatible mutation report and extracts survivor data.
7
+ #
8
+ # Returns a +Report+ value object carrying:
9
+ # - +survivor_ids+ — stable IDs of survived mutants
10
+ # - +coverage_map+ — stableId → [test_files] from prior coveredBy data
11
+ # - +git_sha+ — git HEAD at the time the report was written (may be nil)
12
+ #
13
+ # Scope validation is intentionally shallow: checks schemaVersion presence
14
+ # and at least one file path overlap with config.includes.
15
+ class SurvivorLoader
16
+ # Value object returned by #load.
17
+ Report = Struct.new(:survivor_ids, :coverage_map, :git_sha)
18
+
19
+ class FileNotFoundError < StandardError; end
20
+ class InvalidReportError < StandardError; end
21
+ class ScopeMismatchError < StandardError; end
22
+
23
+ # @param path [String] path to a Stryker-compatible JSON report
24
+ # @param include_paths [Array<String>] from config.includes; used for scope validation
25
+ def initialize(path, include_paths: [])
26
+ @path = path
27
+ @include_paths = include_paths
28
+ end
29
+
30
+ # @return [Report]
31
+ def load
32
+ raw = read_file
33
+ report = parse_json(raw)
34
+ validate_scope(report)
35
+ build_report(report)
36
+ end
37
+
38
+ private
39
+
40
+ def build_report(report)
41
+ entries = known_entries(report)
42
+ Report.new(
43
+ survivor_ids: extract_survivor_ids(entries),
44
+ coverage_map: extract_coverage_map(entries),
45
+ git_sha: report["gitSha"]
46
+ )
47
+ end
48
+
49
+ # Returns mutant entries that have a stableId, warning about those that don't.
50
+ def known_entries(report)
51
+ all_mutants(report).select do |entry|
52
+ if entry["stableId"]
53
+ true
54
+ else
55
+ warn "henitai: survivor report entry missing stableId — skipping"
56
+ false
57
+ end
58
+ end
59
+ end
60
+
61
+ def extract_survivor_ids(entries)
62
+ entries.filter_map { |e| e["stableId"] if e["status"] == "Survived" }
63
+ end
64
+
65
+ def extract_coverage_map(entries)
66
+ entries.each_with_object({}) do |entry, map|
67
+ covered = Array(entry["coveredBy"]).compact
68
+ map[entry["stableId"]] = covered unless covered.empty?
69
+ end
70
+ end
71
+
72
+ def read_file
73
+ File.read(@path)
74
+ rescue Errno::ENOENT
75
+ raise FileNotFoundError, "Survivor report not found: #{@path}"
76
+ end
77
+
78
+ def parse_json(raw)
79
+ JSON.parse(raw)
80
+ rescue JSON::ParserError => e
81
+ raise InvalidReportError, "Invalid JSON in survivor report #{@path}: #{e.message}"
82
+ end
83
+
84
+ def validate_scope(report)
85
+ validate_schema_version!(report)
86
+ return if @include_paths.empty?
87
+
88
+ report_files = normalized_report_files(report)
89
+ include_dirs_raw = normalized_include_dirs_raw
90
+ include_dirs_abs = normalized_include_dirs_abs(include_dirs_raw)
91
+
92
+ return if any_report_file_overlaps?(report_files, include_dirs_raw, include_dirs_abs)
93
+
94
+ raise ScopeMismatchError,
95
+ "Survivor report #{@path} has no file overlap with configured includes — " \
96
+ "did you pass a report from a different project?"
97
+ end
98
+
99
+ def validate_schema_version!(report)
100
+ return if report.key?("schemaVersion")
101
+
102
+ raise InvalidReportError,
103
+ "Survivor report #{@path} is missing schemaVersion — is this a Henitai report?"
104
+ end
105
+
106
+ def normalized_report_files(report)
107
+ (report.fetch("files", {}) || {}).keys.map { |p| strip_trailing_slash(p.to_s) }
108
+ end
109
+
110
+ def normalized_include_dirs_raw
111
+ @include_paths.map { |p| strip_trailing_slash(p.to_s) }.uniq
112
+ end
113
+
114
+ def normalized_include_dirs_abs(dirs_raw)
115
+ dirs_raw.map { |p| File.expand_path(p) }.uniq
116
+ end
117
+
118
+ def strip_trailing_slash(path)
119
+ path.sub(%r{/\z}, "")
120
+ end
121
+
122
+ def any_report_file_overlaps?(report_files, include_dirs_raw, include_dirs_abs)
123
+ report_files.any? do |file|
124
+ include_dirs_raw.any? { |inc| path_prefix_match?(file, inc) } ||
125
+ include_dirs_abs.any? { |inc_abs| path_prefix_match?(File.expand_path(file), inc_abs) }
126
+ end
127
+ end
128
+
129
+ def path_prefix_match?(path, dir)
130
+ return false if path.empty? || dir.empty?
131
+
132
+ path == dir || path.start_with?(dir + File::SEPARATOR)
133
+ end
134
+
135
+ def all_mutants(report)
136
+ files = report.fetch("files", {}) || {}
137
+ files.values.compact.flat_map { |file_data| file_data.fetch("mutants", []) }
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Filters a mutant list to those that match a set of prior survivor stable IDs.
5
+ #
6
+ # After calling #select, #unmatched_ids reports which survivor IDs had no
7
+ # corresponding mutant in the current generation. A high unmatched ratio
8
+ # indicates that the source has drifted and a full run is recommended.
9
+ class SurvivorSelector
10
+ DRIFT_THRESHOLD = 0.5
11
+ class SelectionError < StandardError; end
12
+
13
+ def initialize(survivor_ids:)
14
+ @survivor_ids = survivor_ids.to_set
15
+ @unmatched_ids = nil
16
+ end
17
+
18
+ def select(mutants)
19
+ current_index = mutants.to_h { |m| [m.stable_id, m] }
20
+ matched_ids, @unmatched_ids = @survivor_ids.partition { |id| current_index.key?(id) }
21
+ matched_ids.filter_map { |id| current_index[id] }
22
+ end
23
+
24
+ def unmatched_ids
25
+ raise SelectionError, "Call #select before accessing #unmatched_ids" if @unmatched_ids.nil?
26
+
27
+ @unmatched_ids
28
+ end
29
+
30
+ def drift_warning?
31
+ return false if @survivor_ids.empty?
32
+
33
+ unmatched_ids.size.to_f / @survivor_ids.size > DRIFT_THRESHOLD
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Splits a matched survivor set into stable and pending subsets by consulting
5
+ # a git diff against the covering tests from the prior report.
6
+ #
7
+ # A survivor is **stable** (can skip re-execution) when:
8
+ # - it has covering test data in the prior report, AND
9
+ # - none of those test files appear in the diff between the prior run's
10
+ # git_sha and the current HEAD.
11
+ #
12
+ # A survivor is **pending** (must execute) when:
13
+ # - git_sha is nil (no anchor → conservative), OR
14
+ # - its coveredBy data is absent or empty, OR
15
+ # - at least one covering test file changed.
16
+ #
17
+ # On any git error the filter conservatively treats all survivors as pending.
18
+ class SurvivorTestFilter
19
+ # @param coverage_map [Hash<String, Array<String>>] stableId → [test_files]
20
+ # @param git_sha [String, nil] git SHA from the prior report
21
+ # @param dirty_source_files [Boolean] true when the current worktree has
22
+ # dirty source files and the survivor shortcut must be disabled
23
+ # @param worktree_changed_files [Array<String>] dirty tracked/untracked files
24
+ # @param diff_analyzer [GitDiffAnalyzer]
25
+ def initialize(
26
+ coverage_map:,
27
+ git_sha:,
28
+ dirty_source_files: false,
29
+ worktree_changed_files: [],
30
+ diff_analyzer: GitDiffAnalyzer.new
31
+ )
32
+ @coverage_map = coverage_map
33
+ @git_sha = git_sha
34
+ @dirty_source_files = dirty_source_files
35
+ @worktree_changed_files = Array(worktree_changed_files)
36
+ @diff_analyzer = diff_analyzer
37
+ end
38
+
39
+ # @param mutants [Array<Mutant>]
40
+ # @return [Hash<Symbol, Array<Mutant>>] { stable: [...], pending: [...] }
41
+ def apply(mutants)
42
+ return { stable: [], pending: mutants } if @dirty_source_files
43
+ return { stable: [], pending: mutants } if @git_sha.nil?
44
+
45
+ changed = changed_test_files
46
+ return { stable: [], pending: mutants } if changed.nil?
47
+
48
+ mutants.each_with_object({ stable: [], pending: [] }) do |mutant, result|
49
+ bucket = stable_survivor?(mutant, changed) ? :stable : :pending
50
+ result[bucket] << mutant
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def stable_survivor?(mutant, changed)
57
+ covering = @coverage_map[mutant.stable_id]
58
+ return false if covering.nil? || covering.empty?
59
+
60
+ covering.none? { |test_file| changed.include?(test_file) }
61
+ end
62
+
63
+ # Returns a Set of changed test file paths, or nil on any git error
64
+ # (nil triggers conservative fallback in #apply — all survivors pending).
65
+ def changed_test_files
66
+ committed = @diff_analyzer.changed_files(from: @git_sha, to: "HEAD")
67
+ (committed + @worktree_changed_files).to_set
68
+ rescue StandardError
69
+ nil
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.1.10"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/henitai.rb CHANGED
@@ -26,6 +26,7 @@ module Henitai
26
26
  autoload :PerTestCoverageSelector, "henitai/per_test_coverage_selector"
27
27
  autoload :Subject, "henitai/subject"
28
28
  autoload :Mutant, "henitai/mutant"
29
+ autoload :MutantIdentity, "henitai/mutant_identity"
29
30
  autoload :Operator, "henitai/operator"
30
31
  autoload :Operators, "henitai/operators"
31
32
  autoload :SourceParser, "henitai/source_parser"
@@ -39,6 +40,10 @@ module Henitai
39
40
  autoload :EquivalenceDetector, "henitai/equivalence_detector"
40
41
  autoload :StaticFilter, "henitai/static_filter"
41
42
  autoload :StillbornFilter, "henitai/stillborn_filter"
43
+ autoload :SurvivorLoader, "henitai/survivor_loader"
44
+ autoload :SurvivorSelector, "henitai/survivor_selector"
45
+ autoload :SurvivorTestFilter, "henitai/survivor_test_filter"
46
+ autoload :SurvivorActivationCache, "henitai/survivor_activation_cache"
42
47
  autoload :ScenarioExecutionResult, "henitai/scenario_execution_result"
43
48
  autoload :CoverageFormatter, "henitai/coverage_formatter"
44
49
  autoload :MinitestCoverageReporter, "henitai/minitest_coverage_reporter"
@@ -47,6 +52,9 @@ module Henitai
47
52
  autoload :SamplingStrategy, "henitai/sampling_strategy"
48
53
  autoload :TestPrioritizer, "henitai/test_prioritizer"
49
54
  autoload :ExecutionEngine, "henitai/execution_engine"
55
+ autoload :ParallelExecutionRunner, "henitai/parallel_execution_runner"
56
+ autoload :ProcessWorkerRunner, "henitai/process_worker_runner"
57
+ autoload :ProcessWakeup, "henitai/process_wakeup"
50
58
  autoload :Runner, "henitai/runner"
51
59
  autoload :Reporter, "henitai/reporter"
52
60
  autoload :Integration, "henitai/integration"
data/sig/henitai.rbs CHANGED
@@ -73,6 +73,38 @@ module Henitai
73
73
  module Operators
74
74
  end
75
75
 
76
+ module Integration::ChildDebugSupport
77
+ private
78
+
79
+ def run_rspec_runner: (Array[String]) -> untyped
80
+ def build_rspec_runner: () -> untyped
81
+ def configure_rspec_runner: (untyped) -> void
82
+ def load_rspec_spec_files: (Array[String]) -> void
83
+ def run_rspec_specs: (untyped) -> untyped
84
+ def debug_child_timeout_dump: (Integer) -> void
85
+ def install_debug_timeout_trap: () -> void
86
+ def debug_child_thread_dump: (String) -> void
87
+ def debug_child_puts: (String) -> void
88
+ def debug_child?: () -> bool
89
+ def debug_child_rspec_trace: (
90
+ test_files: Array[String],
91
+ rspec_options: Array[String],
92
+ rspec_argv: Array[String]
93
+ ) -> void
94
+ def debug_child_rspec_exit: (untyped) -> void
95
+ def debug_child_example_count: (String) -> void
96
+ def debug_child_activation_start: (String) -> void
97
+ def debug_child_activation_end: (untyped, test_files: Array[String]) -> void
98
+ def debug_child_mutant_meta: (Mutant) -> void
99
+ def debug_child_activation_check: () -> void
100
+ def suppress_simplecov!: () -> void
101
+ def suppress_coverage!: () -> void
102
+ def loaded_feature_map: (Array[String]) -> Array[[String, bool]]
103
+ def loaded_feature?: (String) -> bool
104
+ def rspec_world_example_count: () -> Integer?
105
+ def pause: (Float) -> void
106
+ end
107
+
76
108
  class Configuration
77
109
  DEFAULT_TIMEOUT: Float
78
110
  DEFAULT_OPERATORS: Symbol
@@ -106,11 +138,14 @@ module Henitai
106
138
  private
107
139
 
108
140
  def load_raw_configuration: (String) -> Hash[Symbol, untyped]
141
+ def detect_integration: () -> String
109
142
  def apply_defaults: (Hash[Symbol, untyped]) -> void
110
143
  def apply_general_defaults: (Hash[Symbol, untyped]) -> void
111
144
  def apply_mutation_defaults: (Hash[Symbol, untyped]) -> void
112
145
  def apply_analysis_defaults: (Hash[Symbol, untyped]) -> void
113
146
  def merge_defaults: (Hash[Symbol, untyped], Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
147
+ def resolve_integration_default: (untyped) -> untyped
148
+ def default_dashboard: (Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
114
149
  def symbolize_keys: (untyped) -> untyped
115
150
  end
116
151
 
@@ -171,8 +206,11 @@ module Henitai
171
206
  attr_accessor duration: Float?
172
207
  attr_accessor covered_by: Array[String]?
173
208
  attr_accessor tests_completed: Integer?
209
+ attr_reader precomputed_stable_id: String?
210
+ attr_reader precomputed_activation_source: String?
174
211
 
175
- def initialize: (subject: Subject, operator: String, nodes: Hash[Symbol, untyped], description: String, location: Hash[Symbol, untyped]) -> void
212
+ def initialize: (subject: Subject, operator: String, nodes: Hash[Symbol, untyped], description: String, location: Hash[Symbol, untyped], ?precomputed_stable_id: String?, ?precomputed_activation_source: String?) -> void
213
+ def stable_id: () -> String
176
214
  def killed?: () -> bool
177
215
  def survived?: () -> bool
178
216
  def pending?: () -> bool
@@ -256,9 +294,12 @@ module Henitai
256
294
  def self.for: (String) -> untyped
257
295
 
258
296
  class Base
297
+ include ChildDebugSupport
298
+
259
299
  def select_tests: (Subject) -> Array[String]
260
300
  def test_files: () -> Array[String]
261
301
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
302
+ def spawn_mutant: (mutant: Mutant, test_files: Array[String]) -> ChildHandle
262
303
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
263
304
  def per_test_coverage_supported?: () -> bool
264
305
  def wait_with_timeout: (Integer, Float) -> untyped
@@ -268,9 +309,26 @@ module Henitai
268
309
  private
269
310
 
270
311
  def pause: (Float) -> void
312
+ def wait_nonblocking: (Integer) -> untyped
271
313
  def handle_timeout: (Integer) -> Symbol
272
314
  def cleanup_child_process: (Integer) -> void
273
- def rspec_options: () -> Array[String]
315
+ def suppress_simplecov!: () -> void
316
+ def suppress_coverage!: () -> void
317
+ def run_tests: (Array[String]) -> Integer
318
+ def debug_child?: () -> bool
319
+ def debug_child_puts: (String) -> void
320
+ def debug_child_rspec_trace: (test_files: Array[String], rspec_options: Array[String], rspec_argv: Array[String]) -> void
321
+ def debug_child_rspec_exit: (untyped status) -> void
322
+ def debug_child_activation_start: (String mutant_id) -> void
323
+ def debug_child_activation_end: (untyped activation_result, test_files: Array[String]) -> void
324
+ def debug_child_mutant_meta: (Mutant) -> void
325
+ def debug_child_activation_check: () -> void
326
+ def debug_child_example_count: (String) -> void
327
+ def loaded_feature_map: (Array[String]) -> Array[[String, bool]]
328
+ def loaded_feature?: (String) -> bool
329
+ def rspec_world_example_count: () -> Integer?
330
+ def run_child_activation_and_tests: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
331
+ def with_non_interactive_stdin: () { () -> untyped } -> untyped
274
332
  def subprocess_env: () -> Hash[String, String]
275
333
  def scenario_log_support: () -> ScenarioLogSupport
276
334
  def with_subprocess_env: () { () -> untyped } -> untyped
@@ -300,9 +358,25 @@ module Henitai
300
358
  def stderr_stream: () -> IO
301
359
  end
302
360
 
361
+ module SchedulerDiagnostics
362
+ def self.enabled?: () -> bool
363
+ def self.child_started: (Integer?) -> void
364
+ def self.child_ended: (Integer?) -> void
365
+ def self.summary: () -> Hash[Symbol, untyped]
366
+ def self.reset!: () -> void
367
+ end
368
+
369
+ class ChildHandle
370
+ attr_accessor pid: Integer?
371
+ attr_accessor log_paths: Hash[Symbol, String]
372
+
373
+ def initialize: (pid: Integer?, log_paths: Hash[Symbol, String]) -> void
374
+ end
375
+
303
376
  class RspecProcessRunner
304
377
  def run_mutant: (Rspec, mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
305
378
  def run_suite: (Rspec, Array[String], timeout: Float) -> ScenarioExecutionResult
379
+ def spawn_mutant: (Rspec, mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> ChildHandle
306
380
  end
307
381
 
308
382
  class Rspec < Base
@@ -312,13 +386,21 @@ module Henitai
312
386
  def select_tests: (Subject) -> Array[String]
313
387
  def test_files: () -> Array[String]
314
388
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
389
+ def spawn_mutant: (mutant: Mutant, test_files: Array[String]) -> ChildHandle
315
390
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
316
391
  def per_test_coverage_supported?: () -> bool
317
392
 
318
393
  private
319
394
 
395
+ def mutant_log_name: (Mutant) -> String
320
396
  def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
397
+ def suppress_simplecov!: () -> void
398
+ def suppress_coverage!: () -> void
399
+ def install_debug_timeout_trap: () -> void
400
+ def debug_child_thread_dump: (String) -> void
401
+ def debug_child_example_count: (String) -> void
321
402
  def suite_command: (Array[String]) -> Array[String]
403
+ def rspec_suite_runner_script: () -> String
322
404
  def build_result: (untyped, Hash[Symbol, String]) -> ScenarioExecutionResult
323
405
  def scenario_status: (untyped) -> Symbol
324
406
  def exit_status_for: (untyped) -> Integer?
@@ -339,17 +421,29 @@ module Henitai
339
421
  def requires_source_file_transitively?: (String, String, ?Array[String]) -> bool
340
422
  def required_files: (String) -> Array[String]
341
423
  def resolve_required_file: (String, String, String) -> String?
424
+ def loaded_feature_map: (Array[String]) -> Array[[String, bool]]
425
+ def loaded_feature?: (String) -> bool
342
426
  def relative_candidates: (String, String) -> Array[String]
343
427
  def require_candidates: (String, String) -> Array[String]
344
428
  def expand_candidates: (String, String) -> Array[String]
345
429
  end
346
430
 
431
+ module CoverageRuntimeSuppressors
432
+ def self.suppress_simplecov!: () -> void
433
+ def self.suppress_coverage!: () -> void
434
+ end
435
+
347
436
  module SimpleCovStartSuppressor
348
437
  def start: (*untyped) -> nil
349
438
  end
350
439
 
440
+ module CoverageStartSuppressor
441
+ def start: (*untyped) -> nil
442
+ end
443
+
351
444
  class Minitest < Rspec
352
445
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
446
+ def spawn_mutant: (mutant: Mutant, test_files: Array[String]) -> ChildHandle
353
447
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
354
448
 
355
449
  private
@@ -358,6 +452,7 @@ module Henitai
358
452
  def run_tests: (Array[String]) -> Integer
359
453
  def preload_environment: () -> void
360
454
  def setup_load_path: () -> void
455
+ def suppress_minitest_autorun!: () -> nil
361
456
  def suppress_simplecov!: () -> void
362
457
  def subprocess_env: () -> Hash[String, String]
363
458
  def cleanup_suite_process: (Integer?, untyped) -> void
@@ -372,15 +467,21 @@ module Henitai
372
467
 
373
468
  class GitDiffAnalyzer
374
469
  def changed_files: (from: String, to: String, ?dir: String) -> Array[String]
470
+ def working_tree_changed_files: (?dir: String) -> Array[String]
471
+ def head_sha: (?dir: String) -> String?
375
472
  end
376
473
 
377
474
  class MutantGenerator
378
475
  def generate: (Array[Subject], Array[untyped], ?config: untyped) -> Array[Mutant]
379
476
  end
380
477
 
478
+ module MutantIdentity
479
+ def self.stable_id: (Mutant) -> String
480
+ end
481
+
381
482
  class MutantHistoryStore
382
483
  def initialize: (path: String) -> void
383
- def record: (Result, version: String, ?recorded_at: Time) -> void
484
+ def record: (untyped, version: String, ?recorded_at: Time) -> void
384
485
  def trend_report: () -> Hash[Symbol, untyped]
385
486
  end
386
487
 
@@ -420,6 +521,16 @@ module Henitai
420
521
  def run: (Array[Mutant], untyped, untyped, untyped, ?Hash[Symbol, untyped]) -> void
421
522
  end
422
523
 
524
+ class ProcessWorkerRunner
525
+ SCHEDULER_POLL_INTERVAL: Float
526
+ PROCESS_DRAIN_WINDOW: Float
527
+
528
+ Slot: untyped
529
+
530
+ def initialize: (worker_count: Integer) -> void
531
+ def run: (Array[Mutant], untyped, untyped, untyped?, ?Hash[Symbol, untyped]) -> Array[ScenarioExecutionResult]
532
+ end
533
+
423
534
  class StaticFilter
424
535
  def initialize: (?coverage_report_reader: CoverageReportReader) -> void
425
536
  def apply: (Array[Mutant], untyped) -> Array[Mutant]
@@ -441,9 +552,7 @@ module Henitai
441
552
  def parallel_execution?: (untyped, Array[Mutant]) -> bool
442
553
  def worker_count: (untyped) -> Integer
443
554
  def run_linear: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
444
- def run_parallel: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
445
- def pipe_stdin?: () -> bool
446
- def start_stdin_watcher: () { () -> void } -> Thread
555
+ def run_parallel: (Array[Mutant], untyped, untyped, untyped?) -> void
447
556
  def process_mutant: (Mutant, untyped, untyped, untyped?, ?untyped) -> void
448
557
  def prioritized_tests_for: (Mutant, untyped, untyped) -> Array[String]
449
558
  def test_prioritizer: () -> TestPrioritizer
@@ -457,6 +566,7 @@ module Henitai
457
566
  SERIALIZER_METHODS: Hash[Symbol, Symbol]
458
567
 
459
568
  def self.activate!: (untyped) -> Symbol?
569
+ def self.activation_source_for: (Mutant) -> String?
460
570
  def initialize: () -> void
461
571
  def activate!: (untyped) -> Symbol?
462
572
 
@@ -512,7 +622,10 @@ module Henitai
512
622
  private
513
623
 
514
624
  def report_lines: (Result) -> Array[String]
515
- def summary_lines: (Result) -> Array[String]
625
+ def summary_lines: (untyped) -> Array[String]
626
+ def full_summary_lines: (untyped) -> Array[String]
627
+ def partial_summary_lines: (untyped) -> Array[String]
628
+ def append_survivor_stats: (Array[String], untyped) -> void
516
629
  def survived_detail_lines: (Result) -> Array[String]
517
630
  def survived_mutant_lines: (Mutant) -> Array[String]
518
631
  def survived_mutant_header: (Mutant) -> String
@@ -536,6 +649,13 @@ module Henitai
536
649
 
537
650
  private
538
651
 
652
+ def write_canonical: (Hash[Symbol, untyped]) -> void
653
+ def write_session_snapshot: (Hash[Symbol, untyped]) -> void
654
+ def write_activation_recipes: (untyped) -> void
655
+ def survived_mutants_for: (untyped) -> Array[Mutant]
656
+ def session_snapshot_path: (String) -> String
657
+ def session_recipe_path: (String) -> String
658
+ def canonical_path: () -> String
539
659
  def report_path: () -> String
540
660
  def write_history_report: () -> void
541
661
  def history_store_path: () -> String
@@ -595,8 +715,12 @@ module Henitai
595
715
  attr_reader started_at: Time
596
716
  attr_reader finished_at: Time
597
717
  attr_reader thresholds: Hash[Symbol, Integer]
718
+ attr_reader survivor_stats: Hash[Symbol, untyped]?
719
+ attr_reader session_id: String
720
+ attr_reader git_sha: String?
598
721
 
599
- def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?) -> void
722
+ def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?, ?partial_rerun: bool, ?survivor_stats: Hash[Symbol, untyped]?, ?session_id: String, ?git_sha: String?) -> void
723
+ def partial_rerun?: () -> bool
600
724
  def killed: () -> Integer
601
725
  def survived: () -> Integer
602
726
  def equivalent: () -> Integer
@@ -609,6 +733,8 @@ module Henitai
609
733
 
610
734
  private
611
735
 
736
+ def base_schema: () -> Hash[Symbol, untyped]
737
+ def unmatched_survivor_ids: () -> Array[String]
612
738
  def build_files_section: () -> Hash[Symbol, untyped]
613
739
  def mutant_to_schema: (Mutant) -> Hash[Symbol, untyped]
614
740
  def coverage_schema: (Mutant) -> Hash[Symbol, untyped]
@@ -620,11 +746,56 @@ module Henitai
620
746
  def stryker_status: (Symbol) -> String
621
747
  end
622
748
 
749
+ class SurvivorLoader
750
+ FileNotFoundError: singleton(StandardError)
751
+ InvalidReportError: singleton(StandardError)
752
+ ScopeMismatchError: singleton(StandardError)
753
+
754
+ class Report
755
+ attr_reader survivor_ids: Array[String]
756
+ attr_reader coverage_map: Hash[String, Array[String]]
757
+ attr_reader git_sha: String?
758
+
759
+ def initialize: (survivor_ids: Array[String], coverage_map: Hash[String, Array[String]], git_sha: String?) -> void
760
+ end
761
+
762
+ def initialize: (String, ?include_paths: Array[String]) -> void
763
+ def load: () -> Report
764
+ end
765
+
766
+ class SurvivorTestFilter
767
+ def initialize: (
768
+ coverage_map: Hash[String, Array[String]],
769
+ git_sha: String?,
770
+ ?dirty_source_files: bool,
771
+ ?worktree_changed_files: Array[String],
772
+ ?diff_analyzer: GitDiffAnalyzer
773
+ ) -> void
774
+ def apply: (Array[Mutant]) -> Hash[Symbol, Array[Mutant]]
775
+ end
776
+
777
+ class SurvivorSelector
778
+ DRIFT_THRESHOLD: Float
779
+
780
+ def initialize: (survivor_ids: Array[String]) -> void
781
+ def select: (Array[Mutant]) -> Array[Mutant]
782
+ def unmatched_ids: () -> Array[String]
783
+ def drift_warning?: () -> bool
784
+ end
785
+
786
+ class SurvivorActivationCache
787
+ FILENAME: String
788
+
789
+ def self.compute: (Array[Mutant]) -> Hash[String, Hash[String, untyped]]
790
+ def self.load: (String) -> Hash[String, Hash[String, untyped]]?
791
+ def self.write: (String, Hash[String, Hash[String, untyped]]) -> void
792
+ end
793
+
623
794
  class Runner
624
795
  attr_reader config: Configuration
625
796
  attr_reader result: untyped
626
797
 
627
- def initialize: (?config: Configuration, ?subjects: Array[Subject], ?since: String) -> void
798
+ def initialize: (?config: Configuration, ?subjects: Array[Subject], ?since: String, ?survivors_from: String?) -> void
628
799
  def run: () -> Result
629
800
  def resolve_subjects: (?Array[String]) -> untyped
630
801
  def generate_mutants: (untyped) -> untyped
@@ -656,6 +827,26 @@ module Henitai
656
827
  def mutants_for: (Array[Subject], Array[String]) -> Array[Mutant]
657
828
  def with_reports_dir: () { () -> untyped } -> untyped
658
829
  def result_thresholds: () -> Hash[Symbol, Integer]?
830
+ def survivor_rerun?: () -> bool
831
+ def apply_survivor_selection: (Array[Mutant]) -> Array[Mutant]
832
+ def try_recipe_run: () -> Array[Mutant]?
833
+ def load_survivor_report: () -> SurvivorLoader::Report
834
+ def run_from_recipes: (SurvivorLoader::Report, Array[String]?) -> Array[Mutant]?
835
+ def recipe_fast_path_safe?: (SurvivorLoader::Report, Array[String]?) -> bool
836
+ def recipe_selector_and_stubs: (Array[String], Hash[String, Hash[String, untyped]]) -> [SurvivorSelector, Array[Mutant]]
837
+ def load_activation_recipes: (Array[String]) -> Hash[String, Hash[String, untyped]]?
838
+ def build_stub_mutant: (String, Hash[String, untyped]) -> Mutant
839
+ def stub_subject_from_recipe: (Hash[String, untyped]) -> Subject
840
+ def recipe_location: (Hash[String, untyped]?) -> Hash[Symbol, untyped]
841
+ def finalize_survivor_split: (SurvivorSelector, Array[Mutant], Hash[Symbol, Array[Mutant]]) -> Array[Mutant]
842
+ def dirty_worktree_changed_files: () -> Array[String]?
843
+ def dirty_source_files?: (Array[String]?, ?git_sha: String?) -> bool
844
+ def committed_changed_files: (String?) -> Array[String]
845
+ def in_include_root?: (String, Array[String]) -> bool
846
+ def warn_survivor_drift: (SurvivorSelector) -> void
847
+ def build_survivor_stats: (SurvivorSelector, Array[Mutant], Hash[Symbol, Array[Mutant]]) -> Hash[Symbol, untyped]
848
+ def test_filter: (SurvivorLoader::Report, ?dirty_source_files: bool) -> SurvivorTestFilter
849
+ def safe_head_sha: () -> String?
659
850
  end
660
851
 
661
852
  class CoverageBootstrapper