Class: Henitai::SurvivorRerunStrategy

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(survivors_from:, config:, git_diff_analyzer:) ⇒ SurvivorRerunStrategy

Returns a new instance of SurvivorRerunStrategy.

Parameters:

  • survivors_from: (String, nil)
  • config: (Configuration)
  • git_diff_analyzer: (Object)


20
21
22
23
24
25
# File 'lib/henitai/survivor_rerun_strategy.rb', line 20

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_statsHash[Symbol, untyped]? (readonly)

Returns the value of attribute survivor_stats.

Returns:

  • (Hash[Symbol, untyped], nil)


18
19
20
# File 'lib/henitai/survivor_rerun_strategy.rb', line 18

def survivor_stats
  @survivor_stats
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/henitai/survivor_rerun_strategy.rb', line 27

def active?
  !@survivors_from.nil?
end

#apply_selection(mutants) ⇒ Array[Mutant]

Parameters:

Returns:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/henitai/survivor_rerun_strategy.rb', line 42

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

Parameters:

  • (String)
  • (Hash[String, untyped])

Returns:



100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/henitai/survivor_rerun_strategy.rb', line 100

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]

Parameters:

Returns:

  • (Hash[Symbol, untyped])


173
174
175
176
177
178
179
180
181
# File 'lib/henitai/survivor_rerun_strategy.rb', line 173

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_filesArray[String]

Parameters:

  • (String, nil)

Returns:

  • (Array[String])


1195
# File 'sig/henitai.rbs', line 1195

def committed_changed_files: (String?) -> Array[String]

#dirty_source_files?(dirty_worktree_files, git_sha: nil) ⇒ Boolean

Parameters:

  • (Array[String], nil)
  • git_sha: (String, nil) (defaults to: nil)

Returns:

  • (Boolean)


158
159
160
# File 'lib/henitai/survivor_rerun_strategy.rb', line 158

def dirty_source_files?(dirty_worktree_files, git_sha: nil)
  dirty_source_detector.dirty?(dirty_worktree_files, git_sha: git_sha)
end

#dirty_worktree_changed_filesArray[String]?

Returns:

  • (Array[String], nil)


152
153
154
155
156
# File 'lib/henitai/survivor_rerun_strategy.rb', line 152

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]

Parameters:

Returns:



135
136
137
138
139
140
# File 'lib/henitai/survivor_rerun_strategy.rb', line 135

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?Boolean

Parameters:

  • (String)
  • (Array[String])

Returns:

  • (Boolean)


1196
# File 'sig/henitai.rbs', line 1196

def in_include_root?: (String, Array[String]) -> bool

#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.

Parameters:

  • (Array[String])

Returns:

  • (Hash[String, Hash[String, untyped]], nil)


91
92
93
94
95
96
97
98
# File 'lib/henitai/survivor_rerun_strategy.rb', line 91

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_reportSurvivorLoader::Report



59
60
61
# File 'lib/henitai/survivor_rerun_strategy.rb', line 59

def load_survivor_report
  SurvivorLoader.new(@survivors_from, include_paths: Array(@config.includes)).load
end

#normalize_pathString

Parameters:

  • (String)

Returns:

  • (String)


1197
# File 'sig/henitai.rbs', line 1197

def normalize_path: (String) -> String

#recipe_fast_path_safe?(loaded, dirty_worktree_files) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


75
76
77
# File 'lib/henitai/survivor_rerun_strategy.rb', line 75

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]

Parameters:

  • (Hash[String, untyped], nil)

Returns:

  • (Hash[Symbol, untyped])


123
124
125
126
127
128
129
130
131
132
133
# File 'lib/henitai/survivor_rerun_strategy.rb', line 123

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).

Parameters:

  • (Array[String])
  • (Hash[String, Hash[String, untyped]])

Returns:



82
83
84
85
86
87
# File 'lib/henitai/survivor_rerun_strategy.rb', line 82

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]?

Parameters:

Returns:



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/henitai/survivor_rerun_strategy.rb', line 63

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

Parameters:

  • (Hash[String, untyped])

Returns:



114
115
116
117
118
119
120
121
# File 'lib/henitai/survivor_rerun_strategy.rb', line 114

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

Parameters:

Returns:



142
143
144
145
146
147
148
149
150
# File 'lib/henitai/survivor_rerun_strategy.rb', line 142

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_runArray[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.

Returns:



34
35
36
37
38
39
40
# File 'lib/henitai/survivor_rerun_strategy.rb', line 34

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.

Parameters:



168
169
170
171
# File 'lib/henitai/survivor_rerun_strategy.rb', line 168

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