henitai 0.1.2 → 0.1.3

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -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 +112 -7
  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 +192 -90
  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/unary_operator.rb +36 -0
  25. data/lib/henitai/operators/update_operator.rb +70 -0
  26. data/lib/henitai/operators.rb +4 -0
  27. data/lib/henitai/parallel_execution_runner.rb +135 -0
  28. data/lib/henitai/per_test_coverage_selector.rb +60 -0
  29. data/lib/henitai/result.rb +16 -4
  30. data/lib/henitai/runner.rb +75 -11
  31. data/lib/henitai/source_parser.rb +12 -1
  32. data/lib/henitai/static_filter.rb +20 -41
  33. data/lib/henitai/version.rb +1 -1
  34. data/lib/henitai.rb +3 -0
  35. data/sig/henitai.rbs +59 -10
  36. metadata +16 -3
@@ -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.3"
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
@@ -154,6 +154,8 @@ module Henitai
154
154
  attr_accessor status: Symbol
155
155
  attr_accessor killing_test: String?
156
156
  attr_accessor duration: Float?
157
+ attr_accessor covered_by: Array[String]?
158
+ attr_accessor tests_completed: Integer?
157
159
 
158
160
  def initialize: (subject: Subject, operator: String, nodes: Hash[Symbol, untyped], description: String, location: Hash[Symbol, untyped]) -> void
159
161
  def killed?: () -> bool
@@ -223,15 +225,30 @@ module Henitai
223
225
  def test_files: () -> Array[String]
224
226
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
225
227
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
228
+ def per_test_coverage_supported?: () -> bool
229
+ def wait_with_timeout: (Integer, Float) -> untyped
230
+ def reap_child: (Integer) -> void
231
+ def cleanup_process_group: (Integer) -> void
226
232
 
227
233
  private
228
234
 
229
235
  def pause: (Float) -> void
236
+ def handle_timeout: (Integer) -> Symbol
237
+ def cleanup_child_process: (Integer) -> void
238
+ def rspec_options: () -> Array[String]
239
+ def subprocess_env: () -> Hash[String, String]
240
+ def scenario_log_support: () -> ScenarioLogSupport
241
+ def with_subprocess_env: () { () -> untyped } -> untyped
242
+ def restore_subprocess_env: (Hash[String, String?]) -> void
243
+ def spawn_suite_process: (Array[String], Hash[Symbol, String]) -> Integer
230
244
  end
231
245
 
232
246
  class ScenarioLogSupport
233
247
  def capture_child_output: (Hash[Symbol, String]) { () -> untyped } -> untyped
234
248
  def with_coverage_dir: (String) { () -> untyped } -> untyped
249
+ def read_log_file: (String) -> String
250
+ def write_combined_log: (String, String, String) -> void
251
+ def combined_log: (String, String) -> String
235
252
 
236
253
  private
237
254
 
@@ -248,6 +265,11 @@ module Henitai
248
265
  def stderr_stream: () -> IO
249
266
  end
250
267
 
