mutineer-mongoid 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- metadata +143 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
require_relative "result"
|
|
5
|
+
require_relative "coverage_map"
|
|
6
|
+
require_relative "daemon_client"
|
|
7
|
+
require_relative "progress"
|
|
8
|
+
require_relative "job_plan"
|
|
9
|
+
|
|
10
|
+
module Mutineer
|
|
11
|
+
# Daemon execution backend. Boots the app ONCE in a persistent subprocess under
|
|
12
|
+
# the app's own bundle and forks per mutant, so a Rails run pays the boot cost
|
|
13
|
+
# once instead of per mutant. Tool-side this only discovers jobs and builds the
|
|
14
|
+
# ready-to-`load` payload (Prism); the daemon needs no Prism/mutineer.
|
|
15
|
+
#
|
|
16
|
+
# When jobs > 1 each worker runs against its OWN database, which is what makes
|
|
17
|
+
# `--jobs N` safe under Rails (#26): parallel verdicts are identical to serial.
|
|
18
|
+
#
|
|
19
|
+
# Job collection, `--since` filtering and coverage selection live in {JobPlan},
|
|
20
|
+
# which the in-process path calls too, so the daemon path can never drift from
|
|
21
|
+
# it on which mutants run or which tests narrow a mutant (score parity).
|
|
22
|
+
#
|
|
23
|
+
# Unlike {ExternalBackend}, which is a leaf {Runner} calls into, this module owns
|
|
24
|
+
# its orchestration and takes that shared vocabulary from {JobPlan}.
|
|
25
|
+
module DaemonBackend
|
|
26
|
+
# Default per-mutant timeout on the daemon path (seconds), overridden by
|
|
27
|
+
# config.daemon_timeout. Coverage narrowing usually keeps each job short; this
|
|
28
|
+
# still covers a slow suite or full-suite fallback when the map is unavailable.
|
|
29
|
+
# Named like its in-process counterpart {Isolation::DEFAULT_TIMEOUT}, not like
|
|
30
|
+
# {ExternalBackend::SMOKE_TIMEOUT}, which bounds a different thing.
|
|
31
|
+
DEFAULT_TIMEOUT = 60
|
|
32
|
+
|
|
33
|
+
# The daemon's per-mutant tempfile, written into the source dir so
|
|
34
|
+
# require_relative resolves. Kept in step with DaemonServer#sweep_temps.
|
|
35
|
+
DAEMON_TEMP_GLOB = "mutineer_daemon*.rb"
|
|
36
|
+
|
|
37
|
+
# Full daemon run: collect jobs, build the coverage map once, then execute
|
|
38
|
+
# serially or across N worker daemons. Fail-fast forces serial so the survivor
|
|
39
|
+
# set matches jobs 1.
|
|
40
|
+
#
|
|
41
|
+
# @param config [Mutineer::Config] run configuration (daemon set).
|
|
42
|
+
# @param operator_classes [Array<Class>] resolved operators.
|
|
43
|
+
# @return [Array(Mutineer::AggregateResult, Hash<String,String>, Hash)] aggregate,
|
|
44
|
+
# source map, and the {JobPlan.collect_jobs} extras.
|
|
45
|
+
def self.execute(config, operator_classes)
|
|
46
|
+
jobs, ignored_results, source_map, extras = JobPlan.collect_jobs(config, operator_classes)
|
|
47
|
+
jobs = JobPlan.filter_since(jobs, source_map, config) if config.since
|
|
48
|
+
abs_tests = config.tests.map { |t| File.expand_path(t, config.project_root) }
|
|
49
|
+
|
|
50
|
+
# Nothing to mutate (`--since` matched no changed line, or every mutant is
|
|
51
|
+
# suppressed). Return before booting anything: the coverage daemon and the
|
|
52
|
+
# worker daemons below each boot the whole app, and README documents
|
|
53
|
+
# `--since origin/<base>` for PR CI, where a docs-only PR is routine.
|
|
54
|
+
if jobs.empty?
|
|
55
|
+
# The daemon sweeps orphaned temps at boot and nothing boots here, so sweep
|
|
56
|
+
# tool-side. A file a hard-killed run left in app/models breaks the app's own
|
|
57
|
+
# Zeitwerk boot, not just Mutineer's next run.
|
|
58
|
+
JobPlan.sweep_orphans(JobPlan.source_dirs(config), DAEMON_TEMP_GLOB)
|
|
59
|
+
return [AggregateResult.new(ignored_results), source_map, extras]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build the coverage map once (app-side). nil when the build fails: runners
|
|
63
|
+
# fall back to the full --test set (and emit a stderr warning) rather than
|
|
64
|
+
# mis-scoring everything as no_coverage.
|
|
65
|
+
coverage_map = build_coverage_map(config, abs_tests)
|
|
66
|
+
|
|
67
|
+
# Worker count = resolved --jobs, capped at the job count (no idle daemons).
|
|
68
|
+
# >1 → N concurrent daemon handles, each on its OWN worker DB (N-handles, the
|
|
69
|
+
# spike-proven shape). 1 → the serial single-daemon path. --fail-fast forces
|
|
70
|
+
# serial: parallel's stop flag fires on the first survivor by WALL-CLOCK, not
|
|
71
|
+
# input index, so the verdict set would diverge from serial (a different,
|
|
72
|
+
# non-deterministic survivor set/score). The "identical to --jobs 1" guarantee
|
|
73
|
+
# below only holds when fail-fast cannot race.
|
|
74
|
+
worker_count = [config.jobs || 1, 1].max
|
|
75
|
+
worker_count = 1 if config.fail_fast
|
|
76
|
+
worker_count = [worker_count, jobs.size].min
|
|
77
|
+
|
|
78
|
+
results =
|
|
79
|
+
if worker_count > 1
|
|
80
|
+
run_parallel(jobs, worker_count, config, abs_tests, coverage_map, source_map)
|
|
81
|
+
else
|
|
82
|
+
run_serial(jobs, config, abs_tests, coverage_map, source_map)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[AggregateResult.new(results + ignored_results), source_map, extras]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build the coverage map via a short-lived daemon (boots the app once, captures
|
|
89
|
+
# per-test coverage app-side, ships the map back). Returns a query-only
|
|
90
|
+
# CoverageMap, or nil when the build fails / returns empty. Callers then run the
|
|
91
|
+
# full --test set. An empty map that came with boot lines returns a map with
|
|
92
|
+
# those lines only: it narrows nothing, but still classifies `ran_at_load`.
|
|
93
|
+
# Each capture is bounded by capture_timeout (#101); the client's wait for
|
|
94
|
+
# the whole map is not, because its length grows with the number of test
|
|
95
|
+
# files. A normal nonempty map scores like in-process;
|
|
96
|
+
# nil falls back to the full suite (more testing, not comparable).
|
|
97
|
+
#
|
|
98
|
+
# @param config [Mutineer::Config] the run config.
|
|
99
|
+
# @param abs_tests [Array<String>] absolute --test paths.
|
|
100
|
+
# @return [Mutineer::CoverageMap, nil]
|
|
101
|
+
def self.build_coverage_map(config, abs_tests)
|
|
102
|
+
client = DaemonClient.new(boot: boot_config(config, abs_tests, coverage: true),
|
|
103
|
+
app_root: config.project_root).start
|
|
104
|
+
data = begin
|
|
105
|
+
client.coverage
|
|
106
|
+
ensure
|
|
107
|
+
client.quit
|
|
108
|
+
end
|
|
109
|
+
# A red unmutated suite must abort, even when the shipped map is empty.
|
|
110
|
+
# Falling back to the full --test set would treat those failures as kills.
|
|
111
|
+
if data.is_a?(Hash) && Array(data["failed_clean_tests"]).any?
|
|
112
|
+
JobPlan.abort_if_unclean!(CoverageMap.from_data(
|
|
113
|
+
map: data["map"] || {},
|
|
114
|
+
failed_test_files: data["failed_test_files"] || [],
|
|
115
|
+
project_root: config.project_root,
|
|
116
|
+
failed_clean_tests: data["failed_clean_tests"]
|
|
117
|
+
))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
unless data && !(data["map"] || {}).empty?
|
|
121
|
+
reason = data.is_a?(Hash) && data["error"] ? data["error"] : "empty map"
|
|
122
|
+
warn_coverage_fallback(reason)
|
|
123
|
+
# No narrowing ({job_result} runs every test), but the lines and
|
|
124
|
+
# methods that ran at boot still classify a survivor on one of them.
|
|
125
|
+
load_lines = data.is_a?(Hash) ? Array(data["load_lines"]) : []
|
|
126
|
+
load_methods = data.is_a?(Hash) ? Array(data["load_methods"]) : []
|
|
127
|
+
return nil if load_lines.empty? && load_methods.empty?
|
|
128
|
+
|
|
129
|
+
return CoverageMap.from_data(map: {}, failed_test_files: [], project_root: config.project_root,
|
|
130
|
+
load_lines: load_lines, load_methods: load_methods)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
CoverageMap.from_data(map: data["map"], failed_test_files: data["failed_test_files"] || [],
|
|
134
|
+
project_root: config.project_root, load_lines: data["load_lines"] || [],
|
|
135
|
+
load_methods: data["load_methods"] || [])
|
|
136
|
+
rescue DaemonBootTimeout
|
|
137
|
+
raise # a second daemon for the mutant runs would hang just as long
|
|
138
|
+
rescue DaemonBootError => e
|
|
139
|
+
warn_coverage_fallback("#{e.class}: #{e.message}")
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Stderr note when daemon coverage is unavailable (full --test set per mutant).
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
146
|
+
# @param reason [String] short cause (boot error message, empty map, …).
|
|
147
|
+
# @return [void]
|
|
148
|
+
def self.warn_coverage_fallback(reason = "unknown")
|
|
149
|
+
warn "[mutineer] daemon coverage map unavailable (#{reason}); running every " \
|
|
150
|
+
"mutant against the full --test set (score not comparable to an in-process run)."
|
|
151
|
+
end
|
|
152
|
+
private_class_method :warn_coverage_fallback
|
|
153
|
+
|
|
154
|
+
# Serial path: one daemon (worker 0), one mutant at a time. Honors --fail-fast
|
|
155
|
+
# (stop at the first survivor).
|
|
156
|
+
#
|
|
157
|
+
# @api private
|
|
158
|
+
# @return [Array<Mutineer::Result>] results in input order.
|
|
159
|
+
def self.run_serial(jobs, config, abs_tests, coverage_map, source_map)
|
|
160
|
+
client = DaemonClient.new(boot: boot_config(config, abs_tests),
|
|
161
|
+
app_root: config.project_root).start
|
|
162
|
+
results = []
|
|
163
|
+
progress = Progress.new(jobs.size)
|
|
164
|
+
begin
|
|
165
|
+
jobs.each_with_index do |job, i|
|
|
166
|
+
r = job_result(job, i, client, 0, config, coverage_map, abs_tests, source_map)
|
|
167
|
+
results << r
|
|
168
|
+
progress.tick
|
|
169
|
+
break if config.fail_fast && r.survived?
|
|
170
|
+
end
|
|
171
|
+
ensure
|
|
172
|
+
client.quit
|
|
173
|
+
end
|
|
174
|
+
results
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Parallel path: N daemon handles, each pinned to its own worker slot (own DB).
|
|
178
|
+
# A shared queue of job indices feeds N tool-side threads; results are placed by
|
|
179
|
+
# input index so the verdict set matches serial. Callers must not pass fail_fast
|
|
180
|
+
# here ({execute} forces serial for fail-fast). Per-mutant crashes are classified
|
|
181
|
+
# in {job_result}, shared with the serial path; a {DaemonBootError} ends the run
|
|
182
|
+
# here rather than scoring the remainder against a daemon that has given up.
|
|
183
|
+
#
|
|
184
|
+
# @api private
|
|
185
|
+
# @return [Array<Mutineer::Result>] one result per input job, in input order.
|
|
186
|
+
def self.run_parallel(jobs, worker_count, config, abs_tests, coverage_map, source_map)
|
|
187
|
+
results = Array.new(jobs.size)
|
|
188
|
+
progress = Progress.new(jobs.size)
|
|
189
|
+
queue = Queue.new
|
|
190
|
+
jobs.each_index { |i| queue << i }
|
|
191
|
+
|
|
192
|
+
# Built one at a time so a refused spawn part-way (EMFILE under a high --jobs)
|
|
193
|
+
# can still quit the daemons already up. Array.new would lose every reference.
|
|
194
|
+
clients = []
|
|
195
|
+
begin
|
|
196
|
+
worker_count.times do
|
|
197
|
+
clients << DaemonClient.new(boot: boot_config(config, abs_tests),
|
|
198
|
+
app_root: config.project_root).start
|
|
199
|
+
end
|
|
200
|
+
rescue StandardError
|
|
201
|
+
clients.each(&:quit)
|
|
202
|
+
raise
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
clients.each_with_index.map do |client, worker|
|
|
206
|
+
Thread.new do
|
|
207
|
+
# The abort below is re-raised by join and reported once there; without
|
|
208
|
+
# this Ruby also dumps the thread's backtrace, which the serial path never
|
|
209
|
+
# does. Same fault, same output, whatever --jobs is set to.
|
|
210
|
+
Thread.current.report_on_exception = false
|
|
211
|
+
loop do
|
|
212
|
+
i = begin
|
|
213
|
+
queue.pop(true)
|
|
214
|
+
rescue ThreadError
|
|
215
|
+
break
|
|
216
|
+
end
|
|
217
|
+
results[i] = job_result(jobs[i], i, client, worker, config, coverage_map, abs_tests, source_map)
|
|
218
|
+
progress.tick
|
|
219
|
+
end
|
|
220
|
+
rescue DaemonBootError
|
|
221
|
+
# The daemon gave up for good. Stop feeding the other workers rather
|
|
222
|
+
# than letting them score the rest of the run against a dead client;
|
|
223
|
+
# Thread#join re-raises this and ends the run.
|
|
224
|
+
queue.clear
|
|
225
|
+
raise
|
|
226
|
+
ensure
|
|
227
|
+
client.quit
|
|
228
|
+
end
|
|
229
|
+
end.each(&:join)
|
|
230
|
+
|
|
231
|
+
# Every job was popped by some worker and every pop assigns, so no slot can
|
|
232
|
+
# be nil here: an escaping exception aborts the run via join instead.
|
|
233
|
+
results
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Build the payload for one job, run it on the given daemon/worker, and attach
|
|
237
|
+
# the subject/mutation/id. Shared body of both daemon paths, so `--jobs 1` and
|
|
238
|
+
# `--jobs N` classify an identical fault identically.
|
|
239
|
+
#
|
|
240
|
+
# Error model, in one place because both paths call this: a crash while running
|
|
241
|
+
# ONE mutant is {DaemonClient}'s business: it respawns and answers `"error"`.
|
|
242
|
+
# Nothing is caught here on purpose — anything reaching this far is either
|
|
243
|
+
# {DaemonBootError}, which must end the run, or a defect, which must stay visible.
|
|
244
|
+
#
|
|
245
|
+
# @param job [Array(Mutineer::Subject, Mutineer::Mutation, String)] the work item.
|
|
246
|
+
# @param req_id [Integer] request id (echoed back for IPC ordering safety).
|
|
247
|
+
# @param client [Mutineer::DaemonClient] the daemon handle to run on.
|
|
248
|
+
# @param worker [Integer] the worker slot (→ worker DB) this daemon routes to.
|
|
249
|
+
# @api private
|
|
250
|
+
# @raise [Mutineer::DaemonBootError] when the daemon has given up; ends the run.
|
|
251
|
+
# @return [Mutineer::Result] the decorated result.
|
|
252
|
+
def self.job_result(job, req_id, client, worker, config, coverage_map, abs_tests, source_map)
|
|
253
|
+
subject, mutation, id = job
|
|
254
|
+
source = source_map[subject.file]
|
|
255
|
+
mutated = mutation.apply(source)
|
|
256
|
+
# Skip an invalid mutant tool-side: never ship a payload that would fail to
|
|
257
|
+
# load and read as a false `killed`.
|
|
258
|
+
# Narrow to covering tests (shared with the in-process path via
|
|
259
|
+
# JobPlan.coverage_selection, so scores match). :verdict = no_coverage/uncapturable,
|
|
260
|
+
# no fork. No map, or an empty one (build failed) → run the full --test set
|
|
261
|
+
# (fallback, not narrowed).
|
|
262
|
+
sel = coverage_map && !coverage_map.map.empty? && JobPlan.coverage_selection(subject.file, mutation, subject, source, coverage_map)
|
|
263
|
+
r =
|
|
264
|
+
if Parser.parse_string(mutated).errors.any?
|
|
265
|
+
Result.skipped
|
|
266
|
+
elsif sel && sel[0] == :verdict
|
|
267
|
+
sel[1]
|
|
268
|
+
else
|
|
269
|
+
verdict = client.request(
|
|
270
|
+
id: req_id, worker: worker, timeout: config.daemon_timeout || DEFAULT_TIMEOUT,
|
|
271
|
+
payload: { "code" => mutated, "source_file" => File.expand_path(subject.file, config.project_root) },
|
|
272
|
+
tests: sel ? sel[1] : abs_tests
|
|
273
|
+
)
|
|
274
|
+
# A survivor whose line ran at load, as in-process (Runner.run).
|
|
275
|
+
JobPlan.load_verdict(result_for(verdict), subject.file, mutation, subject, source, coverage_map)
|
|
276
|
+
end
|
|
277
|
+
r.with(subject: subject, mutation: mutation, id: id)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# The boot config the daemon needs to boot the app once: where to boot, the
|
|
281
|
+
# --require files to load after it, the test load roots (so
|
|
282
|
+
# `require "test_helper"` resolves in every fork), framework, and whether this
|
|
283
|
+
# is Rails.
|
|
284
|
+
#
|
|
285
|
+
# @param config [Mutineer::Config] the run config.
|
|
286
|
+
# @param abs_tests [Array<String>] absolute --test paths.
|
|
287
|
+
# @param coverage [Boolean] whether this daemon builds the coverage map.
|
|
288
|
+
# @return [Hash] the boot config shipped to the daemon.
|
|
289
|
+
def self.boot_config(config, abs_tests, coverage: false)
|
|
290
|
+
{
|
|
291
|
+
project_root: config.project_root,
|
|
292
|
+
boot: File.expand_path(config.boot || "config/environment", config.project_root),
|
|
293
|
+
# Required after the boot, as in-process (Runner.execute); also part of
|
|
294
|
+
# the coverage digest.
|
|
295
|
+
require_paths: config.require_paths.map { |f| File.expand_path(f, config.project_root) },
|
|
296
|
+
load_paths: JobPlan.test_load_roots(abs_tests),
|
|
297
|
+
cache_dir: File.expand_path(config.cache_dir, config.project_root),
|
|
298
|
+
source_dirs: JobPlan.source_dirs(config), # so the daemon can sweep orphan mutant temps
|
|
299
|
+
framework: config.framework,
|
|
300
|
+
rails: config.rails,
|
|
301
|
+
# Schema for per-worker DB isolation. Sent when present; the daemon loads
|
|
302
|
+
# it over a worker's copy of the test DB only when that copy is out of date.
|
|
303
|
+
schema: schema_path(config),
|
|
304
|
+
# Coverage narrowing. Only the short-lived map-building daemon starts
|
|
305
|
+
# Coverage (before boot); worker daemons boot with it OFF (no wasted
|
|
306
|
+
# instrumentation/memory across every mutant fork). `sources`/`tests` are the
|
|
307
|
+
# map-build inputs.
|
|
308
|
+
coverage: coverage,
|
|
309
|
+
# The map-building daemon's capture limit (--capture-timeout); nil = default.
|
|
310
|
+
capture_timeout: config.capture_timeout,
|
|
311
|
+
sources: config.sources.map { |s| File.expand_path(s, config.project_root) },
|
|
312
|
+
tests: abs_tests
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Absolute path to the app's `db/schema.rb` if it exists, else nil. Each worker
|
|
317
|
+
# DB starts as a copy of the test DB; the daemon loads this file over the copy
|
|
318
|
+
# when the copy's schema differs. `structure.sql` apps get nil and keep the
|
|
319
|
+
# copy as it is.
|
|
320
|
+
#
|
|
321
|
+
# @param config [Mutineer::Config] the run config.
|
|
322
|
+
# @api private
|
|
323
|
+
# @return [String, nil] absolute schema path or nil.
|
|
324
|
+
def self.schema_path(config)
|
|
325
|
+
path = File.expand_path("db/schema.rb", config.project_root)
|
|
326
|
+
File.exist?(path) ? path : nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Map a daemon verdict string to a Result. The daemon reports the four
|
|
330
|
+
# run-time states it can decide; pre-fork states (skipped/no_coverage/…) are
|
|
331
|
+
# resolved tool-side before a request is ever sent.
|
|
332
|
+
#
|
|
333
|
+
# @param verdict [String] the daemon's verdict word.
|
|
334
|
+
# @api private
|
|
335
|
+
# @return [Mutineer::Result] the matching result.
|
|
336
|
+
def self.result_for(verdict)
|
|
337
|
+
case verdict
|
|
338
|
+
when "survived" then Result.survived
|
|
339
|
+
when "killed" then Result.killed
|
|
340
|
+
when "timeout" then Result.timeout
|
|
341
|
+
else Result.error("daemon verdict: #{verdict}")
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The module's contract is {execute} (the backend entry point) plus the two the
|
|
346
|
+
# tests drive directly: {boot_config} from the zero-dep suite and
|
|
347
|
+
# {build_coverage_map} from the daemon suite. Everything else is daemon-pipeline
|
|
348
|
+
# internals with no caller outside this file.
|
|
349
|
+
private_class_method :run_serial, :run_parallel, :job_result, :schema_path, :result_for
|
|
350
|
+
end
|
|
351
|
+
end
|