henitai 0.1.2 → 0.1.4

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/README.md +3 -1
  4. data/assets/schema/henitai.schema.json +0 -4
  5. data/lib/henitai/arid_node_filter.rb +3 -0
  6. data/lib/henitai/available_cpu_count.rb +79 -0
  7. data/lib/henitai/cli.rb +7 -4
  8. data/lib/henitai/configuration.rb +3 -5
  9. data/lib/henitai/configuration_validator.rb +1 -11
  10. data/lib/henitai/coverage_bootstrapper.rb +128 -10
  11. data/lib/henitai/coverage_report_reader.rb +67 -0
  12. data/lib/henitai/eager_load.rb +11 -0
  13. data/lib/henitai/equivalence_detector.rb +60 -1
  14. data/lib/henitai/execution_engine.rb +34 -22
  15. data/lib/henitai/integration/rspec_process_runner.rb +58 -0
  16. data/lib/henitai/integration.rb +195 -110
  17. data/lib/henitai/mutant.rb +3 -1
  18. data/lib/henitai/mutant_generator.rb +25 -48
  19. data/lib/henitai/operator.rb +6 -1
  20. data/lib/henitai/operators/assignment_expression.rb +7 -23
  21. data/lib/henitai/operators/conditional_expression.rb +1 -7
  22. data/lib/henitai/operators/method_chain_unwrap.rb +41 -0
  23. data/lib/henitai/operators/regex_mutator.rb +89 -0
  24. data/lib/henitai/operators/string_literal.rb +2 -1
  25. data/lib/henitai/operators/unary_operator.rb +36 -0
  26. data/lib/henitai/operators/update_operator.rb +70 -0
  27. data/lib/henitai/operators.rb +4 -0
  28. data/lib/henitai/parallel_execution_runner.rb +135 -0
  29. data/lib/henitai/per_test_coverage_selector.rb +60 -0
  30. data/lib/henitai/reporter.rb +14 -2
  31. data/lib/henitai/result.rb +16 -4
  32. data/lib/henitai/runner.rb +75 -11
  33. data/lib/henitai/scenario_execution_result.rb +31 -2
  34. data/lib/henitai/source_parser.rb +12 -1
  35. data/lib/henitai/static_filter.rb +20 -41
  36. data/lib/henitai/version.rb +1 -1
  37. data/lib/henitai.rb +3 -0
  38. data/sig/henitai.rbs +66 -10
  39. metadata +17 -4
@@ -11,13 +11,15 @@ module Henitai
11
11
  include UnparseHelper
12
12
 
13
13
  SCHEMA_VERSION = "1.0"
14
+ DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
14
15
 
15
- attr_reader :mutants, :started_at, :finished_at
16
+ attr_reader :mutants, :started_at, :finished_at, :thresholds
16
17
 
17
- def initialize(mutants:, started_at:, finished_at:)
18
+ def initialize(mutants:, started_at:, finished_at:, thresholds: nil)
18
19
  @mutants = mutants
19
20
  @started_at = started_at
20
21
  @finished_at = finished_at
22
+ @thresholds = DEFAULT_THRESHOLDS.merge(thresholds || {})
21
23
  end
22
24
 
23
25
  # @return [Integer] number of killed mutants
@@ -88,7 +90,7 @@ module Henitai
88
90
  def to_stryker_schema
89
91
  {
90
92
  schemaVersion: SCHEMA_VERSION,
91
- thresholds: { high: 80, low: 60 },
93
+ thresholds: thresholds,
92
94
  files: build_files_section
93
95
  }
94
96
  end
@@ -119,7 +121,17 @@ module Henitai
119
121
  status: stryker_status(mutant.status),
120
122
  description: mutant.description,
121
123
  duration: duration_for(mutant)
122
- }.compact
124
+ }.compact.merge(coverage_schema(mutant))
125
+ end
126
+
127
+ def coverage_schema(mutant)
128
+ covered_by = Array(mutant.covered_by).compact
129
+ return {} if covered_by.empty?
130
+
131
+ {
132
+ coveredBy: covered_by,
133
+ testsCompleted: mutant.tests_completed || covered_by.size
134
+ }
123
135
  end