268
+ class RspecProcessRunner
269
+ def run_mutant: (Rspec, mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
270
+ def run_suite: (Rspec, Array[String], timeout: Float) -> ScenarioExecutionResult
271
+ end
272
+
251
273
  class Rspec < Base
252
274
  REQUIRE_DIRECTIVE_PATTERN: Regexp
253
275
  DEFAULT_SUITE_TIMEOUT: Float
@@ -256,13 +278,12 @@ module Henitai
256
278
  def test_files: () -> Array[String]
257
279
  def run_mutant: (mutant: Mutant, test_files: Array[String], timeout: Float) -> ScenarioExecutionResult
258
280
  def run_suite: (Array[String], ?timeout: Float) -> ScenarioExecutionResult
281
+ def per_test_coverage_supported?: () -> bool
259
282
 
260
283
  private
261
284
 
262
285
  def run_in_child: (mutant: Mutant, test_files: Array[String], log_paths: Hash[Symbol, String]) -> Integer
263
286
  def suite_command: (Array[String]) -> Array[String]
264
- def wait_with_timeout: (Integer, Float) -> untyped
265
- def handle_timeout: (Integer) -> Symbol
266
287
  def build_result: (untyped, Hash[Symbol, String]) -> ScenarioExecutionResult
267
288
  def scenario_status: (untyped) -> Symbol
268
289
  def exit_status_for: (untyped) -> Integer?
@@ -271,8 +292,7 @@ module Henitai
271
292
  def combined_log: (String, String) -> String
272
293
  def scenario_log_paths: (String) -> Hash[Symbol, String]
273
294
  def run_tests: (Array[String]) -> Integer
274
- def scenario_log_support: () -> ScenarioLogSupport
275
- def rspec_options: () -> Array[String]
295
+ def with_subprocess_env: () { () -> untyped } -> untyped
276
296
  def spec_files: () -> Array[String]
277
297
  def fallback_spec_files: (Subject) -> Array[String]
278
298
  def selection_patterns: (Subject) -> Array[String]
@@ -283,7 +303,6 @@ module Henitai
283
303
  def relative_candidates: (String, String) -> Array[String]
284
304
  def require_candidates: (String, String) -> Array[String]
285
305
  def expand_candidates: (String, String) -> Array[String]
286
- def reap_child: (Integer) -> void
287
306
  end
288
307
 
289
308
  class Minitest < Rspec
@@ -293,11 +312,11 @@ module Henitai
293
312
  private
294
313
 
295
314
  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
315
  def run_tests: (Array[String]) -> Integer
298
316
  def preload_environment: () -> void
299
317
  def setup_load_path: () -> void
300
318
  def subprocess_env: () -> Hash[String, String]
319
+ def cleanup_suite_process: (Integer?, untyped) -> void
301
320
  def spec_files: () -> Array[String]
302
321
  end
303
322
  end
@@ -347,13 +366,29 @@ module Henitai
347
366
  def fallback_source: (untyped) -> String
348
367
  end
349
368
 
369
+ class CoverageReportReader
370
+ def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
371
+ def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
372
+ end
373
+
374
+ class ParallelExecutionRunner
375
+ def initialize: (worker_count: Integer) -> void
376
+ def run: (Array[Mutant], untyped, untyped, untyped, ?Hash[Symbol, untyped]) -> void
377
+ end
378
+
350
379
  class StaticFilter
380
+ def initialize: (?coverage_report_reader: CoverageReportReader) -> void
351
381
  def apply: (Array[Mutant], untyped) -> Array[Mutant]
352
382
  def coverage_lines_for: (untyped) -> Hash[String, Array[Integer]]
353
383
  def coverage_lines_by_file: (?String) -> Hash[String, Array[Integer]]
354
384
  def test_lines_by_file: (?String) -> Hash[String, Hash[String, Array[Integer]]]
355
385
  end
356
386
 
387
+ class PerTestCoverageSelector
388
+ def initialize: (?coverage_report_reader: CoverageReportReader) -> void
389
+ def filter: (Array[String], Mutant, reports_dir: String) -> Array[String]
390
+ end
391
+
357
392
  class ExecutionEngine
358
393
  def run: (Array[Mutant], untyped, untyped, ?progress_reporter: untyped) -> Array[Mutant]
359
394
 
@@ -363,6 +398,8 @@ module Henitai
363
398
  def worker_count: (untyped) -> Integer
364
399
  def run_linear: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
365
400
  def run_parallel: (Array[Mutant], untyped, untyped, untyped?, untyped) -> void
401
+ def pipe_stdin?: () -> bool
402
+ def start_stdin_watcher: () { () -> void } -> Thread
366
403
  def process_mutant: (Mutant, untyped, untyped, untyped?, ?untyped) -> void
367
404
  def prioritized_tests_for: (Mutant, untyped, untyped) -> Array[String]
368
405
  def test_prioritizer: () -> TestPrioritizer
@@ -507,12 +544,14 @@ module Henitai
507
544
  include UnparseHelper
508
545
 
509
546
  SCHEMA_VERSION: String
547
+ DEFAULT_THRESHOLDS: Hash[Symbol, Integer]
510
548
 
511
549
  attr_reader mutants: Array[Mutant]
512
550
  attr_reader started_at: Time
513
551
  attr_reader finished_at: Time
552
+ attr_reader thresholds: Hash[Symbol, Integer]
514
553
 
515
- def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time) -> void
554
+ def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?) -> void
516
555
  def killed: () -> Integer
517
556
  def survived: () -> Integer
518
557
  def equivalent: () -> Integer
@@ -527,6 +566,7 @@ module Henitai
527
566
 
528
567
  def build_files_section: () -> Hash[Symbol, untyped]
529
568
  def mutant_to_schema: (Mutant) -> Hash[Symbol, untyped]
569
+ def coverage_schema: (Mutant) -> Hash[Symbol, untyped]
530
570
  def replacement_for: (Mutant) -> String
531
571
  def location_for: (Mutant) -> Hash[Symbol, untyped]
532
572
  def line_column: (Mutant, Symbol) -> Hash[Symbol, Integer]
@@ -566,19 +606,28 @@ module Henitai
566
606
 
567
607
  def build_result: (Array[Mutant], Time, Time) -> Result
568
608
  def persist_history: (Result, Time) -> void
569
- def bootstrap_coverage: (Array[String]) -> void
609
+ def refresh_coverage_for_targeted_run: (Array[Mutant], Array[String]) -> Array[Mutant]
610
+ def bootstrap_coverage: (Array[String], ?Array[String]?) -> void
611
+ def bootstrap_mutants: (Array[String], Array[Subject]) -> Thread
612
+ def mutants_for: (Array[Subject], Array[String]) -> Array[Mutant]
613
+ def scoped_bootstrap_test_files: (Array[Subject]) -> Array[String]?
614
+ def targeted_run?: () -> bool
615
+ def retry_full_bootstrap?: (Array[Mutant]) -> bool
570
616
  def with_reports_dir: () { () -> untyped } -> untyped
617
+ def result_thresholds: () -> Hash[Symbol, Integer]?
571
618
  end
572
619
 
573
620
  class CoverageBootstrapper
574
621
  def initialize: (?static_filter: StaticFilter) -> void
575
- def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base) -> void
622
+ def ensure!: (source_files: Array[String], config: Configuration, integration: Integration::Base, ?test_files: Array[String]?) -> void
576
623
 
577
624
  private
578
625
 
579
626
  def static_filter: () -> StaticFilter
580
627
  def coverage_available?: (Array[String], Configuration) -> bool
581
- def bootstrap_coverage: (Integration::Base, Configuration) -> void
628
+ def coverage_fresh?: (Array[String], Configuration, Integration::Base, Array[String]?) -> bool
629
+ def coverage_report_path: (Configuration) -> String
630
+ def bootstrap_coverage: (Integration::Base, Configuration, ?Array[String]?) -> void
582
631
  def source_file_paths: (Array[String]) -> Array[String]
583
632
  end
584
633
 
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.3
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-13 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
@@ -149,6 +160,7 @@ metadata:
149
160
  homepage_uri: https://github.com/martinotten/henitai
150
161
  source_code_uri: https://github.com/martinotten/henitai/tree/v0.1.2
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: []