henitai 0.2.0 → 0.3.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/README.md +138 -5
  4. data/assets/schema/henitai.schema.json +41 -0
  5. data/lib/henitai/available_cpu_count.rb +16 -23
  6. data/lib/henitai/canonical_report_merger.rb +107 -0
  7. data/lib/henitai/canonical_report_writer.rb +22 -0
  8. data/lib/henitai/checkpoint_reporter.rb +79 -0
  9. data/lib/henitai/cli/clean_command.rb +54 -0
  10. data/lib/henitai/cli/command_support.rb +52 -0
  11. data/lib/henitai/cli/init_command.rb +64 -0
  12. data/lib/henitai/cli/operator_command.rb +96 -0
  13. data/lib/henitai/cli/options.rb +84 -0
  14. data/lib/henitai/cli/run_command.rb +136 -0
  15. data/lib/henitai/cli/run_options.rb +108 -0
  16. data/lib/henitai/cli.rb +27 -407
  17. data/lib/henitai/composite_progress_reporter.rb +42 -0
  18. data/lib/henitai/configuration.rb +42 -9
  19. data/lib/henitai/configuration_validator/rules.rb +161 -0
  20. data/lib/henitai/configuration_validator/scalars.rb +169 -0
  21. data/lib/henitai/configuration_validator.rb +20 -240
  22. data/lib/henitai/coverage_bootstrapper.rb +54 -5
  23. data/lib/henitai/coverage_formatter.rb +5 -1
  24. data/lib/henitai/coverage_report_reader.rb +28 -5
  25. data/lib/henitai/eager_load.rb +36 -5
  26. data/lib/henitai/execution_engine/env_scope.rb +67 -0
  27. data/lib/henitai/execution_engine.rb +115 -53
  28. data/lib/henitai/generated_artifacts.rb +58 -0
  29. data/lib/henitai/incremental_filter.rb +100 -0
  30. data/lib/henitai/integration/base.rb +171 -0
  31. data/lib/henitai/integration/child_debug_support.rb +119 -0
  32. data/lib/henitai/integration/child_runtime_control.rb +50 -0
  33. data/lib/henitai/integration/coverage_suppression.rb +52 -0
  34. data/lib/henitai/integration/minitest.rb +101 -0
  35. data/lib/henitai/integration/minitest_load_path.rb +14 -0
  36. data/lib/henitai/integration/minitest_suite_command.rb +17 -0
  37. data/lib/henitai/integration/minitest_test_runner.rb +39 -0
  38. data/lib/henitai/integration/mutant_run_support.rb +77 -0
  39. data/lib/henitai/integration/rails_environment_preloader.rb +14 -0
  40. data/lib/henitai/integration/rspec_child_runner.rb +61 -0
  41. data/lib/henitai/integration/rspec_test_selection.rb +138 -0
  42. data/lib/henitai/integration/scenario_log_support.rb +160 -0
  43. data/lib/henitai/integration.rb +22 -846
  44. data/lib/henitai/minitest_coverage_reporter.rb +4 -1
  45. data/lib/henitai/mutant/activator.rb +15 -79
  46. data/lib/henitai/mutant/parameter_source.rb +98 -0
  47. data/lib/henitai/mutant.rb +14 -6
  48. data/lib/henitai/mutant_history_store/sql.rb +90 -0
  49. data/lib/henitai/mutant_history_store/verdict_cache.rb +84 -0
  50. data/lib/henitai/mutant_history_store.rb +67 -81
  51. data/lib/henitai/mutant_identity.rb +33 -2
  52. data/lib/henitai/mutation_skip_directives.rb +227 -0
  53. data/lib/henitai/operator.rb +3 -1
  54. data/lib/henitai/operators/equality_identity_operator.rb +51 -0
  55. data/lib/henitai/operators/equality_operator.rb +8 -3
  56. data/lib/henitai/operators.rb +1 -0
  57. data/lib/henitai/per_test_coverage.rb +66 -0
  58. data/lib/henitai/per_test_coverage_collector.rb +23 -6
  59. data/lib/henitai/per_test_coverage_selector.rb +11 -33
  60. data/lib/henitai/process_worker_runner.rb +48 -334
  61. data/lib/henitai/reporter/dashboard_metadata_provider.rb +113 -0
  62. data/lib/henitai/reporter.rb +258 -125
  63. data/lib/henitai/reports_directory_lock.rb +76 -0
  64. data/lib/henitai/result.rb +88 -23
  65. data/lib/henitai/runner.rb +137 -218
  66. data/lib/henitai/scenario_execution_result.rb +12 -0
  67. data/lib/henitai/slot_scheduler/draining.rb +146 -0
  68. data/lib/henitai/slot_scheduler/process_control.rb +43 -0
  69. data/lib/henitai/slot_scheduler.rb +272 -0
  70. data/lib/henitai/static_filter.rb +16 -6
  71. data/lib/henitai/survivor_rerun_strategy.rb +195 -0
  72. data/lib/henitai/survivor_test_filter.rb +1 -1
  73. data/lib/henitai/test_prioritizer.rb +28 -2
  74. data/lib/henitai/timeout_calibrator.rb +44 -0
  75. data/lib/henitai/unparse_helper.rb +5 -2
  76. data/lib/henitai/verdict_fingerprint.rb +155 -0
  77. data/lib/henitai/version.rb +1 -1
  78. data/lib/henitai.rb +16 -1
  79. data/sig/configuration_validator.rbs +46 -22
  80. data/sig/henitai.rbs +386 -96
  81. metadata +45 -3
  82. data/lib/henitai/parallel_execution_runner.rb +0 -153
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Survivor-rerun fast path for {Runner}.
5
+ #
6
+ # When +--survivors-from+ is given, this collaborator loads the prior report
7
+ # and either:
8
+ #
9
+ # * builds stub Mutants directly from +activation-recipes.json+ (the recipe
10
+ # fast path, bypassing source parsing and mutant generation), or
11
+ # * filters a freshly generated mutant list down to the prior survivors.
12
+ #
13
+ # In both cases it records {#survivor_stats} (matched/unmatched counts and the
14
+ # drift warning) for the Runner to attach to its Result.
15
+ class SurvivorRerunStrategy
16
+ attr_reader :survivor_stats
17
+
18
+ def initialize(survivors_from:, config:, git_diff_analyzer:)
19
+ @survivors_from = survivors_from
20
+ @config = config
21
+ @git_diff_analyzer = git_diff_analyzer
22
+ @survivor_stats = nil
23
+ end
24
+
25
+ def active?
26
+ !@survivors_from.nil?
27
+ end
28
+
29
+ # Attempts to run survivors directly from pre-computed activation recipes,
30
+ # bypassing source parsing and mutant generation entirely.
31
+ # Returns the mutant array on success, or nil if recipes are unavailable.
32
+ def try_recipe_run
33
+ dirty_worktree_files = dirty_worktree_changed_files
34
+ loaded = load_survivor_report
35
+ return nil unless recipe_fast_path_safe?(loaded, dirty_worktree_files)
36
+
37
+ run_from_recipes(loaded, dirty_worktree_files)
38
+ end
39
+
40
+ def apply_selection(mutants)
41
+ dirty_worktree_files = dirty_worktree_changed_files
42
+ loaded = load_survivor_report
43
+ selector = SurvivorSelector.new(survivor_ids: loaded.survivor_ids)
44
+ selected = selector.select(mutants)
45
+ finalize_survivor_split(
46
+ selector,
47
+ selected,
48
+ test_filter(
49
+ loaded,
50
+ dirty_source_files: dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha)
51
+ ).apply(selected)
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def load_survivor_report
58
+ SurvivorLoader.new(@survivors_from, include_paths: Array(@config.includes)).load
59
+ end
60
+
61
+ def run_from_recipes(loaded, dirty_worktree_files)
62
+ recipes = load_activation_recipes(loaded.survivor_ids)
63
+ return nil if recipes.nil?
64
+
65
+ selector, stubs = recipe_selector_and_stubs(loaded.survivor_ids, recipes)
66
+ split = test_filter(
67
+ loaded,
68
+ dirty_source_files: dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha)
69
+ ).apply(stubs)
70
+ finalize_survivor_split(selector, stubs, split)
71
+ end
72
+
73
+ def recipe_fast_path_safe?(loaded, dirty_worktree_files)
74
+ !dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha)
75
+ end
76
+
77
+ # Builds stub Mutants from recipes and a SurvivorSelector primed with the
78
+ # survivor ID set. The selector is given a synthetic #select call so that
79
+ # #drift_warning? / #unmatched_ids are available (all IDs will be matched).
80
+ def recipe_selector_and_stubs(survivor_ids, recipes)
81
+ stubs = survivor_ids.map { |id| build_stub_mutant(id, recipes[id]) }
82
+ selector = SurvivorSelector.new(survivor_ids:)
83
+ selector.select(stubs)
84
+ [selector, stubs]
85
+ end
86
+
87
+ # Returns the recipe hash if the file exists and covers every survivor ID;
88
+ # otherwise returns nil to trigger the normal generation path.
89
+ def load_activation_recipes(survivor_ids)
90
+ path = File.join(File.dirname(@survivors_from), SurvivorActivationCache::FILENAME)
91
+ recipes = SurvivorActivationCache.load(path)
92
+ return nil if recipes.nil?
93
+ return nil unless survivor_ids.all? { |id| recipes.key?(id) }
94
+
95
+ recipes
96
+ end
97
+
98
+ def build_stub_mutant(stable_id, recipe)
99
+ mutant = Mutant.new(
100
+ subject: stub_subject_from_recipe(recipe),
101
+ operator: recipe.fetch("operator"),
102
+ nodes: { original: nil, mutated: nil },
103
+ description: recipe.fetch("description"),
104
+ location: recipe_location(recipe["location"]),
105
+ precomputed_stable_id: stable_id,
106
+ precomputed_activation_source: recipe.fetch("activationSource")
107
+ )
108
+ mutant.covered_by = recipe["coveredBy"]
109
+ mutant
110
+ end
111
+
112
+ def stub_subject_from_recipe(recipe)
113
+ Subject.new(
114
+ namespace: recipe["namespace"],
115
+ method_name: recipe["methodName"],
116
+ method_type: (recipe["methodType"] || "instance").to_sym,
117
+ source_location: { file: recipe["sourceFile"], range: nil }
118
+ )
119
+ end
120
+
121
+ def recipe_location(loc)
122
+ return {} unless loc.is_a?(Hash)
123
+
124
+ {
125
+ file: loc["file"],
126
+ start_line: loc["startLine"],
127
+ end_line: loc["endLine"],
128
+ start_col: loc["startCol"],
129
+ end_col: loc["endCol"]
130
+ }.compact
131
+ end
132
+
133
+ def finalize_survivor_split(selector, selected, split)
134
+ split[:stable].each { |m| m.status = :survived }
135
+ warn_survivor_drift(selector) if selector.drift_warning?
136
+ @survivor_stats = build_survivor_stats(selector, selected, split)
137
+ split[:stable] + split[:pending]
138
+ end
139
+
140
+ def test_filter(loaded, dirty_source_files:)
141
+ SurvivorTestFilter.new(
142
+ coverage_map: loaded.coverage_map,
143
+ git_sha: loaded.git_sha,
144
+ dirty_source_files:,
145
+ worktree_changed_files: Array(dirty_worktree_changed_files),
146
+ diff_analyzer: @git_diff_analyzer
147
+ )
148
+ end
149
+
150
+ def dirty_worktree_changed_files
151
+ @dirty_worktree_changed_files ||= @git_diff_analyzer.working_tree_changed_files
152
+ rescue StandardError
153
+ nil
154
+ end
155
+
156
+ def dirty_source_files?(dirty_worktree_files, git_sha: nil)
157
+ return true if dirty_worktree_files.nil?
158
+
159
+ all_changed = dirty_worktree_files + committed_changed_files(git_sha)
160
+ include_roots = Array(@config.includes).map { |path| normalize_path(path) }
161
+ all_changed.any? { |path| in_include_root?(normalize_path(path), include_roots) }
162
+ rescue StandardError
163
+ true
164
+ end
165
+
166
+ def committed_changed_files(git_sha)
167
+ return [] unless git_sha
168
+
169
+ @git_diff_analyzer.changed_files(from: git_sha, to: "HEAD")
170
+ end
171
+
172
+ def in_include_root?(path, include_roots)
173
+ include_roots.any? { |root| path == root || path.start_with?("#{root}/") }
174
+ end
175
+
176
+ def normalize_path(path)
177
+ File.expand_path(path)
178
+ end
179
+
180
+ def warn_survivor_drift(selector)
181
+ warn "henitai: WARNING: #{selector.unmatched_ids.size} prior survivors " \
182
+ "could not be matched; the source may have drifted - consider a full run"
183
+ end
184
+
185
+ def build_survivor_stats(selector, selected, split)
186
+ {
187
+ matched: selected.size,
188
+ unmatched_count: selector.unmatched_ids.size,
189
+ unmatched_ids: selector.unmatched_ids,
190
+ skipped_count: split[:stable].size,
191
+ drift_warning: selector.drift_warning?
192
+ }
193
+ end
194
+ end
195
+ end
@@ -57,7 +57,7 @@ module Henitai
57
57
  covering = @coverage_map[mutant.stable_id]
