Class: Henitai::Reporter::Terminal
- Includes:
- UnparseHelper
- Defined in:
- lib/henitai/reporter.rb,
sig/henitai.rbs
Overview
Terminal reporter.
Constant Summary collapse
- PROGRESS_GLYPHS =
{ killed: "·", survived: "S", timeout: "T", ignored: "I" }.freeze
Instance Attribute Summary collapse
-
#color_enabled ⇒ Boolean
readonly
Returns the value of attribute color_enabled.
Attributes inherited from Base
Instance Method Summary collapse
- #append_survivor_stats(lines, result) ⇒ void
- #colorize(text, color) ⇒ String
- #count_status(result, status) ⇒ Integer
-
#display_mutated_source(mutant) ⇒ String
Prefer the mutant's memoized unparse (shared with the JSON reporter); string literals keep the visible-escape rendering below.
-
#display_unparse(node) ⇒ String
Like safe_unparse but makes invisible characters visible in terminal output.
-
#empty_since_scope_line(result) ⇒ String?
A zero-mutant summary with every counter at 0 and n/a scores is baffling without its cause; name it when the run was scoped by --since.
-
#executed_only_score_line(result) ⇒ String?
Cached verdicts make the combined MS/MSI partly synthetic; the executed-only pair keeps this run's own signal visible.
- #flush ⇒ void
- #format_duration(duration) ⇒ String
- #format_percent(value) ⇒ String
- #format_row(label, value) ⇒ String
- #full_summary_lines(result) ⇒ Array[String]
-
#initialize(config:, history_store: nil, color_enabled: !ENV.key?("NO_COLOR"))) ⇒ Terminal
constructor
A new instance of Terminal.
- #mutated_line(mutant) ⇒ String
- #original_line(mutant) ⇒ String
- #partial_summary_lines(result) ⇒ Array[String]
- #progress(mutant, scenario_result: nil) ⇒ void
- #report(result) ⇒ void
- #report_lines(result) ⇒ Array[String]
- #reused_mutants(result) ⇒ Array[Mutant]
- #reused_verdicts_line(result) ⇒ String?
- #scenario_output(scenario_result) ⇒ String
- #score_color(score) ⇒ String?
- #score_line(result) ⇒ String
- #should_show_logs?(scenario_result) ⇒ Boolean
-
#source_slice(node) ⇒ String?
Single-line original source of a parsed node; nil for synthetic nodes or multi-line slices (those fall back to normalized unparsing).
- #str_node?(node) ⇒ Boolean
- #summary_lines(result) ⇒ Array[String]
- #survived_detail_lines(result) ⇒ Array[String]
- #survived_mutant_header(mutant) ⇒ String
- #survived_mutant_lines(mutant) ⇒ Array[String]
Methods included from UnparseHelper
#fallback_source, #safe_unparse
Methods inherited from Base
#authoritative?, #canonical_path
Constructor Details
#initialize(config:, history_store: nil, color_enabled: !ENV.key?("NO_COLOR"))) ⇒ Terminal
Returns a new instance of Terminal.
87 88 89 90 |
# File 'lib/henitai/reporter.rb', line 87 def initialize(config:, history_store: nil, color_enabled: !ENV.key?("NO_COLOR")) super(config:, history_store:) @color_enabled = color_enabled end |
Instance Attribute Details
#color_enabled ⇒ Boolean (readonly)
Returns the value of attribute color_enabled.
112 113 114 |
# File 'lib/henitai/reporter.rb', line 112 def color_enabled @color_enabled end |
Instance Method Details
#append_survivor_stats(lines, result) ⇒ void
This method returns an undefined value.
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/henitai/reporter.rb', line 196 def append_survivor_stats(lines, result) return unless result.respond_to?(:survivor_stats) stats = result.survivor_stats # : Hash[Symbol, untyped]? return unless stats lines << format_row("Matched", stats.fetch(:matched)) lines << format_row("Skipped", stats.fetch(:skipped_count, 0)) lines << format_row("Unmatched", stats.fetch(:unmatched_count)) lines << format_row("Drift warning", stats.fetch(:drift_warning) ? "yes" : "no") end |
#colorize(text, color) ⇒ String
319 320 321 322 323 |
# File 'lib/henitai/reporter.rb', line 319 def colorize(text, color) return text unless color_enabled "\e[#{color}m#{text}\e[0m" end |
#count_status(result, status) ⇒ Integer
294 295 296 |
# File 'lib/henitai/reporter.rb', line 294 def count_status(result, status) result.mutants.count { |mutant| mutant.status == status } end |
#display_mutated_source(mutant) ⇒ String
Prefer the mutant's memoized unparse (shared with the JSON reporter); string literals keep the visible-escape rendering below.
242 243 244 245 246 247 248 |
# File 'lib/henitai/reporter.rb', line 242 def display_mutated_source(mutant) node = mutant.mutated_node return node.children.first.inspect if str_node?(node) return mutant.mutated_source if mutant.respond_to?(:mutated_source) display_unparse(node) end |
#display_unparse(node) ⇒ String
Like safe_unparse but makes invisible characters visible in terminal output. For string literal nodes the inner value is shown via #inspect so that e.g. "" vs " " vs "\n" are unambiguous. Nodes parsed from real source render their original source slice — unparsing is expensive and this path runs once per survivor; synthetic nodes unparse as before.
255 256 257 258 259 |
# File 'lib/henitai/reporter.rb', line 255 def display_unparse(node) return node.children.first.inspect if str_node?(node) source_slice(node) || safe_unparse(node) end |
#empty_since_scope_line(result) ⇒ String?
A zero-mutant summary with every counter at 0 and n/a scores is baffling without its cause; name it when the run was scoped by --since.
147 148 149 150 151 152 |
# File 'lib/henitai/reporter.rb', line 147 def empty_since_scope_line(result) return nil unless result.mutants.empty? return nil unless result.respond_to?(:since) && result.since "No mutants: no configured source files changed since #{result.since}." end |
#executed_only_score_line(result) ⇒ String?
Cached verdicts make the combined MS/MSI partly synthetic; the executed-only pair keeps this run's own signal visible.
173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/henitai/reporter.rb', line 173 def executed_only_score_line(result) return nil unless result.respond_to?(:executed_scoring_summary) summary = result.executed_scoring_summary # : Hash[Symbol, untyped]? return nil unless summary format( "Executed-only MS %<score>s | MSI %<indicator>s", score: format_percent(summary[:mutation_score]), indicator: format_percent(summary[:mutation_score_indicator]) ) end |
#flush ⇒ void
This method returns an undefined value.
335 336 337 |
# File 'lib/henitai/reporter.rb', line 335 def flush $stdout.flush end |
#format_duration(duration) ⇒ String
298 299 300 |
# File 'lib/henitai/reporter.rb', line 298 def format_duration(duration) format("%.2fs", duration) end |
#format_percent(value) ⇒ String
302 303 304 |
# File 'lib/henitai/reporter.rb', line 302 def format_percent(value) value.nil? ? "n/a" : format("%.2f%%", value) end |
#format_row(label, value) ⇒ String
290 291 292 |
# File 'lib/henitai/reporter.rb', line 290 def format_row(label, value) format("%<label>-12s %<value>s", label:, value:) end |
#full_summary_lines(result) ⇒ Array[String]
130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/henitai/reporter.rb', line 130 def full_summary_lines(result) [ "Mutation testing summary", score_line(result), format_row("Killed", count_status(result, :killed)), format_row("Survived", count_status(result, :survived)), format_row("Timeout", count_status(result, :timeout)), format_row("No coverage", count_status(result, :no_coverage)), format_row("Duration", format_duration(result.duration)), reused_verdicts_line(result), executed_only_score_line(result), empty_since_scope_line(result) ].compact end |
#mutated_line(mutant) ⇒ String
236 237 238 |
# File 'lib/henitai/reporter.rb', line 236 def mutated_line(mutant) format("+ %s", display_mutated_source(mutant)) end |
#original_line(mutant) ⇒ String
232 233 234 |
# File 'lib/henitai/reporter.rb', line 232 def original_line(mutant) format("- %s", display_unparse(mutant.original_node)) end |
#partial_summary_lines(result) ⇒ Array[String]
186 187 188 189 190 191 192 193 194 |
# File 'lib/henitai/reporter.rb', line 186 def partial_summary_lines(result) lines = [ "Partial survivor rerun", format_row("Survived", count_status(result, :survived)), format_row("Duration", format_duration(result.duration)) ] append_survivor_stats(lines, result) lines end |
#progress(mutant, scenario_result: nil) ⇒ void
This method returns an undefined value.
96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/henitai/reporter.rb', line 96 def progress(mutant, scenario_result: nil) glyph = PROGRESS_GLYPHS[mutant.status] return unless glyph print(glyph) return flush unless should_show_logs?(scenario_result) output = scenario_output(scenario_result) print("\n") print("log: #{scenario_result.log_path}\n") print(output) unless output.empty? $stdout.flush end |
#report(result) ⇒ void
This method returns an undefined value.
92 93 94 |
# File 'lib/henitai/reporter.rb', line 92 def report(result) puts report_lines(result) end |
#report_lines(result) ⇒ Array[String]
114 115 116 117 118 119 120 |
# File 'lib/henitai/reporter.rb', line 114 def report_lines(result) lines = summary_lines(result) detail_lines = survived_detail_lines(result) return lines if detail_lines.empty? lines + [""] + detail_lines end |
#reused_mutants(result) ⇒ Array[Mutant]
167 168 169 |
# File 'lib/henitai/reporter.rb', line 167 def reused_mutants(result) result.mutants.select { |mutant| mutant.respond_to?(:from_cache?) && mutant.from_cache? } end |
#reused_verdicts_line(result) ⇒ String?
154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/henitai/reporter.rb', line 154 def reused_verdicts_line(result) reused = reused_mutants(result) return nil if reused.empty? format( "%<reused>d of %<total>d verdicts reused from history (%<killed>d killed, %<survived>d survived)", reused: reused.size, total: result.mutants.size, killed: reused.count { |mutant| mutant.status == :killed }, survived: reused.count { |mutant| mutant.status == :survived } ) end |
#scenario_output(scenario_result) ⇒ String
331 332 333 |
# File 'lib/henitai/reporter.rb', line 331 def scenario_output(scenario_result) scenario_result.failure_tail(all_logs: config.all_logs) end |
#score_color(score) ⇒ String?
306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/henitai/reporter.rb', line 306 def score_color(score) return nil if score.nil? thresholds = config.thresholds || {} high = thresholds.fetch(:high, 80) low = thresholds.fetch(:low, 60) return "32" if score >= high return "33" if score >= low "31" end |
#score_line(result) ⇒ String
276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/henitai/reporter.rb', line 276 def score_line(result) summary = result.scoring_summary line = [ format("MS %s", format_percent(summary[:mutation_score])), format("MSI %s", format_percent(summary[:mutation_score_indicator])), format( "Equivalence uncertainty %s", summary[:equivalence_uncertainty] || "n/a" ) ].join(" | ") color = score_color(summary[:mutation_score]) color ? colorize(line, color) : line end |
#should_show_logs?(scenario_result) ⇒ Boolean
325 326 327 328 329 |
# File 'lib/henitai/reporter.rb', line 325 def should_show_logs?(scenario_result) return false unless scenario_result.respond_to?(:failure_tail) scenario_result.should_show_logs?(all_logs: config.all_logs) end |
#source_slice(node) ⇒ String?
Single-line original source of a parsed node; nil for synthetic nodes or multi-line slices (those fall back to normalized unparsing).
267 268 269 270 271 272 273 274 |
# File 'lib/henitai/reporter.rb', line 267 def source_slice(node) return nil unless node.respond_to?(:location) source = node.location&.expression&.source source unless source.nil? || source.include?("\n") rescue StandardError nil end |
#str_node?(node) ⇒ Boolean
261 262 263 |
# File 'lib/henitai/reporter.rb', line 261 def str_node?(node) node.respond_to?(:type) && node.respond_to?(:children) && node.type == :str end |
#summary_lines(result) ⇒ Array[String]
122 123 124 125 126 127 128 |
# File 'lib/henitai/reporter.rb', line 122 def summary_lines(result) if result.respond_to?(:partial_rerun?) && result.partial_rerun? partial_summary_lines(result) else full_summary_lines(result) end end |
#survived_detail_lines(result) ⇒ Array[String]
208 209 210 211 212 213 |
# File 'lib/henitai/reporter.rb', line 208 def survived_detail_lines(result) survivors = result.mutants.select(&:survived?) return [] if survivors.empty? ["Survived mutants"] + survivors.flat_map { |mutant| survived_mutant_lines(mutant) } end |
#survived_mutant_header(mutant) ⇒ String
223 224 225 226 227 228 229 230 |
# File 'lib/henitai/reporter.rb', line 223 def survived_mutant_header(mutant) format( "%<file>s:%<line>d %<operator>s", file: mutant.location.fetch(:file), line: mutant.location.fetch(:start_line), operator: mutant.operator ) end |
#survived_mutant_lines(mutant) ⇒ Array[String]
215 216 217 218 219 220 221 |
# File 'lib/henitai/reporter.rb', line 215 def survived_mutant_lines(mutant) [ survived_mutant_header(mutant), original_line(mutant), mutated_line(mutant) ] end |