evilution 0.22.7 → 0.24.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +8 -0
  3. data/CHANGELOG.md +28 -0
  4. data/README.md +37 -7
  5. data/lib/evilution/cli/command.rb +37 -0
  6. data/lib/evilution/cli/commands/environment_show.rb +20 -0
  7. data/lib/evilution/cli/commands/init.rb +24 -0
  8. data/lib/evilution/cli/commands/mcp.rb +19 -0
  9. data/lib/evilution/cli/commands/run.rb +68 -0
  10. data/lib/evilution/cli/commands/session_diff.rb +30 -0
  11. data/lib/evilution/cli/commands/session_gc.rb +46 -0
  12. data/lib/evilution/cli/commands/session_list.rb +51 -0
  13. data/lib/evilution/cli/commands/session_show.rb +27 -0
  14. data/lib/evilution/cli/commands/subjects.rb +50 -0
  15. data/lib/evilution/cli/commands/tests_list.rb +43 -0
  16. data/lib/evilution/cli/commands/util_mutation.rb +66 -0
  17. data/lib/evilution/cli/commands/version.rb +17 -0
  18. data/lib/evilution/cli/commands.rb +4 -0
  19. data/lib/evilution/cli/dispatcher.rb +23 -0
  20. data/lib/evilution/cli/parsed_args.rb +12 -0
  21. data/lib/evilution/cli/parser/command_extractor.rb +77 -0
  22. data/lib/evilution/cli/parser/file_args.rb +41 -0
  23. data/lib/evilution/cli/parser/options_builder.rb +103 -0
  24. data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
  25. data/lib/evilution/cli/parser.rb +88 -0
  26. data/lib/evilution/cli/printers/environment.rb +53 -0
  27. data/lib/evilution/cli/printers/session_detail.rb +76 -0
  28. data/lib/evilution/cli/printers/session_diff.rb +57 -0
  29. data/lib/evilution/cli/printers/session_list.rb +48 -0
  30. data/lib/evilution/cli/printers/subjects.rb +35 -0
  31. data/lib/evilution/cli/printers/tests_list.rb +45 -0
  32. data/lib/evilution/cli/printers/util_mutation.rb +35 -0
  33. data/lib/evilution/cli/printers.rb +4 -0
  34. data/lib/evilution/cli/result.rb +9 -0
  35. data/lib/evilution/cli.rb +30 -850
  36. data/lib/evilution/config.rb +31 -3
  37. data/lib/evilution/integration/base.rb +23 -55
  38. data/lib/evilution/integration/minitest.rb +22 -4
  39. data/lib/evilution/integration/rspec.rb +28 -8
  40. data/lib/evilution/isolation/fork.rb +11 -9
  41. data/lib/evilution/isolation/in_process.rb +11 -9
  42. data/lib/evilution/mcp/info_tool.rb +261 -0
  43. data/lib/evilution/mcp/mutate_tool.rb +112 -19
  44. data/lib/evilution/mcp/server.rb +3 -4
  45. data/lib/evilution/mcp/session_diff_tool.rb +5 -1
  46. data/lib/evilution/mcp/session_list_tool.rb +5 -1
  47. data/lib/evilution/mcp/session_show_tool.rb +5 -1
  48. data/lib/evilution/mcp/session_tool.rb +157 -0
  49. data/lib/evilution/reporter/cli.rb +2 -1
  50. data/lib/evilution/reporter/html/assets/style.css +68 -0
  51. data/lib/evilution/reporter/html/baseline_keys.rb +28 -0
  52. data/lib/evilution/reporter/html/diff_formatter.rb +27 -0
  53. data/lib/evilution/reporter/html/escape.rb +12 -0
  54. data/lib/evilution/reporter/html/namespace.rb +11 -0
  55. data/lib/evilution/reporter/html/report.rb +68 -0
  56. data/lib/evilution/reporter/html/section.rb +21 -0
  57. data/lib/evilution/reporter/html/sections/baseline_comparison.rb +46 -0
  58. data/lib/evilution/reporter/html/sections/error_details.rb +30 -0
  59. data/lib/evilution/reporter/html/sections/error_entry.rb +22 -0
  60. data/lib/evilution/reporter/html/sections/file_section.rb +47 -0
  61. data/lib/evilution/reporter/html/sections/header.rb +29 -0
  62. data/lib/evilution/reporter/html/sections/mutation_map.rb +32 -0
  63. data/lib/evilution/reporter/html/sections/summary_cards.rb +11 -0
  64. data/lib/evilution/reporter/html/sections/survived_details.rb +35 -0
  65. data/lib/evilution/reporter/html/sections/survived_entry.rb +36 -0
  66. data/lib/evilution/reporter/html/sections/truncation_notice.rb +17 -0
  67. data/lib/evilution/reporter/html/sections.rb +4 -0
  68. data/lib/evilution/reporter/html/stylesheet.rb +14 -0
  69. data/lib/evilution/reporter/html/templates/baseline_comparison.html.erb +8 -0
  70. data/lib/evilution/reporter/html/templates/error_details.html.erb +6 -0
  71. data/lib/evilution/reporter/html/templates/error_entry.html.erb +10 -0
  72. data/lib/evilution/reporter/html/templates/file_section.html.erb +9 -0
  73. data/lib/evilution/reporter/html/templates/header.html.erb +4 -0
  74. data/lib/evilution/reporter/html/templates/mutation_map.html.erb +6 -0
  75. data/lib/evilution/reporter/html/templates/report.html.erb +17 -0
  76. data/lib/evilution/reporter/html/templates/summary_cards.html.erb +23 -0
  77. data/lib/evilution/reporter/html/templates/survived_details.html.erb +21 -0
  78. data/lib/evilution/reporter/html/templates/survived_entry.html.erb +8 -0
  79. data/lib/evilution/reporter/html/templates/truncation_notice.html.erb +1 -0
  80. data/lib/evilution/reporter/html.rb +11 -349
  81. data/lib/evilution/reporter/json.rb +12 -8
  82. data/lib/evilution/result/mutation_result.rb +5 -1
  83. data/lib/evilution/result/summary.rb +9 -1
  84. data/lib/evilution/runner/baseline_runner.rb +71 -0
  85. data/lib/evilution/runner/diagnostics.rb +105 -0
  86. data/lib/evilution/runner/isolation_resolver.rb +134 -0
  87. data/lib/evilution/runner/mutation_executor.rb +255 -0
  88. data/lib/evilution/runner/mutation_planner.rb +126 -0
  89. data/lib/evilution/runner/report_publisher.rb +60 -0
  90. data/lib/evilution/runner/subject_pipeline.rb +121 -0
  91. data/lib/evilution/runner.rb +57 -692
  92. data/lib/evilution/version.rb +1 -1
  93. metadata +71 -2
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require_relative "namespace"
5
+ require_relative "escape"
6
+
7
+ class Evilution::Reporter::HTML::Section
8
+ TEMPLATE_DIR = File.expand_path("templates", __dir__)
9
+
10
+ def self.template(name)
11
+ path = File.join(TEMPLATE_DIR, "#{name}.html.erb")
12
+ erb = ERB.new(File.read(path), trim_mode: "-")
13
+ erb.def_method(self, "render")
14
+ end
15
+
16
+ private
17
+
18
+ def h(text)
19
+ Evilution::Reporter::HTML::Escape.call(text)
20
+ end
21
+ end
@@ -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,47 @@
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
+
8
+ class Evilution::Reporter::HTML::Sections::FileSection < Evilution::Reporter::HTML::Section
9
+ template "file_section"
10
+
11
+ def initialize(path, results, suggestion:, baseline_keys:)
12
+ @path = path
13
+ @results = results
14
+ @suggestion = suggestion
15
+ @baseline_keys = baseline_keys
16
+ end
17
+
18
+ private
19
+
20
+ def killed_count
21
+ @results.count(&:killed?)
22
+ end
23
+
24
+ def survived_count
25
+ @results.count(&:survived?)
26
+ end
27
+
28
+ def total
29
+ @results.length
30
+ end
31
+
32
+ def map_html
33
+ Evilution::Reporter::HTML::Sections::MutationMap.new(@results).render
34
+ end
35
+
36
+ def survived_html
37
+ Evilution::Reporter::HTML::Sections::SurvivedDetails.render_if(
38
+ @results.select(&:survived?),
39
+ suggestion: @suggestion,
40
+ baseline_keys: @baseline_keys
41
+ )
42
+ end
43
+
44
+ def error_html
45
+ Evilution::Reporter::HTML::Sections::ErrorDetails.render_if(@results.select(&:error?))
46
+ end
47
+ 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,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,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,9 @@
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
+ </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,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,23 @@
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.skipped.positive? -%>
13
+ <div class="card"><span class="card-value"><%= @summary.skipped %></span><span class="card-label">Skipped</span></div>
14
+ <%- end -%>
15
+ <div class="card"><span class="card-value"><%= format("%.2f", @summary.duration) %>s</span><span class="card-label">Duration</span></div>
16
+ <%- if @summary.duration.positive? -%>
17
+ <div class="card"><span class="card-value"><%= format("%.1f%%", @summary.efficiency * 100) %></span><span class="card-label">Efficiency</span></div>
18
+ <div class="card"><span class="card-value"><%= format("%.2f", @summary.mutations_per_second) %>/s</span><span class="card-label">Rate</span></div>
19
+ <%- end -%>
20
+ <%- if @summary.peak_memory_mb -%>
21
+ <div class="card"><span class="card-value"><%= format("%.1f", @summary.peak_memory_mb) %> MB</span><span class="card-label">Peak Memory</span></div>
22
+ <%- end -%>
23
+ </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>