Class: Mutineer::DaemonClient
- Inherits:
-
Object
- Object
- Mutineer::DaemonClient
- Defined in:
- lib/mutineer/daemon_client.rb
Overview
#26/#27 Phase 2a — the TOOL-side handle for the app-side daemon.
Spawns daemon_server.rb UNDER THE APP'S BUNDLE/RUBY (cleaned env so the gem's
bundler context never leaks; the daemon file is loaded by absolute path with
-r, which bypasses the app bundle that has no mutineer), completes the ready
handshake, then ships per-mutant payloads and reads structured verdicts. If the
daemon dies mid-run it respawns (bounded) and marks the in-flight mutant error
rather than corrupting the run. Reuses the cleaned-env spawn + stderr-drain proven
in the spike driver and the spawn discipline of ExternalBackend.
Constant Summary collapse
- DAEMON_PATH =
Absolute path to the daemon entry, loaded app-side by
-r(bypasses the bundle). File.("daemon_server.rb", __dir__)
- MAX_RESTARTS =
How many times to respawn a crashing daemon before aborting the run.
3
Instance Method Summary collapse
-
#coverage ⇒ Hash?
#26/U7: ask the daemon to build the coverage map app-side and return it.
-
#initialize(boot:, app_root:, ruby_version: nil, gemfile: nil, errio: $stderr) ⇒ DaemonClient
constructor
A new instance of DaemonClient.
-
#quit ⇒ void
Graceful shutdown; leaves no orphaned daemon/child.
-
#request(id:, payload:, tests:, timeout:, worker: 0) ⇒ String
Run one mutant: ship the payload + covering tests, return the verdict string.
-
#start ⇒ self
Spawn the daemon and complete the ready handshake.
Constructor Details
#initialize(boot:, app_root:, ruby_version: nil, gemfile: nil, errio: $stderr) ⇒ DaemonClient
Returns a new instance of DaemonClient.
32 33 34 35 36 37 38 39 |
# File 'lib/mutineer/daemon_client.rb', line 32 def initialize(boot:, app_root:, ruby_version: nil, gemfile: nil, errio: $stderr) @boot = boot @app_root = app_root @ruby_version = ruby_version @gemfile = gemfile || File.join(app_root, "Gemfile") @errio = errio @restarts = 0 end |
Instance Method Details
#coverage ⇒ Hash?
#26/U7: ask the daemon to build the coverage map app-side and return it.
One-shot control message (no id). Returns {"map"=>..., "failed_test_files"=>...}
(possibly with an "error"), or nil if the daemon vanished — the caller then
falls back to running the full test set (no narrowing) rather than mis-scoring.
85 86 87 88 89 90 |
# File 'lib/mutineer/daemon_client.rb', line 85 def coverage send_line("cmd" => "coverage") read_line rescue Errno::EPIPE, IOError nil end |
#quit ⇒ void
This method returns an undefined value.
Graceful shutdown; leaves no orphaned daemon/child.
95 96 97 98 99 100 101 102 |
# File 'lib/mutineer/daemon_client.rb', line 95 def quit return unless @stdin send_line("cmd" => "quit") rescue nil # rubocop:disable Style/RescueModifier @wait_thr&.join ensure close_io end |
#request(id:, payload:, tests:, timeout:, worker: 0) ⇒ String
Run one mutant: ship the payload + covering tests, return the verdict string.
On a daemon crash (EOF/dead pipe) respawn (bounded) and return "error" for
this mutant — never a wrong verdict, never a wedged run.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/mutineer/daemon_client.rb', line 61 def request(id:, payload:, tests:, timeout:, worker: 0) # A crash can surface on the WRITE (daemon died idle between requests → # Errno::EPIPE) as well as the read (EOF), so guard both: either way, respawn # for future mutants and score THIS one error (re-running a crash-causing # mutant could loop). Never let a dead pipe abort the whole run. reply = begin send_line("id" => id, "worker" => worker, "payload" => payload, "tests" => tests, "timeout" => timeout) read_line rescue Errno::EPIPE, IOError nil end return reply["verdict"] if reply && reply["id"] == id restart! "error" end |
#start ⇒ self
Spawn the daemon and complete the ready handshake. Raises DaemonBootError on failure (surfaced by the CLI as a clean runtime error, not a hang).
45 46 47 48 |
# File 'lib/mutineer/daemon_client.rb', line 45 def start spawn_daemon self end |