Module: Mutineer::DaemonServer
- Defined in:
- lib/mutineer/daemon_server.rb
Overview
App-side daemon (persistent worker).
Runs UNDER THE APP'S OWN BUNDLE/RUBY (the tool's DaemonClient spawns it via
bundle exec ruby). It boots the app ONCE, then serves per-mutant test-run
requests over stdin/stdout as newline-delimited JSON. For each request it
FORKS a child that loads the mutated source text the tool sent, runs the
covering tests, and exits with a status the parent decodes into a verdict.
HARD CONSTRAINT: this file must be loadable WITHOUT Prism or the rest of
mutineer. The app's Ruby may be < 3.4 (no stdlib Prism) and its bundle has no
mutineer. So it requires ONLY stdlib + the app's own boot file; it
re-implements the fork/timeout/decode loop rather than requiring
isolation.rb (which pulls in Prism). All parsing/mutation happened
tool-side; the daemon only loads text.
Protocol (one JSON object per line, both directions):
boot in : {"cmd":"boot","project_root":"...","boot":"config/environment",
"load_paths":["test"],"framework":"minitest","rails":true,"schema":"db/schema.rb"}
ready out: {"ready":true,"ruby":"3.3.6"} (or {"ready":false,"error":"..."} then exit)
run in : {"id":N,"worker":I,"payload":{"code":"<ruby>","source_file":"app/models/order.rb"},
"tests":["test/models/order_test.rb"],"timeout":30}
verdict : {"id":N,"verdict":"survived"|"killed"|"error"|"timeout"}
quit in : {"cmd":"quit"}
Worker isolation: when the app is Rails, each fork is routed to its own
database <db>-<worker> via RailsWorkerDb BEFORE any test loads, so
concurrent workers cannot clobber each other's transactional fixtures.
worker defaults to 0 (serial). SQLite this pass; Postgres provisioning is
not yet implemented.
Verdict mapping: child exit 0=survived (suite passed), 1=killed (suite
failed), 2=error (child raised AROUND the test: load, boot, or worker-DB
routing failure); parent-detected timeout. Tagging an in-test DB error (one
fired inside a test body, vs at routing time) as error rather than
killed is only observable under the concurrent gate and is not yet
implemented.
Constant Summary collapse
- POLL =
Poll interval (seconds) for the per-fork deadline wait loop.
0.02
Class Method Summary collapse
-
.run(input: $stdin, output: $stdout, errio: $stderr) ⇒ void
Serve the protocol on the given IO pair (defaults to stdio).
Class Method Details
.run(input: $stdin, output: $stdout, errio: $stderr) ⇒ void
This method returns an undefined value.
Serve the protocol on the given IO pair (defaults to stdio). Returns on quit.
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 |
# File 'lib/mutineer/daemon_server.rb', line 54 def run(input: $stdin, output: $stdout, errio: $stderr) @errio = errio @output = output boot_line = input.gets return if boot_line.nil? # client vanished before boot boot!(JSON.parse(boot_line.strip)) output.puts(JSON.generate("ready" => true, "ruby" => RUBY_VERSION)) output.flush input.each_line do |line| line = line.strip next if line.empty? begin req = JSON.parse(line) rescue JSON::ParserError => e # A corrupt line has no id to address a reply to (and the client only # ever sends valid JSON, so it cannot be a pending request). Log and # read on rather than write an unaddressable verdict onto the channel. @errio.puts("[daemon] dropped unparseable line: #{e.}") next end break if req["cmd"] == "quit" # Build the coverage map app-side and ship it to the tool, which then # selects covering tests per mutant. One-shot control message. if req["cmd"] == "coverage" output.puts(JSON.generate(build_coverage_map)) output.flush next end output.puts(JSON.generate(run_mutant(req))) output.flush end end |