58
58
  return false if covering.nil? || covering.empty?
59
59
 
60
- covering.none? { |test_file| changed.include?(test_file) }
60
+ !changed.intersect?(covering)
61
61
  end
62
62
 
63
63
  # Returns a Set of changed test file paths, or nil on any git error
@@ -2,15 +2,41 @@
2
2
 
3
3
  module Henitai
4
4
  # Orders test files so previously effective tests run first.
5
+ #
6
+ # Primary key: kill-history count (a test that killed neighbouring mutants
7
+ # probably kills this one). Tiebreaker: measured per-test-file runtime,
8
+ # ascending, so the first kill costs as little wall-clock as possible.
9
+ # Untimed tests sort after timed ones by original inventory order; with no
10
+ # timing source at all the ordering is exactly the historical behavior.
5
11
  class TestPrioritizer
12
+ # @param timing_source [#call, nil] returns a Hash of test file path to
13
+ # wall-clock seconds; resolved lazily on first sort.
14
+ def initialize(timing_source: nil)
15
+ @timing_source = timing_source
16
+ end
17
+
6
18
  def sort(tests, _mutant, history)
7
19
  Array(tests).each_with_index.sort_by do |test, index|
8
- [-history_count(history, test), index]
20
+ [-history_count(history, test), runtime(test), index]
9
21
  end.map(&:first)
