Class: Mutineer::Reporter
- Inherits:
-
Object
- Object
- Mutineer::Reporter
- Defined in:
- lib/mutineer/reporter.rb
Overview
Renders an AggregateResult: the summary block, mutation score, and per-file
survivor diffs. Stream discipline: the report goes to out (stdout),
diagnostics/warnings go to err (stderr), so mutineer ... > report.txt
captures only the report.
source_map is { file_path => raw source string }, used to extract the
containing source line for each survivor diff.
Constant Summary collapse
- BROKEN_SHARE_LIMIT =
Share of attempted mutants that may produce no verdict before
--thresholdstops trusting the score. A handful of flaky mutants in a large run is noise; a mostly-broken run is not a score. Deliberately not a flag: no one has needed a different number yet. 0.10- BROKEN_FLOOR =
A share alone gives a small run no tolerance at all: on a
--sincePR that yields 8 mutants, one timeout is 12.5%. README recommends exactly that workflow, so one bad mutant never fails the gate on its own, at any size. 1
Instance Method Summary collapse
-
#exit_code(threshold:) ⇒ Object
0 pass / 1 below threshold or untestable-with-errors.
-
#human_report(out, err, threshold) ⇒ void
Renders the human report.
-
#initialize(aggregate, source_map) ⇒ Reporter
constructor
A new instance of Reporter.
-
#report(out: $stdout, err: $stderr, threshold: 0.0, format: "human", output: nil, baseline: nil) ⇒ Object
Single entry point.
Constructor Details
#initialize(aggregate, source_map) ⇒ Reporter
Returns a new instance of Reporter.
28 29 30 31 |
# File 'lib/mutineer/reporter.rb', line 28 def initialize(aggregate, source_map) @agg = aggregate @source_map = source_map end |
Instance Method Details
#exit_code(threshold:) ⇒ Object
0 pass / 1 below threshold or untestable-with-errors. Usage errors (exit 2) are the CLI's job. When the score is nil (nothing killed or survived), pure no_coverage / all-ignored / empty still skip the gate; if any mutant was errored, timed out, or uncapturable, fail the gate so a broken harness cannot green CI under --threshold.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/mutineer/reporter.rb', line 102 def exit_code(threshold:) return 0 if threshold.nil? || threshold <= 0 score = @agg.mutation_score if score.nil? broken = @agg.errored_count + @agg.timeout_count + @agg.uncapturable_count return 1 if @agg.total.positive? && broken.positive? return 0 # pure no_coverage / ignored / empty — gate skipped end # A score computed over a small slice of what was attempted is not this # suite's score. Without this, 90 errored mutants and 10 that ran (9 killed) # reports 90% and exits 0, so CI cannot tell a complete run from a broken one. return 1 if broken_share_exceeded? score >= threshold ? 0 : 1 end |
#human_report(out, err, threshold) ⇒ void
This method returns an undefined value.
Renders the human report.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/mutineer/reporter.rb', line 78 def human_report(out, err, threshold) if @agg.total.zero? err.puts "No mutations generated — verify target files contain in-scope " \ "operators and are reached by the suite." return end out.puts "Mutineer — Mutation Results" out.puts "=========================" out.puts summary(out) out.puts score_line(out, err) per_source(out) survivors(out) verdict(out, threshold) if threshold && threshold.positive? end |
#report(out: $stdout, err: $stderr, threshold: 0.0, format: "human", output: nil, baseline: nil) ⇒ Object
Single entry point. Branches on format ("human" | "json" | "html") and
routes the rendered report to output (a file, with a stderr confirmation)
or to out. Diagnostics always go to err.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/mutineer/reporter.rb', line 36 def report(out: $stdout, err: $stderr, threshold: 0.0, format: "human", output: nil, baseline: nil) rendered = if format == "json" json_report(baseline) elsif format == "html" html_report else sio = StringIO.new human_report(sio, err, threshold) baseline_section(sio, baseline) if baseline sio.string end if output abs = File.(output) File.write(abs, rendered) err.puts "Report written to #{abs}" else out.print rendered end # Both ways a run can fail the gate on completeness, said here rather than in # the human renderer: --format json is the documented CI path, and a run that # exits 1 must say why on every format, not only the one a person reads. return unless threshold&.positive? if @agg.mutation_score.nil? && broken_nil_score? err.puts "[mutineer] nothing could be scored (#{broken_counts_detail}), so the " \ "--threshold gate fails. See no_verdict[] in --format json for the cause of each." elsif broken_share_exceeded? err.puts "[mutineer] #{no_verdict_ratio}: #{broken_counts_detail}. The score covers " \ "only part of the run, so the --threshold gate fails. See no_verdict[] in " \ "--format json for the cause of each." end end |