124
136
 
125
137
  def replacement_for(mutant)
@@ -35,18 +35,24 @@ module Henitai
35
35
  end
36
36
 
37
37
  # Entry point — runs the full pipeline and returns a Result.
38
+ #
39
+ # Coverage bootstrap (Gate 0) runs in a background thread so that Gate 1
40
+ # (subject resolution) and Gate 2 (mutant generation) proceed concurrently.
41
+ # The thread is joined before Gate 3 (static filtering), which is the first
42
+ # phase that requires coverage data.
43
+ #
44
+ # For targeted runs (`subjects:` provided), the bootstrap is further scoped
45
+ # to the spec files that cover the requested subjects rather than the full
46
+ # suite, reducing the baseline run time proportionally.
47
+ #
38
48
  # @return [Result]
39
49
  def run
40
50
  started_at = Time.now
41
51
  source_files = self.source_files
42
- bootstrap_coverage(source_files)
43
52
  subjects = resolve_subjects(source_files)
44
- mutants = generate_mutants(subjects)
45
- mutants = filter_mutants(mutants)
46
- mutants = execute_mutants(mutants)
47
- finished_at = Time.now
53
+ mutants = execute_mutants(mutants_for(subjects, source_files))
48
54
 
49
- build_result(mutants, started_at, finished_at)
55
+ build_result(mutants, started_at, Time.now)
50
56
  end
51
57
 
52
58
  private
@@ -69,6 +75,29 @@ module Henitai
69
75
  static_filter.apply(mutants, config)
70
76
  end
71
77
 
78
+ def mutants_for(subjects, source_files)
79
+ bootstrap_thread = bootstrap_mutants(source_files, subjects)
80
+ mutants = generate_mutants(subjects)
81
+ bootstrap_thread.value
82
+
83
+ filtered_mutants = filter_mutants(mutants)
84
+ return filtered_mutants unless targeted_run?
85
+
86
+ refresh_coverage_for_targeted_run(filtered_mutants, source_files)
87
+ end
88
+
89
+ def refresh_coverage_for_targeted_run(mutants, source_files)
90
+ return mutants unless retry_full_bootstrap?(mutants)
91
+
92
+ bootstrap_coverage(source_files)
93
+ filter_mutants(mutants)
94
+ end
95
+
96
+ def bootstrap_mutants(source_files, subjects)
97
+ scoped_tests = scoped_bootstrap_test_files(subjects)
98
+ Thread.new { bootstrap_coverage(source_files, scoped_tests) }
99
+ end
100
+
72
101
  def execute_mutants(mutants)