10
22
  end
11
23
 
12
24
  private
13
25
 
26
+ def runtime(test)
27
+ durations = timing_durations
28
+ history_key_candidates(test).each do |key|
29
+ value = durations[key]
30
+ return value.to_f unless value.nil?
31
+ end
32
+
33
+ Float::INFINITY
34
+ end
35
+
36
+ def timing_durations
37
+ @timing_durations ||= @timing_source&.call || {}
38
+ end
39
+
14
40
  def history_count(history, test)
15
41
  return 0 unless history.respond_to?(:fetch)
16
42
 
@@ -45,7 +71,7 @@ module Henitai
45
71
 
46
72
  def relative_history_key(path)
47
73
  pathname = Pathname.new(path)
48
- return unless pathname.absolute?
74
+ return path unless pathname.absolute?
49
75
 
50
76
  pathname.relative_path_from(Pathname.pwd).to_s
51
77
  rescue StandardError
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Derives a per-mutant timeout from measured per-test-file durations.
5
+ #
6
+ # The calibrated value is `multiplier × sum(durations of the selected test
7
+ # files)`, clamped to a floor so a near-instant baseline doesn't flag normal
8
+ # jitter as a timeout. Returns nil — caller falls back to the static
9
+ # default — when any selected test lacks timing data, so a partially
10
+ # measured run is treated as uncalibratable rather than under-estimated.
11
+ class TimeoutCalibrator
12
+ FLOOR_SECONDS = 2.0
13
+
14
+ # @param timing_source [#call] returns a Hash of test file path to
15
+ # wall-clock seconds; resolved lazily on first use.
16
+ # @param multiplier [Numeric]
17
+ def initialize(timing_source:, multiplier:)
18
+ @timing_source = timing_source
19
+ @multiplier = multiplier
20
+ end
21
+
22
+ # @param test_files [Array<String>]
23
+ # @return [Float, nil]
24
+ def timeout_for(test_files)
25
+ files = Array(test_files)
26
+ return nil if files.empty? || durations.empty?
27
+
28
+ baselines = files.map { |file| durations[normalize(file)] }
29
+ return nil if baselines.any?(&:nil?)
30
+
31
+ [@multiplier * baselines.sum, FLOOR_SECONDS].max
32
+ end
33
+
34
+ private
35
+
36
+ def durations
37
+ @durations ||= (@timing_source.call || {}).transform_keys { |key| normalize(key) }
38
+ end
39
+
40
+ def normalize(path)
41
+ File.expand_path(path.to_s)
42
+ end
43
+ end
44
+ end
@@ -9,8 +9,11 @@ module Henitai
9
9
 
