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,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutator"
|
|
4
|
+
|
|
5
|
+
# Visits every node that sends a message to a receiver, or defines
|
|
6
|
+
# something on one, and hands that receiver to #receiver_found before the
|
|
7
|
+
# receiver itself is visited.
|
|
8
|
+
#
|
|
9
|
+
# A plain call is one such node, but not the only one. An operator or
|
|
10
|
+
# logical write on an attribute or an index (`a.b += c`, `a[b] ||= c`) and
|
|
11
|
+
# an attribute or index target of a multiple assignment (`a.b, c[d] = e`)
|
|
12
|
+
# each hold their receiver in a node of their own. An operator that treats
|
|
13
|
+
# "the receiver of a call" specially — replacing it with `nil` only earns a
|
|
14
|
+
# NoMethodError — has to mean all of them.
|
|
15
|
+
#
|
|
16
|
+
# A definition has a receiver too: the object a singleton method is defined
|
|
17
|
+
# on (`def self.build`, `def helper.extra`) and the one a singleton class is
|
|
18
|
+
# opened on (`class << self`). Replacing it does not change the method that
|
|
19
|
+
# is already there — `def nil.build` defines another one, on `nil` — so the
|
|
20
|
+
# mutant could never be killed.
|
|
21
|
+
#
|
|
22
|
+
# The includer defines #receiver_found(node). `node` is nil where the
|
|
23
|
+
# message or the definition has no explicit receiver.
|
|
24
|
+
module Evilution::Mutator::ReceiverPositions
|
|
25
|
+
def visit_call_node(node)
|
|
26
|
+
receiver_found(node.receiver)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_call_operator_write_node(node)
|
|
31
|
+
receiver_found(node.receiver)
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_call_or_write_node(node)
|
|
36
|
+
receiver_found(node.receiver)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def visit_call_and_write_node(node)
|
|
41
|
+
receiver_found(node.receiver)
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def visit_index_operator_write_node(node)
|
|
46
|
+
receiver_found(node.receiver)
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_index_or_write_node(node)
|
|
51
|
+
receiver_found(node.receiver)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def visit_index_and_write_node(node)
|
|
56
|
+
receiver_found(node.receiver)
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def visit_call_target_node(node)
|
|
61
|
+
receiver_found(node.receiver)
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_index_target_node(node)
|
|
66
|
+
receiver_found(node.receiver)
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_def_node(node)
|
|
71
|
+
receiver_found(node.receiver)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def visit_singleton_class_node(node)
|
|
76
|
+
receiver_found(node.expression)
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -4,7 +4,10 @@ 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,
|
|
10
|
+
Evilution::Mutator::Operator::SafeNavigationInsertion
|
|
8
11
|
].freeze
|
|
9
12
|
|
|
10
13
|
def self.for_profile(profile)
|
|
@@ -35,6 +38,8 @@ class Evilution::Mutator::Registry
|
|
|
35
38
|
Evilution::Mutator::Operator::NilReplacement,
|
|
36
39
|
Evilution::Mutator::Operator::IntegerLiteral,
|
|
37
40
|
Evilution::Mutator::Operator::FloatLiteral,
|
|
41
|
+
Evilution::Mutator::Operator::ComplexLiteral,
|
|
42
|
+
Evilution::Mutator::Operator::RationalLiteral,
|
|
38
43
|
Evilution::Mutator::Operator::StringLiteral,
|
|
39
44
|
Evilution::Mutator::Operator::ArrayLiteral,
|
|
40
45
|
Evilution::Mutator::Operator::HashLiteral,
|
|
@@ -136,7 +141,49 @@ class Evilution::Mutator::Registry
|
|
|
136
141
|
Evilution::Mutator::Operator::ReceiverConstructorSwap,
|
|
137
142
|
Evilution::Mutator::Operator::BlockBodyToNil,
|
|
138
143
|
Evilution::Mutator::Operator::BlockBodyToRaise,
|
|
139
|
-
Evilution::Mutator::Operator::BlockBodyPromotion
|
|
144
|
+
Evilution::Mutator::Operator::BlockBodyPromotion,
|
|
145
|
+
Evilution::Mutator::Operator::DataStructMember,
|
|
146
|
+
Evilution::Mutator::Operator::PinOperatorRemoval,
|
|
147
|
+
Evilution::Mutator::Operator::RightwardAssignment,
|
|
148
|
+
Evilution::Mutator::Operator::NumberedParameterSwap,
|
|
149
|
+
Evilution::Mutator::Operator::ForwardedArgumentDrop,
|
|
150
|
+
Evilution::Mutator::Operator::PatternWildcardWidening,
|
|
151
|
+
Evilution::Mutator::Operator::NoMatchingPatternElse,
|
|
152
|
+
Evilution::Mutator::Operator::ArgumentOrderPermutation,
|
|
153
|
+
Evilution::Mutator::Operator::KeywordValueSwap,
|
|
154
|
+
Evilution::Mutator::Operator::ComparisonOperandSwap,
|
|
155
|
+
Evilution::Mutator::Operator::IntegerDivisionToFdiv,
|
|
156
|
+
Evilution::Mutator::Operator::RoundHalfModeSwap,
|
|
157
|
+
Evilution::Mutator::Operator::OffByOneBoundary,
|
|
158
|
+
Evilution::Mutator::Operator::FormatSpecifierSwap,
|
|
159
|
+
Evilution::Mutator::Operator::AliasRemoval,
|
|
160
|
+
Evilution::Mutator::Operator::RegexpCharacterTypeComplement,
|
|
161
|
+
Evilution::Mutator::Operator::RegexpAnchorPromotion,
|
|
162
|
+
Evilution::Mutator::Operator::RegexpAlternationBranchDeletion,
|
|
163
|
+
Evilution::Mutator::Operator::RegexpQuantifierMinimumSwap,
|
|
164
|
+
Evilution::Mutator::Operator::RegexpCaptureToPassive,
|
|
165
|
+
Evilution::Mutator::Operator::RegexpNamedGroupRename,
|
|
166
|
+
Evilution::Mutator::Operator::RegexpOptionRemoval,
|
|
167
|
+
Evilution::Mutator::Operator::RescueHandlerPromotion,
|
|
168
|
+
Evilution::Mutator::Operator::RescueHandlerConcatenation,
|
|
169
|
+
Evilution::Mutator::Operator::RescueElseConcatenation,
|
|
170
|
+
Evilution::Mutator::Operator::ReturnKeywordRemoval,
|
|
171
|
+
Evilution::Mutator::Operator::MemoizationBreak,
|
|
172
|
+
Evilution::Mutator::Operator::ShortCircuitToEager,
|
|
173
|
+
Evilution::Mutator::Operator::FreezeRemoval,
|
|
174
|
+
Evilution::Mutator::Operator::RescueClassWidening,
|
|
175
|
+
Evilution::Mutator::Operator::VisibilityRemoval,
|
|
176
|
+
Evilution::Mutator::Operator::AttrAccessorSwap,
|
|
177
|
+
Evilution::Mutator::Operator::SleepToZero,
|
|
178
|
+
Evilution::Mutator::Operator::IndexReceiverToSelf,
|
|
179
|
+
Evilution::Mutator::Operator::IndexToKeyPredicate,
|
|
180
|
+
Evilution::Mutator::Operator::IndexRangeToDrop,
|
|
181
|
+
Evilution::Mutator::Operator::IndexWriteToValue,
|
|
182
|
+
Evilution::Mutator::Operator::ConstantReadToNil,
|
|
183
|
+
Evilution::Mutator::Operator::ConstantNamespaceStrip,
|
|
184
|
+
Evilution::Mutator::Operator::ConstantWriteToNil,
|
|
185
|
+
Evilution::Mutator::Operator::VariableReadToNil,
|
|
186
|
+
Evilution::Mutator::Operator::OperatorWriteReceiverPromotion
|
|
140
187
|
].each { |op| registry.register(op) }
|
|
141
188
|
registry
|
|
142
189
|
end
|
|
@@ -152,11 +199,20 @@ class Evilution::Mutator::Registry
|
|
|
152
199
|
|
|
153
200
|
def mutations_for(subject, filter: nil, operator_options: {})
|
|
154
201
|
@operators.flat_map do |operator_class|
|
|
202
|
+
next [] unless operator_class.subject_kinds.include?(subject.kind)
|
|
203
|
+
|
|
155
204
|
operator = build_operator(operator_class, operator_options)
|
|
156
205
|
operator.call(subject, filter: filter)
|
|
157
206
|
end
|
|
158
207
|
end
|
|
159
208
|
|
|
209
|
+
# Whether any registered operator mutates subjects of this one's kind. A
|
|
210
|
+
# subject none does would only ever be listed with no mutations to its
|
|
211
|
+
# name.
|
|
212
|
+
def accepts?(subject)
|
|
213
|
+
@operators.any? { |operator_class| operator_class.subject_kinds.include?(subject.kind) }
|
|
214
|
+
end
|
|
215
|
+
|
|
160
216
|
def operator_count
|
|
161
217
|
@operators.length
|
|
162
218
|
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
|
|
@@ -14,6 +14,10 @@ module Evilution::Parallel::WorkQueue::Channel::Frame
|
|
|
14
14
|
# parent itself forked, transferred over a private pipe inside our process
|
|
15
15
|
# tree. No external/untrusted input ever reaches this code. See
|
|
16
16
|
# .rubocop.yml (Security/MarshalLoad) for the full rationale.
|
|
17
|
+
#
|
|
18
|
+
# Trusted is not the same as well-formed: a process the worker forked holds
|
|
19
|
+
# the pipe too. A frame of the announced length that does not load is a
|
|
20
|
+
# BadFrame; one shorter than announced is the end of the stream, nil.
|
|
17
21
|
def decode(header, payload)
|
|
18
22
|
return nil if header.nil? || header.bytesize < 4
|
|
19
23
|
|
|
@@ -21,5 +25,7 @@ module Evilution::Parallel::WorkQueue::Channel::Frame
|
|
|
21
25
|
return nil if payload.nil? || payload.bytesize < length
|
|
22
26
|
|
|
23
27
|
Marshal.load(payload)
|
|
28
|
+
rescue TypeError, ArgumentError => e
|
|
29
|
+
raise Evilution::Parallel::WorkQueue::Channel::BadFrame, "#{e.class}: #{e.message.gsub(/\s+/, " ")}"
|
|
24
30
|
end
|
|
25
31
|
end
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "io/wait"
|
|
3
4
|
require_relative "../work_queue"
|
|
4
5
|
|
|
5
6
|
module Evilution::Parallel::WorkQueue::Channel
|
|
7
|
+
# A frame that arrived but is not one the peer wrote: bytes that do not
|
|
8
|
+
# decode, or a frame that stops before the length its header announced.
|
|
9
|
+
#
|
|
10
|
+
# The pipes of the pool are inherited by the processes a worker forks, so
|
|
11
|
+
# the code under test can write into them. After such a frame the stream is
|
|
12
|
+
# out of step and nothing read from it later can be trusted; the reader
|
|
13
|
+
# gives the peer up. The end of the stream is not this: it reads as nil.
|
|
14
|
+
class BadFrame < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Seconds a frame may take to arrive once its first byte has, header and
|
|
17
|
+
# payload together. A writer sends a frame in one call, so the rest follows
|
|
18
|
+
# at once; a header that announces bytes nobody will send must not hold the
|
|
19
|
+
# reader for ever.
|
|
20
|
+
FRAME_TIMEOUT = 10
|
|
21
|
+
|
|
6
22
|
module_function
|
|
7
23
|
|
|
8
24
|
def write(io, object)
|
|
@@ -10,14 +26,51 @@ module Evilution::Parallel::WorkQueue::Channel
|
|
|
10
26
|
io.flush
|
|
11
27
|
end
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
# Waits for a frame without a limit, since an idle peer is no fault, and
|
|
30
|
+
# for the rest of one that has begun no longer than `frame_timeout`: one
|
|
31
|
+
# allowance for the whole frame.
|
|
32
|
+
def read(io, frame_timeout: FRAME_TIMEOUT)
|
|
33
|
+
io.wait_readable
|
|
34
|
+
allowance = Allowance.new(deadline: now + frame_timeout, seconds: frame_timeout)
|
|
35
|
+
header = read_exactly(io, 4, allowance)
|
|
36
|
+
return nil if header.bytesize < 4
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
payload = io.read(length)
|
|
38
|
+
payload = read_exactly(io, header.unpack1("N"), allowance)
|
|
19
39
|
Frame.decode(header, payload)
|
|
20
40
|
end
|
|
41
|
+
|
|
42
|
+
# What is left of the time a frame is given, and the figure it was given,
|
|
43
|
+
# which the message of a frame cut short names.
|
|
44
|
+
Allowance = Data.define(:deadline, :seconds)
|
|
45
|
+
private_constant :Allowance
|
|
46
|
+
|
|
47
|
+
# Up to `count` bytes: fewer only when the stream ended first.
|
|
48
|
+
def read_exactly(io, count, allowance)
|
|
49
|
+
buffer = String.new
|
|
50
|
+
|
|
51
|
+
while buffer.bytesize < count
|
|
52
|
+
chunk = io.read_nonblock(count - buffer.bytesize, exception: false)
|
|
53
|
+
return buffer if chunk.nil?
|
|
54
|
+
|
|
55
|
+
if chunk == :wait_readable
|
|
56
|
+
await(io, allowance, buffer.bytesize, count)
|
|
57
|
+
else
|
|
58
|
+
buffer << chunk
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
buffer
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def await(io, allowance, arrived, count)
|
|
65
|
+
remaining = allowance.deadline - now
|
|
66
|
+
return if remaining.positive? && io.wait_readable(remaining)
|
|
67
|
+
|
|
68
|
+
raise BadFrame, "frame cut short: #{arrived} of #{count} bytes arrived within #{allowance.seconds}s"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def now
|
|
72
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
73
|
+
end
|
|
21
74
|
end
|
|
22
75
|
|
|
23
76
|
require_relative "channel/frame"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dispatcher"
|
|
4
|
+
require_relative "../channel"
|
|
5
|
+
|
|
6
|
+
# Reads what a worker sent and says what it is: a result of an item the worker
|
|
7
|
+
# holds, the end of its stream (nil), or something that cannot be trusted.
|
|
8
|
+
#
|
|
9
|
+
# The result pipe is inherited by the processes a worker forks, so a frame on
|
|
10
|
+
# it is not always the worker's. One that does not decode is UNREADABLE, and
|
|
11
|
+
# so is one that decodes to something other than a result: what a stray
|
|
12
|
+
# process writes may happen to be a marshal dump, and taken at its word it
|
|
13
|
+
# would move the Dispatcher's counters or be raised as the error of the run.
|
|
14
|
+
# After either, the stream is out of step, and the Dispatcher gives the worker
|
|
15
|
+
# up.
|
|
16
|
+
#
|
|
17
|
+
# Keeping the wire-level judgement here leaves the Dispatcher with what to do
|
|
18
|
+
# about each answer.
|
|
19
|
+
module Evilution::Parallel::WorkQueue::Dispatcher::Reply
|
|
20
|
+
UNREADABLE = Object.new.freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def from(worker)
|
|
25
|
+
message = worker.read_result
|
|
26
|
+
return nil if message.nil?
|
|
27
|
+
|
|
28
|
+
result_of?(worker, message) ? message : UNREADABLE
|
|
29
|
+
rescue Evilution::Parallel::WorkQueue::Channel::BadFrame
|
|
30
|
+
UNREADABLE
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def result_of?(worker, message)
|
|
34
|
+
return false unless message.is_a?(Array) && message.length == 3
|
|
35
|
+
|
|
36
|
+
index, status, value = message
|
|
37
|
+
return false unless worker.in_flight_indices.include?(index)
|
|
38
|
+
|
|
39
|
+
status == :ok || (status == :error && value.is_a?(Exception))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -74,8 +74,9 @@ class Evilution::Parallel::WorkQueue::Dispatcher
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def handle(worker, io_to_worker, result_ios)
|
|
77
|
-
message = worker
|
|
77
|
+
message = Reply.from(worker)
|
|
78
78
|
return handle_dead(worker, io_to_worker, result_ios) if message.nil?
|
|
79
|
+
return handle_unreadable(worker, io_to_worker, result_ios) if message.equal?(Reply::UNREADABLE)
|
|
79
80
|
|
|
80
81
|
record(message, worker)
|
|
81
82
|
return false if recycle_and_dispatch(worker, io_to_worker, result_ios)
|
|
@@ -96,6 +97,17 @@ class Evilution::Parallel::WorkQueue::Dispatcher
|
|
|
96
97
|
@deadlines.refresh(worker)
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# After a frame that is not the worker's, nothing read from its pipe later
|
|
101
|
+
# can be trusted: the stream is out of step. The worker is given up the way
|
|
102
|
+
# a dead one is, after being killed, since it may well be alive. Only the
|
|
103
|
+
# items it held lose their result.
|
|
104
|
+
def handle_unreadable(worker, io_to_worker, result_ios)
|
|
105
|
+
worker.kill
|
|
106
|
+
mark_unfinished(worker, Evilution::Parallel::WorkQueue::UNREADABLE)
|
|
107
|
+
retire_or_replace(worker, io_to_worker, result_ios)
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
99
111
|
# A worker that exited without replying loses only its in-flight item(s)
|
|
100
112
|
# (marked :died) and is recycled; the run continues rather than aborting.
|
|
101
113
|
def handle_dead(worker, io_to_worker, result_ios)
|
|
@@ -183,3 +195,4 @@ class Evilution::Parallel::WorkQueue::Dispatcher
|
|
|
183
195
|
end
|
|
184
196
|
|
|
185
197
|
require_relative "dispatcher/deadline_tracker"
|
|
198
|
+
require_relative "dispatcher/reply"
|
|
@@ -23,6 +23,11 @@ module Evilution::Parallel::WorkQueue::Worker::Loop
|
|
|
23
23
|
Evilution::Parallel::WorkQueue::Channel.write(
|
|
24
24
|
res_io, [Evilution::Parallel::WorkQueue::STATS, busy, wall]
|
|
25
25
|
)
|
|
26
|
+
rescue Evilution::Parallel::WorkQueue::Channel::BadFrame
|
|
27
|
+
# A command that does not decode leaves a stream this worker cannot follow.
|
|
28
|
+
# It ends without the stats frame, which the parent, still waiting for a
|
|
29
|
+
# result, would take for one; the closed pipe tells the parent to replace it.
|
|
30
|
+
nil
|
|
26
31
|
ensure
|
|
27
32
|
cmd_io.close unless cmd_io.closed?
|
|
28
33
|
res_io.close unless res_io.closed?
|
|
@@ -91,8 +91,8 @@ class Evilution::Parallel::WorkQueue::Worker
|
|
|
91
91
|
@in_flight_indices << index
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
def read_result
|
|
95
|
-
Evilution::Parallel::WorkQueue::Channel.read(@res_read)
|
|
94
|
+
def read_result(frame_timeout: Evilution::Parallel::WorkQueue::Channel::FRAME_TIMEOUT)
|
|
95
|
+
Evilution::Parallel::WorkQueue::Channel.read(@res_read, frame_timeout: frame_timeout)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def shutdown
|
|
@@ -138,16 +138,25 @@ class Evilution::Parallel::WorkQueue::Worker
|
|
|
138
138
|
|
|
139
139
|
private
|
|
140
140
|
|
|
141
|
+
# The grace period covers the read as well as the wait for it: a stats frame
|
|
142
|
+
# that begins inside it and never completes must not outlast it.
|
|
141
143
|
def drain_stats
|
|
142
144
|
zero = Timing.new(busy: 0.0, wall: 0.0)
|
|
145
|
+
deadline = monotonic_now + Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD
|
|
143
146
|
return zero unless @res_read.wait_readable(Evilution::Parallel::WorkQueue::TIMING_GRACE_PERIOD)
|
|
144
147
|
|
|
145
|
-
message = read_result
|
|
146
|
-
return zero
|
|
148
|
+
message = read_result(frame_timeout: [deadline - monotonic_now, 0].max)
|
|
149
|
+
return zero unless message.is_a?(Array)
|
|
147
150
|
|
|
148
151
|
tag, busy, wall = message
|
|
149
152
|
return zero unless tag == Evilution::Parallel::WorkQueue::STATS
|
|
150
153
|
|
|
151
154
|
Timing.new(busy: busy, wall: wall)
|
|
155
|
+
rescue Evilution::Parallel::WorkQueue::Channel::BadFrame
|
|
156
|
+
zero
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def monotonic_now
|
|
160
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
152
161
|
end
|
|
153
162
|
end
|
|
@@ -16,6 +16,7 @@ class Evilution::Parallel::WorkQueue
|
|
|
16
16
|
Unfinished = Data.define(:reason)
|
|
17
17
|
TIMED_OUT = Unfinished.new(reason: :timeout)
|
|
18
18
|
DIED = Unfinished.new(reason: :died)
|
|
19
|
+
UNREADABLE = Unfinished.new(reason: :unreadable)
|
|
19
20
|
|
|
20
21
|
def initialize(size:, hooks: nil, prefetch: 1, item_timeout: nil, worker_max_items: nil)
|
|
21
22
|
Validators::PositiveInt.call!(:size, size)
|
|
@@ -80,17 +81,16 @@ class Evilution::Parallel::WorkQueue
|
|
|
80
81
|
deadline = monotonic_now + TIMING_GRACE_PERIOD
|
|
81
82
|
|
|
82
83
|
until io_to_worker.empty?
|
|
83
|
-
|
|
84
|
-
break
|
|
85
|
-
break unless poll_and_apply(io_to_worker, remaining)
|
|
84
|
+
break if deadline - monotonic_now <= 0
|
|
85
|
+
break unless poll_and_apply(io_to_worker, deadline)
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
def poll_and_apply(io_to_worker,
|
|
90
|
-
readable, = IO.select(io_to_worker.keys, nil, nil,
|
|
89
|
+
def poll_and_apply(io_to_worker, deadline)
|
|
90
|
+
readable, = IO.select(io_to_worker.keys, nil, nil, deadline - monotonic_now)
|
|
91
91
|
return false unless readable
|
|
92
92
|
|
|
93
|
-
readable.each { |io| apply_final_timing(io_to_worker.delete(io), io) }
|
|
93
|
+
readable.each { |io| apply_final_timing(io_to_worker.delete(io), io, deadline) }
|
|
94
94
|
true
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -98,15 +98,21 @@ class Evilution::Parallel::WorkQueue
|
|
|
98
98
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
# The timings are a courtesy: a frame that cannot be read here costs the
|
|
102
|
+
# figures and nothing else. The read gets what is left of the grace period,
|
|
103
|
+
# so a frame that begins inside it and never completes does not outlast it.
|
|
104
|
+
def apply_final_timing(worker, io, deadline)
|
|
105
|
+
allowed = [deadline - monotonic_now, 0].max
|
|
106
|
+
message = Evilution::Parallel::WorkQueue::Channel.read(io, frame_timeout: allowed)
|
|
107
|
+
return unless message.is_a?(Array)
|
|
104
108
|
|
|
105
109
|
tag, busy_time, wall_time = message
|
|
106
110
|
return unless tag == STATS
|
|
107
111
|
|
|
108
112
|
worker.busy_time = busy_time
|
|
109
113
|
worker.wall_time = wall_time
|
|
114
|
+
rescue Evilution::Parallel::WorkQueue::Channel::BadFrame
|
|
115
|
+
nil
|
|
110
116
|
end
|
|
111
117
|
end
|
|
112
118
|
|
|
@@ -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,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"
|