73
102
  execution_engine.run(
74
103
  mutants,
@@ -94,13 +123,33 @@ module Henitai
94
123
  @result = Result.new(
95
124
  mutants:,
96
125
  started_at:,
97
- finished_at:
126
+ finished_at:,
127
+ thresholds: result_thresholds
98
128
  )
99
129
  persist_history(@result, finished_at)
100
130
  report(@result)
101
131
  @result
102
132
  end
103
133
 
134
+ # Returns the spec files to use for the coverage bootstrap.
135
+ #
136
+ # For full runs (no subject pattern given), returns nil so the bootstrapper
137
+ # falls back to the integration's full test-file list.
138
+ #
139
+ # For targeted runs, returns the union of test files selected for each
140
+ # resolved subject. Falls back to nil (all tests) if the selection is empty,
141
+ # so the bootstrapper always has a non-empty file list.
142
+ def scoped_bootstrap_test_files(subjects)
143
+ return nil if pattern_subjects.empty?
144
+
145
+ files = subjects.flat_map { |subject| integration.select_tests(subject) }.uniq
146
+ files.empty? ? nil : files
147
+ end
148
+
149
+ def bootstrap_coverage(source_files, test_files = nil)
150
+ coverage_bootstrapper.ensure!(source_files:, config:, integration:, test_files:)
151
+ end
152
+
104
153
  def subject_resolver
105
154
  @subject_resolver ||= SubjectResolver.new
106
155
  end
@@ -125,10 +174,6 @@ module Henitai
125
174
  @coverage_bootstrapper ||= CoverageBootstrapper.new
126
175
  end
127
176
 
128
- def bootstrap_coverage(source_files)
129
- coverage_bootstrapper.ensure!(source_files:, config:, integration:)
130
- end
131
-
132
177
  def integration
133
178
  @integration ||= Integration.for(config.integration).new
134
179
  end
@@ -172,6 +217,19 @@ module Henitai
172
217
  Array(@subjects)
173
218
  end
174
219
 
220
+ def targeted_run?
221
+ !pattern_subjects.empty?
222
+ end
223
+
224
+ def retry_full_bootstrap?(mutants)
225
+ executable_mutants = Array(mutants).reject do |mutant|
226
+ %i[ignored compile_error equivalent].include?(mutant.status)
227
+ end
228
+ return false if executable_mutants.empty?
229
+
230
+ executable_mutants.all? { |mutant| mutant.status == :no_coverage }
231
+ end
232
+
175
233
  def unique_subjects(subjects)
176
234
  subjects.uniq { |subject| [subject.expression, subject.source_file] }
177
235
  end
@@ -179,5 +237,11 @@ module Henitai
179
237
  def normalize_path(path)
180
238
  File.expand_path(path)
181
239
  end
240
+
241
+ def result_thresholds
242
+ return nil unless config.respond_to?(:thresholds)
243
+
244
+ config.thresholds
245
+ end
182
246
  end
183
247
  end
@@ -5,6 +5,16 @@ module Henitai
5
5
  class ScenarioExecutionResult
6
6
  attr_reader :status, :stdout, :stderr, :exit_status, :log_path
7
7
 
8
+ def self.build(wait_result:, stdout:, stderr:, log_path:)
9
+ new(
10
+ status: status_for(wait_result),
11
+ stdout: stdout,
12
+ stderr: stderr,
13
+ log_path: log_path,
14
+ exit_status: exit_status_for(wait_result)
15
+ )
16
+ end
17
+
8
18
  def initialize(status:, stdout:, stderr:, log_path:, exit_status: nil)
9
19
  @status = status
10
20
  @stdout = stdout.to_s
@@ -51,11 +61,11 @@ module Henitai
51
61
  log_text.lines.last(lines).join
52
62
  end
53
63
 
54
- def should_show_logs?(all_logs: false)
64
+ def should_show_logs?(all_logs: nil)
55
65
  all_logs || timeout?
56
66
  end
57
67
 
58
- def failure_tail(all_logs: false, lines: 12)
68
+ def failure_tail(all_logs: nil, lines: 12)
59
69
  return combined_output if all_logs
60
70
  return "" unless should_show_logs?(all_logs:)
61
71
 
@@ -64,6 +74,25 @@ module Henitai
64
74
 
65
75
  private
66
76
 
77
+ class << self
78
+ private
79
+
80
+ def status_for(wait_result)
81
+ return :timeout if wait_result == :timeout
82
+ return :compile_error if exit_status_for(wait_result) == 2
83
+ return :survived if wait_result.respond_to?(:success?) && wait_result.success?
84
+
85
+ :killed
86
+ end
87
+
88
+ def exit_status_for(wait_result)
89
+ return nil if wait_result == :timeout
90
+ return nil unless wait_result.respond_to?(:exitstatus)
91
+
92
+ wait_result.exitstatus
93
+ end
94
+ end
95
+
67
96
  def stream_section(name, content)
68
97
  "#{name}:\n#{content}"
69
98
  end
@@ -12,12 +12,23 @@ module Henitai
12
12
  class SourceParser
13
13
  DEFAULT_PATH = "(string)"
14
14
 
15
+ @cache = {}
16
+
15
17
  def self.parse(source, path: DEFAULT_PATH)
16
18
  new.parse(source, path:)
17
19
  end
18
20
 
21
+ # Returns the parsed AST for +path+, re-using a cached result when the
22
+ # file's mtime has not changed. This avoids parsing the same file twice
23
+ # across pipeline phases (e.g. SubjectResolver then MutantGenerator).
19
24
  def self.parse_file(path)
20
- new.parse_file(path)
25
+ key = [path, File.mtime(path)]
26
+ @cache[key] ||= new.parse_file(path)
27
+ end
28
+
29
+ # Clears the parse cache. Intended for test isolation.
30
+ def self.clear_cache!
31
+ @cache.clear
21
32
  end
22
33
 
23
34
  def parse(source, path: DEFAULT_PATH)
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require_relative "coverage_report_reader"
4
4
 
5
5
  module Henitai
6
6
  # Applies static, pre-execution filtering to generated mutants.
7
7
  class StaticFilter
8
- DEFAULT_COVERAGE_REPORT_PATH = "coverage/.resultset.json"
9
- DEFAULT_PER_TEST_COVERAGE_REPORT_PATH = "coverage/henitai_per_test.json"
8
+ DEFAULT_COVERAGE_REPORT_PATH = CoverageReportReader::DEFAULT_COVERAGE_REPORT_PATH
9
+ DEFAULT_PER_TEST_COVERAGE_REPORT_PATH = CoverageReportReader::DEFAULT_PER_TEST_COVERAGE_REPORT_PATH
10
+
11
+ def initialize(coverage_report_reader: CoverageReportReader.new)
12
+ @coverage_report_reader = coverage_report_reader
13
+ end
10
14
 
11
15
  # This method is the gate-level filter orchestrator.
12
16
  def apply(mutants, config)
@@ -41,31 +45,17 @@ module Henitai
41
45
  end
42
46
 
43
47
  def coverage_lines_by_file(path = DEFAULT_COVERAGE_REPORT_PATH)
44
- return {} unless File.exist?(path)
45
-
46
- coverage = Hash.new { |hash, key| hash[key] = [] }
47
- JSON.parse(File.read(path)).each_value do |result|
48
- result.fetch("coverage", {}).each do |file, file_coverage|
49
- coverage[normalize_path(file)].concat(covered_lines(file_coverage))
50
- end
51
- end
52
-
53
- coverage.transform_values(&:uniq).transform_values(&:sort)
48
+ coverage_report_reader.coverage_lines_by_file(path)
54
49
  end
55
50
 
56
51
  def test_lines_by_file(path = DEFAULT_PER_TEST_COVERAGE_REPORT_PATH)
57
- return {} unless File.exist?(path)
58
-
59
- parsed = JSON.parse(File.read(path))
60
- return {} unless parsed.is_a?(Hash)
61
-
62
- parsed.transform_values do |coverage|
63
- normalize_test_coverage(coverage)
64
- end
52
+ coverage_report_reader.test_lines_by_file(path)
65
53
  end
66
54
 
67
55
  private
68
56
 
57
+ attr_reader :coverage_report_reader
58
+
69
59
  def ignored?(mutant, config)
70
60
  source = source_for(mutant)
71
61
  return false unless source
@@ -151,23 +141,6 @@ module Henitai
151
141
  (m.captures.first.to_i..m.captures.last.to_i)
152
142
  end
153
143
 
154
- def covered_lines(file_coverage)
155
- Array(file_coverage["lines"]).each_with_index.filter_map do |count, index|
156
- index + 1 if count.to_i.positive?
157
- end
158
- end
159
-
160
- def normalize_test_coverage(coverage)
161
- case coverage
162
- when Hash
163
- coverage.transform_values do |lines|
164
- Array(lines).grep(Integer).uniq.sort
165
- end
166
- else
167
- Array(coverage).grep(Integer).uniq.sort
168
- end
169
- end
170
-
171
144
  def coverage_lines_from_test_lines(test_lines)
172
145
  coverage = Hash.new { |hash, key| hash[key] = [] }
173
146
 
@@ -183,10 +156,16 @@ module Henitai
183
156
  end
184
157
 
185
158
  def normalize_path(path)
159
+ @normalize_path_cache ||= {}
160
+ return @normalize_path_cache[path] if @normalize_path_cache.key?(path)
161
+
186
162
  expanded = File.expand_path(path)
187
- File.realpath(expanded)
188
- rescue Errno::ENOENT, Errno::ENOTDIR
189
- expanded
163
+ resolved = begin
164
+ File.realpath(expanded)
165
+ rescue Errno::ENOENT, Errno::ENOTDIR
166
+ expanded
167
+ end
168
+ @normalize_path_cache[path] = resolved
190
169
  end
191
170
 
192
171
  def equivalence_detector
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/henitai.rb CHANGED
@@ -22,6 +22,8 @@ module Henitai
22
22
 
23
23
  autoload :Configuration, "henitai/configuration"
24
24
  autoload :CoverageBootstrapper, "henitai/coverage_bootstrapper"
25
+ autoload :CoverageReportReader, "henitai/coverage_report_reader"
26
+ autoload :PerTestCoverageSelector, "henitai/per_test_coverage_selector"
25
27
  autoload :Subject, "henitai/subject"
26
28
  autoload :Mutant, "henitai/mutant"
27
29
  autoload :Operator, "henitai/operator"
@@ -33,6 +35,7 @@ module Henitai
33
35
  autoload :MutantGenerator, "henitai/mutant_generator"
34
36
  autoload :MutantHistoryStore, "henitai/mutant_history_store"
35
37
  autoload :AridNodeFilter, "henitai/arid_node_filter"
38
+ autoload :AvailableCpuCount, "henitai/available_cpu_count"
36
39
  autoload :EquivalenceDetector, "henitai/equivalence_detector"
37
40
  autoload :StaticFilter, "henitai/static_filter"
38
41
  autoload :StillbornFilter, "henitai/stillborn_filter"
data/sig/henitai.rbs CHANGED
@@ -112,6 +112,7 @@ module Henitai
112
112
  attr_reader exit_status: Integer?
113
113
  attr_reader log_path: String
114
114
 
115
+ def self.build: (wait_result: untyped, stdout: String?, stderr: String?, log_path: String) -> ScenarioExecutionResult
115
116
  def initialize: (status: Symbol, stdout: String?, stderr: String?, log_path: String, ?exit_status: Integer?) -> void
116
117
  def survived?: () -> bool
117
118
  def killed?: () -> bool
@@ -121,6 +122,11 @@ module Henitai
121
122
  def tail: (?Integer) -> String
122
123
  def should_show_logs?: (?all_logs: bool) -> bool
123
124
  def failure_tail: (?all_logs: bool, ?lines: Integer) -> String
125
+
126
+ private
127
+
128
+ def self.status_for: (untyped) -> Symbol
129
+ def self.exit_status_for: (untyped) -> Integer?
124
130
  end
125
131
 
126
132
  class Subject
@@ -154,6 +160,8 @@ module Henitai
154
160
  attr_accessor status: Symbol
155
161
  attr_accessor killing_test: String?
156
162
  attr_accessor duration: Float?
163
+ attr_accessor covered_by: Array[String]?
164
+ attr_accessor tests_completed: Integer?
157
165
 
158
166
  def initialize: (subject: Subject, operator: String, nodes: Hash[Symbol, untyped], description: String, location: Hash[Symbol, untyped]) -> void
159
167
  def killed?: () -> bool
@@ -223,15 +231,30 @@ module Henitai
223
231
  def test_files: () -> Array[String]
224
232
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
225
233
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
234
+ def per_test_coverage_supported?: () -> bool
235
+ def wait_with_timeout: (Integer, Float) -> untyped
236
+ def reap_child: (Integer) -> void
237
+ def cleanup_process_group: (Integer) -> void
226
238
 
227
239
  private
228
240
 
229
241
  def pause: (Float) -> void
242
+ def handle_timeout: (Integer) -> Symbol
243
+ def cleanup_child_process: (Integer) -> void
244
+ def rspec_options: () -> Array[String]
245
+ def subprocess_env: () -> Hash[String, String]
246
+ def scenario_log_support: () -> ScenarioLogSupport
247
+ def with_subprocess_env: () { () -> untyped } -> untyped
248
+ def restore_subprocess_env: (Hash[String, String?]) -> void
249
+ def spawn_suite_process: (Array[String], Hash[Symbol, String]) -> Integer
230
250
  end
231
251
 
232
252
  class ScenarioLogSupport
233
253
  def capture_child_output: (Hash[Symbol, String]) { () -> untyped } -> untyped
234
254
  def with_coverage_dir: (String) { () -> untyped } -> untyped
255
+ def read_log_file: (String) -> String
256
+ def write_combined_log: (String, String, String) -> void
257
+ def combined_log: (String, String) -> String
235
258
 
236
259
  private
237
260
 
@@ -248,6 +271,11 @@ module Henitai
248
271
  def stderr_stream: () -> IO
249
272
  end
250
273
 
274
+ class RspecProcessRunner
275
+ def run_mutant: (Rspec, mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
276
+ def run_suite: (Rspec, Array[String], timeout: Float) -> ScenarioExecutionResult
277
+ end
278
+
251
279
  class Rspec < Base
252
280
  REQUIRE_DIRECTIVE_PATTERN: Regexp
253
281
  DEFAULT_SUITE_TIMEOUT: Float
@@ -256,13 +284,12 @@ module Henitai
256
284
  def test_files: () -> Array[String]
257
285
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
258
286
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
287
+ def per_test_coverage_supported?: () -> bool
259
288
 
260
289
  private
261
290
 
262
291
  def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
263
292
  def suite_command: (Array[String]) -> Array[String]
264
- def wait_with_timeout: (Integer, Float) -> untyped
265
- def handle_timeout: (Integer) -> Symbol
266
293
  def build_result: (untyped, Hash[Symbol, String]) -> ScenarioExecutionResult
267
294
  def scenario_status: (untyped) -> Symbol
268
295
  def exit_status_for: (untyped) -> Integer?
@@ -271,8 +298,7 @@ module Henitai
271
298
  def combined_log: (String, String) -> String
272
299
  def scenario_log_paths: (String) -> Hash[Symbol, String]
273
300
  def run_tests: (Array[String]) -> Integer
274
- def scenario_log_support: () -> ScenarioLogSupport
275
- def rspec_options: () -> Array[String]
301
+ def with_subprocess_env: () { () -> untyped } -> untyped
276
302
  def spec_files: () -> Array[String]
277
303
  def fallback_spec_files: (Subject) -> Array[String]
278
304
  def selection_patterns: (Subject) -> Array[String]
@@ -283,7 +309,6 @@ module Henitai
283
309
  def relative_candidates: (String, String) -> Array[String]
284
310
  def require_candidates: (String, String) -> Array[String]
285
311
  def expand_candidates: (String, String) -> Array[String]
286
- def reap_child: (Integer) -> void
287
312
  end
288
313
 
289
314
  class Minitest < Rspec
@@ -293,11 +318,11 @@ module Henitai
293
318
  private
294
319
 
295
320
  def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
296
- def suite_command: (Array[String]) -> Array[String]
297
321
  def run_tests: (Array[String]) -> Integer
298
322
  def preload_environment: () -> void
299
323
  def setup_load_path: () -> void
300
324
  def subprocess_env: () -> Hash[String, String]
325
+ def cleanup_suite_process: (Integer?, untyped) -> void
301
326
  def spec_files: () -> Array[String]
302
327
  end
303
328
  end
@@ -347,13 +372,29 @@ module Henitai
347
372
  def fallback_source: (untyped) -> String
348
373
  end
349
374
 
375
+ class CoverageReportReader
376
+ def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
377
+ def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
378
+ end
379
+
380
+ class ParallelExecutionRunner
381
+ def initialize: (worker_count: Integer) -> void
382
+ def run: (Array[Mutant], untyped, untyped, untyped, ?Hash[Symbol, untyped]) -> void
383
+ end
384
+
350
385
  class StaticFilter
386
+ def initialize: (?coverage_report_reader: CoverageReportReader) -> void
351
387
  def apply: (Array[Mutant], untyped) -> Array[Mutant]
352
388
  def coverage_lines_for: (untyped) -> Hash[String, Array[Integer]]
353
389
  def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
354
390
  def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
355
391
  end
356
392
 
393
+ class PerTestCoverageSelector
394
+ def initialize: (?coverage_report_reader: CoverageReportReader) -> void
395
+ def filter: (Array[String], Mutant, reports_dir: String) -> Array[String]
396
+ end
397
+
357
398
  class ExecutionEngine
358
399
  def run: (Array[Mutant], untyped, untyped, ?progress_reporter: untyped) -> Array[Mutant]
359
400
 
@@ -363,6 +404,8 @@ module Henitai
363
404
  def worker_count: (untyped) -> Integer
364
405
  def run_linear: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
365
406
  def run_parallel: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
407
+ def pipe_stdin?: () -> bool
408
+ def start_stdin_watcher: () { () -> void } -> Thread
366
409
  def process_mutant: (Mutant, untyped, untyped, untyped?, ?untyped) -> void
367
410
  def prioritized_tests_for: (Mutant, untyped, untyped) -> Array[String]
368
411
  def test_prioritizer: () -> TestPrioritizer
@@ -437,6 +480,7 @@ module Henitai
437
480
  def survived_mutant_header: (Mutant) -> String
438
481
  def original_line: (Mutant) -> String
439
482
  def mutated_line: (Mutant) -> String
483
+ def display_unparse: (untyped) -> String
440
484
  def score_line: (Result) -> String
441
485
  def format_row: (String, untyped) -> String
442
486
  def count_status: (Result, Symbol) -> Integer
@@ -507,12 +551,14 @@ module Henitai
507
551
  include UnparseHelper
508
552
 
509
553
  SCHEMA_VERSION: String
554
+ DEFAULT_THRESHOLDS: Hash[Symbol, Integer]
510
555
 
511
556
  attr_reader mutants: Array[Mutant]
512
557
  attr_reader started_at: Time
513
558
  attr_reader finished_at: Time
559
+ attr_reader thresholds: Hash[Symbol, Integer]
514
560
 
515
- def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time) -> void
561
+ def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?) -> void
516
562
  def killed: () -> Integer