10
10
  def safe_unparse(node)
11
11
  Unparser.unparse(node)
12
- rescue StandardError
13
- # Unparser does not support all AST node types, so fall back gracefully.
12
+ rescue Unparser::UnknownNodeError, Unparser::InvalidNodeError,
13
+ Unparser::UnsupportedNodeError, EncodingError
14
+ # Unparser does not support all AST node types, and some mutated string
15
+ # segments carry incompatible encodings; fall back gracefully for those.
16
+ # Other failures (e.g. NoMethodError) are bugs and must surface.
14
17
  fallback_source(node)
15
18
  end
16
19
 
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "find"
5
+ require "json"
6
+
7
+ require_relative "generated_artifacts"
8
+
9
+ module Henitai
10
+ # Content fingerprints that decide whether a stored verdict is still
11
+ # trustworthy: the subject's source (method body) and the covering test
12
+ # files must both be byte-identical to what was recorded. Survived verdicts
13
+ # additionally record the full-map covering-set membership plus a run-level
14
+ # dependency fingerprint (ADR-11). Any read failure yields nil / a mismatch
15
+ # — conservative, never reuse on doubt.
16
+ module VerdictFingerprint
17
+ # Files that influence test behavior but never appear in any covering
18
+ # set: helpers, support code, fixtures/factories, lockfile and tool
19
+ # config. One combined hash over all of them gates survivor reuse.
20
+ DEPENDENCY_GLOBS = %w[
21
+ spec/spec_helper.rb
22
+ spec/rails_helper.rb
23
+ spec/support/**/*
24
+ spec/fixtures/**/*
25
+ spec/factories/**/*
26
+ test/test_helper.rb
27
+ test/support/**/*
28
+ test/fixtures/**/*
29
+ test/factories/**/*
30
+ Gemfile.lock
31
+ .henitai.yml
32
+ .rspec
33
+ ].freeze
34
+
35
+ module_function
36
+
37
+ # SHA256 of the subject's source lines. nil when the subject has no
38
+ # source range or the file cannot be read.
39
+ def subject_source_hash(mutant)
40
+ subject = mutant.subject
41
+ return nil unless subject.respond_to?(:source_file) && subject.respond_to?(:source_range)
42
+
43
+ range = subject.source_range
44
+ return nil unless range
45
+
46
+ lines = File.readlines(subject.source_file)
47
+ Digest::SHA256.hexdigest(lines[(range.begin - 1)..(range.end - 1)].to_a.join)
48
+ rescue SystemCallError, IOError
49
+ nil
50
+ end
51
+
52
+ # JSON fingerprint of the covering test files: their sorted paths plus a
53
+ # combined content hash. nil when no tests are known or any is unreadable.
54
+ def tests_fingerprint(test_files)
55
+ paths = Array(test_files).map(&:to_s).sort
56
+ return nil if paths.empty?
57
+
58
+ sha = combined_content_sha(paths)
59
+ return nil unless sha
60
+
61
+ JSON.generate("paths" => paths, "sha" => sha)
62
+ end
63
+
64
+ # True when every recorded test file still exists with identical content.
65
+ def tests_fingerprint_current?(fingerprint_json)
66
+ return false if fingerprint_json.nil?
67
+
68
+ fingerprint = JSON.parse(fingerprint_json)
69
+ combined_content_sha(fingerprint.fetch("paths")) == fingerprint.fetch("sha")
70
+ rescue StandardError
71
+ false
72
+ end
73
+
74
+ # Existing dependency files resolved against +root+, sorted. Shared by
75
+ # the fingerprint below and by the coverage freshness watch — a stale
76
+ # per-test map after a dependency edit would let the test selector omit
77
+ # a newly covering test. Uses Find with pruning instead of Dir.glob so
78
+ # generated subtrees (thousands of mutation-log files under fixture
79
+ # projects) are never walked at all.
80
+ def dependency_files(root = Dir.pwd)
81
+ files = DEPENDENCY_GLOBS.flat_map do |pattern|
82
+ base = File.join(root, pattern)
83
+ pattern.include?("*") ? pruned_tree_files(File.dirname(base.sub("/**/*", "/x"))) : [base]
84
+ end
85
+ files.select { |path| File.file?(path) }.uniq.sort
86
+ end
87
+
88
+ def pruned_tree_files(dir)
89
+ return [] unless File.directory?(dir)
90
+
91
+ collected = [] # : Array[String]
92
+ Find.find(dir) do |path|
93
+ Find.prune if File.directory?(path) && GeneratedArtifacts.generated_dir?(path)
94
+ collected << path if File.file?(path)
95
+ end
96
+ collected
97
+ end
98
+
99
+ # Run-level combined SHA over the dependency file set. Missing files are
100
+ # simply excluded (not an error); a read failure yields nil — no reuse.
101
+ def dependency_fingerprint(root = Dir.pwd) = combined_content_sha(dependency_files(root))
102
+
103
+ # Fingerprint recorded for a Survived verdict: the sorted full-map
104
+ # intersection set (paths + combined content sha) plus the run-level
105
+ # dependency sha. nil when any input is unavailable — the verdict is
106
+ # then never reusable.
107
+ def survivor_tests_fingerprint(test_files, dependency_sha:)
108
+ return nil if dependency_sha.nil?
109
+
110
+ paths = Array(test_files).map(&:to_s).sort
111
+ return nil if paths.empty?
112
+
113
+ sha = combined_content_sha(paths)
114
+ return nil unless sha
115
+
116
+ JSON.generate("paths" => paths, "sha" => sha, "dependencies" => dependency_sha)
117
+ end
118
+
119
+ # True when the recorded covering set still equals the live one in
120
+ # membership AND content, and the dependency fingerprint is unchanged.
121
+ # Any parse/read failure or missing field resolves to stale.
122
+ def survivor_fingerprint_current?(fingerprint_json, live_paths:, dependency_sha:)
123
+ return false if fingerprint_json.nil? || dependency_sha.nil?
124
+
125
+ survivor_fingerprint_matches?(JSON.parse(fingerprint_json), live_paths, dependency_sha)
126
+ rescue StandardError
127
+ false
128
+ end
129
+
130
+ def survivor_fingerprint_matches?(fingerprint, live_paths, dependency_sha)
131
+ recorded_paths = fingerprint.fetch("paths")
132
+ return false if recorded_paths.empty?
133
+
134
+ recorded_paths == Array(live_paths).map(&:to_s).sort &&
135
+ fingerprint.fetch("dependencies") == dependency_sha &&
136
+ combined_content_sha(recorded_paths) == fingerprint.fetch("sha")
137
+ end
138
+
139
+ def combined_content_sha(paths)
140
+ digest = Digest::SHA256.new
141
+ paths.each do |path|
142
+ content = File.read(path)
143
+ digest.update(path.bytesize.to_s)
144
+ digest.update(":")
145
+ digest.update(path)
146
+ digest.update(content.bytesize.to_s)
147
+ digest.update(":")
148
+ digest.update(content)
149
+ end
150
+ digest.hexdigest
151
+ rescue StandardError
152
+ nil
153
+ end
154
+ end
155
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/henitai.rb CHANGED
@@ -20,9 +20,13 @@ module Henitai
20
20
  # Raised when coverage data cannot be bootstrapped or validated.
