evilution 0.24.0 → 0.26.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.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +210 -0
- data/.claude/prompts/architect.md +14 -1
- data/.claude/skills/create-issue/SKILL.md +55 -0
- data/CHANGELOG.md +51 -0
- data/README.md +80 -4
- data/exe/evil +6 -0
- data/lib/evilution/ast/constant_names.rb +34 -0
- data/lib/evilution/ast/source_surgeon.rb +15 -1
- data/lib/evilution/cli/commands/compare.rb +68 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +21 -1
- data/lib/evilution/cli/printers/compare.rb +159 -0
- data/lib/evilution/cli.rb +1 -0
- data/lib/evilution/compare/categorizer.rb +109 -0
- data/lib/evilution/compare/detector.rb +21 -0
- data/lib/evilution/compare/fingerprint.rb +83 -0
- data/lib/evilution/compare/invalid_input.rb +12 -0
- data/lib/evilution/compare/normalizer.rb +106 -0
- data/lib/evilution/compare/record.rb +16 -0
- data/lib/evilution/compare.rb +6 -0
- data/lib/evilution/config.rb +165 -3
- data/lib/evilution/example_filter.rb +143 -0
- data/lib/evilution/integration/base.rb +4 -155
- data/lib/evilution/integration/crash_detector.rb +5 -2
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
- data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
- data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
- data/lib/evilution/integration/loading.rb +6 -0
- data/lib/evilution/integration/minitest.rb +10 -5
- data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
- data/lib/evilution/integration/rspec.rb +82 -7
- data/lib/evilution/isolation/fork.rb +25 -0
- data/lib/evilution/load_path/subpath_resolver.rb +25 -0
- data/lib/evilution/load_path.rb +4 -0
- data/lib/evilution/mcp/info_tool.rb +77 -5
- data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
- data/lib/evilution/mcp/mutate_tool.rb +34 -186
- data/lib/evilution/mutation.rb +43 -3
- data/lib/evilution/mutator/base.rb +39 -1
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
- data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
- data/lib/evilution/parallel/work_queue.rb +149 -31
- data/lib/evilution/parallel_db_warning.rb +68 -0
- data/lib/evilution/reporter/cli.rb +37 -11
- data/lib/evilution/reporter/html/assets/style.css +17 -0
- data/lib/evilution/reporter/html/sections/file_section.rb +15 -0
- data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
- data/lib/evilution/reporter/html/templates/file_section.html.erb +3 -0
- data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
- data/lib/evilution/reporter/html/templates/summary_cards.html.erb +3 -0
- data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
- data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
- data/lib/evilution/reporter/json.rb +8 -2
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
- data/lib/evilution/reporter/suggestion/registry.rb +64 -0
- data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
- data/lib/evilution/reporter/suggestion.rb +8 -1327
- data/lib/evilution/result/mutation_result.rb +5 -1
- data/lib/evilution/result/summary.rb +13 -1
- data/lib/evilution/runner/baseline_runner.rb +23 -2
- data/lib/evilution/runner/isolation_resolver.rb +12 -1
- data/lib/evilution/runner/mutation_executor.rb +83 -13
- data/lib/evilution/runner/subject_pipeline.rb +18 -8
- data/lib/evilution/runner.rb +6 -0
- data/lib/evilution/source_ast_cache.rb +39 -0
- data/lib/evilution/spec_ast_cache.rb +166 -0
- data/lib/evilution/spec_resolver.rb +6 -1
- data/lib/evilution/spec_selector.rb +39 -0
- data/lib/evilution/temp_dir_tracker.rb +23 -3
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +7 -5
- metadata +46 -5
- data/lib/evilution/mcp/session_diff_tool.rb +0 -63
- data/lib/evilution/mcp/session_list_tool.rb +0 -50
- data/lib/evilution/mcp/session_show_tool.rb +0 -57
|
@@ -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,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>
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
<%- if @summary.unresolved.positive? -%>
|
|
10
10
|
<div class="card"><span class="card-value"><%= @summary.unresolved %></span><span class="card-label">Unresolved</span></div>
|
|
11
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 -%>
|
|
12
15
|
<%- if @summary.skipped.positive? -%>
|
|
13
16
|
<div class="card"><span class="card-value"><%= @summary.skipped %></span><span class="card-label">Skipped</span></div>
|
|
14
17
|
<%- end -%>
|
|
@@ -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>
|
|
@@ -29,7 +29,8 @@ class Evilution::Reporter::JSON
|
|
|
29
29
|
timed_out: map_details(summary.results.select(&:timeout?)),
|
|
30
30
|
errors: map_details(summary.results.select(&:error?)),
|
|
31
31
|
equivalent: map_details(summary.equivalent_results),
|
|
32
|
-
unresolved: map_details(summary.unresolved_results)
|
|
32
|
+
unresolved: map_details(summary.unresolved_results),
|
|
33
|
+
unparseable: map_details(summary.unparseable_results)
|
|
33
34
|
}
|
|
34
35
|
append_disabled_to_report(report, summary)
|
|
35
36
|
report
|
|
@@ -55,6 +56,7 @@ class Evilution::Reporter::JSON
|
|
|
55
56
|
neutral: summary.neutral,
|
|
56
57
|
equivalent: summary.equivalent,
|
|
57
58
|
unresolved: summary.unresolved,
|
|
59
|
+
unparseable: summary.unparseable,
|
|
58
60
|
score: summary.score.round(4),
|
|
59
61
|
duration: summary.duration.round(4),
|
|
60
62
|
killtime: summary.killtime.round(4),
|
|
@@ -78,7 +80,11 @@ class Evilution::Reporter::JSON
|
|
|
78
80
|
duration: result.duration.round(4),
|
|
79
81
|
diff: mutation.diff
|
|
80
82
|
}
|
|
81
|
-
|
|
83
|
+
if result.status == :survived
|
|
84
|
+
detail[:suggestion] = @suggestion.suggestion_for(mutation)
|
|
85
|
+
unified = mutation.unified_diff
|
|
86
|
+
detail[:unified_diff] = unified if unified
|
|
87
|
+
end
|
|
82
88
|
detail[:test_command] = result.test_command if result.test_command
|
|
83
89
|
append_memory_fields(detail, result)
|
|
84
90
|
append_error_fields(detail, result)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../suggestion"
|
|
4
|
+
|
|
5
|
+
module Evilution::Reporter::Suggestion::DiffHelpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse_method_name(subject_name)
|
|
9
|
+
subject_name.split(/[#.]/).last
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sanitize_method_name(name)
|
|
13
|
+
name.gsub(/[^a-zA-Z0-9_]/, "_").gsub(/_+/, "_").gsub(/\A_|_\z/, "")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def extract_diff_lines(diff)
|
|
17
|
+
lines = diff.split("\n")
|
|
18
|
+
original = lines.find { |l| l.start_with?("- ") }
|
|
19
|
+
mutated = lines.find { |l| l.start_with?("+ ") }
|
|
20
|
+
[clean_diff_line(original, "- "), clean_diff_line(mutated, "+ ")]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clean_diff_line(line, prefix)
|
|
24
|
+
return nil if line.nil?
|
|
25
|
+
|
|
26
|
+
line.sub(/^#{Regexp.escape(prefix)}/, "").strip
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../suggestion"
|
|
4
|
+
|
|
5
|
+
# rubocop:disable Style/OneClassPerFile
|
|
6
|
+
module Evilution::Reporter::Suggestion::Templates
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Evilution::Reporter::Suggestion::Registry
|
|
10
|
+
# rubocop:enable Style/OneClassPerFile
|
|
11
|
+
def self.default
|
|
12
|
+
return @default if @default
|
|
13
|
+
|
|
14
|
+
require_relative "templates/generic"
|
|
15
|
+
require_relative "templates/rspec"
|
|
16
|
+
require_relative "templates/minitest"
|
|
17
|
+
|
|
18
|
+
registry = new
|
|
19
|
+
Evilution::Reporter::Suggestion::Templates::Generic::GENERIC_ENTRIES.each do |op, text|
|
|
20
|
+
registry.register_generic(op, text)
|
|
21
|
+
end
|
|
22
|
+
Evilution::Reporter::Suggestion::Templates::Rspec::RSPEC_ENTRIES.each do |op, blk|
|
|
23
|
+
registry.register_concrete(op, integration: :rspec, block: blk)
|
|
24
|
+
end
|
|
25
|
+
Evilution::Reporter::Suggestion::Templates::Minitest::MINITEST_ENTRIES.each do |op, blk|
|
|
26
|
+
registry.register_concrete(op, integration: :minitest, block: blk)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@default = registry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.reset!
|
|
33
|
+
@default = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@generic = {}
|
|
38
|
+
@concrete = Hash.new { |h, k| h[k] = {} }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_generic(operator_name, text)
|
|
42
|
+
@generic[operator_name] = text
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def register_concrete(operator_name, integration:, block:)
|
|
47
|
+
@concrete[integration][operator_name] = block
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def generic(operator_name)
|
|
52
|
+
@generic[operator_name]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def concrete(operator_name, integration:)
|
|
56
|
+
@concrete.fetch(integration, {})[operator_name]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def each_generic_operator(&)
|
|
60
|
+
return @generic.each_key unless block_given?
|
|
61
|
+
|
|
62
|
+
@generic.each_key(&)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../registry"
|
|
4
|
+
|
|
5
|
+
module Evilution::Reporter::Suggestion::Templates::Generic
|
|
6
|
+
GENERIC_ENTRIES = {
|
|
7
|
+
"comparison_replacement" => "Add a test for the boundary condition where the comparison operand equals the threshold exactly",
|
|
8
|
+
"arithmetic_replacement" => "Add a test that verifies the arithmetic result, not just truthiness of the outcome",
|
|
9
|
+
"boolean_operator_replacement" => "Add a test where only one of the boolean conditions is true to distinguish && from ||",
|
|
10
|
+
"boolean_literal_replacement" => "Add a test that exercises the false/true branch explicitly",
|
|
11
|
+
"nil_replacement" => "Add a test that asserts the return value is not nil",
|
|
12
|
+
"integer_literal" => "Add a test that checks the exact numeric value, not just > 0 or truthy",
|
|
13
|
+
"float_literal" => "Add a test that checks the exact floating-point value returned",
|
|
14
|
+
"string_literal" => "Add a test that asserts the string content, not just its presence",
|
|
15
|
+
"array_literal" => "Add a test that verifies the array contents or length",
|
|
16
|
+
"hash_literal" => "Add a test that verifies the hash keys and values",
|
|
17
|
+
"symbol_literal" => "Add a test that checks the exact symbol returned",
|
|
18
|
+
"conditional_negation" => "Add tests for both the true and false branches of this conditional",
|
|
19
|
+
"conditional_branch" => "Add a test that exercises the removed branch of this conditional",
|
|
20
|
+
"statement_deletion" => "Add a test that depends on the side effect of this statement",
|
|
21
|
+
"method_body_replacement" => "Add a test that checks the method's return value or side effects",
|
|
22
|
+
"negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
|
|
23
|
+
"return_value_removal" => "Add a test that uses the return value of this method",
|
|
24
|
+
"collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
|
|
25
|
+
"method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
|
|
26
|
+
"argument_removal" => "Add a test that verifies the correct arguments are passed to this method call",
|
|
27
|
+
"compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)",
|
|
28
|
+
"superclass_removal" => "Add a test that exercises inherited behavior from the superclass",
|
|
29
|
+
"mixin_removal" => "Add a test that exercises behavior provided by the included/extended module",
|
|
30
|
+
"local_variable_assignment" => "Add a test that depends on the assigned variable being stored, not just the value expression",
|
|
31
|
+
"instance_variable_write" => "Add a test that verifies the instance variable is set correctly, not just the return value",
|
|
32
|
+
"class_variable_write" => "Add a test that verifies the class variable is set correctly and affects shared state",
|
|
33
|
+
"global_variable_write" => "Add a test that verifies the global variable is set correctly, not just the value expression",
|
|
34
|
+
"rescue_removal" => "Add a test that triggers the rescued exception and verifies the rescue handler behavior",
|
|
35
|
+
"rescue_body_replacement" => "Add a test that triggers the rescued exception and verifies the rescue body produces the correct result",
|
|
36
|
+
"inline_rescue" => "Add a test that triggers the inline rescue and verifies the fallback value is used correctly",
|
|
37
|
+
"ensure_removal" => "Add a test that verifies the ensure cleanup code runs and its side effects are observable",
|
|
38
|
+
"break_statement" => "Add a test that verifies the break condition and the value returned when the loop exits early",
|
|
39
|
+
"next_statement" => "Add a test that verifies the next condition and the value yielded when the iteration skips",
|
|
40
|
+
"redo_statement" => "Add a test that verifies the redo restarts the iteration and the retry logic is necessary",
|
|
41
|
+
"bang_method" => "Add a test that distinguishes in-place mutation from copy semantics (bang vs non-bang)",
|
|
42
|
+
"bitwise_replacement" => "Add a test that checks the exact bitwise result to distinguish &, |, and ^ operators",
|
|
43
|
+
"bitwise_complement" => "Add a test that verifies the bitwise complement (~) result, not just the sign or magnitude",
|
|
44
|
+
"zsuper_removal" => "Add a test that verifies inherited behavior from super is needed, not just the subclass logic",
|
|
45
|
+
"explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters",
|
|
46
|
+
"index_to_fetch" => "Add a test that distinguishes [] (returns nil for missing keys) from .fetch (raises KeyError)",
|
|
47
|
+
"index_to_dig" => "Add a test that verifies chained [] access returns the correct nested value",
|
|
48
|
+
"index_assignment_removal" => "Add a test that verifies the []= assignment side effect is observable (the collection is modified)",
|
|
49
|
+
"pattern_matching_guard" => "Add a test with input that matches the pattern but fails the guard to verify filtering",
|
|
50
|
+
"pattern_matching_alternative" => "Add a test with input that matches only one specific alternative to verify each branch is reachable",
|
|
51
|
+
"pattern_matching_array" => "Add a test that verifies each element position in the array pattern matches the expected type or value",
|
|
52
|
+
"collection_return" => "Add a test that verifies the method returns a non-empty collection, not just any array or hash",
|
|
53
|
+
"scalar_return" => "Add a test that verifies the method returns a non-zero/non-empty scalar value, not just any type"
|
|
54
|
+
}.freeze
|
|
55
|
+
end
|