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.
@@ -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 (R14): the report goes to `out` (stdout),
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 (R20/R21). Branches on `format` ("human" | "json") and
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 (KTD7). survivors/no_coverage are sorted
94
- # by (file, line, operator) so output is byte-stable regardless of --jobs
95
- # worker finish order (R22).
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
- # C8: null (not 0.0) on an empty denominator, matching the nil-vs-0.0
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
- # #9: same shape as no_coverage; additive key.
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
- # #10: equivalent mutants the user suppressed emitted with their stable
128
- # id so the user can audit what is silenced (and copy ids for survivors
129
- # they want to add). Excluded from the score; never in `survivors`.
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
- # #11: per-source breakdown (additive; #13 consumes it). Sorted by file so
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
- # #13: additive baseline-delta block, present only with --baseline. Existing
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
- # #23: one self-contained HTML file (inline CSS, no external assets) the
144
- # overall score + summary counts, a per-source table, and every surviving
145
- # mutant with its stable id and diff. All source/diff/identifier text is
146
- # HTML-escaped (CGI.escapeHTML) so a `<`/`>` in source can never break the
147
- # markup. Reuses survivor_json/per_source_json so one run yields one set of
148
- # facts regardless of --format.
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
- # #13: the same delta facts the human report prints, for dashboards. new_survivors
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 doesn't depend on --jobs finish order.
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
- # #10: the stable, copy-pasteable id (next to the human-readable token) so a
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 what the user already suppressed.
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 (C1): Prism offsets are byte offsets; byteindex/byterindex/
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
- # #9: a broken harness, not a coverage gap report it distinctly from No coverage.
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
- # #10: equivalent mutants the user suppressed; excluded from the denominator.
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
- # #11: one line per source after the global summary, so a multi-source run
425
- # shows which file is weak. Omitted for a single-source run the global
426
- # summary already says everything (ponytail: no redundant one-line block).
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
- # #13: the --baseline delta, appended after the normal report. Names every NEW
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)
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mutineer
4
- # Immutable outcome of running one mutant. Seven 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
- # (#9), so coverage was lost. Excluded from the
13
- # denominator exactly like no_coverage, but reported
14
- # separately: it signals a broken harness (a test that
15
- # failed to run), not a genuine coverage gap.
16
- # ignored a known-equivalent mutant the user suppressed (#10), via
17
- # an inline `# mutineer:disable-line` comment or a
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 (M3/#9): both excluded from
26
- # the score denominator.
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 (#10).
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
- # (KTD-4): no-coverage, uncapturable, skipped (invalid), errored, timeout,
98
- # and ignored (#10 equivalent-mutant suppression) are each excluded and
99
- # surfaced separately — so suppressing every survivor reaches 100%. An empty
100
- # denominator yields a nil score (rendered "N/A"), never 0.0
101
- # distinguishing "no testable mutants" from "0% killed".
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
- # #11: split into { source_file => AggregateResult } so the Reporter
155
- # (per-source breakdown) and #13 (per-source roll-up / baseline diff) can
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