517
563
  def survived: () -> Integer
518
564
  def equivalent: () -> Integer
@@ -527,6 +573,7 @@ module Henitai
527
573
 
528
574
  def build_files_section: () -> Hash[Symbol, untyped]
529
575
  def mutant_to_schema: (Mutant) -> Hash[Symbol, untyped]
576
+ def coverage_schema: (Mutant) -> Hash[Symbol, untyped]
530
577
  def replacement_for: (Mutant) -> String
531
578
  def location_for: (Mutant) -> Hash[Symbol, untyped]
532
579
  def line_column: (Mutant, Symbol) -> Hash[Symbol, Integer]
@@ -566,19 +613,28 @@ module Henitai
566
613
 
567
614
  def build_result: (Array[Mutant], Time, Time) -> Result
568
615
  def persist_history: (Result, Time) -> void
569
- def bootstrap_coverage: (Array[String]) -> void
616
+ def refresh_coverage_for_targeted_run: (Array[Mutant], Array[String]) -> Array[Mutant]
617
+ def bootstrap_coverage: (Array[String], ?Array[String]?) -> void
618
+ def bootstrap_mutants: (Array[String], Array[Subject]) -> Thread
619
+ def mutants_for: (Array[Subject], Array[String]) -> Array[Mutant]
620
+ def scoped_bootstrap_test_files: (Array[Subject]) -> Array[String]?
621
+ def targeted_run?: () -> bool
622
+ def retry_full_bootstrap?: (Array[Mutant]) -> bool
570
623
  def with_reports_dir: () { () -> untyped } -> untyped
