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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/regexp_pattern"
|
|
5
|
+
|
|
6
|
+
# Swap a regexp quantifier between "zero or more" and "one or more": `*`
|
|
7
|
+
# becomes `+` and `+` becomes `*`, keeping a lazy (`*?`) or possessive (`*+`)
|
|
8
|
+
# quantifier lazy or possessive.
|
|
9
|
+
#
|
|
10
|
+
# Only the empty case changes: `/\A\d*\z/` accepts "" and `/\A\d+\z/` does
|
|
11
|
+
# not. A survivor means no example checks how the pattern treats an absent
|
|
12
|
+
# repetition. RegexSimplification's quantifier removal changes the many case
|
|
13
|
+
# too; this isolates the empty one.
|
|
14
|
+
#
|
|
15
|
+
# Making a recursive `\g<name>` call mandatory can make the pattern recurse
|
|
16
|
+
# forever, so each mutant is compiled first and dropped if it does not.
|
|
17
|
+
class Evilution::Mutator::Operator::RegexpQuantifierMinimumSwap < Evilution::Mutator::Base
|
|
18
|
+
SWAPS = {
|
|
19
|
+
zero_or_more: "+", zero_or_more_reluctant: "+?", zero_or_more_possessive: "++",
|
|
20
|
+
one_or_more: "*", one_or_more_reluctant: "*?", one_or_more_possessive: "*+"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def visit_regular_expression_node(node)
|
|
24
|
+
pattern = Evilution::AST::RegexpPattern.parse(node)
|
|
25
|
+
pattern.tokens.each { |token| swap(node, pattern, token) } if pattern
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def swap(node, pattern, token)
|
|
32
|
+
return unless token.type == :quantifier
|
|
33
|
+
|
|
34
|
+
replacement = SWAPS[token.token]
|
|
35
|
+
return if replacement.nil?
|
|
36
|
+
return unless pattern.compiles?(token.start_offset, token.end_offset, replacement)
|
|
37
|
+
|
|
38
|
+
add_mutation(
|
|
39
|
+
offset: token.start_offset,
|
|
40
|
+
length: token.end_offset - token.start_offset,
|
|
41
|
+
replacement: replacement,
|
|
42
|
+
node: node
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Widen what a rescue clause catches: `rescue KeyError` becomes
|
|
6
|
+
# `rescue StandardError` and `rescue Exception`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example raises a sibling error that has to get past
|
|
9
|
+
# the handler, so nothing shows the clause needs to be as narrow as it is.
|
|
10
|
+
#
|
|
11
|
+
# A bare `rescue` is `rescue StandardError`, so the two are never offered as
|
|
12
|
+
# mutants of each other; both go to `Exception` only. So does a clause naming
|
|
13
|
+
# a core class outside `StandardError` (`Interrupt`, `LoadError`), which
|
|
14
|
+
# `StandardError` would not widen but replace. A class of the program's own
|
|
15
|
+
# is taken to be a `StandardError`. A clause that already names `Exception`
|
|
16
|
+
# cannot be widened. A rescue modifier (`x rescue y`) takes no class and is
|
|
17
|
+
# left alone.
|
|
18
|
+
class Evilution::Mutator::Operator::RescueClassWidening < Evilution::Mutator::Base
|
|
19
|
+
WIDER = %w[StandardError Exception].freeze
|
|
20
|
+
|
|
21
|
+
# The core classes that descend from Exception but not from StandardError.
|
|
22
|
+
OUTSIDE_STANDARD_ERROR = %w[
|
|
23
|
+
NoMemoryError ScriptError LoadError NotImplementedError SyntaxError SecurityError SignalException Interrupt
|
|
24
|
+
SystemExit SystemStackError
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def visit_rescue_node(node)
|
|
28
|
+
wider_than(node.exceptions).each { |name| widen(node, name) }
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# The classes of WIDER above everything the clause lists.
|
|
35
|
+
def wider_than(exceptions)
|
|
36
|
+
listed = exceptions.map { |exception| exception.slice.delete_prefix("::") }
|
|
37
|
+
return [] if listed.include?(WIDER.last)
|
|
38
|
+
return [WIDER.last] if listed.empty? || listed.include?(WIDER.first) || listed.intersect?(OUTSIDE_STANDARD_ERROR)
|
|
39
|
+
|
|
40
|
+
WIDER
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def widen(node, name)
|
|
44
|
+
exceptions = node.exceptions
|
|
45
|
+
return insert_after_keyword(node, name) if exceptions.empty?
|
|
46
|
+
|
|
47
|
+
offset = exceptions.first.location.start_offset
|
|
48
|
+
add_mutation(offset:, length: exceptions.last.location.end_offset - offset, replacement: name, node:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def insert_after_keyword(node, name)
|
|
52
|
+
add_mutation(offset: node.keyword_loc.end_offset, length: 0, replacement: " #{name}", node:)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Move the else clause of a rescue into the code it guards, keeping the rescue:
|
|
7
|
+
#
|
|
8
|
+
# begin begin
|
|
9
|
+
# fetch(id) fetch(id)
|
|
10
|
+
# rescue StandardError -> notify
|
|
11
|
+
# default rescue StandardError
|
|
12
|
+
# else default
|
|
13
|
+
# notify end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# The else code still runs only when the body succeeded; the one thing that
|
|
17
|
+
# changes is that an error it raises is now caught by the rescue, where an
|
|
18
|
+
# else clause is not protected. A survivor means no test lets the else code
|
|
19
|
+
# fail in a way the rescue would swallow.
|
|
20
|
+
#
|
|
21
|
+
# Only a rescue with at least one broad clause — a bare `rescue`, or one
|
|
22
|
+
# naming StandardError or Exception — is mutated. Under a rescue for specific
|
|
23
|
+
# classes the moved code changes behaviour only if it raises one of them,
|
|
24
|
+
# which it rarely does, so those mutants would mostly survive without saying
|
|
25
|
+
# anything. An empty else clause and a rescue around an empty body are
|
|
26
|
+
# skipped; ensure clauses and every rescue clause stay as written.
|
|
27
|
+
class Evilution::Mutator::Operator::RescueElseConcatenation < Evilution::Mutator::Base
|
|
28
|
+
BROAD_EXCEPTIONS = %w[StandardError ::StandardError Exception ::Exception].freeze
|
|
29
|
+
|
|
30
|
+
def visit_begin_node(node)
|
|
31
|
+
else_clause = node.else_clause
|
|
32
|
+
move_else(node, else_clause) if node.statements && else_clause && else_clause.statements && broad_rescue?(node)
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def broad_rescue?(node)
|
|
39
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.any? do |clause|
|
|
40
|
+
clause.exceptions.empty? || clause.exceptions.any? { |exception| BROAD_EXCEPTIONS.include?(exception.slice) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# One edit from the end of the body to the end of the else body: the else
|
|
45
|
+
# statements go first, then the rescue clauses as they were, with the
|
|
46
|
+
# whitespace (and a one-line `;`) that led into `else` trimmed off.
|
|
47
|
+
def move_else(node, else_clause)
|
|
48
|
+
body_end = node.statements.location.end_offset
|
|
49
|
+
continuation = Evilution::Mutator::RescueHandlers.continuation(@file_source, node.statements)
|
|
50
|
+
|
|
51
|
+
add_mutation(
|
|
52
|
+
offset: body_end,
|
|
53
|
+
length: else_clause.statements.location.end_offset - body_end,
|
|
54
|
+
replacement: "#{continuation}#{else_clause.statements.slice}#{rescue_clauses_text(body_end, else_clause)}",
|
|
55
|
+
node: else_clause
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def rescue_clauses_text(body_end, else_clause)
|
|
60
|
+
byteslice_source(body_end, else_clause.else_keyword_loc.start_offset - body_end).rstrip.delete_suffix(";")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Run a rescue handler after the code it protects as well, keeping the rescue:
|
|
7
|
+
#
|
|
8
|
+
# begin begin
|
|
9
|
+
# fetch(id) fetch(id)
|
|
10
|
+
# rescue NotFound -> rollback
|
|
11
|
+
# rollback rescue NotFound
|
|
12
|
+
# end rollback
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# The failure path is untouched; only a successful run changes, now also
|
|
16
|
+
# doing what the handler does. A survivor means the handler is harmless when
|
|
17
|
+
# nothing failed — the tests never show it is meant for the failure only.
|
|
18
|
+
# Dropping the rescue as well would repeat RescueRemoval and hide this signal
|
|
19
|
+
# behind any test of the failure path.
|
|
20
|
+
#
|
|
21
|
+
# With several rescue clauses each handler is used in its own mutant. An empty
|
|
22
|
+
# handler adds nothing and is skipped, as is one that only produces a value
|
|
23
|
+
# (`nil`, a literal, a variable, a bare `next`): run after the body it changes
|
|
24
|
+
# nothing but the return value, which RescueHandlerPromotion already probes.
|
|
25
|
+
# Handlers that only work inside the rescue are skipped too (see
|
|
26
|
+
# Mutator::RescueHandlers). A rescue modifier sits in value position, where a
|
|
27
|
+
# statement sequence does not fit, and is left alone.
|
|
28
|
+
class Evilution::Mutator::Operator::RescueHandlerConcatenation < Evilution::Mutator::Base
|
|
29
|
+
VALUE_TYPES = [
|
|
30
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::IntegerNode, Prism::FloatNode,
|
|
31
|
+
Prism::StringNode, Prism::SymbolNode, Prism::SelfNode, Prism::ConstantReadNode,
|
|
32
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
def visit_begin_node(node)
|
|
36
|
+
append_handlers(node) if node.rescue_clause && node.statements
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def append_handlers(node)
|
|
43
|
+
body_end = node.statements.location.end_offset
|
|
44
|
+
separator = Evilution::Mutator::RescueHandlers.continuation(@file_source, node.statements)
|
|
45
|
+
|
|
46
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.each do |clause|
|
|
47
|
+
handler = clause.statements
|
|
48
|
+
next if handler.nil? || value_only?(handler)
|
|
49
|
+
next unless Evilution::Mutator::RescueHandlers.movable?(handler, clause.reference)
|
|
50
|
+
|
|
51
|
+
add_mutation(offset: body_end, length: 0, replacement: "#{separator}#{handler.slice}", node: clause)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def value_only?(handler)
|
|
56
|
+
handler.body.all? { |statement| value?(statement) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A method call may act, so only literals, plain reads and a bare `next` /
|
|
60
|
+
# `return` count.
|
|
61
|
+
def value?(statement)
|
|
62
|
+
case statement
|
|
63
|
+
when *VALUE_TYPES then true
|
|
64
|
+
when Prism::ArrayNode, Prism::HashNode then statement.elements.empty?
|
|
65
|
+
when Prism::NextNode, Prism::ReturnNode then statement.arguments.nil?
|
|
66
|
+
else false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Run a rescue handler instead of the code it protects:
|
|
7
|
+
# `begin; fetch(id); rescue NotFound; default; end` becomes
|
|
8
|
+
# `begin; default; end`, a method-level rescue does the same to the method
|
|
9
|
+
# body, and `fetch(id) rescue default` becomes `default`.
|
|
10
|
+
#
|
|
11
|
+
# The happy path never runs, so a survivor means the tests accept the
|
|
12
|
+
# fallback value where the real result was expected — they never check what
|
|
13
|
+
# the protected code returns.
|
|
14
|
+
#
|
|
15
|
+
# With several rescue clauses each handler is promoted in its own mutant. An
|
|
16
|
+
# else clause goes with the body, since it only runs when nothing is rescued;
|
|
17
|
+
# an ensure clause stays. An empty handler becomes `nil`, what a swallowed
|
|
18
|
+
# error returns.
|
|
19
|
+
#
|
|
20
|
+
# Handlers that only make sense inside the rescue are skipped, since promoted
|
|
21
|
+
# they fail for that reason alone; Mutator::RescueHandlers holds the rule.
|
|
22
|
+
class Evilution::Mutator::Operator::RescueHandlerPromotion < Evilution::Mutator::Base
|
|
23
|
+
def visit_begin_node(node)
|
|
24
|
+
promote_handlers(node) if node.rescue_clause && node.statements
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_rescue_modifier_node(node)
|
|
29
|
+
fallback = node.rescue_expression
|
|
30
|
+
replace(node, fallback.slice, node) if Evilution::Mutator::RescueHandlers.movable?(fallback, nil)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def promote_handlers(node)
|
|
37
|
+
start_offset = node.statements.location.start_offset
|
|
38
|
+
end_offset = protected_end(node)
|
|
39
|
+
|
|
40
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.each do |clause|
|
|
41
|
+
next unless Evilution::Mutator::RescueHandlers.movable?(clause.statements, clause.reference)
|
|
42
|
+
|
|
43
|
+
handler = clause.statements ? clause.statements.slice : "nil"
|
|
44
|
+
add_mutation(offset: start_offset, length: end_offset - start_offset, replacement: handler, node: clause)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The body, every rescue clause and an else clause are replaced; an ensure
|
|
49
|
+
# clause and the closing `end` stay. A rescue clause's location runs on
|
|
50
|
+
# through the clauses after it, and an else clause's up to the keyword that
|
|
51
|
+
# follows it, so the span ends at the last clause's own content.
|
|
52
|
+
def protected_end(node)
|
|
53
|
+
else_clause = node.else_clause
|
|
54
|
+
return Evilution::Mutator::RescueHandlers.new(node).clauses.last.location.end_offset if else_clause.nil?
|
|
55
|
+
|
|
56
|
+
return else_clause.statements.location.end_offset if else_clause.statements
|
|
57
|
+
|
|
58
|
+
else_clause.else_keyword_loc.end_offset
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def replace(target, replacement, node)
|
|
62
|
+
location = target.location
|
|
63
|
+
add_mutation(offset: location.start_offset, length: location.length, replacement: replacement, node: node)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Drop the `return` keyword and keep its value: `return :neg if x.negative?`
|
|
7
|
+
# becomes `:neg if x.negative?`.
|
|
8
|
+
#
|
|
9
|
+
# Control flow continues past the point where the method used to leave, so a
|
|
10
|
+
# survivor means no test notices the code after a guard clause or an early
|
|
11
|
+
# return running anyway. ReturnValueRemoval keeps the early exit and drops the
|
|
12
|
+
# value; this keeps the value and drops the exit. Several values become an
|
|
13
|
+
# array, as `return` would make them: `return a, b` becomes `[a, b]`.
|
|
14
|
+
#
|
|
15
|
+
# A return in tail position — the method's last statement, the last statement
|
|
16
|
+
# of a branch that is itself in tail position, or the tail of a lambda — is
|
|
17
|
+
# skipped: its value is the result either way. A return inside a block always
|
|
18
|
+
# leaves the method, so it is never tail position. A bare `return` has no
|
|
19
|
+
# value to keep.
|
|
20
|
+
class Evilution::Mutator::Operator::ReturnKeywordRemoval < Evilution::Mutator::Base
|
|
21
|
+
# For each node that passes a value on, the parts whose value becomes its
|
|
22
|
+
# own. With an else clause a begin body's value is not the result, the
|
|
23
|
+
# else's is; each rescue handler's value can be the result too.
|
|
24
|
+
TAIL_PARTS = {
|
|
25
|
+
Prism::StatementsNode => ->(node) { [node.body.last] },
|
|
26
|
+
Prism::ParenthesesNode => ->(node) { [node.body] },
|
|
27
|
+
Prism::ElseNode => ->(node) { [node.statements] },
|
|
28
|
+
Prism::IfNode => ->(node) { [node.statements, node.subsequent] },
|
|
29
|
+
Prism::UnlessNode => ->(node) { [node.statements, node.else_clause] },
|
|
30
|
+
Prism::CaseNode => ->(node) { [*node.conditions.map(&:statements), node.else_clause] },
|
|
31
|
+
Prism::CaseMatchNode => ->(node) { [*node.conditions.map(&:statements), node.else_clause] },
|
|
32
|
+
Prism::BeginNode => lambda { |node|
|
|
33
|
+
[node.else_clause || node.statements, *Evilution::Mutator::RescueHandlers.new(node).clauses.map(&:statements)]
|
|
34
|
+
}
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def initialize(**options)
|
|
38
|
+
super
|
|
39
|
+
@tail_returns = Set.new.compare_by_identity
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def visit_def_node(node)
|
|
43
|
+
@tail_returns.merge(tail_returns(node.body))
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_lambda_node(node)
|
|
48
|
+
@tail_returns.merge(tail_returns(node.body))
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visit_return_node(node)
|
|
53
|
+
drop_keyword(node) if node.arguments && !@tail_returns.include?(node)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def drop_keyword(node)
|
|
60
|
+
arguments = node.arguments.arguments
|
|
61
|
+
value = if arguments.length == 1 && !arguments.first.is_a?(Prism::SplatNode)
|
|
62
|
+
arguments.first.slice
|
|
63
|
+
else
|
|
64
|
+
"[#{arguments.map(&:slice).join(", ")}]"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement: value, node: node)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The returns whose value is the value of `node` itself.
|
|
71
|
+
def tail_returns(node)
|
|
72
|
+
return [node] if node.is_a?(Prism::ReturnNode)
|
|
73
|
+
|
|
74
|
+
parts = TAIL_PARTS[node.class]
|
|
75
|
+
return [] if parts.nil?
|
|
76
|
+
|
|
77
|
+
parts.call(node).compact.flat_map { |part| tail_returns(part) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Turn a rightward pattern match into a pattern predicate: `value => [a, b]`
|
|
6
|
+
# becomes `value in [a, b]`.
|
|
7
|
+
#
|
|
8
|
+
# On a mismatch the original raises NoMatchingPatternError, while the predicate
|
|
9
|
+
# returns false and leaves the pattern's variables nil. A survivor means no
|
|
10
|
+
# example feeds the match a value it rejects, so the failure mode is untested.
|
|
11
|
+
class Evilution::Mutator::Operator::RightwardAssignment < Evilution::Mutator::Base
|
|
12
|
+
def visit_match_required_node(node)
|
|
13
|
+
replace_operator(node) unless irrefutable?(node.pattern)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def replace_operator(node)
|
|
20
|
+
location = node.operator_loc
|
|
21
|
+
|
|
22
|
+
add_mutation(
|
|
23
|
+
offset: location.start_offset,
|
|
24
|
+
length: location.length,
|
|
25
|
+
replacement: keyword_for(node),
|
|
26
|
+
node: node
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# `=>` is punctuation and may touch its neighbours (`value=>Integer`), but
|
|
31
|
+
# `in` is a word: written flush against them it would merge into a single
|
|
32
|
+
# name. A space is added on each side that has none.
|
|
33
|
+
def keyword_for(node)
|
|
34
|
+
location = node.operator_loc
|
|
35
|
+
before = node.value.location.end_offset == location.start_offset ? " " : ""
|
|
36
|
+
after = node.pattern.location.start_offset == location.end_offset ? " " : ""
|
|
37
|
+
|
|
38
|
+
"#{before}in#{after}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A bare name (`value => captured`, `value => _`) captures any value, so the
|
|
42
|
+
# match can never raise and the predicate form would behave the same.
|
|
43
|
+
def irrefutable?(pattern)
|
|
44
|
+
pattern.is_a?(Prism::LocalVariableTargetNode)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/round_half_mode"
|
|
5
|
+
|
|
6
|
+
# Swap the tie-breaking mode of `round`: `amount.round(2, half: :even)`
|
|
7
|
+
# becomes `half: :up` and `half: :down`.
|
|
8
|
+
#
|
|
9
|
+
# The modes differ only for a value sitting exactly on a tie (`2.5`), so a
|
|
10
|
+
# survivor means no example rounds one — the case banker's rounding in money
|
|
11
|
+
# code exists for.
|
|
12
|
+
#
|
|
13
|
+
# Only a literal mode is swapped; one held in a variable is left alone.
|
|
14
|
+
# Integer, Float, Rational and BigDecimal all read `half:` the same way.
|
|
15
|
+
class Evilution::Mutator::Operator::RoundHalfModeSwap < Evilution::Mutator::Base
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
mode = Evilution::AST::RoundHalfMode.of(node)
|
|
18
|
+
swap(node, mode) if mode
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def swap(node, mode)
|
|
25
|
+
others = Evilution::AST::RoundHalfMode::MODES - [mode.unescaped.to_sym]
|
|
26
|
+
|
|
27
|
+
others.each do |other|
|
|
28
|
+
add_mutation(
|
|
29
|
+
offset: mode.location.start_offset,
|
|
30
|
+
length: mode.location.length,
|
|
31
|
+
replacement: other.inspect,
|
|
32
|
+
node: node
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/never_nil"
|
|
5
|
+
|
|
6
|
+
# Replace a plain call with safe navigation: `user.name` becomes
|
|
7
|
+
# `user&.name`, and the same for the `||=`, `&&=` and operator-write forms.
|
|
8
|
+
#
|
|
9
|
+
# The mutant only differs when the receiver is nil: the call is then skipped
|
|
10
|
+
# and the expression is nil, where the plain call raises NoMethodError or
|
|
11
|
+
# runs a method nil has (`nil.to_a` is `[]`). A survivor means no test
|
|
12
|
+
# reaches the call with a nil receiver and checks what comes of it.
|
|
13
|
+
#
|
|
14
|
+
# It touches nearly every call and survives wherever the receiver is never
|
|
15
|
+
# nil, so it is registered for the strict profile only. Receivers that can
|
|
16
|
+
# never be nil (`self`, literals other than `nil`, constants) are skipped,
|
|
17
|
+
# since the mutant is equivalent there.
|
|
18
|
+
class Evilution::Mutator::Operator::SafeNavigationInsertion < Evilution::Mutator::Base
|
|
19
|
+
# A constant could hold nil, but one that receives a call does not in
|
|
20
|
+
# practice.
|
|
21
|
+
CONSTANTS = [Prism::ConstantReadNode, Prism::ConstantPathNode].freeze
|
|
22
|
+
private_constant :CONSTANTS
|
|
23
|
+
|
|
24
|
+
def visit_call_node(node)
|
|
25
|
+
mutate_plain_call(node)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_call_or_write_node(node)
|
|
30
|
+
mutate_plain_call(node)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_call_and_write_node(node)
|
|
35
|
+
mutate_plain_call(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_call_operator_write_node(node)
|
|
40
|
+
mutate_plain_call(node)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def mutate_plain_call(node)
|
|
47
|
+
operator = node.call_operator_loc
|
|
48
|
+
return unless operator && operator.slice == "."
|
|
49
|
+
return if never_nil?(node.receiver)
|
|
50
|
+
|
|
51
|
+
add_mutation(offset: operator.start_offset, length: operator.length, replacement: "&.", node: node)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def never_nil?(receiver)
|
|
55
|
+
Evilution::AST::NeverNil.node?(receiver) || CONSTANTS.include?(receiver.class)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/never_nil"
|
|
4
5
|
|
|
5
6
|
# Replace safe navigation with a plain call: `user&.name` becomes
|
|
6
7
|
# `user.name`, and the same for the `||=`, `&&=` and operator-write forms.
|
|
@@ -12,29 +13,6 @@ require_relative "../operator"
|
|
|
12
13
|
# Receivers that can never be nil — `self` and literals — are skipped, since
|
|
13
14
|
# the mutant is equivalent there.
|
|
14
15
|
class Evilution::Mutator::Operator::SafeNavigationRemoval < Evilution::Mutator::Base
|
|
15
|
-
NEVER_NIL_RECEIVERS = [
|
|
16
|
-
Prism::SelfNode,
|
|
17
|
-
Prism::StringNode,
|
|
18
|
-
Prism::InterpolatedStringNode,
|
|
19
|
-
Prism::XStringNode,
|
|
20
|
-
Prism::InterpolatedXStringNode,
|
|
21
|
-
Prism::SymbolNode,
|
|
22
|
-
Prism::InterpolatedSymbolNode,
|
|
23
|
-
Prism::IntegerNode,
|
|
24
|
-
Prism::FloatNode,
|
|
25
|
-
Prism::RationalNode,
|
|
26
|
-
Prism::ImaginaryNode,
|
|
27
|
-
Prism::ArrayNode,
|
|
28
|
-
Prism::HashNode,
|
|
29
|
-
Prism::RangeNode,
|
|
30
|
-
Prism::RegularExpressionNode,
|
|
31
|
-
Prism::InterpolatedRegularExpressionNode,
|
|
32
|
-
Prism::TrueNode,
|
|
33
|
-
Prism::FalseNode,
|
|
34
|
-
Prism::LambdaNode
|
|
35
|
-
].freeze
|
|
36
|
-
private_constant :NEVER_NIL_RECEIVERS
|
|
37
|
-
|
|
38
16
|
def visit_call_node(node)
|
|
39
17
|
mutate_safe_navigation(node)
|
|
40
18
|
super
|
|
@@ -59,7 +37,7 @@ class Evilution::Mutator::Operator::SafeNavigationRemoval < Evilution::Mutator::
|
|
|
59
37
|
|
|
60
38
|
def mutate_safe_navigation(node)
|
|
61
39
|
return unless node.safe_navigation?
|
|
62
|
-
return if
|
|
40
|
+
return if Evilution::AST::NeverNil.node?(node.receiver)
|
|
63
41
|
|
|
64
42
|
operator = node.call_operator_loc
|
|
65
43
|
add_mutation(offset: operator.start_offset, length: operator.length, replacement: ".", node: node)
|
|
@@ -43,6 +43,8 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
43
43
|
end_with?: [:start_with?],
|
|
44
44
|
ceil: [:floor],
|
|
45
45
|
floor: [:ceil],
|
|
46
|
+
round: %i[floor ceil],
|
|
47
|
+
truncate: [:floor],
|
|
46
48
|
transform_keys: [:transform_values],
|
|
47
49
|
transform_values: [:transform_keys],
|
|
48
50
|
append: [:prepend],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Make a short-circuit operator evaluate both sides: `a && b` becomes
|
|
6
|
+
# `a & b`, `a || b` becomes `a | b`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example depends on the right side being skipped: it
|
|
9
|
+
# guards nothing (a nil receiver, a side effect), and both sides are plain
|
|
10
|
+
# booleans, since `&` and `|` give another value for anything else.
|
|
11
|
+
#
|
|
12
|
+
# `&` and `|` bind tighter than `&&` and `||`, so an operand that would
|
|
13
|
+
# regroup (`a == 1 && b` is not `a == 1 & b`) is parenthesized.
|
|
14
|
+
class Evilution::Mutator::Operator::ShortCircuitToEager < Evilution::Mutator::Base
|
|
15
|
+
def visit_and_node(node)
|
|
16
|
+
mutate_to_eager(node, "&")
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visit_or_node(node)
|
|
21
|
+
mutate_to_eager(node, "|")
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# An operand that only jumps (`a || return`) has no value to pass on; the
|
|
28
|
+
# result does not parse and is dropped.
|
|
29
|
+
def mutate_to_eager(node, operator)
|
|
30
|
+
replacement = "#{receiver_source(node.left)} #{operator} #{receiver_source(node.right)}"
|
|
31
|
+
|
|
32
|
+
replace_span(node:, target: node, replacement:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Take the wait out of a sleep whose duration is not a literal:
|
|
6
|
+
# `sleep delay` becomes `sleep 0`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means the pause decides nothing an example observes. A killed
|
|
9
|
+
# mutant means the suite depends on wall-clock time, which is worth knowing
|
|
10
|
+
# too.
|
|
11
|
+
#
|
|
12
|
+
# A literal duration (`sleep 5`, `sleep 0.5`) is left to the literal
|
|
13
|
+
# operators, which already take it to zero. Only `Kernel#sleep` is touched: a
|
|
14
|
+
# `sleep` sent to another receiver may be something else.
|
|
15
|
+
class Evilution::Mutator::Operator::SleepToZero < Evilution::Mutator::Base
|
|
16
|
+
LITERAL_DURATIONS = [Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode].freeze
|
|
17
|
+
KERNEL = %w[Kernel ::Kernel].freeze
|
|
18
|
+
|
|
19
|
+
def visit_call_node(node)
|
|
20
|
+
duration = duration_of(node)
|
|
21
|
+
add_mutation(offset: duration.location.start_offset, length: duration.location.length, replacement: "0", node:) if duration
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# The single argument of a `sleep` call, when it is one this operator
|
|
29
|
+
# replaces.
|
|
30
|
+
def duration_of(node)
|
|
31
|
+
return unless node.name == :sleep && kernel_call?(node) && node.arguments
|
|
32
|
+
|
|
33
|
+
arguments = node.arguments.arguments
|
|
34
|
+
return unless arguments.one?
|
|
35
|
+
|
|
36
|
+
duration = arguments.first
|
|
37
|
+
duration unless duration.is_a?(Prism::SplatNode) || LITERAL_DURATIONS.include?(duration.class)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def kernel_call?(node)
|
|
41
|
+
node.receiver.nil? || KERNEL.include?(node.receiver.slice)
|
|
42
|
+
end
|
|
43
|
+
end
|