21
21
  class CoverageError < StandardError; end
22
22
 
23
+ # Raised when another process is using the configured reports directory.
24
+ class ConcurrentRunError < StandardError; end
25
+
23
26
  autoload :Configuration, "henitai/configuration"
24
27
  autoload :CoverageBootstrapper, "henitai/coverage_bootstrapper"
25
28
  autoload :CoverageReportReader, "henitai/coverage_report_reader"
29
+ autoload :PerTestCoverage, "henitai/per_test_coverage"
26
30
  autoload :PerTestCoverageSelector, "henitai/per_test_coverage_selector"
27
31
  autoload :Subject, "henitai/subject"
28
32
  autoload :Mutant, "henitai/mutant"
@@ -31,19 +35,29 @@ module Henitai
31
35
  autoload :Operators, "henitai/operators"
32
36
  autoload :SourceParser, "henitai/source_parser"
33
37
  autoload :SubjectResolver, "henitai/subject_resolver"
38
+ autoload :GeneratedArtifacts, "henitai/generated_artifacts"
34
39
  autoload :GitDiffAnalyzer, "henitai/git_diff_analyzer"
35
40
  autoload :GitDiffError, "henitai/git_diff_analyzer"
36
41
  autoload :MutantGenerator, "henitai/mutant_generator"