624
+ def result_thresholds: () -> Hash[Symbol, Integer]?
571
625
  end
572
626
 
573
627
  class CoverageBootstrapper
574
628
  def initialize: (?static_filter: StaticFilter) -> void
575
- def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base) -> void
629
+ def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base, ?test_files: Array[String]?) -> void
576
630
 
577
631
  private
578
632
 
579
633
  def static_filter: () -> StaticFilter
580
634
  def coverage_available?: (Array[String], Configuration) -> bool
581
- def bootstrap_coverage: (Integration::Base, Configuration) -> void
635
+ def coverage_fresh?: (Array[String], Configuration, Integration::Base, Array[String]?) -> bool
636
+ def coverage_report_path: (Configuration) -> String
637
+ def bootstrap_coverage: (Integration::Base, Configuration, ?Array[String]?) -> void
582
638
  def source_file_paths: (Array[String]) -> Array[String]
583
639
  end
584
640
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: henitai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Otten
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-04-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: prism
@@ -88,15 +89,19 @@ files:
88
89
  - exe/henitai
89
90
  - lib/henitai.rb
90
91
  - lib/henitai/arid_node_filter.rb
92
+ - lib/henitai/available_cpu_count.rb
91
93
  - lib/henitai/cli.rb
92
94
  - lib/henitai/configuration.rb
