mutineer 0.11.1 → 0.11.3
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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +39 -4
- data/lib/mutineer/baseline.rb +14 -16
- data/lib/mutineer/cli.rb +39 -41
- data/lib/mutineer/config.rb +17 -19
- data/lib/mutineer/coverage_map.rb +54 -52
- data/lib/mutineer/daemon_backend.rb +316 -0
- data/lib/mutineer/daemon_client.rb +60 -36
- data/lib/mutineer/daemon_server.rb +65 -61
- data/lib/mutineer/external_backend.rb +119 -11
- data/lib/mutineer/file_swap.rb +16 -15
- data/lib/mutineer/rails_worker_db.rb +44 -43
- data/lib/mutineer/reporter.rb +29 -31
- data/lib/mutineer/result.rb +25 -26
- data/lib/mutineer/runner.rb +69 -284
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +23 -23
- metadata +2 -1
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mutineer
|
|
4
|
-
#
|
|
4
|
+
# Per-worker database isolation for the daemon path.
|
|
5
5
|
#
|
|
6
|
-
# Loaded APP-SIDE by {DaemonServer} (a sibling gem file, pulled in by absolute
|
|
7
|
-
# so it bypasses the app bundle
|
|
8
|
-
# `daemon_server.rb` under a bundle that has no mutineer). It uses the app's
|
|
9
|
-
# already-booted ActiveRecord and NEVER `require "active_record"
|
|
6
|
+
# Loaded APP-SIDE by {DaemonServer} (a sibling gem file, pulled in by absolute
|
|
7
|
+
# path so it bypasses the app bundle, the same trick {DaemonClient} uses to run
|
|
8
|
+
# `daemon_server.rb` under a bundle that has no mutineer). It uses the app's
|
|
9
|
+
# OWN already-booted ActiveRecord and NEVER `require "active_record"`: every
|
|
10
10
|
# method that touches AR first confirms {available?}, so the daemon core stays
|
|
11
11
|
# framework-agnostic and the gem keeps its zero-runtime-dependency promise.
|
|
12
12
|
#
|
|
13
|
-
# Isolation model
|
|
14
|
-
# forks
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
13
|
+
# Isolation model: each parallel worker gets its OWN database so concurrent
|
|
14
|
+
# forks cannot clobber each other's transactional fixtures. {after_fork} runs
|
|
15
|
+
# inside a freshly-forked child and points that child's connection at the
|
|
16
|
+
# worker's database BEFORE any test loads; transactional fixtures then
|
|
17
|
+
# repopulate that isolated database per test.
|
|
18
18
|
#
|
|
19
|
-
# Scope:
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# than silently mis-routing.
|
|
19
|
+
# Scope: SQLite adapter only (per-worker file, hermetic). Postgres per-worker
|
|
20
|
+
# DBs (`CREATE DATABASE <db>-<worker>`) are not implemented yet; a non-SQLite
|
|
21
|
+
# config raises a clear NotImplementedError rather than silently mis-routing.
|
|
23
22
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# is deferred to U6 with the parallel gate — noted, not silently skipped.
|
|
23
|
+
# Routing failures surface as `error` via {verify_connection!}. Tagging an
|
|
24
|
+
# in-test DB failure as `error` (not `killed`) is only observable under
|
|
25
|
+
# concurrent load and is not yet implemented.
|
|
28
26
|
module RailsWorkerDb
|
|
29
|
-
# True when the app has ActiveRecord loaded
|
|
30
|
-
# other method here may touch AR. Never triggers an autoload/require of
|
|
27
|
+
# True when the app has ActiveRecord loaded. The only condition under which
|
|
28
|
+
# any other method here may touch AR. Never triggers an autoload/require of
|
|
29
|
+
# AR itself.
|
|
31
30
|
#
|
|
32
31
|
# @return [Boolean]
|
|
33
32
|
def self.available?
|
|
@@ -35,8 +34,9 @@ module Mutineer
|
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
# Derive a per-worker database path from a base path by inserting `-<worker>`
|
|
38
|
-
# before the extension. Pure string transform (no AR) so it is unit-testable
|
|
39
|
-
# the zero-dep suite. `storage/test.sqlite3`, worker 1 ->
|
|
37
|
+
# before the extension. Pure string transform (no AR) so it is unit-testable
|
|
38
|
+
# in the zero-dep suite. `storage/test.sqlite3`, worker 1 ->
|
|
39
|
+
# `storage/test-1.sqlite3`.
|
|
40
40
|
#
|
|
41
41
|
# @param database [String] the base database path.
|
|
42
42
|
# @param worker [Integer] the worker slot index (0..N-1).
|
|
@@ -47,9 +47,9 @@ module Mutineer
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Build the AR connection config for one worker by copying the app's current
|
|
50
|
-
# (default test) config and swapping in the per-worker database path. SQLite
|
|
51
|
-
# this pass
|
|
52
|
-
# instead of mis-routing
|
|
50
|
+
# (default test) config and swapping in the per-worker database path. SQLite
|
|
51
|
+
# only this pass: a non-SQLite adapter raises so the SQLite-first scope fails
|
|
52
|
+
# loud instead of mis-routing.
|
|
53
53
|
#
|
|
54
54
|
# @param worker [Integer] the worker slot index.
|
|
55
55
|
# @return [Hash] a symbol-keyed AR configuration hash for the worker database.
|
|
@@ -57,16 +57,15 @@ module Mutineer
|
|
|
57
57
|
def self.worker_db_config(worker)
|
|
58
58
|
hash = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
59
59
|
adapter = hash[:adapter].to_s
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# exist until Postgres worker creation is implemented.
|
|
60
|
+
# Config shaping (per_worker_config) is already adapter-general: it derives
|
|
61
|
+
# correct SQLite and Postgres worker-DB names. What is gated is runtime
|
|
62
|
+
# provisioning: SQLite files are created on connect, but Postgres needs an
|
|
63
|
+
# explicit `CREATE DATABASE` per worker. Until that lands, refuse non-SQLite
|
|
64
|
+
# loudly rather than route to a database that does not exist.
|
|
66
65
|
unless adapter.start_with?("sqlite")
|
|
67
66
|
raise NotImplementedError,
|
|
68
67
|
"worker-DB isolation currently provisions SQLite only (got adapter #{adapter.inspect}); " \
|
|
69
|
-
"Postgres per-worker provisioning is
|
|
68
|
+
"Postgres per-worker provisioning is not yet supported. Use a SQLite test DB, or drop --jobs."
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
per_worker_config(hash, worker)
|
|
@@ -74,10 +73,10 @@ module Mutineer
|
|
|
74
73
|
|
|
75
74
|
# Pure config-shaping (no AR): given a connection config hash, return the
|
|
76
75
|
# per-worker variant with its database swapped to the worker's own name.
|
|
77
|
-
# Adapter-general
|
|
78
|
-
# and Postgres (`myapp_test`
|
|
79
|
-
# fall out of {worker_database_path}. Extracted
|
|
80
|
-
#
|
|
76
|
+
# Adapter-general: SQLite (`storage/test.sqlite3` -> `storage/test-<w>.sqlite3`)
|
|
77
|
+
# and Postgres (`myapp_test` -> `myapp_test-<w>`, Rails `parallelize` naming)
|
|
78
|
+
# both fall out of {worker_database_path}. Extracted and unit-tested so the
|
|
79
|
+
# Postgres shape is proven ready without a live database.
|
|
81
80
|
#
|
|
82
81
|
# @param config_hash [Hash] a connection config hash (symbol or string keys).
|
|
83
82
|
# @param worker [Integer] the worker slot index.
|
|
@@ -94,11 +93,12 @@ module Mutineer
|
|
|
94
93
|
hash.merge(database: worker_database_path(database, worker))
|
|
95
94
|
end
|
|
96
95
|
|
|
97
|
-
# Child-side (after fork): route this process's ActiveRecord at the worker's
|
|
98
|
-
# database and confirm it is reachable, so a routing failure reads as
|
|
99
|
-
# (via the daemon's child rescue) rather than a false verdict. Loads
|
|
100
|
-
# into the worker database when a schema path is given
|
|
101
|
-
# runs with `force: true`), covering a fresh worker
|
|
96
|
+
# Child-side (after fork): route this process's ActiveRecord at the worker's
|
|
97
|
+
# own database and confirm it is reachable, so a routing failure reads as
|
|
98
|
+
# `error` (via the daemon's child rescue) rather than a false verdict. Loads
|
|
99
|
+
# the schema into the worker database when a schema path is given
|
|
100
|
+
# (idempotent: schema.rb runs with `force: true`), covering a fresh worker
|
|
101
|
+
# file.
|
|
102
102
|
#
|
|
103
103
|
# @param worker [Integer] the worker slot index.
|
|
104
104
|
# @param schema_path [String, nil] absolute path to `db/schema.rb`, or nil to skip.
|
|
@@ -126,8 +126,9 @@ module Mutineer
|
|
|
126
126
|
$stdout = original
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
# Force a round-trip to the freshly-routed connection so a broken route fails
|
|
130
|
-
# (→ `error`) instead of later masquerading as a test failure
|
|
129
|
+
# Force a round-trip to the freshly-routed connection so a broken route fails
|
|
130
|
+
# HERE (→ `error`) instead of later masquerading as a test failure
|
|
131
|
+
# (→ false `killed`).
|
|
131
132
|
#
|
|
132
133
|
# @return [void]
|
|
133
134
|
def self.verify_connection!
|
data/lib/mutineer/reporter.rb
CHANGED
|
@@ -7,7 +7,7 @@ require_relative "mutation"
|
|
|
7
7
|
|
|
8
8
|
module Mutineer
|
|
9
9
|
# Renders an AggregateResult: the summary block, mutation score, and per-file
|
|
10
|
-
# survivor diffs. Stream discipline
|
|
10
|
+
# survivor diffs. Stream discipline: the report goes to `out` (stdout),
|
|
11
11
|
# diagnostics/warnings go to `err` (stderr), so `mutineer ... > report.txt`
|
|
12
12
|
# captures only the report.
|
|
13
13
|
#
|
|
@@ -19,7 +19,7 @@ module Mutineer
|
|
|
19
19
|
@source_map = source_map
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Single entry point
|
|
22
|
+
# Single entry point. Branches on `format` ("human" | "json" | "html") and
|
|
23
23
|
# routes the rendered report to `output` (a file, with a stderr confirmation)
|
|
24
24
|
# or to `out`. Diagnostics always go to `err`.
|
|
25
25
|
def report(out: $stdout, err: $stderr, threshold: 0.0, format: "human", output: nil, baseline: nil)
|
|
@@ -90,10 +90,9 @@ module Mutineer
|
|
|
90
90
|
|
|
91
91
|
private
|
|
92
92
|
|
|
93
|
-
# Canonical machine-readable schema
|
|
94
|
-
#
|
|
95
|
-
# worker finish order
|
|
96
|
-
# Renders the JSON report.
|
|
93
|
+
# Canonical machine-readable schema. survivors/no_coverage are sorted by
|
|
94
|
+
# (file, line, operator) so output is byte-stable regardless of --jobs
|
|
95
|
+
# worker finish order.
|
|
97
96
|
#
|
|
98
97
|
# @api private
|
|
99
98
|
# @param baseline [Mutineer::Baseline::Delta, nil] baseline delta.
|
|
@@ -101,7 +100,7 @@ module Mutineer
|
|
|
101
100
|
def json_report(baseline = nil)
|
|
102
101
|
killed = @agg.killed_count
|
|
103
102
|
survived = @agg.survived_count
|
|
104
|
-
#
|
|
103
|
+
# null (not 0.0) on an empty denominator, matching the nil-vs-0.0
|
|
105
104
|
# discipline in AggregateResult; and the SAME rounding as the human report
|
|
106
105
|
# (one run must not yield two scores by --format).
|
|
107
106
|
score = @agg.mutation_score
|
|
@@ -121,31 +120,31 @@ module Mutineer
|
|
|
121
120
|
.sort_by { |h| [h[:file], h[:line], h[:operator]] },
|
|
122
121
|
no_coverage: @agg.results.select(&:no_coverage?).map { |r| no_coverage_json(r) }
|
|
123
122
|
.sort_by { |h| [h[:file], h[:line]] },
|
|
124
|
-
#
|
|
123
|
+
# Same shape as no_coverage; additive key.
|
|
125
124
|
uncapturable: @agg.results.select(&:uncapturable?).map { |r| no_coverage_json(r) }
|
|
126
125
|
.sort_by { |h| [h[:file], h[:line]] },
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
126
|
+
# Equivalent mutants the user suppressed: emitted with their stable id so
|
|
127
|
+
# the user can audit what is silenced (and copy ids for survivors they
|
|
128
|
+
# want to add). Excluded from the score; never in `survivors`.
|
|
130
129
|
ignored: @agg.results.select(&:ignored?).map { |r| ignored_json(r) }
|
|
131
130
|
.sort_by { |h| [h[:file], h[:line], h[:operator]] },
|
|
132
|
-
#
|
|
131
|
+
# Per-source breakdown (additive; baseline consumes it). Sorted by file so
|
|
133
132
|
# output is byte-stable. Reuses AggregateResult via by_source.
|
|
134
133
|
per_source: @agg.by_source.map { |file, agg| per_source_json(file, agg) }
|
|
135
134
|
.sort_by { |h| h[:file] }
|
|
136
135
|
}
|
|
137
|
-
#
|
|
136
|
+
# Additive baseline-delta block, present only with --baseline. Existing
|
|
138
137
|
# consumers ignore the extra key; schema_version stays 1.1.
|
|
139
138
|
doc[:baseline] = baseline_json(baseline) if baseline
|
|
140
139
|
"#{JSON.generate(doc)}\n"
|
|
141
140
|
end
|
|
142
141
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
142
|
+
# One self-contained HTML file (inline CSS, no external assets): the overall
|
|
143
|
+
# score + summary counts, a per-source table, and every surviving mutant with
|
|
144
|
+
# its stable id and diff. All source/diff/identifier text is HTML-escaped
|
|
145
|
+
# (CGI.escapeHTML) so a `<`/`>` in source can never break the markup. Reuses
|
|
146
|
+
# survivor_json/per_source_json so one run yields one set of facts regardless
|
|
147
|
+
# of --format.
|
|
149
148
|
def html_report
|
|
150
149
|
score = @agg.mutation_score
|
|
151
150
|
survivors = @agg.surviving_mutants.map { |r| survivor_json(r) }
|
|
@@ -251,9 +250,9 @@ module Mutineer
|
|
|
251
250
|
# HTML-escapes any text destined for the document (stdlib CGI).
|
|
252
251
|
def esc(text) = CGI.escapeHTML(text.to_s)
|
|
253
252
|
|
|
254
|
-
#
|
|
253
|
+
# The same delta facts the human report prints, for dashboards. new_survivors
|
|
255
254
|
# reuse the ignored_json shape (subject/file/line/operator/token/id) and sort
|
|
256
|
-
# byte-stably so output
|
|
255
|
+
# byte-stably so output does not depend on --jobs finish order.
|
|
257
256
|
def baseline_json(delta)
|
|
258
257
|
{
|
|
259
258
|
regressed: delta.regressed,
|
|
@@ -300,7 +299,7 @@ module Mutineer
|
|
|
300
299
|
file: file,
|
|
301
300
|
line: start_line,
|
|
302
301
|
operator: m.operator.to_s,
|
|
303
|
-
#
|
|
302
|
+
# The stable, copy-pasteable id (next to the human-readable token) so a
|
|
304
303
|
# user can paste it straight into .mutineer.yml `ignore:`.
|
|
305
304
|
id: result.id,
|
|
306
305
|
token: token,
|
|
@@ -308,7 +307,7 @@ module Mutineer
|
|
|
308
307
|
}
|
|
309
308
|
end
|
|
310
309
|
|
|
311
|
-
# An entry under the JSON `ignored:` key
|
|
310
|
+
# An entry under the JSON `ignored:` key: what the user already suppressed.
|
|
312
311
|
def ignored_json(result)
|
|
313
312
|
m = result.mutation
|
|
314
313
|
file = result.subject.file
|
|
@@ -329,7 +328,7 @@ module Mutineer
|
|
|
329
328
|
# mutation's 1-based start line, the full original line-block it touches, the
|
|
330
329
|
# spliced mutated block, and a single-line token label for the header.
|
|
331
330
|
def diff_for(m, source)
|
|
332
|
-
# Byte math
|
|
331
|
+
# Byte math: Prism offsets are byte offsets; byteindex/byterindex/
|
|
333
332
|
# byteslice keep line splicing correct for multibyte sources.
|
|
334
333
|
line_begin = m.start_offset.zero? ? 0 : (source.byterindex("\n", m.start_offset - 1) || -1) + 1
|
|
335
334
|
line_end = source.byteindex("\n", m.end_offset) || source.bytesize
|
|
@@ -370,9 +369,9 @@ module Mutineer
|
|
|
370
369
|
out.puts format("Survived: %-6d No coverage: %d", @agg.survived_count, @agg.no_coverage_count)
|
|
371
370
|
out.puts format("Skipped: %-6d Errored: %d", @agg.skipped_invalid_count,
|
|
372
371
|
@agg.errored_count + @agg.timeout_count)
|
|
373
|
-
#
|
|
372
|
+
# A broken harness, not a coverage gap: report it distinctly from No coverage.
|
|
374
373
|
out.puts format("Uncapturable: %-6d (tests failed to run)", @agg.uncapturable_count)
|
|
375
|
-
#
|
|
374
|
+
# Equivalent mutants the user suppressed; excluded from the denominator.
|
|
376
375
|
out.puts format("Ignored: %-6d (equivalent, suppressed)", @agg.ignored_count)
|
|
377
376
|
end
|
|
378
377
|
|
|
@@ -421,10 +420,9 @@ module Mutineer
|
|
|
421
420
|
parts.join(", ")
|
|
422
421
|
end
|
|
423
422
|
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
# Writes the per-source breakdown.
|
|
423
|
+
# One line per source after the global summary, so a multi-source run shows
|
|
424
|
+
# which file is weak. Omitted for a single-source run: the global summary
|
|
425
|
+
# already says everything (no redundant one-line block).
|
|
428
426
|
#
|
|
429
427
|
# @param out [IO] output stream.
|
|
430
428
|
# @return [void]
|
|
@@ -443,7 +441,7 @@ module Mutineer
|
|
|
443
441
|
end
|
|
444
442
|
end
|
|
445
443
|
|
|
446
|
-
#
|
|
444
|
+
# The --baseline delta, appended after the normal report. Names every NEW
|
|
447
445
|
# survivor (subject (file:line) operator) and the score delta when it dropped,
|
|
448
446
|
# then a one-line REGRESSION/OK verdict so CI logs show which gate fired.
|
|
449
447
|
def baseline_section(out, delta)
|
data/lib/mutineer/result.rb
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mutineer
|
|
4
|
-
# Immutable outcome of running one mutant.
|
|
5
|
-
# killed
|
|
6
|
-
# survived
|
|
7
|
-
# error
|
|
8
|
-
# timeout
|
|
9
|
-
# skipped
|
|
10
|
-
# no_coverage
|
|
11
|
-
# uncapturable
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# ignored
|
|
17
|
-
#
|
|
4
|
+
# Immutable outcome of running one mutant. Eight distinct states:
|
|
5
|
+
# killed - a test failed/errored, so the mutation was caught.
|
|
6
|
+
# survived - every test passed, so the mutation went undetected.
|
|
7
|
+
# error - the child crashed (unhandled exception): exit status 2.
|
|
8
|
+
# timeout - the parent SIGKILLed a child that overran its wall clock.
|
|
9
|
+
# skipped - the mutated source failed to re-parse (invalid); no fork.
|
|
10
|
+
# no_coverage - no test exercises the mutated line; not run, not scored.
|
|
11
|
+
# uncapturable - the line's would-be covering test errored during capture,
|
|
12
|
+
# so coverage was lost. Excluded from the denominator exactly
|
|
13
|
+
# like no_coverage, but reported separately: it signals a
|
|
14
|
+
# broken harness (a test that failed to run), not a genuine
|
|
15
|
+
# coverage gap.
|
|
16
|
+
# ignored - a known-equivalent mutant the user suppressed, via an
|
|
17
|
+
# inline `# mutineer:disable-line` comment or a
|
|
18
18
|
# `.mutineer.yml` `ignore:` id. A pre-fork classification
|
|
19
19
|
# (never run); excluded from the denominator so a strong
|
|
20
20
|
# file can reach 100%.
|
|
@@ -22,14 +22,14 @@ module Mutineer
|
|
|
22
22
|
# `error` and `skipped` are deliberately distinct: skipped is a pre-fork
|
|
23
23
|
# validity failure (counted separately by the reporter), error is a runtime
|
|
24
24
|
# crash. Never conflate them via `details` string parsing. `no_coverage` and
|
|
25
|
-
# `uncapturable` are pre-fork selection results
|
|
26
|
-
#
|
|
25
|
+
# `uncapturable` are pre-fork selection results: both excluded from the score
|
|
26
|
+
# denominator.
|
|
27
27
|
#
|
|
28
28
|
# `subject`, `mutation`, and `id` are nil when the Result is built by
|
|
29
29
|
# Isolation/Runner (which only know the outcome); the orchestrator attaches
|
|
30
30
|
# them afterwards via `result.with(subject:, mutation:, id:)` so the Reporter
|
|
31
31
|
# can render survivor diffs and emit the stable id. `id` is the content-based
|
|
32
|
-
# MutantId
|
|
32
|
+
# MutantId.
|
|
33
33
|
Result = Data.define(:status, :details, :subject, :mutation, :id) do
|
|
34
34
|
# Builds a killed result.
|
|
35
35
|
#
|
|
@@ -93,12 +93,12 @@ module Mutineer
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Aggregates a flat list of Results into counts, the mutation score, and the
|
|
96
|
-
# surviving-mutant list. The score denominator is killed + survived ONLY
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
96
|
+
# surviving-mutant list. The score denominator is killed + survived ONLY:
|
|
97
|
+
# no-coverage, uncapturable, skipped (invalid), errored, timeout, and ignored
|
|
98
|
+
# (equivalent-mutant suppression) are each excluded and surfaced separately,
|
|
99
|
+
# so suppressing every survivor reaches 100%. An empty denominator yields a
|
|
100
|
+
# nil score (rendered "N/A"), never 0.0, distinguishing "no testable mutants"
|
|
101
|
+
# from "0% killed".
|
|
102
102
|
class AggregateResult
|
|
103
103
|
attr_reader :results
|
|
104
104
|
|
|
@@ -151,9 +151,8 @@ module Mutineer
|
|
|
151
151
|
# @return [Array<Mutineer::Result>] surviving results.
|
|
152
152
|
def surviving_mutants = @results.select(&:survived?)
|
|
153
153
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
# reuse the same aggregate math.
|
|
154
|
+
# Groups results by source file so the Reporter (per-source breakdown) and
|
|
155
|
+
# baseline diff can reuse the same aggregate math.
|
|
157
156
|
#
|
|
158
157
|
# @return [Hash<String, Mutineer::AggregateResult>] source-file groups.
|
|
159
158
|
def by_source
|