Class: ActiveMutator::Worker

Inherits:
Object
  • Object
show all
Defined in:
lib/active_mutator/worker.rb

Overview

Runs INSIDE a fork. Insertion order relative to RSpec's setup (which loads the spec files, and with them the app) DIFFERS by mutant kind:

Class-body: insert BEFORE setup. `RSpec.describe SomeClass` binds
`metadata[:described_class]` to the constant AT LOAD TIME, and a class-body
mutant reloads the constant to a NEW object via ClosureReload; a group
loaded first would keep the pre-mutation object and falsely survive. So we
require the subject file, reload, THEN let setup load the groups — every
group binds to the mutated object.

Def: insert AFTER setup. A def mutant class_evals the live method in
place, so it must be the LAST thing to touch that method. Inserting before
setup let a file loaded during spec-load (a concern/decorator/monkeypatch
that reopens the class but isn't transitively required by the subject
file) silently redefine the method back to the original, reporting a false
survivor. Loading everything first, then inserting, closes that window.
Requiring the subject after setup also means spec_helper's load-time setup
runs first, so a subject that depends on it still loads in non-preloaded
projects.

The explicit require of the subject file guarantees the target constant exists before insertion regardless of preload: preloaded projects (Rails/Zeitwerk, or a preloaded spec helper) already have it in $LOADED_FEATURES so it's a no-op, while non-preloaded projects (plain gems whose spec files require the lib themselves, or --no-preload-helper) get it loaded rather than relying on spec-load to define it.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mutation, example_ids, writer) ⇒ Worker

Returns a new instance of Worker.



36
37
38
39
40
# File 'lib/active_mutator/worker.rb', line 36

def initialize(mutation, example_ids, writer)
  @mutation = mutation
  @example_ids = example_ids
  @writer = writer
end

Class Method Details

.run(mutation, example_ids, writer) ⇒ Object



32
33
34
# File 'lib/active_mutator/worker.rb', line 32

def self.run(mutation, example_ids, writer)
  new(mutation, example_ids, writer).run
end

Instance Method Details

#runObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/active_mutator/worker.rb', line 42

def run
  require "rspec/core"
  devnull = File.open(File::NULL, "w")
  runner = RSpec::Core::Runner.new(RSpec::Core::ConfigurationOptions.new(@example_ids))
  if @mutation.subject.class_body?
    require @mutation.subject.file # no-op if already loaded; guarantees the constant exists
    insert_mutation                # BEFORE setup: groups bind described_class to the mutated object
    runner.setup(devnull, devnull) # loads spec files
  else
    runner.setup(devnull, devnull) # loads spec files -> the app, in dependency order
    require @mutation.subject.file # no-op if already loaded; guarantees the constant exists
    insert_mutation                # AFTER load: nothing left can redefine the method back
  end
  # One failure kills the mutant; running the rest of the covering set
  # is pure waste inside the fork.
  RSpec.configuration.fail_fast = 1
  after_fork_hygiene
  code = runner.run_specs(covering_groups)
  emit(code.zero? ? "survived" : "killed")
rescue ClosureReload::Skip => e
  emit("skipped", details: e.message)
rescue ClosureReload::MutantLoadError => e
  # The mutation made the class unloadable; a real suite would fail on it.
  emit("killed", details: "mutated class failed to load: #{e.message}")
rescue StandardError, ScriptError => e
  emit("error", details: "#{e.class}: #{e.message}")
end