93
95
  - lib/henitai/configuration_validator.rb
94
96
  - lib/henitai/coverage_bootstrapper.rb
95
97
  - lib/henitai/coverage_formatter.rb
98
+ - lib/henitai/coverage_report_reader.rb
99
+ - lib/henitai/eager_load.rb
96
100
  - lib/henitai/equivalence_detector.rb
97
101
  - lib/henitai/execution_engine.rb
98
102
  - lib/henitai/git_diff_analyzer.rb
99
103
  - lib/henitai/integration.rb
104
+ - lib/henitai/integration/rspec_process_runner.rb
100
105
  - lib/henitai/minitest_simplecov.rb
101
106
  - lib/henitai/mutant.rb
102
107
  - lib/henitai/mutant/activator.rb
@@ -113,13 +118,19 @@ files:
113
118
  - lib/henitai/operators/equality_operator.rb
114
119
  - lib/henitai/operators/hash_literal.rb
115
120
  - lib/henitai/operators/logical_operator.rb
121
+ - lib/henitai/operators/method_chain_unwrap.rb
116
122
  - lib/henitai/operators/method_expression.rb
117
123
  - lib/henitai/operators/pattern_match.rb
118
124
  - lib/henitai/operators/range_literal.rb
125
+ - lib/henitai/operators/regex_mutator.rb
119
126
  - lib/henitai/operators/return_value.rb
