evilution 1.2.0 → 1.4.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 +100 -0
- data/.rubocop_todo.yml +14 -0
- data/CHANGELOG.md +124 -0
- data/README.md +142 -28
- data/docs/architecture.md +123 -9
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +39 -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/included_block.rb +27 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/parser.rb +120 -16
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +15 -15
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -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 +101 -42
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +4 -1
- data/lib/evilution/cli/parser/options_builder.rb +4 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +8 -2
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +3 -1
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- 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 +46 -1
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +2 -0
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- 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/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +10 -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 +2 -1
- data/lib/evilution/isolation/in_process.rb +21 -4
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -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 +8 -3
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- 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/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -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/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/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- 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_anchor_to_predicate.rb +136 -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_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/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +54 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/rails_warmup.rb +61 -0
- 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/html/report.rb +1 -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/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +26 -11
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
- 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.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.rb +46 -1
- data/lib/evilution/runner.rb +4 -1
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +51 -0
- data/script/memory_check +62 -16
- metadata +108 -4
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../runner"
|
|
4
4
|
require_relative "../ast/inheritance_scanner"
|
|
5
|
+
require_relative "../ast/uncovered_code"
|
|
6
|
+
require_relative "../diagnostic"
|
|
5
7
|
require_relative "../git/changed_files"
|
|
6
8
|
|
|
7
9
|
class Evilution::Runner::SubjectPipeline
|
|
@@ -12,6 +14,7 @@ class Evilution::Runner::SubjectPipeline
|
|
|
12
14
|
|
|
13
15
|
def call
|
|
14
16
|
subjects = parse_subjects
|
|
17
|
+
@subjects_by_file = subjects.group_by(&:file_path)
|
|
15
18
|
subjects = filter_by_descendants(subjects) if descendants_target?
|
|
16
19
|
subjects = filter_by_target(subjects) if method_target?
|
|
17
20
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
@@ -22,6 +25,28 @@ class Evilution::Runner::SubjectPipeline
|
|
|
22
25
|
@target_files ||= resolve_target_files
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
# Warns about the targeted lines that hold code outside every subject and
|
|
29
|
+
# that no mutation reached either. mutated_lines: the lines mutations were
|
|
30
|
+
# generated for, per file -- known only once #call's subjects are planned.
|
|
31
|
+
#
|
|
32
|
+
# A line range is checked as given; a whole file only when it has no
|
|
33
|
+
# subjects at all -- every class with an `include` or a constant would
|
|
34
|
+
# otherwise warn.
|
|
35
|
+
def report_uncovered_code(mutated_lines = {})
|
|
36
|
+
by_file = @subjects_by_file || {}
|
|
37
|
+
@subjects_by_file = nil
|
|
38
|
+
@uncovered_code = target_files.filter_map do |file|
|
|
39
|
+
uncovered_entry(file, by_file.fetch(file, []), mutated_lines.fetch(file, []))
|
|
40
|
+
end
|
|
41
|
+
@uncovered_code.each { |entry| warn_uncovered(entry) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Those lines, as `{ file:, lines: ["2-4", "9"] }` per file, once
|
|
45
|
+
# #report_uncovered_code has run.
|
|
46
|
+
def uncovered_code
|
|
47
|
+
@uncovered_code || []
|
|
48
|
+
end
|
|
49
|
+
|
|
25
50
|
private
|
|
26
51
|
|
|
27
52
|
attr_reader :config, :parser
|
|
@@ -30,6 +55,26 @@ class Evilution::Runner::SubjectPipeline
|
|
|
30
55
|
target_files.flat_map { |file| parser.call(file) }
|
|
31
56
|
end
|
|
32
57
|
|
|
58
|
+
def uncovered_entry(file, file_subjects, mutated_lines)
|
|
59
|
+
range = config.line_ranges[file]
|
|
60
|
+
return unless range || file_subjects.empty?
|
|
61
|
+
|
|
62
|
+
lines = Evilution::AST::UncoveredCode.call(file, file_subjects, lines: range, mutated_lines: mutated_lines)
|
|
63
|
+
{ file: file, lines: lines.map { |run| line_label(run) } } unless lines.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Spelled the way a range is passed on the command line: `2-4`, or `9`.
|
|
67
|
+
def line_label(run)
|
|
68
|
+
run.size == 1 ? run.first.to_s : "#{run.first}-#{run.last}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def warn_uncovered(entry)
|
|
72
|
+
Evilution::Diagnostic.warn(
|
|
73
|
+
"[evilution] #{entry[:file]}:#{entry[:lines].join(", ")} holds code outside every subject " \
|
|
74
|
+
"(class-body code such as DSL calls and constants is not mutated); no mutations target those lines."
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
33
78
|
def source_glob_target?
|
|
34
79
|
config.target&.start_with?("source:")
|
|
35
80
|
end
|
|
@@ -124,7 +169,7 @@ class Evilution::Runner::SubjectPipeline
|
|
|
124
169
|
end
|
|
125
170
|
|
|
126
171
|
def class_matcher(target)
|
|
127
|
-
->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
|
|
172
|
+
->(s) { s.name == target || s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
|
|
128
173
|
end
|
|
129
174
|
|
|
130
175
|
def filter_by_line_ranges(subjects)
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -38,6 +38,7 @@ class Evilution::Runner
|
|
|
38
38
|
baseline_result = run_baseline(subjects)
|
|
39
39
|
|
|
40
40
|
plan = mutation_planner.call(subjects)
|
|
41
|
+
subject_pipeline.report_uncovered_code(plan.mutated_lines)
|
|
41
42
|
release_subject_nodes(subjects)
|
|
42
43
|
clear_operator_caches
|
|
43
44
|
execution = run_mutations(plan.enabled, baseline_result)
|
|
@@ -52,7 +53,9 @@ class Evilution::Runner
|
|
|
52
53
|
disabled_mutations: plan.disabled_mutations,
|
|
53
54
|
unresolved_target_files: target_spec_audit.call(audited_files),
|
|
54
55
|
target_file_count: audited_files.length,
|
|
55
|
-
infra_retried: execution.infra_retried
|
|
56
|
+
infra_retried: execution.infra_retried,
|
|
57
|
+
uncovered_code: subject_pipeline.uncovered_code,
|
|
58
|
+
baseline_failures: baseline_result ? baseline_result.failures : [])
|
|
56
59
|
output_report(summary)
|
|
57
60
|
save_session(summary)
|
|
58
61
|
|
data/lib/evilution/subject.rb
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# What mutations are generated for and reported against. A :method subject is
|
|
4
|
+
# a method definition; a :constant subject is a definition assigned to a
|
|
5
|
+
# constant outside any method, such as `Point = Data.define(:x, :y)`. A :scope
|
|
6
|
+
# or :aasm subject is the body of a callable handed to a class-body
|
|
7
|
+
# declaration.
|
|
3
8
|
class Evilution::Subject
|
|
4
|
-
attr_reader :name, :file_path, :line_number, :source, :node
|
|
9
|
+
attr_reader :name, :file_path, :line_number, :source, :node, :kind
|
|
5
10
|
|
|
6
|
-
def initialize(name:, file_path:, line_number:, source:, node:)
|
|
11
|
+
def initialize(name:, file_path:, line_number:, source:, node:, kind: :method)
|
|
7
12
|
@name = name
|
|
8
13
|
@file_path = file_path
|
|
9
14
|
@line_number = line_number
|
|
10
15
|
@source = source
|
|
11
16
|
@node = node
|
|
17
|
+
@kind = kind
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def release_node!
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "evilution/ast/inheritance_scanner"
|
|
|
14
14
|
require_relative "evilution/ast/pattern"
|
|
15
15
|
require_relative "evilution/ast/sorbet_sig_detector"
|
|
16
16
|
require_relative "evilution/ast/local_reads"
|
|
17
|
+
require_relative "evilution/ast/regexp_pattern"
|
|
17
18
|
require_relative "evilution/ast/pattern/matcher"
|
|
18
19
|
require_relative "evilution/ast/pattern/parser"
|
|
19
20
|
require_relative "evilution/hooks"
|
|
@@ -30,6 +31,56 @@ require_relative "evilution/mutator/operator/nil_replacement"
|
|
|
30
31
|
require_relative "evilution/mutator/operator/boolean_operator_replacement"
|
|
31
32
|
require_relative "evilution/mutator/operator/boolean_operand_promotion"
|
|
32
33
|
require_relative "evilution/mutator/operator/boolean_expression_to_nil"
|
|
34
|
+
require_relative "evilution/mutator/operator/call_to_nil"
|
|
35
|
+
require_relative "evilution/mutator/operator/safe_navigation_removal"
|
|
36
|
+
require_relative "evilution/mutator/operator/attribute_write_to_read"
|
|
37
|
+
require_relative "evilution/mutator/operator/argument_propagation"
|
|
38
|
+
require_relative "evilution/mutator/operator/argument_list_removal"
|
|
39
|
+
require_relative "evilution/mutator/operator/symbol_to_proc_replacement"
|
|
40
|
+
require_relative "evilution/mutator/operator/dynamic_dispatch_resolution"
|
|
41
|
+
require_relative "evilution/mutator/operator/keyword_argument_removal"
|
|
42
|
+
require_relative "evilution/mutator/operator/double_negation_removal"
|
|
43
|
+
require_relative "evilution/mutator/operator/regexp_anchor_to_predicate"
|
|
44
|
+
require_relative "evilution/mutator/operator/reduce_to_sum"
|
|
45
|
+
require_relative "evilution/mutator/operator/array_coercion_to_literal"
|
|
46
|
+
require_relative "evilution/mutator/operator/to_i_to_integer"
|
|
47
|
+
require_relative "evilution/mutator/operator/coercion_emptying"
|
|
48
|
+
require_relative "evilution/mutator/operator/const_get_to_constant_path"
|
|
49
|
+
require_relative "evilution/mutator/operator/proc_to_lambda"
|
|
50
|
+
require_relative "evilution/mutator/operator/dig_to_fetch_chain"
|
|
51
|
+
require_relative "evilution/mutator/operator/inequality_to_negated_identity"
|
|
52
|
+
require_relative "evilution/mutator/operator/binary_operand_promotion"
|
|
53
|
+
require_relative "evilution/mutator/operator/receiver_constructor_swap"
|
|
54
|
+
require_relative "evilution/mutator/operator/block_body_to_nil"
|
|
55
|
+
require_relative "evilution/mutator/operator/block_body_to_raise"
|
|
56
|
+
require_relative "evilution/mutator/operator/block_body_promotion"
|
|
57
|
+
require_relative "evilution/mutator/operator/data_struct_member"
|
|
58
|
+
require_relative "evilution/mutator/operator/pin_operator_removal"
|
|
59
|
+
require_relative "evilution/mutator/operator/rightward_assignment"
|
|
60
|
+
require_relative "evilution/mutator/operator/numbered_parameter_swap"
|
|
61
|
+
require_relative "evilution/mutator/operator/forwarded_argument_drop"
|
|
62
|
+
require_relative "evilution/mutator/operator/pattern_wildcard_widening"
|
|
63
|
+
require_relative "evilution/mutator/operator/no_matching_pattern_else"
|
|
64
|
+
require_relative "evilution/mutator/operator/argument_order_permutation"
|
|
65
|
+
require_relative "evilution/mutator/operator/keyword_value_swap"
|
|
66
|
+
require_relative "evilution/mutator/operator/comparison_operand_swap"
|
|
67
|
+
require_relative "evilution/mutator/operator/integer_division_to_fdiv"
|
|
68
|
+
require_relative "evilution/mutator/operator/off_by_one_boundary"
|
|
69
|
+
require_relative "evilution/mutator/operator/format_specifier_swap"
|
|
70
|
+
require_relative "evilution/mutator/operator/exception_swallow"
|
|
71
|
+
require_relative "evilution/mutator/operator/statement_reorder"
|
|
72
|
+
require_relative "evilution/mutator/operator/alias_removal"
|
|
73
|
+
require_relative "evilution/mutator/operator/regexp_character_type_complement"
|
|
74
|
+
require_relative "evilution/mutator/operator/regexp_anchor_promotion"
|
|
75
|
+
require_relative "evilution/mutator/operator/regexp_alternation_branch_deletion"
|
|
76
|
+
require_relative "evilution/mutator/operator/regexp_quantifier_minimum_swap"
|
|
77
|
+
require_relative "evilution/mutator/operator/regexp_capture_to_passive"
|
|
78
|
+
require_relative "evilution/mutator/operator/regexp_named_group_rename"
|
|
79
|
+
require_relative "evilution/mutator/operator/regexp_option_removal"
|
|
80
|
+
require_relative "evilution/mutator/operator/rescue_handler_promotion"
|
|
81
|
+
require_relative "evilution/mutator/operator/rescue_handler_concatenation"
|
|
82
|
+
require_relative "evilution/mutator/operator/rescue_else_concatenation"
|
|
83
|
+
require_relative "evilution/mutator/operator/return_keyword_removal"
|
|
33
84
|
require_relative "evilution/mutator/operator/arithmetic_replacement"
|
|
34
85
|
require_relative "evilution/mutator/operator/string_literal"
|
|
35
86
|
require_relative "evilution/mutator/operator/array_literal"
|
data/script/memory_check
CHANGED
|
@@ -11,6 +11,13 @@ COMPLEX_FIXTURE = File.expand_path("../spec/support/fixtures/memory_check/target
|
|
|
11
11
|
COMPLEX_FIXTURE_SPEC = File.expand_path("../spec/support/fixtures/memory_check/target_examples.rb", __dir__)
|
|
12
12
|
ITERATIONS = Integer(ENV.fetch("MEMORY_CHECK_ITERATIONS", 50))
|
|
13
13
|
MAX_GROWTH_KB = Integer(ENV.fetch("MEMORY_CHECK_MAX_GROWTH_KB", 10_240))
|
|
14
|
+
# Every check that picks mutations at random draws from its own Random seeded
|
|
15
|
+
# with this, so a run exercises the same mutations every time and a failure can
|
|
16
|
+
# be replayed by passing the printed seed back.
|
|
17
|
+
SEED = Integer(ENV.fetch("MEMORY_CHECK_SEED", 42))
|
|
18
|
+
COMPLEX_POOL_SIZE = 10
|
|
19
|
+
COMPLEX_ITERATIONS = Integer(ENV.fetch("MEMORY_CHECK_COMPLEX_ITERATIONS", 100))
|
|
20
|
+
COMPLEX_MAX_GROWTH_KB = Integer(ENV.fetch("MEMORY_CHECK_COMPLEX_MAX_GROWTH_KB", 10_240))
|
|
14
21
|
|
|
15
22
|
def setup_workload
|
|
16
23
|
parser = Evilution::AST::Parser.new
|
|
@@ -21,25 +28,40 @@ def setup_workload
|
|
|
21
28
|
mutations
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
def run_check(name, iterations: ITERATIONS, max_growth_kb: MAX_GROWTH_KB,
|
|
25
|
-
|
|
31
|
+
def run_check(name, iterations: ITERATIONS, max_growth_kb: MAX_GROWTH_KB, picked: nil,
|
|
32
|
+
warmup_iterations: Evilution::Memory::LeakCheck::WARMUP_ITERATIONS, &)
|
|
33
|
+
check = Evilution::Memory::LeakCheck.new(iterations: iterations, max_growth_kb: max_growth_kb,
|
|
34
|
+
warmup_iterations: warmup_iterations)
|
|
26
35
|
result = check.run(&)
|
|
27
|
-
report(name, result)
|
|
36
|
+
report(name, result, picked)
|
|
28
37
|
result[:passed]
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
def report(name, result)
|
|
40
|
+
def report(name, result, picked)
|
|
32
41
|
status = result[:passed] ? "PASS" : "FAIL"
|
|
33
42
|
metrics = format_metrics(result)
|
|
34
43
|
|
|
35
44
|
puts "[#{status}] #{name}"
|
|
36
|
-
puts "
|
|
45
|
+
puts " Sustained growth: #{metrics[:sustained]} (max: #{metrics[:max]})"
|
|
46
|
+
puts " Growth: #{metrics[:growth]}"
|
|
37
47
|
puts " Samples (MB): #{metrics[:samples]}"
|
|
48
|
+
report_picked(picked) unless result[:passed]
|
|
38
49
|
puts
|
|
39
50
|
end
|
|
40
51
|
|
|
52
|
+
# What a failed run exercised, so it can be replayed: the seed, and how often
|
|
53
|
+
# each operator came up among the mutations the check picked.
|
|
54
|
+
def report_picked(picked)
|
|
55
|
+
puts " Seed: #{SEED} (MEMORY_CHECK_SEED)"
|
|
56
|
+
return if picked.nil? || picked.empty?
|
|
57
|
+
|
|
58
|
+
tally = picked.map(&:operator_name).tally.sort_by { |operator, count| [-count, operator.to_s] }
|
|
59
|
+
puts " Operators (#{picked.size} picks): #{tally.map { |operator, count| "#{operator} x#{count}" }.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
|
|
41
62
|
def format_metrics(result)
|
|
42
63
|
{
|
|
64
|
+
sustained: result[:sustained_growth_mb] ? format("%.1f MB", result[:sustained_growth_mb]) : "N/A",
|
|
43
65
|
growth: result[:growth_mb] ? format("%.1f MB", result[:growth_mb]) : "N/A",
|
|
44
66
|
max: format("%.1f MB", result[:max_growth_kb] / 1024.0),
|
|
45
67
|
samples: result[:samples].map { |s| s ? format("%.1f", s / 1024.0) : "N/A" }.join(" -> ")
|
|
@@ -53,15 +75,21 @@ stub_test_command = ->(_m) { { passed: false } }
|
|
|
53
75
|
all_passed = true
|
|
54
76
|
|
|
55
77
|
# 1. InProcess isolation
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
random = Random.new(SEED)
|
|
79
|
+
picked = []
|
|
80
|
+
all_passed &= run_check("InProcess isolation", picked: picked) do
|
|
81
|
+
mutation = mutations.sample(random: random)
|
|
82
|
+
picked << mutation
|
|
58
83
|
isolator = Evilution::Isolation::InProcess.new
|
|
59
84
|
isolator.call(mutation: mutation, test_command: stub_test_command, timeout: 5)
|
|
60
85
|
end
|
|
61
86
|
|
|
62
87
|
# 2. Fork isolation
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
random = Random.new(SEED)
|
|
89
|
+
picked = []
|
|
90
|
+
all_passed &= run_check("Fork isolation", picked: picked) do
|
|
91
|
+
mutation = mutations.sample(random: random)
|
|
92
|
+
picked << mutation
|
|
65
93
|
isolator = Evilution::Isolation::Fork.new
|
|
66
94
|
isolator.call(mutation: mutation, test_command: stub_test_command, timeout: 5)
|
|
67
95
|
end
|
|
@@ -113,13 +141,31 @@ complex_subjects = complex_parser.call(COMPLEX_FIXTURE)
|
|
|
113
141
|
complex_mutations = complex_subjects.flat_map { |s| complex_registry.mutations_for(s) }
|
|
114
142
|
|
|
115
143
|
integration = Evilution::Integration::RSpec.new(test_files: [COMPLEX_FIXTURE_SPEC])
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
complex_pool = complex_mutations.sample(COMPLEX_POOL_SIZE, random: Random.new(SEED))
|
|
145
|
+
complex_turn = complex_pool.cycle
|
|
146
|
+
|
|
147
|
+
# This check used to grow 20-41 MB over 20 mutations, which was put down to
|
|
148
|
+
# per-mutation require() and redefinition warnings and given a 40 MB budget. It
|
|
149
|
+
# was a leak: every run left its examples reachable from
|
|
150
|
+
# RSpec::Core::AnonymousExampleGroup, about 1 MB per mutation (fixed by
|
|
151
|
+
# StateGuard::AnonymousExampleGroupExamples).
|
|
152
|
+
#
|
|
153
|
+
# The first run of a mutation still grows the process, as the heap reaches the
|
|
154
|
+
# size that mutation needs, and the growth depends on which mutation it is. So
|
|
155
|
+
# the check cycles through a fixed pool: warm-up runs each mutation twice, and
|
|
156
|
+
# every measured run repeats work the warm-up already did. Growth from there
|
|
157
|
+
# on is retained per run.
|
|
158
|
+
#
|
|
159
|
+
# The budget is 10 MB over 100 runs, about 100 KB per mutation: a leak of 200 KB
|
|
160
|
+
# per mutation grows about 18 MB once its largest step is taken out, and fails.
|
|
161
|
+
# Measured on Linux (aarch64) over seeds 1, 7, 42 and 99, sustained growth was
|
|
162
|
+
# -0.9 to 0.6 MB on Ruby 3.4.10 and 0.7 to 2.8 MB on 4.0.7. Ruby 4.0.7 creeps
|
|
163
|
+
# by about 30 KB per mutation in RSS with no Ruby objects retained, which is
|
|
164
|
+
# below what this check can tell apart from allocator noise.
|
|
165
|
+
all_passed &= run_check("RSpec integration per-mutation (Config)",
|
|
166
|
+
iterations: COMPLEX_ITERATIONS, max_growth_kb: COMPLEX_MAX_GROWTH_KB,
|
|
167
|
+
warmup_iterations: COMPLEX_POOL_SIZE * 2, picked: complex_pool) do
|
|
168
|
+
integration.call(complex_turn.next)
|
|
123
169
|
end
|
|
124
170
|
|
|
125
171
|
puts all_passed ? "All memory checks passed." : "Some memory checks failed!"
|