37
42
  autoload :MutantHistoryStore, "henitai/mutant_history_store"
43
+ autoload :MutationSkipDirectives, "henitai/mutation_skip_directives"
38
44
  autoload :AridNodeFilter, "henitai/arid_node_filter"
39
45
  autoload :AvailableCpuCount, "henitai/available_cpu_count"
40
46
  autoload :EquivalenceDetector, "henitai/equivalence_detector"
41
47
  autoload :StaticFilter, "henitai/static_filter"
48
+ autoload :IncrementalFilter, "henitai/incremental_filter"
49
+ autoload :VerdictFingerprint, "henitai/verdict_fingerprint"
42
50
  autoload :StillbornFilter, "henitai/stillborn_filter"
51
+ autoload :CanonicalReportMerger, "henitai/canonical_report_merger"
52
+ autoload :CanonicalReportWriter, "henitai/canonical_report_writer"
53
+ autoload :CheckpointReporter, "henitai/checkpoint_reporter"
54
+ autoload :CompositeProgressReporter, "henitai/composite_progress_reporter"
55
+ autoload :ReportsDirectoryLock, "henitai/reports_directory_lock"
43
56
  autoload :SurvivorLoader, "henitai/survivor_loader"
44
57
  autoload :SurvivorSelector, "henitai/survivor_selector"