120
127
  - lib/henitai/operators/safe_navigation.rb
121
128
  - lib/henitai/operators/string_literal.rb
129
+ - lib/henitai/operators/unary_operator.rb
130
+ - lib/henitai/operators/update_operator.rb
131
+ - lib/henitai/parallel_execution_runner.rb
122
132
  - lib/henitai/parser_current.rb
133
+ - lib/henitai/per_test_coverage_selector.rb
123
134
  - lib/henitai/reporter.rb
124
135
  - lib/henitai/result.rb
125
136
  - lib/henitai/rspec_coverage_formatter.rb
@@ -147,8 +158,9 @@ metadata:
147
158
  changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
148
159
  documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
149
160
  homepage_uri: https://github.com/martinotten/henitai
150
- source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.2
161
+ source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.4
151
162
  rubygems_mfa_required: 'true'
163
+ post_install_message:
152
164
  rdoc_options: []
153
165
  require_paths:
154
166
  - lib
@@ -163,7 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
175
  - !ruby/object:Gem::Version
164
176
  version: '0'
165
177
  requirements: []
166
- rubygems_version: 4.0.6
178
+ rubygems_version: 3.3.5
179
+ signing_key:
167
180
  specification_version: 4
168
181
  summary: Mutation testing for Ruby
169
182
  test_files: []