Module: Mutineer::FileSwap

Defined in:
lib/mutineer/file_swap.rb

Overview

#27 (U2): apply one whole-file mutant to the REAL source path for the external (--test-command) backend, and guarantee the original is restored on every exit path. A separate bundle exec subprocess has its own VM and cannot see an in-process load, so the mutant must live on disk while its suite runs — which makes leaving the file mutated the one genuinely dangerous failure mode.

Defense in depth, mirroring the tempfile-orphan discipline (Runner.sweep_orphans, isolation.rb tempfiles):

- the original bytes are held in memory AND written to a sibling backup;
- `ensure` restores from memory around every mutant;
- the backup survives a SIGKILL (which skips `ensure`), so `restore_orphans`
can self-heal a left-mutated tree on the next run's startup.

Only one mutant is in flight per file at a time (the external path is serial), so backups never collide.

Constant Summary collapse

BACKUP_SUFFIX =

Suffix for the on-disk backup; fixed so restore_orphans finds it.

".mutineer-backup"

Class Method Summary collapse

Class Method Details

.restore_orphans(dirs) ⇒ void

This method returns an undefined value.

Startup/after-run self-heal: restore any source file left mutated by a prior interrupted run (a leftover *.mutineer-backup), then remove the backup. Prints one line to stderr when it actually heals something, so a developer knows their working tree was auto-restored (a file they did not touch).

Parameters:

  • dirs (Array<String>)

    directories to sweep for orphaned backups.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/mutineer/file_swap.rb', line 69

def self.restore_orphans(dirs)
  healed = 0
  dirs.uniq.each do |dir|
    Dir.glob(File.join(dir, "*#{BACKUP_SUFFIX}")).each do |backup|
      source_file = backup.delete_suffix(BACKUP_SUFFIX)
      backup_bytes = File.binread(backup)
      if !File.exist?(source_file)
        # A real user file that merely ends in our suffix, with no sibling to
        # restore — leave it untouched (never create a file from it).
        next
      elsif File.binread(source_file) == backup_bytes
        # Redundant backup (e.g. a crash between restore and unlink): nothing to
        # heal, just clear the orphan so the next run doesn't see a false race.
        File.unlink(backup)
      else
        File.binwrite(source_file, backup_bytes)
        File.unlink(backup)
        healed += 1
      end
    end
  end
  return if healed.zero?

  warn "[mutineer] restored #{healed} source file(s) left mutated by a previous interrupted run."
end

.with(source_file, mutated) { ... } ⇒ Object

Writes mutated to source_file, yields, then restores the original bytes on every exit path (normal return, exception, or ensure). Byte-exact: binary read/write preserves encoding, newlines, and trailing bytes.

Parameters:

  • source_file (String)

    path to the real source file.

  • mutated (String)

    mutated source text to write for the duration.

Yields:

  • the block to run while the mutant is on disk.

Returns:

  • (Object)

    the block's return value.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/mutineer/file_swap.rb', line 40

def self.with(source_file, mutated)
  backup = source_file + BACKUP_SUFFIX
  # A backup already on disk means either a prior hard-killed run (restore_orphans
  # should have healed it at startup) or a SECOND mutineer run racing us on the
  # same file. The backup path is shared and unlocked, so proceeding would let
  # us capture the other run's mutant AS the "original" and permanently mutate
  # the tree. Refuse loudly rather than silently corrupt — and do it BEFORE
  # `created` is set, so the ensure below never touches a backup we don't own.
  raise ConcurrentRunError, backup if File.exist?(backup)

  original = File.binread(source_file)
  File.binwrite(backup, original)
  created = true
  File.binwrite(source_file, mutated)
  yield
ensure
  if created
    File.binwrite(source_file, original)
    File.unlink(backup) if File.exist?(backup)
  end
end