45
58
  autoload :SurvivorTestFilter, "henitai/survivor_test_filter"
46
59
  autoload :SurvivorActivationCache, "henitai/survivor_activation_cache"
60
+ autoload :SurvivorRerunStrategy, "henitai/survivor_rerun_strategy"
47
61
  autoload :ScenarioExecutionResult, "henitai/scenario_execution_result"
48
62
  autoload :CoverageFormatter, "henitai/coverage_formatter"
49
63
  autoload :MinitestCoverageReporter, "henitai/minitest_coverage_reporter"
@@ -51,9 +65,10 @@ module Henitai
51
65
  autoload :SyntaxValidator, "henitai/syntax_validator"
52
66
  autoload :SamplingStrategy, "henitai/sampling_strategy"
53
67
  autoload :TestPrioritizer, "henitai/test_prioritizer"
68
+ autoload :TimeoutCalibrator, "henitai/timeout_calibrator"
54
69
  autoload :ExecutionEngine, "henitai/execution_engine"
55
- autoload :ParallelExecutionRunner, "henitai/parallel_execution_runner"
56
70
  autoload :ProcessWorkerRunner, "henitai/process_worker_runner"
71
+ autoload :SlotScheduler, "henitai/slot_scheduler"
57
72
  autoload :ProcessWakeup, "henitai/process_wakeup"
58
73
  autoload :Runner, "henitai/runner"
59
74
  autoload :Reporter, "henitai/reporter"
@@ -1,29 +1,53 @@
1
1
  module Henitai
2
2
  module ConfigurationValidator
3
+ VALID_TOP_LEVEL_KEYS: Array[Symbol]
4
+ VALID_MUTATION_KEYS: Array[Symbol]
5
+ VALID_SAMPLING_KEYS: Array[Symbol]
6
+ VALID_COVERAGE_CRITERIA_KEYS: Array[Symbol]
7
+ VALID_THRESHOLDS_KEYS: Array[Symbol]
8
+ VALID_DASHBOARD_KEYS: Array[Symbol]
9
+ VALID_INTEGRATION_KEYS: Array[Symbol]
10
+ VALID_OPERATORS: Array[Symbol]
11
+ VALIDATION_STEPS: Array[Symbol]
12
+
3
13
  def self.validate!: (Hash[Symbol, untyped]) -> void
