mutineer 0.11.2 → 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 +45 -0
- data/lib/mutineer/baseline.rb +14 -16
- data/lib/mutineer/cli.rb +37 -39
- 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/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
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
|