evilution 1.3.0 → 1.5.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 +132 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +127 -0
- data/README.md +136 -38
- data/docs/architecture.md +135 -11
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +121 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +41 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +187 -21
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +14 -15
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/round_half_mode.rb +38 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/source_surgeon.rb +2 -3
- data/lib/evilution/ast/syntax_check.rb +56 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +67 -30
- data/lib/evilution/cli/exit_guard.rb +26 -4
- data/lib/evilution/cli/parser/file_args.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +1 -1
- data/lib/evilution/cli.rb +3 -2
- data/lib/evilution/config/validators/spec_mappings.rb +2 -1
- data/lib/evilution/config.rb +3 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +45 -0
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +11 -1
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +177 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +80 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +22 -15
- data/lib/evilution/integration/loading/mutation_applier.rb +44 -1
- data/lib/evilution/integration/loading/own_methods.rb +87 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
- data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
- data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard.rb +4 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +84 -11
- data/lib/evilution/isolation/in_process.rb +100 -8
- data/lib/evilution/mcp/info_tool/status_glossary.rb +15 -4
- data/lib/evilution/mcp/info_tool.rb +3 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +21 -9
- data/lib/evilution/mutator/base.rb +69 -5
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +56 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +10 -2
- data/lib/evilution/mutator/operator/array_literal.rb +29 -0
- data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
- data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
- data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
- data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
- data/lib/evilution/mutator/operator/float_literal.rb +54 -19
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
- data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
- data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
- data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
- data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
- data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
- data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
- data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +41 -24
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/string_literal.rb +29 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
- data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
- data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
- data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
- data/lib/evilution/mutator/primitives.rb +24 -2
- data/lib/evilution/mutator/receiver_positions.rb +79 -0
- data/lib/evilution/mutator/registry.rb +58 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
- data/lib/evilution/parallel/work_queue/channel.rb +58 -5
- data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
- data/lib/evilution/parallel/work_queue/worker.rb +13 -4
- data/lib/evilution/parallel/work_queue.rb +15 -9
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +9 -4
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +57 -43
- data/lib/evilution/runner.rb +5 -2
- data/lib/evilution/subject.rb +25 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +90 -0
- data/script/memory_check +62 -16
- metadata +113 -4
|
@@ -12,8 +12,10 @@ require_relative "state_guard/world_example_groups"
|
|
|
12
12
|
require_relative "state_guard/world_sources_by_path"
|
|
13
13
|
require_relative "state_guard/world_filtered_examples"
|
|
14
14
|
require_relative "state_guard/reporter_arrays"
|
|
15
|
+
require_relative "state_guard/anonymous_example_group_examples"
|
|
15
16
|
require_relative "state_guard/example_groups_constants"
|
|
16
17
|
require_relative "state_guard/configuration_state"
|
|
18
|
+
require_relative "../../diagnostic"
|
|
17
19
|
|
|
18
20
|
class Evilution::Integration::RSpec::StateGuard
|
|
19
21
|
DEFAULT_STRATEGIES = [
|
|
@@ -22,6 +24,7 @@ class Evilution::Integration::RSpec::StateGuard
|
|
|
22
24
|
WorldSourcesByPath.new,
|
|
23
25
|
WorldFilteredExamples.new,
|
|
24
26
|
ReporterArrays.new,
|
|
27
|
+
AnonymousExampleGroupExamples.new,
|
|
25
28
|
ExampleGroupsConstants.new,
|
|
26
29
|
ConfigurationState.new
|
|
27
30
|
].freeze
|
|
@@ -43,6 +46,6 @@ class Evilution::Integration::RSpec::StateGuard
|
|
|
43
46
|
def release_one(strategy, captured)
|
|
44
47
|
strategy.release(captured)
|
|
45
48
|
rescue StandardError => e
|
|
46
|
-
warn
|
|
49
|
+
Evilution::Diagnostic.warn("[evilution] state release failed for #{strategy.class.name}: #{e.class}: #{e.message}")
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
4
|
require_relative "base"
|
|
5
|
+
require_relative "known_failures"
|
|
5
6
|
require_relative "../spec_resolver"
|
|
6
7
|
require_relative "../spec_selector"
|
|
7
8
|
require_relative "../related_spec_heuristic"
|
|
@@ -24,6 +25,7 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
24
25
|
fallback_to_full_suite: false,
|
|
25
26
|
spec_selector: nil,
|
|
26
27
|
example_filter: nil,
|
|
28
|
+
known_failures: [],
|
|
27
29
|
framework_loader: FrameworkLoader.new,
|
|
28
30
|
test_file_resolver: nil,
|
|
29
31
|
example_filter_applier: nil,
|
|
@@ -44,6 +46,7 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
44
46
|
@crash_detector_lifecycle = crash_detector_lifecycle
|
|
45
47
|
@result_builder = result_builder
|
|
46
48
|
@state_guard = state_guard
|
|
49
|
+
@known_failures = Evilution::Integration::KnownFailures.new(known_failures)
|
|
47
50
|
super(hooks: hooks)
|
|
48
51
|
end
|
|
49
52
|
|
|
@@ -71,7 +74,7 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
71
74
|
return @result_builder.unresolved_example(mutation) if targets.nil?
|
|
72
75
|
|
|
73
76
|
result = run_targets(targets)
|
|
74
|
-
return result unless
|
|
77
|
+
return result unless confirm?(result, targets, files)
|
|
75
78
|
|
|
76
79
|
run_targets(files)
|
|
77
80
|
end
|
|
@@ -85,8 +88,12 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
85
88
|
# So a survivor is re-run against the whole resolved file, and that run is the
|
|
86
89
|
# one reported. Only survivors pay for it, and only where the subset was
|
|
87
90
|
# actually narrower than the file.
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
#
|
|
92
|
+
# The same goes for a subset in which nothing failed but examples that were
|
|
93
|
+
# failing before any mutation ran: that is no verdict either, and the rest of
|
|
94
|
+
# the file may hold one.
|
|
95
|
+
def confirm?(result, targets, files)
|
|
96
|
+
(result[:passed] || result[:known_failures_only]) && targets != files
|
|
90
97
|
end
|
|
91
98
|
|
|
92
99
|
def run_targets(targets)
|
|
@@ -142,7 +149,8 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
142
149
|
claim_rspec_streams(out, err)
|
|
143
150
|
detector = @crash_detector_lifecycle.register
|
|
144
151
|
status = ::RSpec::Core::Runner.run(args, err, out)
|
|
145
|
-
|
|
152
|
+
flagged = known_failures_only?(status)
|
|
153
|
+
@result_builder.from_run(status, command, detector, examples_loaded:, known_failures_only: flagged)
|
|
146
154
|
rescue StandardError => e
|
|
147
155
|
{ passed: false, error: e.message, test_command: command }
|
|
148
156
|
ensure
|
|
@@ -188,12 +196,22 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
188
196
|
nil
|
|
189
197
|
end
|
|
190
198
|
|
|
199
|
+
# Whether the run failed on nothing but examples the baseline already saw
|
|
200
|
+
# failing. Decided here, in the process that ran them: the names of every
|
|
201
|
+
# failing example need not travel back, only the answer.
|
|
202
|
+
def known_failures_only?(status)
|
|
203
|
+
return false if status.zero? || @known_failures.empty?
|
|
204
|
+
|
|
205
|
+
@known_failures.only?(ExampleIds.failed(::RSpec.world))
|
|
206
|
+
end
|
|
207
|
+
|
|
191
208
|
def reset_examples
|
|
192
209
|
::RSpec.respond_to?(:clear_examples) ? ::RSpec.clear_examples : ::RSpec.reset
|
|
193
210
|
end
|
|
194
211
|
end
|
|
195
212
|
|
|
196
213
|
require_relative "rspec/framework_loader"
|
|
214
|
+
require_relative "rspec/example_ids"
|
|
197
215
|
require_relative "rspec/test_file_resolver"
|
|
198
216
|
require_relative "rspec/unresolved_spec_warner"
|
|
199
217
|
require_relative "rspec/example_filter_applier"
|
|
@@ -21,6 +21,12 @@ module Evilution::Integration::TestUnit::ResultBuilder
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Every test that failed was already failing in the baseline. Still a
|
|
25
|
+
# failed run, but flagged, so it is not taken for a kill.
|
|
26
|
+
def known_failures_only(command)
|
|
27
|
+
{ passed: false, known_failures_only: true, test_command: command }
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
def no_tests_ran(command)
|
|
25
31
|
{
|
|
26
32
|
passed: false,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_unit"
|
|
4
|
+
|
|
5
|
+
# Names a Test::Unit test the same way in the baseline and in a mutation run:
|
|
6
|
+
# the name the framework gives it, `test_adds(CalcTest)`.
|
|
7
|
+
module Evilution::Integration::TestUnit::TestIds
|
|
8
|
+
# Only failures and errors fail a run; pendings, omissions and notifications
|
|
9
|
+
# are faults too, and are left out.
|
|
10
|
+
def self.faults(result)
|
|
11
|
+
result.failures + result.errors
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The ids of the tests that failed or raised in a finished run.
|
|
15
|
+
def self.failed(result)
|
|
16
|
+
faults(result).map(&:test_name)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "test_unit_crash_detector"
|
|
5
|
+
require_relative "known_failures"
|
|
5
6
|
require_relative "loading/test_load_path"
|
|
7
|
+
require_relative "loading/test_class_cache"
|
|
6
8
|
require_relative "../spec_resolver"
|
|
7
9
|
require_relative "../spec_selector"
|
|
10
|
+
require_relative "../baseline"
|
|
8
11
|
|
|
9
12
|
require_relative "../integration"
|
|
10
13
|
|
|
@@ -28,6 +31,12 @@ class Evilution::Integration::TestUnit < Evilution::Integration::Base
|
|
|
28
31
|
Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# Shared by every instance: the canary and the mutation runs build their
|
|
35
|
+
# own, and a file loaded for one must not be loaded again for the other.
|
|
36
|
+
def self.test_classes
|
|
37
|
+
@test_classes ||= Evilution::Integration::Loading::TestClassCache.new { SubjectClassRegistry.descendants }
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
def self.baseline_options
|
|
32
41
|
{
|
|
33
42
|
runner: baseline_runner,
|
|
@@ -40,25 +49,41 @@ class Evilution::Integration::TestUnit < Evilution::Integration::Base
|
|
|
40
49
|
require_relative "test_unit/framework_loader"
|
|
41
50
|
require_relative "test_unit/subject_class_registry"
|
|
42
51
|
require_relative "test_unit/dispatcher"
|
|
52
|
+
require_relative "test_unit/test_ids"
|
|
43
53
|
FrameworkLoader.new.call
|
|
44
54
|
files = baseline_test_files(test_file)
|
|
45
55
|
Evilution::Integration::Loading::TestLoadPath.add!(files)
|
|
46
56
|
new_classes = SubjectClassRegistry.newly_loaded do
|
|
47
57
|
files.each { |f| load(File.expand_path(f)) }
|
|
48
58
|
end
|
|
49
|
-
Dispatcher.call(new_classes, name: "evilution baseline")
|
|
59
|
+
baseline_report(Dispatcher.call(new_classes, name: "evilution baseline"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.baseline_report(result)
|
|
63
|
+
Evilution::Baseline::Report.build(
|
|
64
|
+
passed: result.passed?, failures: baseline_failures(result), failed_ids: TestIds.failed(result)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.baseline_failures(result)
|
|
69
|
+
TestIds.faults(result).map do |fault|
|
|
70
|
+
{ id: fault.test_name, description: "", message: fault.message }
|
|
71
|
+
end
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
def self.baseline_test_files(test_file)
|
|
53
75
|
File.directory?(test_file) ? Dir.glob(File.join(test_file, "**/*_test.rb")) : [test_file]
|
|
54
76
|
end
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
# known_failures: the ids of the tests the baseline saw failing (see TestIds).
|
|
79
|
+
def initialize(test_files: nil, hooks: nil, fallback_to_full_suite: false, spec_selector: nil, known_failures: [])
|
|
57
80
|
require_relative "test_unit/framework_loader"
|
|
58
81
|
require_relative "test_unit/subject_class_registry"
|
|
59
82
|
require_relative "test_unit/dispatcher"
|
|
60
83
|
require_relative "test_unit/test_file_resolver"
|
|
61
84
|
require_relative "test_unit/result_builder"
|
|
85
|
+
require_relative "test_unit/test_ids"
|
|
86
|
+
@known_failures = Evilution::Integration::KnownFailures.new(known_failures)
|
|
62
87
|
@framework_loader = FrameworkLoader.new
|
|
63
88
|
@file_resolver = TestFileResolver.new(
|
|
64
89
|
test_files: test_files,
|
|
@@ -97,22 +122,25 @@ class Evilution::Integration::TestUnit < Evilution::Integration::Base
|
|
|
97
122
|
|
|
98
123
|
detector = reset_crash_detector
|
|
99
124
|
result = Dispatcher.call(new_classes, name: "evilution-mutation")
|
|
125
|
+
# Before the crash check: a test that was already failing may fail by
|
|
126
|
+
# raising now, and a crash in it is no more a kill than its failure was.
|
|
127
|
+
return ResultBuilder.known_failures_only(command) if @known_failures.only?(TestIds.failed(result))
|
|
128
|
+
|
|
100
129
|
result.faults.each { |fault| detector.record(fault) }
|
|
101
130
|
ResultBuilder.call(passed: result.passed?, command: command, detector: detector)
|
|
102
131
|
end
|
|
103
132
|
|
|
104
133
|
def load_test_classes(files)
|
|
105
134
|
Evilution::Integration::Loading::TestLoadPath.add!(files)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
end
|
|
135
|
+
paths = files.map { |f| File.expand_path(f, Evilution.project_base_dir) }
|
|
136
|
+
self.class.test_classes.fetch(paths) { |path| load(path) }
|
|
109
137
|
end
|
|
110
138
|
|
|
111
139
|
# Test::Unit has no public registry-clear analogous to
|
|
112
|
-
# Minitest::Runnable.runnables.clear.
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
140
|
+
# Minitest::Runnable.runnables.clear. Each dispatch is handed the classes of
|
|
141
|
+
# the files resolved for this mutation, so classes of other files sit
|
|
142
|
+
# dormant on ObjectSpace without polluting the run. #reset_state stays as a
|
|
143
|
+
# contract no-op for parity with Minitest.
|
|
116
144
|
def reset_state
|
|
117
145
|
# no-op — see comment above
|
|
118
146
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "tmpdir"
|
|
5
6
|
require_relative "../memory"
|
|
@@ -55,8 +56,23 @@ class Evilution::Isolation::Fork
|
|
|
55
56
|
# group and the TERM/KILL ladder sweeps the whole subtree on timeout (EV-2sh8
|
|
56
57
|
# / GH #1330). The block keeps the marshal-pipe protocol: write a
|
|
57
58
|
# length-prefixed payload, then exit with the pass/fail code.
|
|
59
|
+
#
|
|
60
|
+
# The at_exit hook covers a child that never gets that far. The normal path
|
|
61
|
+
# ends in `exit!`, which skips it, so it runs only when the child is dying.
|
|
62
|
+
#
|
|
63
|
+
# Both reports go through method objects taken before the test command runs.
|
|
64
|
+
# When the mutation under test is one of this file, the child evaluates the
|
|
65
|
+
# mutated source and the methods here are replaced; a method object keeps the
|
|
66
|
+
# definition the child started with, so the result is not written by the
|
|
67
|
+
# mutant it describes.
|
|
58
68
|
def spawn_child(read_io, write_io, sandbox_dir, mutation, test_command)
|
|
59
69
|
@supervisor.spawn(sandbox_dir: sandbox_dir) do
|
|
70
|
+
write = method(:write_result)
|
|
71
|
+
report_death = method(:report_death)
|
|
72
|
+
child_pid = Process.pid
|
|
73
|
+
# A process the tests fork inherits this hook and the pipe with it; its
|
|
74
|
+
# death is not the child's to report.
|
|
75
|
+
at_exit { report_death.call(write_io, $ERROR_INFO, write) if Process.pid == child_pid }
|
|
60
76
|
ENV["TMPDIR"] = sandbox_dir
|
|
61
77
|
# Path-relativizing mutations (e.g. File.join(dir, name) -> name) would
|
|
62
78
|
# otherwise write into the parent's CWD (typically the repo root) and
|
|
@@ -67,18 +83,54 @@ class Evilution::Isolation::Fork
|
|
|
67
83
|
# paths to Evilution::PROJECT_ROOT instead of the sandbox CWD.
|
|
68
84
|
Dir.chdir(sandbox_dir)
|
|
69
85
|
Evilution.in_isolated_worker!
|
|
86
|
+
Evilution.single_mutation_process!
|
|
70
87
|
read_io.close
|
|
71
88
|
suppress_child_output
|
|
72
89
|
@hooks.fire(:worker_process_start, mutation:) if @hooks
|
|
73
90
|
result = execute_in_child(mutation, test_command)
|
|
74
|
-
|
|
75
|
-
write_io.write([payload.bytesize].pack("N"))
|
|
76
|
-
write_io.write(payload)
|
|
77
|
-
write_io.close
|
|
91
|
+
write.call(write_io, result)
|
|
78
92
|
exit!(result[:passed] ? 0 : 1)
|
|
79
93
|
end
|
|
80
94
|
end
|
|
81
95
|
|
|
96
|
+
def write_result(write_io, result)
|
|
97
|
+
payload = Marshal.dump(result)
|
|
98
|
+
write_io.write([payload.bytesize].pack("N"))
|
|
99
|
+
write_io.write(payload)
|
|
100
|
+
write_io.close
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Some exceptions reach no `rescue` in the child. `execute_in_child` leaves
|
|
104
|
+
# everything that is not a ScriptError or StandardError alone, and Ruby may
|
|
105
|
+
# run neither `rescue` nor `ensure` when the stack overflows through
|
|
106
|
+
# `const_missing`, which a mutation of a process-wide `const_missing` does.
|
|
107
|
+
# Without a result the parent sees only an exit status.
|
|
108
|
+
#
|
|
109
|
+
# A child that exits on purpose or on a signal keeps its status as the
|
|
110
|
+
# report: the first is its own decision, the second is usually the parent's.
|
|
111
|
+
def report_death(write_io, error, write)
|
|
112
|
+
return if error.nil? || write_io.closed?
|
|
113
|
+
return if error.is_a?(SystemExit) || error.is_a?(SignalException)
|
|
114
|
+
|
|
115
|
+
write.call(write_io, death_result(error))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Dying while the tests run, after the mutated source loaded, is what a test
|
|
119
|
+
# that crashes does, and counts the same. Dying before that says nothing
|
|
120
|
+
# about the tests.
|
|
121
|
+
def death_result(error)
|
|
122
|
+
testing = Evilution.died_running_tests?(error)
|
|
123
|
+
phase = testing ? "while running the tests" : "before the tests ran"
|
|
124
|
+
result = {
|
|
125
|
+
passed: false,
|
|
126
|
+
error: "child died #{phase}: #{error.class}: #{error.message}",
|
|
127
|
+
error_class: error.class.name,
|
|
128
|
+
error_backtrace: Array(error.backtrace).first(5)
|
|
129
|
+
}
|
|
130
|
+
result[:test_crashed] = true if testing
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
|
|
82
134
|
# The parent owns read_io/write_io (write_io is closed right after spawn so
|
|
83
135
|
# read_io can see EOF), so they are closed here rather than handed to the
|
|
84
136
|
# supervisor. The supervisor reaps the child and removes the sandbox dir; on
|
|
@@ -203,28 +255,48 @@ class Evilution::Isolation::Fork
|
|
|
203
255
|
buf
|
|
204
256
|
end
|
|
205
257
|
|
|
258
|
+
# What arrives is not always what the child wrote: every process the child
|
|
259
|
+
# forks inherits the write end of the pipe. A result that does not load, or
|
|
260
|
+
# loads to something other than a result, costs this mutation its verdict
|
|
261
|
+
# and nothing more.
|
|
206
262
|
def decode_payload(data, handle = nil, terminated: false)
|
|
207
263
|
return empty_result(handle, terminated: terminated) if data.nil? || data.empty?
|
|
208
264
|
|
|
209
|
-
|
|
265
|
+
result = Marshal.load(data)
|
|
266
|
+
return unreadable_result(handle, "expected a Hash, got #{result.class}", terminated: terminated) unless result.is_a?(Hash)
|
|
267
|
+
|
|
268
|
+
{ timeout: false }.merge(result)
|
|
269
|
+
rescue TypeError, ArgumentError => e
|
|
270
|
+
unreadable_result(handle, "#{e.class}: #{e.message.gsub(/\s+/, " ")}", terminated: terminated)
|
|
210
271
|
end
|
|
211
272
|
|
|
212
273
|
def empty_result(handle, terminated: false)
|
|
213
274
|
{ timeout: false, passed: false, error: child_death_reason(handle, terminated: terminated) }
|
|
214
275
|
end
|
|
215
276
|
|
|
277
|
+
def unreadable_result(handle, detail, terminated: false)
|
|
278
|
+
ending = child_ending(handle, terminated: terminated)
|
|
279
|
+
subject = ending.nil? ? "child's result" : "#{ending}; its result"
|
|
280
|
+
{ timeout: false, passed: false, error: "#{subject} was unreadable (#{detail})" }
|
|
281
|
+
end
|
|
282
|
+
|
|
216
283
|
# A child that wrote no payload has usually left its stdout/stderr capture
|
|
217
284
|
# empty too, so its termination status is the only evidence of what happened.
|
|
218
285
|
# Reporting it separates a segfault from an OOM kill from a clean early exit,
|
|
219
286
|
# all of which reach this path identically.
|
|
220
287
|
def child_death_reason(handle, terminated: false)
|
|
221
|
-
|
|
288
|
+
ending = child_ending(handle, terminated: terminated)
|
|
289
|
+
ending.nil? ? "empty result from child" : "#{ending} without writing a result"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def child_ending(handle, terminated: false)
|
|
293
|
+
return "child was terminated after the reap deadline" if terminated
|
|
222
294
|
|
|
223
295
|
status = handle.nil? ? nil : handle.status
|
|
224
|
-
return
|
|
225
|
-
return "child exited #{status.exitstatus}
|
|
296
|
+
return nil unless status.is_a?(Process::Status)
|
|
297
|
+
return "child exited #{status.exitstatus}" unless status.signaled?
|
|
226
298
|
|
|
227
|
-
"child died on #{signal_name(status.termsig)}
|
|
299
|
+
"child died on #{signal_name(status.termsig)}"
|
|
228
300
|
end
|
|
229
301
|
|
|
230
302
|
def signal_name(termsig)
|
|
@@ -239,7 +311,7 @@ class Evilution::Isolation::Fork
|
|
|
239
311
|
|
|
240
312
|
def classify_status(result)
|
|
241
313
|
return :timeout if result[:timeout]
|
|
242
|
-
return :killed if result[:test_crashed]
|
|
314
|
+
return :killed if result[:test_crashed] || result[:load_crashed]
|
|
243
315
|
return :unresolved if result[:unresolved]
|
|
244
316
|
return :error if result[:error]
|
|
245
317
|
return :survived if result[:passed]
|
|
@@ -263,7 +335,8 @@ class Evilution::Isolation::Fork
|
|
|
263
335
|
message: result[:error],
|
|
264
336
|
klass: result[:error_class],
|
|
265
337
|
backtrace: result[:error_backtrace]
|
|
266
|
-
)
|
|
338
|
+
),
|
|
339
|
+
known_failures_only: result[:known_failures_only]
|
|
267
340
|
)
|
|
268
341
|
end
|
|
269
342
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "timeout"
|
|
5
6
|
require "tmpdir"
|
|
@@ -9,14 +10,84 @@ require_relative "../result/mutation_result"
|
|
|
9
10
|
require_relative "../isolation"
|
|
10
11
|
|
|
11
12
|
class Evilution::Isolation::InProcess
|
|
13
|
+
# Raised into the run when its time is up. A SignalException, because the
|
|
14
|
+
# timeout has to end the whole test run and a test framework rescues nearly
|
|
15
|
+
# everything a test raises, Timeout::Error included: it would record one
|
|
16
|
+
# failed test and go on to the next, which may reach the same endless loop
|
|
17
|
+
# with no timer left to stop it. Minitest, Test::Unit and RSpec all let a
|
|
18
|
+
# SignalException through.
|
|
19
|
+
#
|
|
20
|
+
# Not an Interrupt, though it is one of those: tooling that handles the
|
|
21
|
+
# user's Ctrl-C (maxitest) rescues Interrupt and skips every test from then
|
|
22
|
+
# on, for the rest of the process.
|
|
23
|
+
class Expired < SignalException
|
|
24
|
+
def initialize(_message = nil)
|
|
25
|
+
super("SIGALRM")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
12
29
|
@null_out = File.open(File::NULL, "w")
|
|
13
30
|
@null_err = File.open(File::NULL, "w")
|
|
31
|
+
@died = false
|
|
14
32
|
|
|
15
33
|
class << self
|
|
16
34
|
attr_reader :null_out, :null_err
|
|
35
|
+
|
|
36
|
+
# The mutation this process is testing, remembered so that a process dying
|
|
37
|
+
# in the middle of it can say which one it was. Ruby runs no `ensure` on
|
|
38
|
+
# some deaths (a stack overflow through `const_missing`, under the usual
|
|
39
|
+
# stack limit), so the mutation is still on record when the at_exit hook
|
|
40
|
+
# asks. The process is recorded with it: a process the tests fork inherits
|
|
41
|
+
# both the record and the hook, and its death is not this run's.
|
|
42
|
+
#
|
|
43
|
+
# The error stream is recorded as well, as it is before the mutation is
|
|
44
|
+
# tested: `$stderr` is the null stream while it is, and on such a death
|
|
45
|
+
# nothing puts it back.
|
|
46
|
+
def watch(mutation)
|
|
47
|
+
@watched = [Process.pid, mutation, $stderr]
|
|
48
|
+
return if @death_hook_installed
|
|
49
|
+
|
|
50
|
+
@death_hook_installed = true
|
|
51
|
+
at_exit { report_death($ERROR_INFO) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unwatch
|
|
55
|
+
@watched = nil
|
|
56
|
+
@died = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def watched
|
|
60
|
+
pid, mutation, = @watched
|
|
61
|
+
mutation if pid == Process.pid
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def report_death(error, stream: nil)
|
|
65
|
+
mutation = watched
|
|
66
|
+
return if mutation.nil? || error.nil?
|
|
67
|
+
return if error.is_a?(SystemExit) || error.is_a?(SignalException)
|
|
68
|
+
|
|
69
|
+
stream ||= @watched.last
|
|
70
|
+
stream.puts(
|
|
71
|
+
"[evilution] the run died while testing a mutation under in-process isolation: " \
|
|
72
|
+
"#{mutation} (#{error.class}: #{error.message}). " \
|
|
73
|
+
"In-process isolation cannot contain a mutation that takes the Ruby process down; " \
|
|
74
|
+
"re-run with --isolation fork."
|
|
75
|
+
)
|
|
76
|
+
stream.flush
|
|
77
|
+
@died = true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Whether this process is ending on a death that was reported. Such a run
|
|
81
|
+
# measured nothing, which whoever decides the process status has to know:
|
|
82
|
+
# the hook that reports the death runs before the project's own, and an
|
|
83
|
+
# exit from there would skip them.
|
|
84
|
+
def died?
|
|
85
|
+
@died
|
|
86
|
+
end
|
|
17
87
|
end
|
|
18
88
|
|
|
19
89
|
def call(mutation:, test_command:, timeout:)
|
|
90
|
+
self.class.watch(mutation)
|
|
20
91
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
92
|
rss_before = Evilution::Memory.rss_kb
|
|
22
93
|
sandbox_dir = Dir.mktmpdir("evilution-run")
|
|
@@ -27,13 +98,14 @@ class Evilution::Isolation::InProcess
|
|
|
27
98
|
|
|
28
99
|
build_mutation_result(mutation, result, duration, rss_before, rss_after, delta)
|
|
29
100
|
ensure
|
|
101
|
+
self.class.unwatch
|
|
30
102
|
FileUtils.rm_rf(sandbox_dir) if sandbox_dir
|
|
31
103
|
end
|
|
32
104
|
|
|
33
105
|
private
|
|
34
106
|
|
|
35
107
|
# The Dir.chdir block is inside the Timeout.timeout block so that a
|
|
36
|
-
#
|
|
108
|
+
# timeout raised mid-call still unwinds through Dir.chdir's ensure
|
|
37
109
|
# and restores the parent CWD before the rescue clause runs. The sandbox
|
|
38
110
|
# contains any relative-path writes from path-relativizing mutations
|
|
39
111
|
# (EV-wqxu / GH #1278). Evilution.with_isolated_worker signals the rest of
|
|
@@ -41,7 +113,7 @@ class Evilution::Isolation::InProcess
|
|
|
41
113
|
# SourceEvaluator/Integration) to anchor project-relative paths to
|
|
42
114
|
# PROJECT_ROOT for the duration of the call.
|
|
43
115
|
def execute_with_timeout(mutation, test_command, timeout, sandbox_dir)
|
|
44
|
-
result = Timeout.timeout(timeout) do
|
|
116
|
+
result = Timeout.timeout(timeout, Expired) do
|
|
45
117
|
Evilution.with_isolated_worker do
|
|
46
118
|
Dir.chdir(sandbox_dir) do
|
|
47
119
|
suppress_output { test_command.call(mutation) }
|
|
@@ -49,15 +121,34 @@ class Evilution::Isolation::InProcess
|
|
|
49
121
|
end
|
|
50
122
|
end
|
|
51
123
|
{ timeout: false }.merge(result)
|
|
52
|
-
rescue
|
|
124
|
+
rescue Expired
|
|
53
125
|
{ timeout: true }
|
|
54
126
|
rescue ScriptError, StandardError => e
|
|
127
|
+
failure_result(e, e.message)
|
|
128
|
+
rescue SignalException, SystemExit
|
|
129
|
+
raise
|
|
130
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- turned into this mutation's result
|
|
131
|
+
crash_result(e)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# An exception outside StandardError that no test framework held back: a
|
|
135
|
+
# stack overflow that unwound, most likely. Left alone it would end the run.
|
|
136
|
+
# Leaving the test run that way, after the mutated source loaded, is what a
|
|
137
|
+
# test that crashes does, and counts the same.
|
|
138
|
+
def crash_result(error)
|
|
139
|
+
message = "#{error.class}: #{error.message}"
|
|
140
|
+
return failure_result(error, message) unless Evilution.died_running_tests?(error)
|
|
141
|
+
|
|
142
|
+
failure_result(error, "test run ended by #{message}").merge(test_crashed: true)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def failure_result(error, message)
|
|
55
146
|
{
|
|
56
147
|
timeout: false,
|
|
57
148
|
passed: false,
|
|
58
|
-
error:
|
|
59
|
-
error_class:
|
|
60
|
-
error_backtrace: Array(
|
|
149
|
+
error: message,
|
|
150
|
+
error_class: error.class.name,
|
|
151
|
+
error_backtrace: Array(error.backtrace).first(5)
|
|
61
152
|
}
|
|
62
153
|
end
|
|
63
154
|
|
|
@@ -81,7 +172,7 @@ class Evilution::Isolation::InProcess
|
|
|
81
172
|
|
|
82
173
|
def classify_status(result)
|
|
83
174
|
return :timeout if result[:timeout]
|
|
84
|
-
return :killed if result[:test_crashed]
|
|
175
|
+
return :killed if result[:test_crashed] || result[:load_crashed]
|
|
85
176
|
return :unresolved if result[:unresolved]
|
|
86
177
|
return :error if result[:error]
|
|
87
178
|
return :survived if result[:passed]
|
|
@@ -106,7 +197,8 @@ class Evilution::Isolation::InProcess
|
|
|
106
197
|
message: result[:error],
|
|
107
198
|
klass: result[:error_class],
|
|
108
199
|
backtrace: result[:error_backtrace]
|
|
109
|
-
)
|
|
200
|
+
),
|
|
201
|
+
known_failures_only: result[:known_failures_only]
|
|
110
202
|
)
|
|
111
203
|
end
|
|
112
204
|
end
|
|
@@ -8,7 +8,11 @@ module Evilution::MCP::InfoTool::StatusGlossary
|
|
|
8
8
|
{
|
|
9
9
|
"status" => "killed",
|
|
10
10
|
"meaning" => "A test failed when the mutation was applied — the test suite caught the mutation. " \
|
|
11
|
-
"This is the desired outcome."
|
|
11
|
+
"This is the desired outcome. Also a mutation that made its own file raise while loading, " \
|
|
12
|
+
"before any test ran, when the original file still loads (error_message starts with " \
|
|
13
|
+
"'load failed:'), and one whose test process died on an exception no handler saw while " \
|
|
14
|
+
"the tests ran, as a crashing test would (error_message starts with 'child died' under " \
|
|
15
|
+
"fork isolation, 'test run ended by' under in-process isolation).",
|
|
12
16
|
"counted_in_score" => true
|
|
13
17
|
},
|
|
14
18
|
{
|
|
@@ -27,13 +31,20 @@ module Evilution::MCP::InfoTool::StatusGlossary
|
|
|
27
31
|
{
|
|
28
32
|
"status" => "error",
|
|
29
33
|
"meaning" => "Mutation execution raised an unexpected error (syntax error at load time, " \
|
|
30
|
-
"boot failure, test-infrastructure crash). The mutation could not be evaluated."
|
|
34
|
+
"boot failure, test-infrastructure crash). The mutation could not be evaluated. " \
|
|
35
|
+
"Includes a test process that died on an unhandled exception before the tests ran " \
|
|
36
|
+
"('child died ... before the tests ran'; error_class is the exception's own class), and " \
|
|
37
|
+
"results that are no verdict on the mutation at all: a fork child's result that " \
|
|
38
|
+
"was unreadable, a worker's result that could not be read (the worker was replaced), " \
|
|
39
|
+
"a child that died on a signal without writing a result (SIGABRT: the Ruby VM aborted; " \
|
|
40
|
+
"SIGSEGV; SIGKILL, e.g. out of memory). Re-run that one mutation; if a VM abort repeats, " \
|
|
41
|
+
"the mutant crashes the interpreter.",
|
|
31
42
|
"counted_in_score" => false
|
|
32
43
|
},
|
|
33
44
|
{
|
|
34
45
|
"status" => "neutral",
|
|
35
|
-
"meaning" => "
|
|
36
|
-
"
|
|
46
|
+
"meaning" => "No verdict: the tests failed only on examples that were already failing before " \
|
|
47
|
+
"the mutation was applied, or the test process crashed on infrastructure. " \
|
|
37
48
|
"Not a meaningful mutation signal.",
|
|
38
49
|
"counted_in_score" => false
|
|
39
50
|
},
|
|
@@ -18,7 +18,8 @@ class Evilution::MCP::InfoTool < MCP::Tool
|
|
|
18
18
|
tool_name "evilution-info"
|
|
19
19
|
description "Discover what evilution sees before running any mutations. " \
|
|
20
20
|
"One tool, five actions: " \
|
|
21
|
-
"'subjects' lists every
|
|
21
|
+
"'subjects' lists every mutation subject in the target files (methods, and code outside them: " \
|
|
22
|
+
"constant assignments, scopes, AASM and callback declarations) with its file, line, and mutation count; " \
|
|
22
23
|
"'tests' resolves which spec/test files cover the given sources (so you pick the right --spec before mutating); " \
|
|
23
24
|
"'environment' dumps the effective config (version, ruby, config file, timeout, " \
|
|
24
25
|
"integration, isolation, and every other setting); " \
|
|
@@ -36,7 +37,7 @@ class Evilution::MCP::InfoTool < MCP::Tool
|
|
|
36
37
|
type: "string",
|
|
37
38
|
enum: VALID_ACTIONS,
|
|
38
39
|
description: "Which discovery operation to perform. " \
|
|
39
|
-
"'subjects' lists
|
|
40
|
+
"'subjects' lists mutation subjects; 'tests' resolves specs for sources; " \
|
|
40
41
|
"'environment' dumps effective config; 'statuses' returns the result-status glossary; " \
|
|
41
42
|
"'feedback' returns the discussion URL and consent/privacy guidance."
|
|
42
43
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../mutate_tool"
|
|
5
5
|
require_relative "../../reporter/suggestion"
|
|
6
|
+
require_relative "../../diagnostic"
|
|
6
7
|
|
|
7
8
|
module Evilution::MCP::MutateTool::ProgressStreamer
|
|
8
9
|
def self.build(server_context:, suggest_tests:, integration:)
|
|
@@ -21,7 +22,7 @@ module Evilution::MCP::MutateTool::ProgressStreamer
|
|
|
21
22
|
detail = build_suggestion_detail(result.mutation, suggestion)
|
|
22
23
|
server_context.report_progress(survivor_index, message: ::JSON.generate(detail))
|
|
23
24
|
rescue StandardError => e
|
|
24
|
-
warn
|
|
25
|
+
Evilution::Diagnostic.warn("[evilution] progress stream disabled after error: #{e.class}: #{e.message}")
|
|
25
26
|
disabled = true
|
|
26
27
|
end
|
|
27
28
|
end
|