14
+ def self.warn: (String) -> void
4
15
 
5
- private
16
+ module Rules
17
+ def self?.validate_top_level_keys: (Hash[Symbol, untyped]) -> void
18
+ def self?.validate_integration: (Hash[Symbol, untyped]) -> void
19
+ def self?.validate_includes: (Hash[Symbol, untyped]) -> void
20
+ def self?.validate_excludes: (Hash[Symbol, untyped]) -> void
21
+ def self?.validate_jobs: (Hash[Symbol, untyped]) -> void
22
+ def self?.validate_reporters: (Hash[Symbol, untyped]) -> void
23
+ def self?.validate_reports_dir: (Hash[Symbol, untyped]) -> void
24
+ def self?.validate_all_logs: (Hash[Symbol, untyped]) -> void
25
+ def self?.validate_dashboard: (Hash[Symbol, untyped]) -> void
26
+ def self?.validate_mutation: (Hash[Symbol, untyped]) -> void
27
+ def self?.validate_mutation_limits: (Hash[Symbol, untyped]) -> void
28
+ def self?.validate_mutation_filters: (Hash[Symbol, untyped]) -> void
29
+ def self?.validate_coverage_criteria: (Hash[Symbol, untyped]) -> void
30
+ def self?.validate_thresholds: (Hash[Symbol, untyped]) -> void
31
+ def self?.validate_sampling: (untyped) -> void
32
+ def self?.warn_unknown_keys: (Hash[Symbol, untyped], Array[Symbol], ?String?) -> void
33
+ def self?.key_path: (String?, Symbol) -> String
34
+ def self?.ensure_hash!: (untyped, String) -> void
35
+ def self?.configuration_error: (String) -> void
36
+ end
6
37
 
7
- def self.validate_top_level_keys: (Hash[Symbol, untyped]) -> void
8
- def self.validate_integration: (Hash[Symbol, untyped]) -> void
9
- def self.validate_includes: (Hash[Symbol, untyped]) -> void
10
- def self.validate_jobs: (Hash[Symbol, untyped]) -> void
11
- def self.validate_reporters: (Hash[Symbol, untyped]) -> void
12
- def self.validate_all_logs: (Hash[Symbol, untyped]) -> void
13
- def self.validate_dashboard: (Hash[Symbol, untyped]) -> void
14
- def self.validate_mutation: (Hash[Symbol, untyped]) -> void
15
- def self.validate_coverage_criteria: (Hash[Symbol, untyped]) -> void
16
- def self.validate_thresholds: (Hash[Symbol, untyped]) -> void
17
- def self.validate_operator: (untyped) -> void
18
- def self.validate_timeout: (untyped) -> void
19
- def self.validate_threshold: (untyped, String) -> void
20
- def self.validate_boolean: (untyped, String) -> void
21
- def self.validate_optional_string: (untyped, String) -> void
22
- def self.validate_string_array: (untyped, String) -> void
23
- def self.warn_unknown_keys: (Hash[Symbol, untyped], Array[Symbol], ?String) -> void
24
- def self.key_path: (?String, Symbol) -> String
25
- def self.ensure_hash!: (untyped, String) -> void
26
- def self.describe_array_type: (untyped) -> String
27
- def self.configuration_error: (String) -> void
38
+ module Scalars
39
+ def self?.validate_operator: (untyped) -> void
40
+ def self?.validate_timeout: (untyped) -> void
41
+ def self?.validate_threshold: (untyped, String) -> void
42
+ def self?.validate_boolean: (untyped, String) -> void
43
+ def self?.validate_optional_string: (untyped, String) -> void
44
+ def self?.validate_string_array: (untyped, String) -> void
45
+ def self?.validate_ignore_patterns: (untyped) -> void
46
+ def self?.validate_max_flaky_retries: (untyped) -> void
47
+ def self?.validate_sampling_ratio: (untyped) -> void
48
+ def self?.validate_sampling_strategy: (untyped) -> void
49
+ def self?.validate_sampling_completeness: (Hash[Symbol, untyped]) -> void
50
+ def self?.describe_array_type: (untyped) -> String
51
+ end
28
52
  end
29
53
  end