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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap two adjacent commands: `charge(card); send_receipt(user)` becomes
|
|
6
|
+
# `send_receipt(user); charge(card)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example depends on the order the two side effects
|
|
9
|
+
# happen in. Most pairs of statements could swap without anyone noticing, so
|
|
10
|
+
# only pairs that are likely to matter are touched:
|
|
11
|
+
#
|
|
12
|
+
# - both statements are commands: they call a method, append, write an index,
|
|
13
|
+
# yield or call super. Assigning a local or instance variable stores a
|
|
14
|
+
# value, and statements without such effects swap freely.
|
|
15
|
+
# - neither writes a variable the other reads or writes. Such a pair fails at
|
|
16
|
+
# once when swapped, which tests nothing about order.
|
|
17
|
+
# - neither contains control flow (`return`, `break`, `next`, `redo`, `retry`,
|
|
18
|
+
# `raise`, `fail`), which would skip the other statement.
|
|
19
|
+
# - the second is not the last statement of its body, which gives the body
|
|
20
|
+
# its value.
|
|
21
|
+
# - neither contains a heredoc, whose body sits outside the statement's
|
|
22
|
+
# source range.
|
|
23
|
+
# - they are not writes to different literal keys of the same receiver
|
|
24
|
+
# (`opts[:a] = 1; opts[:b] = 2`), which cannot affect each other.
|
|
25
|
+
class Evilution::Mutator::Operator::StatementReorder < Evilution::Mutator::Base
|
|
26
|
+
CONTROL_FLOW_TYPES = [
|
|
27
|
+
Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode
|
|
28
|
+
].freeze
|
|
29
|
+
RAISING_METHODS = %i[raise fail].freeze
|
|
30
|
+
|
|
31
|
+
# Calls that change their receiver without a method name: `a << b`, `a[k] = v`.
|
|
32
|
+
MUTATING_OPERATORS = %i[<< []=].freeze
|
|
33
|
+
METHOD_NAME = /\A[a-zA-Z_]/
|
|
34
|
+
|
|
35
|
+
EFFECT_TYPES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
|
|
36
|
+
|
|
37
|
+
# Statements that store a value rather than act.
|
|
38
|
+
VALUE_WRITE_TYPES = [
|
|
39
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
40
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
41
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
42
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# Nodes that set a variable, and nodes that read one.
|
|
46
|
+
WRITE_TYPES = [
|
|
47
|
+
*VALUE_WRITE_TYPES, Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
48
|
+
Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
|
|
49
|
+
Prism::ClassVariableAndWriteNode, Prism::ClassVariableTargetNode,
|
|
50
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
51
|
+
Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
|
|
52
|
+
].freeze
|
|
53
|
+
LITERAL_KEY_TYPES = [Prism::SymbolNode, Prism::StringNode, Prism::IntegerNode].freeze
|
|
54
|
+
|
|
55
|
+
READ_TYPES = [
|
|
56
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
57
|
+
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
def visit_statements_node(node)
|
|
61
|
+
statements = node.body
|
|
62
|
+
# The last statement stays put, so pairs end one before it.
|
|
63
|
+
statements[0...-1].each_cons(2) do |first, second|
|
|
64
|
+
emit_swap(first, second) if swappable?(first, second)
|
|
65
|
+
end
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def swappable?(first, second)
|
|
72
|
+
[first, second].all? { |statement| command?(statement) && !control_flow?(statement) && !heredoc?(statement) } &&
|
|
73
|
+
independent?(first, second) && !distinct_key_writes?(first, second)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def command?(statement)
|
|
77
|
+
return false if VALUE_WRITE_TYPES.include?(statement.class)
|
|
78
|
+
|
|
79
|
+
nodes_in(statement).any? { |node| effect?(node) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def effect?(node)
|
|
83
|
+
return true if EFFECT_TYPES.include?(node.class)
|
|
84
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
85
|
+
|
|
86
|
+
node.name.to_s.match?(METHOD_NAME) || MUTATING_OPERATORS.include?(node.name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def control_flow?(statement)
|
|
90
|
+
nodes_in(statement).any? do |node|
|
|
91
|
+
CONTROL_FLOW_TYPES.include?(node.class) ||
|
|
92
|
+
(node.is_a?(Prism::CallNode) && node.receiver.nil? && RAISING_METHODS.include?(node.name))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def heredoc?(statement)
|
|
97
|
+
nodes_in(statement).any? do |node|
|
|
98
|
+
node.respond_to?(:heredoc?) && node.heredoc?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def distinct_key_writes?(first, second)
|
|
103
|
+
writes = [index_write(first), index_write(second)]
|
|
104
|
+
return false if writes.include?(nil)
|
|
105
|
+
return false unless writes.first.receiver.slice == writes.last.receiver.slice
|
|
106
|
+
|
|
107
|
+
distinct_literal_keys?(writes.map { |write| write.arguments.arguments.first })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def distinct_literal_keys?(keys)
|
|
111
|
+
keys.all? { |key| LITERAL_KEY_TYPES.include?(key.class) } && keys.first.slice != keys.last.slice
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `h[k] = v` on its own or under a modifier condition (`h[k] = v if v`).
|
|
115
|
+
def index_write(statement)
|
|
116
|
+
if statement.is_a?(Prism::IfNode) || statement.is_a?(Prism::UnlessNode)
|
|
117
|
+
body = statement.statements
|
|
118
|
+
statement = body.body.first if body && body.body.length == 1
|
|
119
|
+
end
|
|
120
|
+
statement if statement.is_a?(Prism::CallNode) && statement.name == :[]=
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def independent?(first, second)
|
|
124
|
+
first_writes = names(first, WRITE_TYPES)
|
|
125
|
+
second_writes = names(second, WRITE_TYPES)
|
|
126
|
+
|
|
127
|
+
!first_writes.intersect?(names(second, READ_TYPES) | second_writes) &&
|
|
128
|
+
!second_writes.intersect?(names(first, READ_TYPES))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def names(statement, types)
|
|
132
|
+
nodes_in(statement).filter_map { |node| node.name if types.include?(node.class) }.to_set
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def nodes_in(node)
|
|
136
|
+
[node, *node.compact_child_nodes.flat_map { |child| nodes_in(child) }]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Whatever separates the two statements — a line break and indentation, or
|
|
140
|
+
# `; ` — stays between them.
|
|
141
|
+
def emit_swap(first, second)
|
|
142
|
+
start_offset = first.location.start_offset
|
|
143
|
+
end_offset = second.location.end_offset
|
|
144
|
+
separator = byteslice_source(first.location.end_offset, second.location.start_offset - first.location.end_offset)
|
|
145
|
+
|
|
146
|
+
add_mutation(
|
|
147
|
+
offset: start_offset,
|
|
148
|
+
length: end_offset - start_offset,
|
|
149
|
+
replacement: "#{second.slice}#{separator}#{first.slice}",
|
|
150
|
+
node: first
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "send_mutation"
|
|
5
|
+
require_relative "collection_replacement"
|
|
6
|
+
|
|
7
|
+
# Apply the call-site selector tables to a symbol block-pass: `map(&:to_s)`
|
|
8
|
+
# becomes `map(&:to_i)`, `map(&:first)` becomes `map(&:last)`.
|
|
9
|
+
#
|
|
10
|
+
# send_mutation and collection_replacement only see explicit calls, so a
|
|
11
|
+
# selector passed as `&:sym` was never swapped — only removed. This reads
|
|
12
|
+
# both tables live, so selectors added to them apply here too.
|
|
13
|
+
#
|
|
14
|
+
# Replacements that are Ruby aliases of the original (`collect` / `map`,
|
|
15
|
+
# `find` / `detect`, `select` / `filter`, `length` / `size` / `count`) are
|
|
16
|
+
# skipped: on the receivers these selectors are normally mapped over, the
|
|
17
|
+
# mutant behaves identically.
|
|
18
|
+
class Evilution::Mutator::Operator::SymbolToProcReplacement < Evilution::Mutator::Base
|
|
19
|
+
TABLES = [
|
|
20
|
+
Evilution::Mutator::Operator::SendMutation::REPLACEMENTS,
|
|
21
|
+
Evilution::Mutator::Operator::CollectionReplacement::REPLACEMENTS
|
|
22
|
+
].freeze
|
|
23
|
+
private_constant :TABLES
|
|
24
|
+
|
|
25
|
+
ALIAS_GROUPS = [
|
|
26
|
+
%i[map collect],
|
|
27
|
+
%i[find detect],
|
|
28
|
+
%i[select filter],
|
|
29
|
+
%i[length size count]
|
|
30
|
+
].freeze
|
|
31
|
+
private_constant :ALIAS_GROUPS
|
|
32
|
+
|
|
33
|
+
def visit_block_argument_node(node)
|
|
34
|
+
symbol = node.expression
|
|
35
|
+
replace_selector(node, symbol) if symbol.is_a?(Prism::SymbolNode)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def replace_selector(node, symbol)
|
|
42
|
+
selector = symbol.unescaped.to_sym
|
|
43
|
+
location = symbol.value_loc
|
|
44
|
+
|
|
45
|
+
replacements_for(selector).each do |replacement|
|
|
46
|
+
add_mutation(
|
|
47
|
+
offset: location.start_offset,
|
|
48
|
+
length: location.length,
|
|
49
|
+
replacement: replacement.to_s,
|
|
50
|
+
node: node
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def replacements_for(selector)
|
|
56
|
+
aliases = ALIAS_GROUPS.find { |group| group.include?(selector) } || []
|
|
57
|
+
TABLES.flat_map { |table| table.fetch(selector, []) }.uniq - aliases
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a lenient `to_i` with the strict `Integer()` conversion:
|
|
6
|
+
# `value.to_i` becomes `Integer(value)`, `value.to_i(16)` becomes
|
|
7
|
+
# `Integer(value, 16)`.
|
|
8
|
+
#
|
|
9
|
+
# `to_i` never fails — malformed input ("abc", "12abc", "") and nil become 0
|
|
10
|
+
# or a leading-digits prefix — where `Integer()` raises. `Integer()` also
|
|
11
|
+
# honours radix prefixes ("0x1A" is 26, "012" is octal 10) that `to_i`
|
|
12
|
+
# ignores. A survivor means no test feeds invalid or prefixed input.
|
|
13
|
+
#
|
|
14
|
+
# Numeric literal receivers are skipped: both conversions agree on them.
|
|
15
|
+
class Evilution::Mutator::Operator::ToIToInteger < Evilution::Mutator::Base
|
|
16
|
+
NUMERIC_LITERALS = [Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode].freeze
|
|
17
|
+
private_constant :NUMERIC_LITERALS
|
|
18
|
+
|
|
19
|
+
def visit_call_node(node)
|
|
20
|
+
rewrite(node) if node.name == :to_i && convertible?(node)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def convertible?(node)
|
|
27
|
+
receiver = node.receiver
|
|
28
|
+
receiver && node.block.nil? && NUMERIC_LITERALS.none? { |type| receiver.is_a?(type) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rewrite(node)
|
|
32
|
+
arguments = node.arguments ? node.arguments.arguments : []
|
|
33
|
+
return if arguments.length > 1 || arguments.any?(Prism::SplatNode)
|
|
34
|
+
|
|
35
|
+
parts = [node.receiver, *arguments].map { |part| source_of(part) }
|
|
36
|
+
replace_span(node: node, target: node, replacement: "Integer(#{parts.join(", ")})")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -49,4 +49,29 @@ module Evilution::Mutator::Primitives
|
|
|
49
49
|
location = child.location
|
|
50
50
|
byteslice_source(location.start_offset, location.length)
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
# `operand`'s source, parenthesized unless it is a primary expression — so
|
|
54
|
+
# it can take a method call: `a + b` becomes `(a + b)` in `(a + b).eql?(c)`.
|
|
55
|
+
def receiver_source(operand)
|
|
56
|
+
primary_operand?(operand) ? source_of(operand) : "(#{source_of(operand)})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
PRIMARY_OPERANDS = [
|
|
60
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
61
|
+
Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::SelfNode,
|
|
62
|
+
Prism::ParenthesesNode, Prism::StringNode, Prism::IntegerNode, Prism::FloatNode, Prism::ArrayNode,
|
|
63
|
+
Prism::RegularExpressionNode
|
|
64
|
+
].freeze
|
|
65
|
+
private_constant :PRIMARY_OPERANDS
|
|
66
|
+
|
|
67
|
+
METHOD_NAME = /\A[[:alpha:]_]/
|
|
68
|
+
private_constant :METHOD_NAME
|
|
69
|
+
|
|
70
|
+
# A named method call or an index (`a.size`, `a[0]`) binds tighter than a
|
|
71
|
+
# following `.`; an operator send (`a + b`, `!a`, `-a`) does not.
|
|
72
|
+
def primary_operand?(operand)
|
|
73
|
+
return true if PRIMARY_OPERANDS.any? { |type| operand.is_a?(type) }
|
|
74
|
+
|
|
75
|
+
operand.is_a?(Prism::CallNode) && (operand.name.match?(METHOD_NAME) || operand.name == :[])
|
|
76
|
+
end
|
|
52
77
|
end
|
|
@@ -4,7 +4,9 @@ require_relative "../mutator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Registry
|
|
6
6
|
STRICT_EXTRA_OPERATORS = [
|
|
7
|
-
Evilution::Mutator::Operator::PredicateToNil
|
|
7
|
+
Evilution::Mutator::Operator::PredicateToNil,
|
|
8
|
+
Evilution::Mutator::Operator::ExceptionSwallow,
|
|
9
|
+
Evilution::Mutator::Operator::StatementReorder
|
|
8
10
|
].freeze
|
|
9
11
|
|
|
10
12
|
def self.for_profile(profile)
|
|
@@ -113,7 +115,55 @@ class Evilution::Mutator::Registry
|
|
|
113
115
|
Evilution::Mutator::Operator::EqualityToIdentity,
|
|
114
116
|
Evilution::Mutator::Operator::LambdaBody,
|
|
115
117
|
Evilution::Mutator::Operator::BeginUnwrap,
|
|
116
|
-
Evilution::Mutator::Operator::BlockParamRemoval
|
|
118
|
+
Evilution::Mutator::Operator::BlockParamRemoval,
|
|
119
|
+
Evilution::Mutator::Operator::CallToNil,
|
|
120
|
+
Evilution::Mutator::Operator::SafeNavigationRemoval,
|
|
121
|
+
Evilution::Mutator::Operator::AttributeWriteToRead,
|
|
122
|
+
Evilution::Mutator::Operator::ArgumentPropagation,
|
|
123
|
+
Evilution::Mutator::Operator::ArgumentListRemoval,
|
|
124
|
+
Evilution::Mutator::Operator::SymbolToProcReplacement,
|
|
125
|
+
Evilution::Mutator::Operator::DynamicDispatchResolution,
|
|
126
|
+
Evilution::Mutator::Operator::KeywordArgumentRemoval,
|
|
127
|
+
Evilution::Mutator::Operator::DoubleNegationRemoval,
|
|
128
|
+
Evilution::Mutator::Operator::RegexpAnchorToPredicate,
|
|
129
|
+
Evilution::Mutator::Operator::ReduceToSum,
|
|
130
|
+
Evilution::Mutator::Operator::ArrayCoercionToLiteral,
|
|
131
|
+
Evilution::Mutator::Operator::ToIToInteger,
|
|
132
|
+
Evilution::Mutator::Operator::CoercionEmptying,
|
|
133
|
+
Evilution::Mutator::Operator::ConstGetToConstantPath,
|
|
134
|
+
Evilution::Mutator::Operator::ProcToLambda,
|
|
135
|
+
Evilution::Mutator::Operator::DigToFetchChain,
|
|
136
|
+
Evilution::Mutator::Operator::InequalityToNegatedIdentity,
|
|
137
|
+
Evilution::Mutator::Operator::BinaryOperandPromotion,
|
|
138
|
+
Evilution::Mutator::Operator::ReceiverConstructorSwap,
|
|
139
|
+
Evilution::Mutator::Operator::BlockBodyToNil,
|
|
140
|
+
Evilution::Mutator::Operator::BlockBodyToRaise,
|
|
141
|
+
Evilution::Mutator::Operator::BlockBodyPromotion,
|
|
142
|
+
Evilution::Mutator::Operator::DataStructMember,
|
|
143
|
+
Evilution::Mutator::Operator::PinOperatorRemoval,
|
|
144
|
+
Evilution::Mutator::Operator::RightwardAssignment,
|
|
145
|
+
Evilution::Mutator::Operator::NumberedParameterSwap,
|
|
146
|
+
Evilution::Mutator::Operator::ForwardedArgumentDrop,
|
|
147
|
+
Evilution::Mutator::Operator::PatternWildcardWidening,
|
|
148
|
+
Evilution::Mutator::Operator::NoMatchingPatternElse,
|
|
149
|
+
Evilution::Mutator::Operator::ArgumentOrderPermutation,
|
|
150
|
+
Evilution::Mutator::Operator::KeywordValueSwap,
|
|
151
|
+
Evilution::Mutator::Operator::ComparisonOperandSwap,
|
|
152
|
+
Evilution::Mutator::Operator::IntegerDivisionToFdiv,
|
|
153
|
+
Evilution::Mutator::Operator::OffByOneBoundary,
|
|
154
|
+
Evilution::Mutator::Operator::FormatSpecifierSwap,
|
|
155
|
+
Evilution::Mutator::Operator::AliasRemoval,
|
|
156
|
+
Evilution::Mutator::Operator::RegexpCharacterTypeComplement,
|
|
157
|
+
Evilution::Mutator::Operator::RegexpAnchorPromotion,
|
|
158
|
+
Evilution::Mutator::Operator::RegexpAlternationBranchDeletion,
|
|
159
|
+
Evilution::Mutator::Operator::RegexpQuantifierMinimumSwap,
|
|
160
|
+
Evilution::Mutator::Operator::RegexpCaptureToPassive,
|
|
161
|
+
Evilution::Mutator::Operator::RegexpNamedGroupRename,
|
|
162
|
+
Evilution::Mutator::Operator::RegexpOptionRemoval,
|
|
163
|
+
Evilution::Mutator::Operator::RescueHandlerPromotion,
|
|
164
|
+
Evilution::Mutator::Operator::RescueHandlerConcatenation,
|
|
165
|
+
Evilution::Mutator::Operator::RescueElseConcatenation,
|
|
166
|
+
Evilution::Mutator::Operator::ReturnKeywordRemoval
|
|
117
167
|
].each { |op| registry.register(op) }
|
|
118
168
|
registry
|
|
119
169
|
end
|
|
@@ -129,6 +179,8 @@ class Evilution::Mutator::Registry
|
|
|
129
179
|
|
|
130
180
|
def mutations_for(subject, filter: nil, operator_options: {})
|
|
131
181
|
@operators.flat_map do |operator_class|
|
|
182
|
+
next [] unless operator_class.subject_kinds.include?(subject.kind)
|
|
183
|
+
|
|
132
184
|
operator = build_operator(operator_class, operator_options)
|
|
133
185
|
operator.call(subject, filter: filter)
|
|
134
186
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../mutator"
|
|
6
|
+
require_relative "../ast/local_reads"
|
|
7
|
+
|
|
8
|
+
# The rescue clauses of a `begin` (or of a method or block body with a rescue),
|
|
9
|
+
# and the rule for which of their handlers can run outside the rescue.
|
|
10
|
+
#
|
|
11
|
+
# Operators that move a handler — promoting it over the body, or running it
|
|
12
|
+
# after the body as well — share the rule: a handler that reads the rescued
|
|
13
|
+
# exception (its `=> e` variable or `$!` / `$@`), re-raises it with a bare
|
|
14
|
+
# `raise` / `fail`, or retries only makes sense inside the rescue, and moved
|
|
15
|
+
# out it would fail for that reason alone.
|
|
16
|
+
class Evilution::Mutator::RescueHandlers
|
|
17
|
+
EXCEPTION_GLOBALS = %i[$! $@].freeze
|
|
18
|
+
RERAISING_METHODS = %i[raise fail].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :clauses
|
|
21
|
+
|
|
22
|
+
def initialize(begin_node)
|
|
23
|
+
@clauses = []
|
|
24
|
+
clause = begin_node.rescue_clause
|
|
25
|
+
while clause
|
|
26
|
+
@clauses << clause
|
|
27
|
+
clause = clause.subsequent
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether a handler can run outside its rescue. A missing handler (an empty
|
|
32
|
+
# rescue clause) can.
|
|
33
|
+
def self.movable?(handler, reference)
|
|
34
|
+
return true if handler.nil?
|
|
35
|
+
|
|
36
|
+
!reads_exception?(handler, reference) && !rescue_only?(handler)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Binding the exception to anything but a local (`=> @error`) leaves it set
|
|
40
|
+
# for code after the rescue, which the handler alone cannot speak for, so
|
|
41
|
+
# such a handler counts as reading it. An underscore name announces a binding
|
|
42
|
+
# that is not read.
|
|
43
|
+
def self.reads_exception?(handler, reference)
|
|
44
|
+
return true if nodes_in(handler).any? { |node| exception_global?(node) }
|
|
45
|
+
return false if reference.nil?
|
|
46
|
+
return true unless reference.is_a?(Prism::LocalVariableTargetNode)
|
|
47
|
+
|
|
48
|
+
name = reference.name.to_s
|
|
49
|
+
!name.start_with?("_") && Evilution::AST::LocalReads.new.call(handler, name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.exception_global?(node)
|
|
53
|
+
node.is_a?(Prism::GlobalVariableReadNode) && EXCEPTION_GLOBALS.include?(node.name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.rescue_only?(handler)
|
|
57
|
+
nodes_in(handler).any? do |node|
|
|
58
|
+
node.is_a?(Prism::RetryNode) ||
|
|
59
|
+
(node.is_a?(Prism::CallNode) && node.receiver.nil? && RERAISING_METHODS.include?(node.name) &&
|
|
60
|
+
node.arguments.nil?)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What goes between the last statement of a body and code appended after
|
|
65
|
+
# it: a new line at that statement's indentation, or `; ` when the body
|
|
66
|
+
# does not end its line (`begin; a; rescue ...`).
|
|
67
|
+
def self.continuation(source, statements)
|
|
68
|
+
end_offset = statements.location.end_offset
|
|
69
|
+
return "; " unless source.byteslice(end_offset, 1) == "\n"
|
|
70
|
+
|
|
71
|
+
start_offset = statements.body.last.location.start_offset
|
|
72
|
+
line_start = (source.byterindex("\n", start_offset - 1) || -1) + 1
|
|
73
|
+
"\n#{source.byteslice(line_start, start_offset - line_start)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.nodes_in(node)
|
|
77
|
+
[node, *node.compact_child_nodes.flat_map { |child| nodes_in(child) }]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private_class_method :reads_exception?, :exception_global?, :rescue_only?, :nodes_in
|
|
81
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "version"
|
|
5
5
|
require_relative "temp_dir_tracker"
|
|
6
|
+
require_relative "diagnostic"
|
|
6
7
|
|
|
7
8
|
# Single owner of the process-lifecycle invariant: every pid spawned here is
|
|
8
9
|
# group-isolated, tracked in a signal-safe registry, group-signalled through a
|
|
@@ -219,8 +220,8 @@ class Evilution::ProcessSupervisor
|
|
|
219
220
|
# group: a later group-kill won't sweep its subtree. Don't raise (spawn
|
|
220
221
|
# must still return a usable handle), but surface it so the leak is
|
|
221
222
|
# debuggable rather than silent.
|
|
222
|
-
warn
|
|
223
|
-
|
|
223
|
+
Evilution::Diagnostic.warn("evilution: could not isolate process #{pid} into its own process " \
|
|
224
|
+
"group (#{e.class}: #{e.message}); grandchildren may survive a kill.")
|
|
224
225
|
end
|
|
225
226
|
|
|
226
227
|
# True once the child has been reaped (now or earlier). WNOHANG returns the
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostic"
|
|
4
|
+
|
|
5
|
+
# Warms Rails state that initialises lazily on a process's first request, once
|
|
6
|
+
# in the parent after the preload, so forked mutations inherit it instead of
|
|
7
|
+
# each paying the cold start.
|
|
8
|
+
#
|
|
9
|
+
# Every step is side-effect free. A real request is deliberately NOT used:
|
|
10
|
+
# controller callbacks change process-global state -- I18n.locale,
|
|
11
|
+
# PaperTrail.request, a session row -- that every fork would then inherit,
|
|
12
|
+
# failing unrelated tests and reporting them as kills.
|
|
13
|
+
#
|
|
14
|
+
# A step whose library is not loaded is skipped silently; a step that raises
|
|
15
|
+
# (no application stylesheet, an unusual asset setup) is skipped with a note.
|
|
16
|
+
# Warm-up is an optimisation and must never fail or skew the run.
|
|
17
|
+
class Evilution::RailsWarmup
|
|
18
|
+
def initialize(application, stream: $stderr)
|
|
19
|
+
@application = application
|
|
20
|
+
@stream = stream
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
step("i18n") { warm_i18n }
|
|
25
|
+
step("routes") { warm_routes }
|
|
26
|
+
step("assets") { warm_assets }
|
|
27
|
+
step("templates") { warm_templates }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def step(name)
|
|
33
|
+
yield
|
|
34
|
+
rescue StandardError, ScriptError => e
|
|
35
|
+
Evilution::Diagnostic.warn("[evilution] rails warmup: skipped #{name} (#{e.class}: #{e.message})", stream: @stream)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def warm_i18n
|
|
39
|
+
::I18n.eager_load! if defined?(::I18n) && ::I18n.respond_to?(:eager_load!)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Rails 8 can draw routes lazily; building url_helpers compiles the helpers
|
|
43
|
+
# module every request would otherwise build on first use.
|
|
44
|
+
def warm_routes
|
|
45
|
+
routes = @application.routes
|
|
46
|
+
routes.eager_load! if routes.respond_to?(:eager_load!)
|
|
47
|
+
routes.url_helpers
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def warm_assets
|
|
51
|
+
::ActionController::Base.helpers.stylesheet_path("application") if defined?(::ActionController::Base)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Optional: jhawthorn/actionview_precompiler compiles templates up front.
|
|
55
|
+
def warm_templates
|
|
56
|
+
return unless Gem.loaded_specs.key?("actionview_precompiler")
|
|
57
|
+
|
|
58
|
+
require "actionview_precompiler"
|
|
59
|
+
::ActionviewPrecompiler.precompile
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
require_relative "../../../baseline"
|
|
5
|
+
|
|
6
|
+
# An example that was failing before any mutation ran fails in every mutation
|
|
7
|
+
# run it takes part in. Where nothing else failed, that failure is not a kill:
|
|
8
|
+
# the mutation is recorded neutral and drops out of the score. Left unsaid, a
|
|
9
|
+
# red spec file would read as full marks, so the score never goes out without
|
|
10
|
+
# saying how many kills were not counted, for which spec file, and why it was
|
|
11
|
+
# red.
|
|
12
|
+
class Evilution::Reporter::CLI::LineFormatters::BaselineNeutralizedNotice
|
|
13
|
+
DETAIL_INDENT = " "
|
|
14
|
+
|
|
15
|
+
def initialize(failure_formatter: Evilution::Baseline::FailureFormatter.new)
|
|
16
|
+
@failure_formatter = failure_formatter
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format(summary)
|
|
20
|
+
neutralizations = summary.baseline_neutralizations
|
|
21
|
+
return nil if neutralizations.empty?
|
|
22
|
+
|
|
23
|
+
neutralizations.flat_map { |neutralization| [headline(neutralization), *detail(neutralization)] }.join("\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def headline(neutralization)
|
|
29
|
+
count = neutralization.count
|
|
30
|
+
subject = count == 1 ? "1 kill" : "#{count} kills"
|
|
31
|
+
verdict = count == 1 ? "that mutation has no verdict" : "those mutations have no verdict"
|
|
32
|
+
"! #{subject} not counted#{spec_files(neutralization)}: only examples already failing in the baseline " \
|
|
33
|
+
"failed, so #{verdict}."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def spec_files(neutralization)
|
|
37
|
+
names = neutralization.spec_file ? [neutralization.spec_file] : neutralization.failures.map(&:spec_file)
|
|
38
|
+
names.empty? ? "" : " for #{names.join(", ")}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One spec file's detail sits straight under the headline that names it;
|
|
42
|
+
# several are each put under their own name.
|
|
43
|
+
def detail(neutralization)
|
|
44
|
+
failures = neutralization.failures
|
|
45
|
+
return failure_lines(failures.first, DETAIL_INDENT) if failures.length == 1
|
|
46
|
+
|
|
47
|
+
failures.flat_map do |failure|
|
|
48
|
+
["#{DETAIL_INDENT}#{failure.spec_file}:", *failure_lines(failure, "#{DETAIL_INDENT} ")]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def failure_lines(failure, indent)
|
|
53
|
+
@failure_formatter.call(failure).map { |line| "#{indent}#{line}" }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../cli"
|
|
4
4
|
require_relative "line_formatters/mutations"
|
|
5
5
|
require_relative "line_formatters/score"
|
|
6
|
+
require_relative "line_formatters/baseline_neutralized_notice"
|
|
6
7
|
require_relative "line_formatters/error_rate_warning"
|
|
7
8
|
require_relative "line_formatters/unresolved_rate_warning"
|
|
8
9
|
require_relative "line_formatters/unresolved_targets"
|
|
@@ -15,6 +16,7 @@ class Evilution::Reporter::CLI::MetricsBlock
|
|
|
15
16
|
DEFAULT_LINES = [
|
|
16
17
|
Evilution::Reporter::CLI::LineFormatters::Mutations.new,
|
|
17
18
|
Evilution::Reporter::CLI::LineFormatters::Score.new,
|
|
19
|
+
Evilution::Reporter::CLI::LineFormatters::BaselineNeutralizedNotice.new,
|
|
18
20
|
Evilution::Reporter::CLI::LineFormatters::ErrorRateWarning.new,
|
|
19
21
|
Evilution::Reporter::CLI::LineFormatters::UnresolvedRateWarning.new,
|
|
20
22
|
Evilution::Reporter::CLI::LineFormatters::UnresolvedTargets.new,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../json"
|
|
4
|
+
|
|
5
|
+
# The summary fields that speak for a red baseline: the kills it took out of
|
|
6
|
+
# the score because only examples that were already failing failed, and why
|
|
7
|
+
# each spec file was red. Each is present only when there is something to say.
|
|
8
|
+
class Evilution::Reporter::JSON::Baseline
|
|
9
|
+
def call(summary)
|
|
10
|
+
fields = {}
|
|
11
|
+
fields[:baseline_neutralized] = summary.baseline_neutralized if summary.baseline_neutralized.positive?
|
|
12
|
+
fields[:baseline_failures] = summary.baseline_failures.map(&:to_h) unless summary.baseline_failures.empty?
|
|
13
|
+
fields
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -8,9 +8,10 @@ require_relative "../reporter"
|
|
|
8
8
|
require_relative "../session/schema"
|
|
9
9
|
|
|
10
10
|
class Evilution::Reporter::JSON
|
|
11
|
-
def initialize(suggest_tests: false, integration: :rspec, subjects: Subjects.new)
|
|
11
|
+
def initialize(suggest_tests: false, integration: :rspec, subjects: Subjects.new, baseline: Baseline.new)
|
|
12
12
|
@suggestion = Evilution::Reporter::Suggestion.new(suggest_tests: suggest_tests, integration: integration)
|
|
13
13
|
@subjects = subjects
|
|
14
|
+
@baseline = baseline
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def call(summary)
|
|
@@ -104,10 +105,13 @@ class Evilution::Reporter::JSON
|
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
# What the run had to say about itself rather than about the mutations: a
|
|
107
|
-
# target that was never tested, work that had to be redone serially
|
|
108
|
+
# target that was never tested, work that had to be redone serially,
|
|
109
|
+
# survivors a red baseline took out of the score.
|
|
108
110
|
def append_diagnostic_summary_fields(data, summary)
|
|
109
111
|
data[:unresolved_target_files] = summary.unresolved_target_files if summary.unresolved_targets?
|
|
110
112
|
data[:infra_retried] = summary.infra_retried if summary.infra_retried.positive?
|
|
113
|
+
data[:uncovered_code] = summary.uncovered_code if summary.uncovered_code?
|
|
114
|
+
data.merge!(@baseline.call(summary))
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
def build_mutation_detail(result)
|
|
@@ -183,3 +187,4 @@ class Evilution::Reporter::JSON
|
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
require_relative "json/subjects"
|
|
190
|
+
require_relative "json/baseline"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
# The kills one red spec file left uncounted -- mutations whose tests failed
|
|
6
|
+
# on nothing but examples that were already failing -- with why it was red.
|
|
7
|
+
#
|
|
8
|
+
# spec_file is nil when no single red file can be named, as in a run given
|
|
9
|
+
# its spec files explicitly; failures then holds all of them.
|
|
10
|
+
Evilution::Result::BaselineNeutralization = Data.define(:spec_file, :count, :failures)
|