evilution 0.23.0 → 0.25.0

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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +210 -0
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +81 -4
  5. data/exe/evil +6 -0
  6. data/lib/evilution/ast/source_surgeon.rb +15 -1
  7. data/lib/evilution/cli/commands/compare.rb +68 -0
  8. data/lib/evilution/cli/parser/command_extractor.rb +78 -0
  9. data/lib/evilution/cli/parser/file_args.rb +41 -0
  10. data/lib/evilution/cli/parser/options_builder.rb +123 -0
  11. data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
  12. data/lib/evilution/cli/parser.rb +27 -196
  13. data/lib/evilution/cli/printers/compare.rb +159 -0
  14. data/lib/evilution/cli.rb +1 -0
  15. data/lib/evilution/compare/categorizer.rb +109 -0
  16. data/lib/evilution/compare/detector.rb +21 -0
  17. data/lib/evilution/compare/fingerprint.rb +83 -0
  18. data/lib/evilution/compare/normalizer.rb +106 -0
  19. data/lib/evilution/compare/record.rb +16 -0
  20. data/lib/evilution/compare.rb +15 -0
  21. data/lib/evilution/config.rb +178 -3
  22. data/lib/evilution/example_filter.rb +143 -0
  23. data/lib/evilution/integration/base.rb +11 -57
  24. data/lib/evilution/integration/crash_detector.rb +5 -2
  25. data/lib/evilution/integration/minitest.rb +25 -7
  26. data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
  27. data/lib/evilution/integration/rspec.rb +99 -12
  28. data/lib/evilution/isolation/fork.rb +26 -0
  29. data/lib/evilution/isolation/in_process.rb +1 -0
  30. data/lib/evilution/mcp/info_tool.rb +77 -5
  31. data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
  32. data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
  33. data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
  34. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
  35. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
  36. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
  37. data/lib/evilution/mcp/mutate_tool.rb +34 -186
  38. data/lib/evilution/mutation.rb +43 -3
  39. data/lib/evilution/mutator/base.rb +39 -1
  40. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
  41. data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
  42. data/lib/evilution/parallel/work_queue.rb +149 -31
  43. data/lib/evilution/parallel_db_warning.rb +68 -0
  44. data/lib/evilution/reporter/cli.rb +38 -11
  45. data/lib/evilution/reporter/html/assets/style.css +85 -0
  46. data/lib/evilution/reporter/html/baseline_keys.rb +28 -0
  47. data/lib/evilution/reporter/html/diff_formatter.rb +27 -0
  48. data/lib/evilution/reporter/html/escape.rb +12 -0
  49. data/lib/evilution/reporter/html/namespace.rb +11 -0
  50. data/lib/evilution/reporter/html/report.rb +68 -0
  51. data/lib/evilution/reporter/html/section.rb +21 -0
  52. data/lib/evilution/reporter/html/sections/baseline_comparison.rb +46 -0
  53. data/lib/evilution/reporter/html/sections/error_details.rb +30 -0
  54. data/lib/evilution/reporter/html/sections/error_entry.rb +22 -0
  55. data/lib/evilution/reporter/html/sections/file_section.rb +62 -0
  56. data/lib/evilution/reporter/html/sections/header.rb +29 -0
  57. data/lib/evilution/reporter/html/sections/mutation_map.rb +32 -0
  58. data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
  59. data/lib/evilution/reporter/html/sections/summary_cards.rb +11 -0
  60. data/lib/evilution/reporter/html/sections/survived_details.rb +35 -0
  61. data/lib/evilution/reporter/html/sections/survived_entry.rb +36 -0
  62. data/lib/evilution/reporter/html/sections/truncation_notice.rb +17 -0
  63. data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
  64. data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
  65. data/lib/evilution/reporter/html/sections.rb +4 -0
  66. data/lib/evilution/reporter/html/stylesheet.rb +14 -0
  67. data/lib/evilution/reporter/html/templates/baseline_comparison.html.erb +8 -0
  68. data/lib/evilution/reporter/html/templates/error_details.html.erb +6 -0
  69. data/lib/evilution/reporter/html/templates/error_entry.html.erb +10 -0
  70. data/lib/evilution/reporter/html/templates/file_section.html.erb +12 -0
  71. data/lib/evilution/reporter/html/templates/header.html.erb +4 -0
  72. data/lib/evilution/reporter/html/templates/mutation_map.html.erb +6 -0
  73. data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
  74. data/lib/evilution/reporter/html/templates/report.html.erb +17 -0
  75. data/lib/evilution/reporter/html/templates/summary_cards.html.erb +26 -0
  76. data/lib/evilution/reporter/html/templates/survived_details.html.erb +21 -0
  77. data/lib/evilution/reporter/html/templates/survived_entry.html.erb +8 -0
  78. data/lib/evilution/reporter/html/templates/truncation_notice.html.erb +1 -0
  79. data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
  80. data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
  81. data/lib/evilution/reporter/html.rb +11 -390
  82. data/lib/evilution/reporter/json.rb +19 -9
  83. data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
  84. data/lib/evilution/reporter/suggestion/registry.rb +64 -0
  85. data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
  86. data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
  87. data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
  88. data/lib/evilution/reporter/suggestion.rb +8 -1327
  89. data/lib/evilution/result/mutation_result.rb +9 -1
  90. data/lib/evilution/result/summary.rb +21 -1
  91. data/lib/evilution/runner/baseline_runner.rb +92 -0
  92. data/lib/evilution/runner/diagnostics.rb +105 -0
  93. data/lib/evilution/runner/isolation_resolver.rb +134 -0
  94. data/lib/evilution/runner/mutation_executor.rb +325 -0
  95. data/lib/evilution/runner/mutation_planner.rb +126 -0
  96. data/lib/evilution/runner/report_publisher.rb +60 -0
  97. data/lib/evilution/runner/subject_pipeline.rb +121 -0
  98. data/lib/evilution/runner.rb +61 -692
  99. data/lib/evilution/source_ast_cache.rb +39 -0
  100. data/lib/evilution/spec_ast_cache.rb +166 -0
  101. data/lib/evilution/spec_resolver.rb +6 -1
  102. data/lib/evilution/spec_selector.rb +39 -0
  103. data/lib/evilution/temp_dir_tracker.rb +23 -3
  104. data/lib/evilution/version.rb +1 -1
  105. data/script/memory_check +7 -5
  106. metadata +75 -2
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::BaselineComparison < Evilution::Reporter::HTML::Section
6
+ template "baseline_comparison"
7
+
8
+ def self.render_if(baseline, summary)
9
+ return "" unless baseline
10
+
11
+ new(baseline, summary).render
12
+ end
13
+
14
+ def initialize(baseline, summary)
15
+ @baseline = baseline
16
+ @summary = summary
17
+ end
18
+
19
+ private
20
+
21
+ def base_score
22
+ (@baseline["summary"] || {})["score"] || 0.0
23
+ end
24
+
25
+ def head_score
26
+ @summary.score
27
+ end
28
+
29
+ def delta
30
+ head_score - base_score
31
+ end
32
+
33
+ def delta_str
34
+ format("%+.2f%%", delta * 100)
35
+ end
36
+
37
+ def delta_class
38
+ if delta.positive?
39
+ "delta-positive"
40
+ elsif delta.negative?
41
+ "delta-negative"
42
+ else
43
+ "delta-neutral"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "error_entry"
5
+
6
+ class Evilution::Reporter::HTML::Sections::ErrorDetails < Evilution::Reporter::HTML::Section
7
+ template "error_details"
8
+
9
+ def self.render_if(errored)
10
+ return "" if errored.empty?
11
+
12
+ new(errored).render
13
+ end
14
+
15
+ def initialize(errored)
16
+ @errored = errored
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :errored
22
+
23
+ def sorted
24
+ errored.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
25
+ end
26
+
27
+ def render_entry(result)
28
+ Evilution::Reporter::HTML::Sections::ErrorEntry.new(result).render
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "../diff_formatter"
5
+
6
+ class Evilution::Reporter::HTML::Sections::ErrorEntry < Evilution::Reporter::HTML::Section
7
+ template "error_entry"
8
+
9
+ def initialize(result)
10
+ @result = result
11
+ end
12
+
13
+ private
14
+
15
+ def message
16
+ @result.error_message.to_s
17
+ end
18
+
19
+ def diff_html
20
+ Evilution::Reporter::HTML::DiffFormatter.call(@result.mutation.diff)
21
+ end
22
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "mutation_map"
5
+ require_relative "survived_details"
6
+ require_relative "error_details"
7
+ require_relative "neutral_details"
8
+ require_relative "unresolved_details"
9
+ require_relative "unparseable_details"
10
+
11
+ class Evilution::Reporter::HTML::Sections::FileSection < Evilution::Reporter::HTML::Section
12
+ template "file_section"
13
+
14
+ def initialize(path, results, suggestion:, baseline_keys:)
15
+ @path = path
16
+ @results = results
17
+ @suggestion = suggestion
18
+ @baseline_keys = baseline_keys
19
+ end
20
+
21
+ private
22
+
23
+ def killed_count
24
+ @results.count(&:killed?)
25
+ end
26
+
27
+ def survived_count
28
+ @results.count(&:survived?)
29
+ end
30
+
31
+ def total
32
+ @results.length
33
+ end
34
+
35
+ def map_html
36
+ Evilution::Reporter::HTML::Sections::MutationMap.new(@results).render
37
+ end
38
+
39
+ def survived_html
40
+ Evilution::Reporter::HTML::Sections::SurvivedDetails.render_if(
41
+ @results.select(&:survived?),
42
+ suggestion: @suggestion,
43
+ baseline_keys: @baseline_keys
44
+ )
45
+ end
46
+
47
+ def error_html
48
+ Evilution::Reporter::HTML::Sections::ErrorDetails.render_if(@results.select(&:error?))
49
+ end
50
+
51
+ def neutral_html
52
+ Evilution::Reporter::HTML::Sections::NeutralDetails.render_if(@results.select(&:neutral?))
53
+ end
54
+
55
+ def unresolved_html
56
+ Evilution::Reporter::HTML::Sections::UnresolvedDetails.render_if(@results.select(&:unresolved?))
57
+ end
58
+
59
+ def unparseable_html
60
+ Evilution::Reporter::HTML::Sections::UnparseableDetails.render_if(@results.select(&:unparseable?))
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "../../../version"
5
+
6
+ class Evilution::Reporter::HTML::Sections::Header < Evilution::Reporter::HTML::Section
7
+ template "header"
8
+
9
+ def initialize(summary)
10
+ @summary = summary
11
+ end
12
+
13
+ private
14
+
15
+ def score_pct
16
+ format("%.2f%%", @summary.score * 100)
17
+ end
18
+
19
+ def score_css_class
20
+ score = @summary.score
21
+ if score >= 0.8
22
+ "score-high"
23
+ elsif score >= 0.5
24
+ "score-medium"
25
+ else
26
+ "score-low"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::MutationMap < Evilution::Reporter::HTML::Section
6
+ template "mutation_map"
7
+
8
+ Entry = Struct.new(:line, :operator_name, :status, :title_attr)
9
+
10
+ def initialize(results)
11
+ @results = results
12
+ end
13
+
14
+ private
15
+
16
+ def entries
17
+ @entries ||= @results.sort_by { |r| r.mutation.line }.map { |r| build_entry(r) }
18
+ end
19
+
20
+ def build_entry(result)
21
+ title = normalize_title(result.error_message)
22
+ title_attr = title ? %( title="#{h(title)}") : ""
23
+ Entry.new(result.mutation.line, result.mutation.operator_name, result.status.to_s, title_attr)
24
+ end
25
+
26
+ def normalize_title(message)
27
+ return nil if message.nil?
28
+
29
+ normalized = message.gsub(/\s+/, " ").strip
30
+ normalized.empty? ? nil : normalized
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::NeutralDetails < Evilution::Reporter::HTML::Section
6
+ template "neutral_details"
7
+
8
+ def self.render_if(neutral)
9
+ return "" if neutral.empty?
10
+
11
+ new(neutral).render
12
+ end
13
+
14
+ def initialize(neutral)
15
+ @neutral = neutral
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :neutral
21
+
22
+ def sorted
23
+ neutral.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::SummaryCards < Evilution::Reporter::HTML::Section
6
+ template "summary_cards"
7
+
8
+ def initialize(summary)
9
+ @summary = summary
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "../../../result/coverage_gap_grouper"
5
+ require_relative "survived_entry"
6
+
7
+ class Evilution::Reporter::HTML::Sections::SurvivedDetails < Evilution::Reporter::HTML::Section
8
+ template "survived_details"
9
+
10
+ def self.render_if(survived, suggestion:, baseline_keys:)
11
+ return "" if survived.empty?
12
+
13
+ new(survived, suggestion: suggestion, baseline_keys: baseline_keys).render
14
+ end
15
+
16
+ def initialize(survived, suggestion:, baseline_keys:)
17
+ @survived = survived
18
+ @suggestion = suggestion
19
+ @baseline_keys = baseline_keys
20
+ end
21
+
22
+ private
23
+
24
+ def gaps
25
+ @gaps ||= Evilution::Result::CoverageGapGrouper.new.call(@survived)
26
+ end
27
+
28
+ def render_entry(result)
29
+ Evilution::Reporter::HTML::Sections::SurvivedEntry.new(
30
+ result,
31
+ suggestion: @suggestion,
32
+ baseline_keys: @baseline_keys
33
+ ).render
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+ require_relative "../diff_formatter"
5
+
6
+ class Evilution::Reporter::HTML::Sections::SurvivedEntry < Evilution::Reporter::HTML::Section
7
+ template "survived_entry"
8
+
9
+ def initialize(result, suggestion:, baseline_keys:)
10
+ @result = result
11
+ @suggestion = suggestion
12
+ @baseline_keys = baseline_keys
13
+ end
14
+
15
+ private
16
+
17
+ def regression?
18
+ @baseline_keys.regression?(@result.mutation)
19
+ end
20
+
21
+ def entry_class
22
+ regression? ? "survived-entry regression" : "survived-entry"
23
+ end
24
+
25
+ def regression_badge
26
+ regression? ? ' <span class="regression-badge">NEW REGRESSION</span>' : ""
27
+ end
28
+
29
+ def suggestion_text
30
+ @suggestion.suggestion_for(@result.mutation)
31
+ end
32
+
33
+ def diff_html
34
+ Evilution::Reporter::HTML::DiffFormatter.call(@result.mutation.diff)
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::TruncationNotice < Evilution::Reporter::HTML::Section
6
+ template "truncation_notice"
7
+
8
+ def initialize(summary)
9
+ @summary = summary
10
+ end
11
+
12
+ def self.render_if(summary)
13
+ return "" unless summary.truncated?
14
+
15
+ new(summary).render
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::UnparseableDetails < Evilution::Reporter::HTML::Section
6
+ template "unparseable_details"
7
+
8
+ def self.render_if(unparseable)
9
+ return "" if unparseable.empty?
10
+
11
+ new(unparseable).render
12
+ end
13
+
14
+ def initialize(unparseable)
15
+ @unparseable = unparseable
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :unparseable
21
+
22
+ def sorted
23
+ unparseable.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::UnresolvedDetails < Evilution::Reporter::HTML::Section
6
+ template "unresolved_details"
7
+
8
+ def self.render_if(unresolved)
9
+ return "" if unresolved.empty?
10
+
11
+ new(unresolved).render
12
+ end
13
+
14
+ def initialize(unresolved)
15
+ @unresolved = unresolved
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :unresolved
21
+
22
+ def sorted
23
+ unresolved.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "namespace"
4
+ require_relative "section"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "namespace"
4
+
5
+ module Evilution::Reporter::HTML::Stylesheet
6
+ PATH = File.expand_path("assets/style.css", __dir__)
7
+ CSS = File.read(PATH).freeze
8
+
9
+ module_function
10
+
11
+ def call
12
+ "<style>\n#{CSS}</style>"
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ <section class="baseline-comparison">
2
+ <h2>Baseline Comparison</h2>
3
+ <div class="comparison-scores">
4
+ <span>Baseline: <%= format("%.2f%%", base_score * 100) %></span>
5
+ <span>Current: <%= format("%.2f%%", head_score * 100) %></span>
6
+ <span class="<%= delta_class %>">Delta: <%= delta_str %></span>
7
+ </div>
8
+ </section>
@@ -0,0 +1,6 @@
1
+ <div class="error-details">
2
+ <h3>Errors (<%= errored.length %>)</h3>
3
+ <%- sorted.each do |r| -%>
4
+ <%= render_entry(r) %>
5
+ <%- end -%>
6
+ </div>
@@ -0,0 +1,10 @@
1
+ <div class="error-entry">
2
+ <div class="error-header">
3
+ <span class="operator"><%= h(@result.mutation.operator_name) %></span>
4
+ <span class="location"><%= h(@result.mutation.file_path) %>:<%= @result.mutation.line %></span>
5
+ </div>
6
+ <pre class="diff"><%= diff_html %></pre>
7
+ <%- unless message.empty? -%>
8
+ <pre class="error-message"><%= h(message) %></pre>
9
+ <%- end -%>
10
+ </div>
@@ -0,0 +1,12 @@
1
+ <section class="file-section">
2
+ <h2 class="file-header">
3
+ <span class="file-path"><%= h(@path) %></span>
4
+ <span class="file-stats"><%= killed_count %> killed / <%= survived_count %> survived / <%= total %> total</span>
5
+ </h2>
6
+ <div class="mutation-map"><%= map_html %></div>
7
+ <%= survived_html %>
8
+ <%= error_html %>
9
+ <%= neutral_html %>
10
+ <%= unresolved_html %>
11
+ <%= unparseable_html %>
12
+ </section>
@@ -0,0 +1,4 @@
1
+ <header>
2
+ <h1>Evilution <span class="version">v<%= h(Evilution::VERSION) %></span></h1>
3
+ <div class="score-badge <%= score_css_class %>"><%= score_pct %></div>
4
+ </header>
@@ -0,0 +1,6 @@
1
+ <%- entries.each_with_index do |entry, i| -%>
2
+ <div class="map-line <%= entry.status %>"<%= entry.title_attr %>>
3
+ <span class="line-number">line <%= entry.line %></span>
4
+ <span class="operator"><%= h(entry.operator_name) %></span>
5
+ <span class="status-badge <%= entry.status %>"><%= entry.status %></span>
6
+ </div><%= "\n" if i < entries.length - 1 %><%- end -%>
@@ -0,0 +1,14 @@
1
+ <div class="neutral-details">
2
+ <h3>Neutral (<%= neutral.length %>) — test infra error, excluded from score</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ <%- if r.error_class -%>
9
+ <span class="error-class">(<%= h(r.error_class) %>)</span>
10
+ <%- end -%>
11
+ </li>
12
+ <%- end -%>
13
+ </ul>
14
+ </div>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Evilution Mutation Report</title>
7
+ <%= stylesheet %>
8
+ </head>
9
+ <body>
10
+ <%= header %>
11
+ <%= summary_cards %>
12
+ <%= baseline_comparison %>
13
+ <%= truncation_notice %>
14
+ <%= file_sections %>
15
+ <footer>Generated by Evilution v<%= h(Evilution::VERSION) %></footer>
16
+ </body>
17
+ </html>
@@ -0,0 +1,26 @@
1
+ <section class="summary-cards">
2
+ <div class="card"><span class="card-value"><%= @summary.total %></span><span class="card-label">Total</span></div>
3
+ <div class="card card-killed"><span class="card-value"><%= @summary.killed %></span><span class="card-label">Killed</span></div>
4
+ <div class="card card-survived"><span class="card-value"><%= @summary.survived %></span><span class="card-label">Survived</span></div>
5
+ <div class="card"><span class="card-value"><%= @summary.timed_out %></span><span class="card-label">Timed Out</span></div>
6
+ <div class="card"><span class="card-value"><%= @summary.errors %></span><span class="card-label">Errors</span></div>
7
+ <div class="card"><span class="card-value"><%= @summary.neutral %></span><span class="card-label">Neutral</span></div>
8
+ <div class="card"><span class="card-value"><%= @summary.equivalent %></span><span class="card-label">Equivalent</span></div>
9
+ <%- if @summary.unresolved.positive? -%>
10
+ <div class="card"><span class="card-value"><%= @summary.unresolved %></span><span class="card-label">Unresolved</span></div>
11
+ <%- end -%>
12
+ <%- if @summary.unparseable.positive? -%>
13
+ <div class="card"><span class="card-value"><%= @summary.unparseable %></span><span class="card-label">Unparseable</span></div>
14
+ <%- end -%>
15
+ <%- if @summary.skipped.positive? -%>
16
+ <div class="card"><span class="card-value"><%= @summary.skipped %></span><span class="card-label">Skipped</span></div>
17
+ <%- end -%>
18
+ <div class="card"><span class="card-value"><%= format("%.2f", @summary.duration) %>s</span><span class="card-label">Duration</span></div>
19
+ <%- if @summary.duration.positive? -%>
20
+ <div class="card"><span class="card-value"><%= format("%.1f%%", @summary.efficiency * 100) %></span><span class="card-label">Efficiency</span></div>
21
+ <div class="card"><span class="card-value"><%= format("%.2f", @summary.mutations_per_second) %>/s</span><span class="card-label">Rate</span></div>
22
+ <%- end -%>
23
+ <%- if @summary.peak_memory_mb -%>
24
+ <div class="card"><span class="card-value"><%= format("%.1f", @summary.peak_memory_mb) %> MB</span><span class="card-label">Peak Memory</span></div>
25
+ <%- end -%>
26
+ </section>
@@ -0,0 +1,21 @@
1
+ <div class="survived-details">
2
+ <h3>Coverage Gaps (<%= gaps.length %>)</h3>
3
+ <%- gaps.each do |gap| -%>
4
+ <%- if gap.single? -%>
5
+ <%= render_entry(gap.mutation_results.first) %>
6
+ <%- else -%>
7
+ <div class="coverage-gap">
8
+ <div class="gap-header">
9
+ <span class="location"><%= h(gap.file_path) %>:<%= gap.line %> (<%= h(gap.subject_name) %>)</span>
10
+ <span class="gap-count"><%= gap.count %> mutations</span>
11
+ <%- gap.operator_names.each do |op| -%>
12
+ <span class="operator-tag"><%= h(op) %></span>
13
+ <%- end -%>
14
+ </div>
15
+ <%- gap.mutation_results.each do |r| -%>
16
+ <%= render_entry(r) %>
17
+ <%- end -%>
18
+ </div>
19
+ <%- end -%>
20
+ <%- end -%>
21
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="<%= entry_class %>">
2
+ <div class="survived-header">
3
+ <span class="operator"><%= h(@result.mutation.operator_name) %><%= regression_badge %></span>
4
+ <span class="location"><%= h(@result.mutation.file_path) %>:<%= @result.mutation.line %></span>
5
+ </div>
6
+ <pre class="diff"><%= diff_html %></pre>
7
+ <div class="suggestion"><%= h(suggestion_text) %></div>
8
+ </div>
@@ -0,0 +1 @@
1
+ <div class="truncation-notice">Truncated: Stopped early due to --fail-fast</div>
@@ -0,0 +1,11 @@
1
+ <div class="unparseable-details">
2
+ <h3>Unparseable (<%= unparseable.length %>) — mutated source did not parse</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ </li>
9
+ <%- end -%>
10
+ </ul>
11
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="unresolved-details">
2
+ <h3>Unresolved (<%= unresolved.length %>) — no test file resolved</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ </li>
9
+ <%- end -%>
10
+ </ul>
11
+ </div>