Class: ActiveMutator::ClosureReload
- Inherits:
-
Object
- Object
- ActiveMutator::ClosureReload
- Defined in:
- lib/active_mutator/closure_reload.rb
Overview
Fork-side insertion for class-body mutants. A def mutant can be
class_eval'd over the live constant; class-level code cannot (re-running
validates ADDS a validator, it doesn't replace one). So: remove the
constant and re-eval the whole mutated file. Anything already attached to
the OLD object — classes that include the module, subclasses, extend
sites — would go stale, so they are removed and re-evaled too (pristine
sources), dependency-first. The fork dies after the run; nothing is
restored.
Every guard failure raises Skip; the Worker reports the mutant as
skipped with the reason. Skipping is honest: a mutant we cannot insert
faithfully must not be counted as survived OR killed.
Known limitations (inherent to remove-and-reload; accepted trade-offs):
- References that hold the target BY VALUE rather than by ancestry are
not discovered and keep pointing at the pre-remove_const object:
aliases (`ALIAS = MyClass`), arrays/registries the target was pushed
into, memoized instances, class variables captured at load time.
- `refine`-based modules are anonymous and do not appear in normal
`ancestors`, so refinements of the target are not discovered/reloaded.
- Re-evaling the target AND each attacher re-runs their class-body side
effects. Non-idempotent load-time effects (self-registration into a
global registry, DescendantsTracker-style hooks) therefore run twice; a
spec asserting such a count can see it doubled (false kill) or masked
(false survival).
- The re-eval order pins the target first (every attacher depends on it)
then sorts the rest by instance-ancestor depth. An `extend`
relationship BETWEEN two non-target attachers can still re-eval out of
order (rare); a full topological sort is deliberately not attempted.
Constant Summary collapse
- Skip =
Class.new(StandardError)
- MutantLoadError =
The MUTATED target source could not be re-evaled — the mutation broke the class so it no longer loads. Every covering spec loads the class, so the suite would fail: the Worker maps this to a kill, not an error.
Class.new(StandardError)
- DEFAULT_CAP =
10
Class Attribute Summary collapse
Instance Method Summary collapse
- #call(cap: self.class.cap) ⇒ Object
-
#initialize(subject, mutated_source) ⇒ ClosureReload
constructor
A new instance of ClosureReload.
Constructor Details
#initialize(subject, mutated_source) ⇒ ClosureReload
Returns a new instance of ClosureReload.
48 49 50 51 |
# File 'lib/active_mutator/closure_reload.rb', line 48 def initialize(subject, mutated_source) @subject = subject @mutated_source = mutated_source end |
Class Attribute Details
.cap ⇒ Object
45 |
# File 'lib/active_mutator/closure_reload.rb', line 45 def cap = @cap || DEFAULT_CAP |
Instance Method Details
#call(cap: self.class.cap) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/active_mutator/closure_reload.rb', line 53 def call(cap: self.class.cap) target = resolve_target closure = compute_closure(target) if closure.size > cap raise Skip, "reload closure (#{closure.size} constants) exceeds cap (#{cap})" end # Re-eval must load dependencies before dependents. Every closure member # carries the target directly (single-pass discovery invariant), so the # target must be re-eval'd first — pin it to the front. `ancestors.size` # can't do this alone: an `extend` puts the target in the extender's # SINGLETON ancestry, so a module that extends the target has an instance # ancestry SMALLER than the target's and would sort before it (NameError # on re-eval). For the remaining attachers, ascending instance-ancestor # depth is a valid order among include/subclass relationships (a # superclass before its subclass, an included module before its # includer); depth is captured while the constants are still live, since # it can't be read after remove_const. rest = closure.reject { |m| m.equal?(target) } ordered = [target, *rest.sort_by { |m| m.ancestors.size }] sources = ordered.map { |mod| [mod.name, source_for(mod)] } sources.each { |name, _| remove_constant(name) } sources.each_with_index do |(_, (file, src)), idx| eval(src, TOPLEVEL_BINDING, file, 1) # rubocop:disable Security/Eval rescue ScriptError, StandardError => e # idx.zero? is the MUTATED target, which by construction depends on # nothing else in the closure (every other member carries the target, # not vice versa). Its source failing to load is therefore the # mutation's own doing — a kill, not a tool error. # # A PRISTINE dependent (idx > 0) failing means the ancestry-depth order # couldn't satisfy a cross-attacher reference (see the ordering note # above): we can't faithfully reinstate the closure, so this is an # honest Skip, not a survived/killed verdict and not a bare error. raise MutantLoadError, e. if idx.zero? raise Skip, "reload re-eval failed (#{e.}); closure could not be reinstated in dependency order" end nil end |