Class: Henitai::SurvivorRerunStrategy
- Inherits:
-
Object
- Object
- Henitai::SurvivorRerunStrategy
- Defined in:
- lib/henitai/survivor_rerun_strategy.rb,
sig/henitai.rbs
Overview
Survivor-rerun fast path for Runner.
When --survivors-from is given, this collaborator loads the prior report
and either:
* builds stub Mutants directly from +activation-recipes.json+ (the recipe
fast path, bypassing source parsing and mutant generation), or
* filters a freshly generated mutant list down to the prior survivors.
In both cases it records #survivor_stats (matched/unmatched counts and the drift warning) for the Runner to attach to its Result.
Instance Attribute Summary collapse
-
#survivor_stats ⇒ Hash[Symbol, untyped]?
readonly
Returns the value of attribute survivor_stats.
Instance Method Summary collapse
- #active? ⇒ Boolean
- #apply_selection(mutants) ⇒ Array[Mutant]
- #build_stub_mutant(stable_id, recipe) ⇒ Mutant
- #build_survivor_stats(selector, selected, split) ⇒ Hash[Symbol, untyped]
- #committed_changed_files(git_sha) ⇒ Array[String]
- #dirty_source_files?(dirty_worktree_files, git_sha: nil) ⇒ Boolean
- #dirty_worktree_changed_files ⇒ Array[String]?
- #finalize_survivor_split(selector, selected, split) ⇒ Array[Mutant]
- #in_include_root?(path, include_roots) ⇒ Boolean
-
#initialize(survivors_from:, config:, git_diff_analyzer:) ⇒ SurvivorRerunStrategy
constructor
A new instance of SurvivorRerunStrategy.
-
#load_activation_recipes(survivor_ids) ⇒ Hash[String, Hash[String, untyped]]?
Returns the recipe hash if the file exists and covers every survivor ID; otherwise returns nil to trigger the normal generation path.
- #load_survivor_report ⇒ SurvivorLoader::Report
- #normalize_path(path) ⇒ String
- #recipe_fast_path_safe?(loaded, dirty_worktree_files) ⇒ Boolean
- #recipe_location(loc) ⇒ Hash[Symbol, untyped]
-
#recipe_selector_and_stubs(survivor_ids, recipes) ⇒ [SurvivorSelector, Array[Mutant]]
Builds stub Mutants from recipes and a SurvivorSelector primed with the survivor ID set.
- #run_from_recipes(loaded, dirty_worktree_files) ⇒ Array[Mutant]?
- #stub_subject_from_recipe(recipe) ⇒ Subject
- #test_filter(loaded, dirty_source_files:) ⇒ SurvivorTestFilter
-
#try_recipe_run ⇒ Array[Mutant]?
Attempts to run survivors directly from pre-computed activation recipes, bypassing source parsing and mutant generation entirely.
- #warn_survivor_drift(selector) ⇒ void
Constructor Details
#initialize(survivors_from:, config:, git_diff_analyzer:) ⇒ SurvivorRerunStrategy
Returns a new instance of SurvivorRerunStrategy.
18 19 20 21 22 23 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 18 def initialize(survivors_from:, config:, git_diff_analyzer:) @survivors_from = survivors_from @config = config @git_diff_analyzer = git_diff_analyzer @survivor_stats = nil end |
Instance Attribute Details
#survivor_stats ⇒ Hash[Symbol, untyped]? (readonly)
Returns the value of attribute survivor_stats.
16 17 18 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 16 def survivor_stats @survivor_stats end |
Instance Method Details
#active? ⇒ Boolean
25 26 27 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 25 def active? !@survivors_from.nil? end |
#apply_selection(mutants) ⇒ Array[Mutant]
40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 40 def apply_selection(mutants) dirty_worktree_files = dirty_worktree_changed_files loaded = load_survivor_report selector = SurvivorSelector.new(survivor_ids: loaded.survivor_ids) selected = selector.select(mutants) finalize_survivor_split( selector, selected, test_filter( loaded, dirty_source_files: dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha) ).apply(selected) ) end |
#build_stub_mutant(stable_id, recipe) ⇒ Mutant
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 98 def build_stub_mutant(stable_id, recipe) mutant = Mutant.new( subject: stub_subject_from_recipe(recipe), operator: recipe.fetch("operator"), nodes: { original: nil, mutated: nil }, description: recipe.fetch("description"), location: recipe_location(recipe["location"]), precomputed_stable_id: stable_id, precomputed_activation_source: recipe.fetch("activationSource") ) mutant.covered_by = recipe["coveredBy"] mutant end |
#build_survivor_stats(selector, selected, split) ⇒ Hash[Symbol, untyped]
185 186 187 188 189 190 191 192 193 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 185 def build_survivor_stats(selector, selected, split) { matched: selected.size, unmatched_count: selector.unmatched_ids.size, unmatched_ids: selector.unmatched_ids, skipped_count: split[:stable].size, drift_warning: selector.drift_warning? } end |
#committed_changed_files(git_sha) ⇒ Array[String]
166 167 168 169 170 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 166 def committed_changed_files(git_sha) return [] unless git_sha @git_diff_analyzer.changed_files(from: git_sha, to: "HEAD") end |
#dirty_source_files?(dirty_worktree_files, git_sha: nil) ⇒ Boolean
156 157 158 159 160 161 162 163 164 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 156 def dirty_source_files?(dirty_worktree_files, git_sha: nil) return true if dirty_worktree_files.nil? all_changed = dirty_worktree_files + committed_changed_files(git_sha) include_roots = Array(@config.includes).map { |path| normalize_path(path) } all_changed.any? { |path| in_include_root?(normalize_path(path), include_roots) } rescue StandardError true end |
#dirty_worktree_changed_files ⇒ Array[String]?
150 151 152 153 154 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 150 def dirty_worktree_changed_files @dirty_worktree_changed_files ||= @git_diff_analyzer.working_tree_changed_files rescue StandardError nil end |
#finalize_survivor_split(selector, selected, split) ⇒ Array[Mutant]
133 134 135 136 137 138 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 133 def finalize_survivor_split(selector, selected, split) split[:stable].each { |m| m.status = :survived } warn_survivor_drift(selector) if selector.drift_warning? @survivor_stats = build_survivor_stats(selector, selected, split) split[:stable] + split[:pending] end |
#in_include_root?(path, include_roots) ⇒ Boolean
172 173 174 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 172 def in_include_root?(path, include_roots) include_roots.any? { |root| path == root || path.start_with?("#{root}/") } end |
#load_activation_recipes(survivor_ids) ⇒ Hash[String, Hash[String, untyped]]?
Returns the recipe hash if the file exists and covers every survivor ID; otherwise returns nil to trigger the normal generation path.
89 90 91 92 93 94 95 96 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 89 def load_activation_recipes(survivor_ids) path = File.join(File.dirname(@survivors_from), SurvivorActivationCache::FILENAME) recipes = SurvivorActivationCache.load(path) return nil if recipes.nil? return nil unless survivor_ids.all? { |id| recipes.key?(id) } recipes end |
#load_survivor_report ⇒ SurvivorLoader::Report
57 58 59 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 57 def load_survivor_report SurvivorLoader.new(@survivors_from, include_paths: Array(@config.includes)).load end |
#normalize_path(path) ⇒ String
176 177 178 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 176 def normalize_path(path) File.(path) end |
#recipe_fast_path_safe?(loaded, dirty_worktree_files) ⇒ Boolean
73 74 75 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 73 def recipe_fast_path_safe?(loaded, dirty_worktree_files) !dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha) end |
#recipe_location(loc) ⇒ Hash[Symbol, untyped]
121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 121 def recipe_location(loc) return {} unless loc.is_a?(Hash) { file: loc["file"], start_line: loc["startLine"], end_line: loc["endLine"], start_col: loc["startCol"], end_col: loc["endCol"] }.compact end |
#recipe_selector_and_stubs(survivor_ids, recipes) ⇒ [SurvivorSelector, Array[Mutant]]
Builds stub Mutants from recipes and a SurvivorSelector primed with the survivor ID set. The selector is given a synthetic #select call so that #drift_warning? / #unmatched_ids are available (all IDs will be matched).
80 81 82 83 84 85 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 80 def recipe_selector_and_stubs(survivor_ids, recipes) stubs = survivor_ids.map { |id| build_stub_mutant(id, recipes[id]) } selector = SurvivorSelector.new(survivor_ids:) selector.select(stubs) [selector, stubs] end |
#run_from_recipes(loaded, dirty_worktree_files) ⇒ Array[Mutant]?
61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 61 def run_from_recipes(loaded, dirty_worktree_files) recipes = load_activation_recipes(loaded.survivor_ids) return nil if recipes.nil? selector, stubs = recipe_selector_and_stubs(loaded.survivor_ids, recipes) split = test_filter( loaded, dirty_source_files: dirty_source_files?(dirty_worktree_files, git_sha: loaded.git_sha) ).apply(stubs) finalize_survivor_split(selector, stubs, split) end |
#stub_subject_from_recipe(recipe) ⇒ Subject
112 113 114 115 116 117 118 119 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 112 def stub_subject_from_recipe(recipe) Subject.new( namespace: recipe["namespace"], method_name: recipe["methodName"], method_type: (recipe["methodType"] || "instance").to_sym, source_location: { file: recipe["sourceFile"], range: nil } ) end |
#test_filter(loaded, dirty_source_files:) ⇒ SurvivorTestFilter
140 141 142 143 144 145 146 147 148 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 140 def test_filter(loaded, dirty_source_files:) SurvivorTestFilter.new( coverage_map: loaded.coverage_map, git_sha: loaded.git_sha, dirty_source_files:, worktree_changed_files: Array(dirty_worktree_changed_files), diff_analyzer: @git_diff_analyzer ) end |
#try_recipe_run ⇒ Array[Mutant]?
Attempts to run survivors directly from pre-computed activation recipes, bypassing source parsing and mutant generation entirely. Returns the mutant array on success, or nil if recipes are unavailable.
32 33 34 35 36 37 38 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 32 def try_recipe_run dirty_worktree_files = dirty_worktree_changed_files loaded = load_survivor_report return nil unless recipe_fast_path_safe?(loaded, dirty_worktree_files) run_from_recipes(loaded, dirty_worktree_files) end |
#warn_survivor_drift(selector) ⇒ void
This method returns an undefined value.
180 181 182 183 |
# File 'lib/henitai/survivor_rerun_strategy.rb', line 180 def warn_survivor_drift(selector) warn "henitai: WARNING: #{selector.unmatched_ids.size} prior survivors " \ "could not be matched; the source may have drifted - consider a full run" end |