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)


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

Returns the value of attribute survivor_stats.

Returns:

  • (Hash[Symbol, untyped], nil)


16
17
18
# File 'lib/henitai/survivor_rerun_strategy.rb', line 16

def survivor_stats
  @survivor_stats
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


25
26
27
# File 'lib/henitai/survivor_rerun_strategy.rb', line 25

def active?
  !@survivors_from.nil?
end

#apply_selection(mutants) ⇒ Array[Mutant]

Parameters:

Returns:



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

Parameters:

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

Returns:



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]

Parameters:

Returns:

  • (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]

Parameters:

  • (String, nil)

Returns:

  • (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

Parameters:

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

Returns:

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

Returns:

  • (Array[String], nil)


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]

Parameters:

Returns:



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

Parameters:

  • (String)
  • (Array[String])

Returns:

  • (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.

Parameters:

  • (Array[String])

Returns:

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


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

Parameters:

  • (String)

Returns:

  • (String)


176
177
178
# File 'lib/henitai/survivor_rerun_strategy.rb', line 176

def normalize_path(path)
  File.expand_path(path)
end

#recipe_fast_path_safe?(loaded, dirty_worktree_files) ⇒ Boolean

Parameters:

Returns:

  • (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]

Parameters:

  • (Hash[String, untyped], nil)

Returns:

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

Parameters:

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

Returns:



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

Parameters:

Returns:



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

Parameters:

  • (Hash[String, untyped])

Returns:



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

Parameters:

Returns:



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_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:



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.

Parameters:



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