evilution 0.26.0 → 0.28.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 +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +23 -2
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +17 -4
- data/lib/evilution/config/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +93 -266
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
|
@@ -4,6 +4,8 @@ require "fileutils"
|
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require_relative "../memory"
|
|
6
6
|
require_relative "../temp_dir_tracker"
|
|
7
|
+
require_relative "../child_output"
|
|
8
|
+
require_relative "../process_cleanup"
|
|
7
9
|
|
|
8
10
|
require_relative "../isolation"
|
|
9
11
|
|
|
@@ -48,19 +50,23 @@ class Evilution::Isolation::Fork
|
|
|
48
50
|
read_io&.close
|
|
49
51
|
write_io&.close
|
|
50
52
|
ensure_reaped(pid)
|
|
51
|
-
restore_original_source
|
|
53
|
+
restore_original_source
|
|
52
54
|
FileUtils.rm_rf(sandbox_dir) if sandbox_dir
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
private
|
|
56
58
|
|
|
57
|
-
def restore_original_source
|
|
59
|
+
def restore_original_source
|
|
58
60
|
Evilution::TempDirTracker.cleanup_all
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
def suppress_child_output
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
if Evilution::ChildOutput.log_dir
|
|
65
|
+
Evilution::ChildOutput.redirect!
|
|
66
|
+
else
|
|
67
|
+
$stdout.reopen(File::NULL, "w")
|
|
68
|
+
$stderr.reopen(File::NULL, "w")
|
|
69
|
+
end
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
def execute_in_child(mutation, test_command)
|
|
@@ -83,7 +89,7 @@ class Evilution::Isolation::Fork
|
|
|
83
89
|
if data.empty?
|
|
84
90
|
{ timeout: false, passed: false, error: "empty result from child" }
|
|
85
91
|
else
|
|
86
|
-
{ timeout: false }.merge(Marshal.load(data))
|
|
92
|
+
{ timeout: false }.merge(Marshal.load(data))
|
|
87
93
|
end
|
|
88
94
|
else
|
|
89
95
|
terminate_child(pid)
|
|
@@ -108,7 +114,7 @@ class Evilution::Isolation::Fork
|
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def terminate_child(pid)
|
|
111
|
-
::
|
|
117
|
+
Evilution::ProcessCleanup.safe_kill("TERM", pid)
|
|
112
118
|
_, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
|
113
119
|
return if status
|
|
114
120
|
|
|
@@ -116,8 +122,8 @@ class Evilution::Isolation::Fork
|
|
|
116
122
|
_, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
|
117
123
|
return if status
|
|
118
124
|
|
|
119
|
-
::
|
|
120
|
-
::
|
|
125
|
+
Evilution::ProcessCleanup.safe_kill("KILL", pid)
|
|
126
|
+
Evilution::ProcessCleanup.safe_wait(pid)
|
|
121
127
|
end
|
|
122
128
|
|
|
123
129
|
def classify_status(result)
|
|
@@ -138,11 +144,15 @@ class Evilution::Isolation::Fork
|
|
|
138
144
|
status: status,
|
|
139
145
|
duration: duration,
|
|
140
146
|
test_command: result[:test_command],
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
memory: Evilution::Result::MemoryStats.from_fields(
|
|
148
|
+
child_rss_kb: result[:child_rss_kb],
|
|
149
|
+
parent_rss_kb: parent_rss_kb
|
|
150
|
+
),
|
|
151
|
+
error: Evilution::Result::ErrorInfo.from_fields(
|
|
152
|
+
message: result[:error],
|
|
153
|
+
klass: result[:error_class],
|
|
154
|
+
backtrace: result[:error_backtrace]
|
|
155
|
+
)
|
|
146
156
|
)
|
|
147
157
|
end
|
|
148
158
|
end
|
|
@@ -80,12 +80,16 @@ class Evilution::Isolation::InProcess
|
|
|
80
80
|
status: status,
|
|
81
81
|
duration: duration,
|
|
82
82
|
test_command: result[:test_command],
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
memory: Evilution::Result::MemoryStats.from_fields(
|
|
84
|
+
child_rss_kb: rss_after,
|
|
85
|
+
memory_delta_kb: memory_delta_kb,
|
|
86
|
+
parent_rss_kb: rss_before
|
|
87
|
+
),
|
|
88
|
+
error: Evilution::Result::ErrorInfo.from_fields(
|
|
89
|
+
message: result[:error],
|
|
90
|
+
klass: result[:error_class],
|
|
91
|
+
backtrace: result[:error_backtrace]
|
|
92
|
+
)
|
|
89
93
|
)
|
|
90
94
|
end
|
|
91
95
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../actions"
|
|
4
|
+
require_relative "../response_formatter"
|
|
5
|
+
|
|
6
|
+
class Evilution::MCP::InfoTool::Actions::Base
|
|
7
|
+
def self.call(**)
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def success(payload)
|
|
15
|
+
Evilution::MCP::InfoTool::ResponseFormatter.success(payload)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def config_error(message)
|
|
19
|
+
Evilution::MCP::InfoTool::ResponseFormatter.error("config_error", message)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../../../config"
|
|
5
|
+
require_relative "../../../version"
|
|
6
|
+
|
|
7
|
+
class Evilution::MCP::InfoTool::Actions::Environment < Evilution::MCP::InfoTool::Actions::Base
|
|
8
|
+
def self.call(**)
|
|
9
|
+
config = Evilution::Config.new(skip_config_file: false)
|
|
10
|
+
config_file = Evilution::Config::CONFIG_FILES.find { |path| File.exist?(path) }
|
|
11
|
+
|
|
12
|
+
success(
|
|
13
|
+
"version" => Evilution::VERSION,
|
|
14
|
+
"ruby" => RUBY_VERSION,
|
|
15
|
+
"config_file" => config_file,
|
|
16
|
+
"settings" => settings(config)
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def settings(config)
|
|
24
|
+
{
|
|
25
|
+
"timeout" => config.timeout,
|
|
26
|
+
"format" => config.format,
|
|
27
|
+
"integration" => config.integration,
|
|
28
|
+
"jobs" => config.jobs,
|
|
29
|
+
"isolation" => config.isolation,
|
|
30
|
+
"baseline" => config.baseline,
|
|
31
|
+
"incremental" => config.incremental,
|
|
32
|
+
"fail_fast" => config.fail_fast,
|
|
33
|
+
"min_score" => config.min_score,
|
|
34
|
+
"suggest_tests" => config.suggest_tests,
|
|
35
|
+
"save_session" => config.save_session,
|
|
36
|
+
"target" => config.target,
|
|
37
|
+
"skip_heredoc_literals" => config.skip_heredoc_literals,
|
|
38
|
+
"ignore_patterns" => config.ignore_patterns
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../../../version"
|
|
5
|
+
require_relative "../../../feedback"
|
|
6
|
+
require_relative "../../../feedback/messages"
|
|
7
|
+
|
|
8
|
+
class Evilution::MCP::InfoTool::Actions::Feedback < Evilution::MCP::InfoTool::Actions::Base
|
|
9
|
+
def self.call(**)
|
|
10
|
+
success(
|
|
11
|
+
"discussion_url" => Evilution::Feedback::DISCUSSION_URL,
|
|
12
|
+
"version" => Evilution::VERSION,
|
|
13
|
+
"guidance_for_agent" => Evilution::Feedback::Messages.info_guidance
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../status_glossary"
|
|
5
|
+
|
|
6
|
+
class Evilution::MCP::InfoTool::Actions::Statuses < Evilution::MCP::InfoTool::Actions::Base
|
|
7
|
+
def self.call(**)
|
|
8
|
+
success("statuses" => Evilution::MCP::InfoTool::StatusGlossary.entries)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../config_factory"
|
|
5
|
+
require_relative "../../../runner"
|
|
6
|
+
require_relative "../../../mutator/registry"
|
|
7
|
+
require_relative "../../../ast/pattern/filter"
|
|
8
|
+
|
|
9
|
+
class Evilution::MCP::InfoTool::Actions::Subjects < Evilution::MCP::InfoTool::Actions::Base
|
|
10
|
+
def self.call(files: nil, line_ranges: nil, target: nil, integration: nil, skip_config: nil, **)
|
|
11
|
+
return config_error("files is required") if files.nil? || files.empty?
|
|
12
|
+
|
|
13
|
+
config = Evilution::MCP::InfoTool::ConfigFactory.subjects(
|
|
14
|
+
files: files, line_ranges: line_ranges,
|
|
15
|
+
target: target, integration: integration, skip_config: skip_config
|
|
16
|
+
)
|
|
17
|
+
runner = Evilution::Runner.new(config: config)
|
|
18
|
+
subjects = runner.parse_and_filter_subjects
|
|
19
|
+
|
|
20
|
+
registry = Evilution::Mutator::Registry.default
|
|
21
|
+
filter = build_subject_filter(config)
|
|
22
|
+
operator_options = { skip_heredoc_literals: config.skip_heredoc_literals? }
|
|
23
|
+
|
|
24
|
+
entries = subjects.map do |subj|
|
|
25
|
+
count = registry.mutations_for(subj, filter: filter, operator_options: operator_options).length
|
|
26
|
+
{ "name" => subj.name, "file" => subj.file_path, "line" => subj.line_number, "mutations" => count }
|
|
27
|
+
ensure
|
|
28
|
+
subj.release_node!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
success(
|
|
32
|
+
"subjects" => entries,
|
|
33
|
+
"total_subjects" => entries.length,
|
|
34
|
+
"total_mutations" => entries.sum { |e| e["mutations"] }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_subject_filter(config)
|
|
42
|
+
return nil if config.ignore_patterns.empty?
|
|
43
|
+
|
|
44
|
+
Evilution::AST::Pattern::Filter.new(config.ignore_patterns)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../config_factory"
|
|
5
|
+
require_relative "../../../runner"
|
|
6
|
+
require_relative "../../../spec_resolver"
|
|
7
|
+
|
|
8
|
+
class Evilution::MCP::InfoTool::Actions::Tests < Evilution::MCP::InfoTool::Actions::Base
|
|
9
|
+
def self.call(files: nil, spec: nil, integration: nil, skip_config: nil, **)
|
|
10
|
+
return config_error("files is required") if files.nil? || files.empty?
|
|
11
|
+
|
|
12
|
+
config = Evilution::MCP::InfoTool::ConfigFactory.tests(
|
|
13
|
+
files: files, spec: spec, integration: integration, skip_config: skip_config
|
|
14
|
+
)
|
|
15
|
+
return explicit_specs_response(files, config.spec_files) if config.spec_files.any?
|
|
16
|
+
|
|
17
|
+
resolver = resolver_for(config.integration)
|
|
18
|
+
resolved, unresolved = resolve_specs(files, resolver)
|
|
19
|
+
success(
|
|
20
|
+
"specs" => resolved,
|
|
21
|
+
"unresolved" => unresolved,
|
|
22
|
+
"total_sources" => files.length,
|
|
23
|
+
"total_specs" => resolved.map { |r| r["spec"] }.uniq.length
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def resolver_for(integration)
|
|
31
|
+
integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
|
|
32
|
+
return Evilution::SpecResolver.new unless integration_class
|
|
33
|
+
|
|
34
|
+
integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def explicit_specs_response(files, spec_files)
|
|
38
|
+
success(
|
|
39
|
+
"specs" => spec_files.map { |f| { "source" => nil, "spec" => f } },
|
|
40
|
+
"unresolved" => [],
|
|
41
|
+
"total_sources" => files.length,
|
|
42
|
+
"total_specs" => spec_files.uniq.length
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_specs(files, resolver)
|
|
47
|
+
resolved = []
|
|
48
|
+
unresolved = []
|
|
49
|
+
files.each do |source|
|
|
50
|
+
found = resolver.call(source)
|
|
51
|
+
if found
|
|
52
|
+
resolved << { "source" => source, "spec" => found }
|
|
53
|
+
else
|
|
54
|
+
unresolved << source
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
[resolved, unresolved]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
require_relative "../../mcp"
|
|
5
|
+
|
|
6
|
+
class Evilution::MCP::InfoTool < MCP::Tool; end unless defined?(Evilution::MCP::InfoTool)
|
|
7
|
+
|
|
8
|
+
module Evilution::MCP::InfoTool::Actions
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless defined?(Evilution::MCP::InfoTool::Actions::Base)
|
|
12
|
+
class Evilution::MCP::InfoTool::Actions::Base # rubocop:disable Lint/EmptyClass
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative "../info_tool"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../info_tool"
|
|
4
|
+
require_relative "../../config"
|
|
5
|
+
|
|
6
|
+
module Evilution::MCP::InfoTool::ConfigFactory
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def subjects(files:, line_ranges:, target:, integration:, skip_config:)
|
|
10
|
+
opts = { target_files: files, line_ranges: line_ranges || {} }
|
|
11
|
+
opts[:skip_config_file] = true if skip_config
|
|
12
|
+
opts[:target] = target if target
|
|
13
|
+
opts[:integration] = integration if integration
|
|
14
|
+
Evilution::Config.new(**opts)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tests(files:, spec:, integration:, skip_config:)
|
|
18
|
+
opts = { target_files: files }
|
|
19
|
+
opts[:skip_config_file] = true if skip_config
|
|
20
|
+
opts[:spec_files] = spec if spec
|
|
21
|
+
opts[:integration] = integration if integration
|
|
22
|
+
Evilution::Config.new(**opts)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../info_tool"
|
|
4
|
+
|
|
5
|
+
module Evilution::MCP::InfoTool::ErrorMapper
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def type_for(error)
|
|
9
|
+
case error
|
|
10
|
+
when Evilution::ConfigError then "config_error"
|
|
11
|
+
when Evilution::ParseError then "parse_error"
|
|
12
|
+
else "runtime_error"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../info_tool"
|
|
4
|
+
|
|
5
|
+
module Evilution::MCP::InfoTool::RequestParser
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse_files(raw_files)
|
|
9
|
+
files = []
|
|
10
|
+
ranges = {}
|
|
11
|
+
|
|
12
|
+
raw_files.each do |arg|
|
|
13
|
+
file, range_str = arg.split(":", 2)
|
|
14
|
+
files << file
|
|
15
|
+
ranges[file] = parse_line_range(range_str) if range_str
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[files, ranges]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse_line_range(str)
|
|
22
|
+
if str.include?("-")
|
|
23
|
+
start_str, end_str = str.split("-", 2)
|
|
24
|
+
start_line = Integer(start_str)
|
|
25
|
+
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
26
|
+
start_line..end_line
|
|
27
|
+
else
|
|
28
|
+
line = Integer(str)
|
|
29
|
+
line..line
|
|
30
|
+
end
|
|
31
|
+
rescue ArgumentError, TypeError
|
|
32
|
+
raise Evilution::ParseError, "invalid line range: #{str.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../info_tool"
|
|
5
|
+
require_relative "error_mapper"
|
|
6
|
+
|
|
7
|
+
module Evilution::MCP::InfoTool::ResponseFormatter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def success(payload)
|
|
11
|
+
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(payload) }])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def error(type, message)
|
|
15
|
+
::MCP::Tool::Response.new(
|
|
16
|
+
[{ type: "text", text: ::JSON.generate({ error: { type: type, message: message } }) }],
|
|
17
|
+
error: true
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error_for(exception)
|
|
22
|
+
error(Evilution::MCP::InfoTool::ErrorMapper.type_for(exception), exception.message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../info_tool"
|
|
4
|
+
require_relative "../../result/mutation_result"
|
|
5
|
+
|
|
6
|
+
module Evilution::MCP::InfoTool::StatusGlossary
|
|
7
|
+
ENTRIES = [
|
|
8
|
+
{
|
|
9
|
+
"status" => "killed",
|
|
10
|
+
"meaning" => "A test failed when the mutation was applied — the test suite caught the mutation. " \
|
|
11
|
+
"This is the desired outcome.",
|
|
12
|
+
"counted_in_score" => true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"status" => "survived",
|
|
16
|
+
"meaning" => "No test failed when the mutation was applied — gap in coverage. " \
|
|
17
|
+
"The test suite did not detect the behavioral change.",
|
|
18
|
+
"counted_in_score" => true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"status" => "timeout",
|
|
22
|
+
"meaning" => "Test run exceeded the configured per-mutation timeout. " \
|
|
23
|
+
"Treated like survived for scoring (counted in the denominator); " \
|
|
24
|
+
"may indicate an infinite loop introduced by the mutation.",
|
|
25
|
+
"counted_in_score" => true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"status" => "error",
|
|
29
|
+
"meaning" => "Mutation execution raised an unexpected error (syntax error at load time, " \
|
|
30
|
+
"boot failure, test-infrastructure crash). The mutation could not be evaluated.",
|
|
31
|
+
"counted_in_score" => false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"status" => "neutral",
|
|
35
|
+
"meaning" => "Baseline tests already failed before the mutation was applied — pre-existing " \
|
|
36
|
+
"test-suite problem (flaky spec, infra collision, fixture setup failure). " \
|
|
37
|
+
"Not a meaningful mutation signal.",
|
|
38
|
+
"counted_in_score" => false
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"status" => "equivalent",
|
|
42
|
+
"meaning" => "Mutation is provably identical to the original source " \
|
|
43
|
+
"(e.g. a no-op replacement that the parser or evaluator treats as semantically equal).",
|
|
44
|
+
"counted_in_score" => false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"status" => "unresolved",
|
|
48
|
+
"meaning" => "No spec/test file resolved for the mutated source — coverage gap, not a failure. " \
|
|
49
|
+
"The file has no corresponding test file the resolver could locate.",
|
|
50
|
+
"counted_in_score" => false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"status" => "unparseable",
|
|
54
|
+
"meaning" => "Mutated source failed to parse (e.g. dangling heredoc after method_body_replacement). " \
|
|
55
|
+
"Short-circuited before execution; no test run was attempted.",
|
|
56
|
+
"counted_in_score" => false
|
|
57
|
+
}
|
|
58
|
+
].each(&:freeze).freeze
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
def entries
|
|
63
|
+
check_drift!
|
|
64
|
+
ENTRIES
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_drift!
|
|
68
|
+
defined_statuses = Evilution::Result::MutationResult::STATUSES.map(&:to_s).sort
|
|
69
|
+
documented = ENTRIES.map { |s| s["status"] }.sort
|
|
70
|
+
return if defined_statuses == documented
|
|
71
|
+
|
|
72
|
+
missing = (defined_statuses - documented) + (documented - defined_statuses)
|
|
73
|
+
raise Evilution::Error, "status glossary drift: #{missing.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|