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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "regexp_parser"
|
|
5
|
+
|
|
6
|
+
require_relative "../ast"
|
|
7
|
+
|
|
8
|
+
# The pattern of a regexp literal, parsed with regexp_parser and addressed by
|
|
9
|
+
# byte offsets into the file, for operators that mutate a regexp's structure.
|
|
10
|
+
#
|
|
11
|
+
# Edits are made by offsets into the original source, never by re-emitting the
|
|
12
|
+
# pattern, so everything outside an edit stays byte for byte as written.
|
|
13
|
+
#
|
|
14
|
+
# regexp_parser counts offsets in characters; they are converted to bytes here
|
|
15
|
+
# so they can go straight to source surgery. Prism accepts patterns whose
|
|
16
|
+
# references point nowhere (`/(?:a)\1/`), so an edit has to be compiled before
|
|
17
|
+
# it is emitted: `#compiles?` does that.
|
|
18
|
+
class Evilution::AST::RegexpPattern
|
|
19
|
+
Token = Data.define(:type, :token, :text, :start_offset, :end_offset)
|
|
20
|
+
|
|
21
|
+
attr_reader :tokens
|
|
22
|
+
|
|
23
|
+
# Returns nil for anything but a plain regexp literal — interpolated
|
|
24
|
+
# patterns are only known at runtime — and when regexp_parser cannot read
|
|
25
|
+
# the pattern (including one that is not valid in its encoding), so callers
|
|
26
|
+
# simply emit no mutants.
|
|
27
|
+
def self.parse(node)
|
|
28
|
+
return nil unless node.is_a?(Prism::RegularExpressionNode)
|
|
29
|
+
|
|
30
|
+
new(node)
|
|
31
|
+
rescue Regexp::Parser::Error
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(node)
|
|
36
|
+
@text = node.content_loc.slice
|
|
37
|
+
@base_offset = node.content_loc.start_offset
|
|
38
|
+
@options = options_of(node)
|
|
39
|
+
@byte_offsets = byte_offsets_of(@text)
|
|
40
|
+
@tokens = scan_tokens
|
|
41
|
+
@root = Regexp::Parser.parse(@text, options: @options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Yields every expression of the parsed pattern with its start and end
|
|
45
|
+
# offsets in the file.
|
|
46
|
+
def each_expression
|
|
47
|
+
@root.each_expression do |expression, _index|
|
|
48
|
+
yield expression, *offsets(expression)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# An expression's start and end offsets in the file, quantifier included.
|
|
53
|
+
def offsets(expression)
|
|
54
|
+
[file_offset(expression.ts), file_offset(expression.ts + expression.full_length)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether the pattern still compiles once the span between the two file
|
|
58
|
+
# offsets is replaced.
|
|
59
|
+
def compiles?(start_offset, end_offset, replacement)
|
|
60
|
+
local_start = start_offset - @base_offset
|
|
61
|
+
mutated = @text.byteslice(0, local_start) + replacement + @text.byteslice((end_offset - @base_offset)..)
|
|
62
|
+
without_warnings { Regexp.new(mutated, @options) }
|
|
63
|
+
true
|
|
64
|
+
rescue RegexpError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Only extended mode changes how a pattern is read: whitespace and `#`
|
|
71
|
+
# comments become tokens of their own. Case-insensitivity and multiline
|
|
72
|
+
# change what a pattern matches, not how it parses or whether it compiles.
|
|
73
|
+
def options_of(node)
|
|
74
|
+
node.extended? ? Regexp::EXTENDED : 0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Bytes before each character position, one entry past the last character.
|
|
78
|
+
def byte_offsets_of(text)
|
|
79
|
+
offsets = [0]
|
|
80
|
+
text.each_char { |char| offsets << (offsets.last + char.bytesize) }
|
|
81
|
+
offsets
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def file_offset(character_index)
|
|
85
|
+
@base_offset + @byte_offsets.fetch(character_index)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def scan_tokens
|
|
89
|
+
Regexp::Scanner.scan(@text, options: @options).map do |type, token, text, start_index, end_index|
|
|
90
|
+
Token.new(type: type, token: token, text: text,
|
|
91
|
+
start_offset: file_offset(start_index), end_offset: file_offset(end_index))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Onigmo warns about patterns such as duplicated class ranges; a mutant can
|
|
96
|
+
# produce them, and the warning would land in the user's output.
|
|
97
|
+
def without_warnings
|
|
98
|
+
verbose = $VERBOSE
|
|
99
|
+
$VERBOSE = nil
|
|
100
|
+
yield
|
|
101
|
+
ensure
|
|
102
|
+
$VERBOSE = verbose
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# The tie-breaking mode of a `round` call: the `:even` of
|
|
7
|
+
# `amount.round(2, half: :even)`. Only a literal mode Ruby accepts counts; a
|
|
8
|
+
# mode held in a variable, `nil` or an unknown symbol is not one.
|
|
9
|
+
module Evilution::AST::RoundHalfMode
|
|
10
|
+
MODES = %i[up even down].freeze
|
|
11
|
+
|
|
12
|
+
# The symbol node holding the mode, or nil when node is not a `round` call
|
|
13
|
+
# with a literal mode.
|
|
14
|
+
def self.of(node)
|
|
15
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :round
|
|
16
|
+
|
|
17
|
+
value = half_value(node.arguments)
|
|
18
|
+
value if value.is_a?(Prism::SymbolNode) && MODES.include?(value.unescaped.to_sym)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The value given to `half:`, whatever it is.
|
|
22
|
+
def self.half_value(arguments)
|
|
23
|
+
return nil unless arguments
|
|
24
|
+
|
|
25
|
+
keywords = arguments.arguments.grep(Prism::KeywordHashNode).flat_map(&:elements)
|
|
26
|
+
pair = keywords.find { |element| half_key?(element) }
|
|
27
|
+
pair.value if pair
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.half_key?(element)
|
|
31
|
+
return false unless element.is_a?(Prism::AssocNode)
|
|
32
|
+
|
|
33
|
+
key = element.key
|
|
34
|
+
key.is_a?(Prism::SymbolNode) && key.unescaped == "half"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method :half_value, :half_key?
|
|
38
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
require_relative "literal_callable"
|
|
6
|
+
|
|
7
|
+
# An ActiveRecord scope declared with a literal body:
|
|
8
|
+
# `scope :recent, -> { where(recent: true) }`, or with `lambda { }` /
|
|
9
|
+
# `proc { }`. The body is what mutations go into; the name is the class
|
|
10
|
+
# method the declaration defines.
|
|
11
|
+
#
|
|
12
|
+
# Only the receiver-less form written directly in a class body counts: that
|
|
13
|
+
# is the call the mutated file re-runs to replace the scope, so the mutated
|
|
14
|
+
# body takes effect. A scope whose body is an object or a variable has no
|
|
15
|
+
# body of its own to mutate.
|
|
16
|
+
module Evilution::AST::ScopeDeclaration
|
|
17
|
+
# The scope declarations among a class body's direct statements.
|
|
18
|
+
def self.in_body(body)
|
|
19
|
+
statements = body.is_a?(Prism::BeginNode) ? body.statements : body
|
|
20
|
+
return [] unless statements
|
|
21
|
+
|
|
22
|
+
statements.body.select { |node| body_of(node) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The node holding the scope's body -- a LambdaNode, or the BlockNode of a
|
|
26
|
+
# `lambda` / `proc` call -- or nil when node is not a scope declaration.
|
|
27
|
+
def self.body_of(node)
|
|
28
|
+
return nil unless scope_call?(node)
|
|
29
|
+
|
|
30
|
+
Evilution::AST::LiteralCallable.body_of(node.arguments.arguments[1])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.scope_name(node)
|
|
34
|
+
node.arguments.arguments.first.unescaped
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.scope_call?(node)
|
|
38
|
+
return false unless node.is_a?(Prism::CallNode) && node.name == :scope && node.receiver.nil?
|
|
39
|
+
|
|
40
|
+
args = node.arguments ? node.arguments.arguments : []
|
|
41
|
+
args.length == 2 && args.first.is_a?(Prism::SymbolNode)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :scope_call?
|
|
45
|
+
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "prism"
|
|
4
|
-
|
|
5
3
|
require_relative "../ast"
|
|
4
|
+
require_relative "syntax_check"
|
|
6
5
|
|
|
7
6
|
module Evilution::AST::SourceSurgeon
|
|
8
7
|
Result = Struct.new(:source, :status, keyword_init: true) do
|
|
@@ -19,7 +18,7 @@ module Evilution::AST::SourceSurgeon
|
|
|
19
18
|
binary = source.b
|
|
20
19
|
binary[offset, length] = replacement.b
|
|
21
20
|
mutated = binary.force_encoding(source.encoding)
|
|
22
|
-
status =
|
|
21
|
+
status = Evilution::AST::SyntaxCheck.valid?(mutated) ? :ok : :unparseable
|
|
23
22
|
Result.new(source: mutated, status: status).freeze
|
|
24
23
|
end
|
|
25
24
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../ast"
|
|
6
|
+
|
|
7
|
+
# Answers whether mutated source is Ruby the running interpreter will accept.
|
|
8
|
+
#
|
|
9
|
+
# Mutations are built and screened with Prism, but Prism and the older
|
|
10
|
+
# parse.y parser do not agree on everything: Prism takes a bare constant
|
|
11
|
+
# operator write inside a method (`def f; LIMIT ||= 1; end`) and a `nil[...]`
|
|
12
|
+
# pattern, both of which parse.y rejects. An interpreter that compiles with
|
|
13
|
+
# parse.y -- Ruby 3.3 by default, any later one started with
|
|
14
|
+
# `--parser=parse.y` -- would be handed a mutant marked parseable and raise
|
|
15
|
+
# SyntaxError evaluating it.
|
|
16
|
+
#
|
|
17
|
+
# So Prism decides first, and where the interpreter does not compile with
|
|
18
|
+
# Prism, parse.y is asked as well. That second parse costs about what the
|
|
19
|
+
# first does and is only paid on those interpreters.
|
|
20
|
+
module Evilution::AST::SyntaxCheck
|
|
21
|
+
# How the interpreter says it compiles: `ruby 3.4.1 (...) +PRISM [...]`.
|
|
22
|
+
PRISM_INTERPRETER = RUBY_DESCRIPTION.include?("+PRISM")
|
|
23
|
+
|
|
24
|
+
def self.valid?(source, prism_interpreter: PRISM_INTERPRETER)
|
|
25
|
+
return false unless Prism.parse(source).success?
|
|
26
|
+
|
|
27
|
+
prism_interpreter || parse_y_accepts?(source)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# RubyVM::AbstractSyntaxTree is built on parse.y whatever the interpreter
|
|
31
|
+
# compiles with. It is CRuby's own: another implementation has no parse.y
|
|
32
|
+
# to ask, and there Prism's answer stands, as it does after a failure that
|
|
33
|
+
# is not about syntax.
|
|
34
|
+
def self.parse_y_accepts?(source)
|
|
35
|
+
return true unless defined?(RubyVM::AbstractSyntaxTree)
|
|
36
|
+
|
|
37
|
+
without_warnings { RubyVM::AbstractSyntaxTree.parse(source) }
|
|
38
|
+
true
|
|
39
|
+
rescue SyntaxError
|
|
40
|
+
false
|
|
41
|
+
rescue StandardError
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# parse.y warns as it parses ("possibly useless use of == in void
|
|
46
|
+
# context"), which a mutant earns far more often than written code does.
|
|
47
|
+
def self.without_warnings
|
|
48
|
+
verbose = $VERBOSE
|
|
49
|
+
$VERBOSE = nil
|
|
50
|
+
yield
|
|
51
|
+
ensure
|
|
52
|
+
$VERBOSE = verbose
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_class_method :parse_y_accepts?, :without_warnings
|
|
56
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
require_relative "included_block"
|
|
6
|
+
|
|
7
|
+
# The lines of a file that hold code no subject covers: statements written in
|
|
8
|
+
# a class, module or `class << self` body, or at the top level, outside every
|
|
9
|
+
# method -- callback macros, DSL calls, logical writes on a constant -- or in a
|
|
10
|
+
# concern's `included` or `prepended` block, which is a class body written
|
|
11
|
+
# elsewhere. Mutations are generated per subject, so a run aimed at these
|
|
12
|
+
# lines generates none of its own, and saying so is the only way a reader can
|
|
13
|
+
# tell "nothing to mutate" from "nothing reached".
|
|
14
|
+
#
|
|
15
|
+
# Visibility keywords without arguments (`private`) carry no behaviour of
|
|
16
|
+
# their own and are left out, as are `require` and `require_relative`: loading
|
|
17
|
+
# a file is not something a mutation of this one could test.
|
|
18
|
+
#
|
|
19
|
+
# A few operators do reach class-body statements -- an `alias_method`, an
|
|
20
|
+
# `include` -- through the first method of the scope. A statement holding a
|
|
21
|
+
# line a mutation was generated for is targeted after all, and is left out
|
|
22
|
+
# whole.
|
|
23
|
+
module Evilution::AST::UncoveredCode
|
|
24
|
+
SCOPE_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
25
|
+
VISIBILITY_KEYWORDS = %i[private protected public module_function].freeze
|
|
26
|
+
LOAD_METHODS = %i[require require_relative].freeze
|
|
27
|
+
private_constant :SCOPE_NODES, :VISIBILITY_KEYWORDS, :LOAD_METHODS
|
|
28
|
+
|
|
29
|
+
# subjects: those parsed from file_path; lines: the Range to keep, or nil
|
|
30
|
+
# for the whole file; mutated_lines: the lines of file_path that mutations
|
|
31
|
+
# were generated for. Returns the uncovered lines as merged Ranges, in
|
|
32
|
+
# source order.
|
|
33
|
+
def self.call(file_path, subjects, lines:, mutated_lines: [])
|
|
34
|
+
covered = subjects.flat_map { |subject| subject_lines(subject) }.to_set
|
|
35
|
+
uncovered = code_lines(File.read(file_path), mutated_lines).reject { |line| covered.include?(line) }
|
|
36
|
+
uncovered = uncovered.select { |line| lines.cover?(line) } if lines
|
|
37
|
+
merge(uncovered.uniq)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.code_lines(source, mutated_lines)
|
|
41
|
+
statements(Prism.parse(source).value.statements)
|
|
42
|
+
.reject { |node| mutated_lines.any? { |line| line.between?(node.start_line, node.end_line) } }
|
|
43
|
+
.flat_map { |node| node_lines(node) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.statements(body)
|
|
47
|
+
nodes = case body
|
|
48
|
+
when Prism::StatementsNode then body.body
|
|
49
|
+
when Prism::BeginNode then body.statements ? body.statements.body : []
|
|
50
|
+
else []
|
|
51
|
+
end
|
|
52
|
+
nodes.flat_map { |node| expand(node) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.expand(node)
|
|
56
|
+
return statements(node.body) if SCOPE_NODES.any? { |type| node.is_a?(type) }
|
|
57
|
+
|
|
58
|
+
hook = Evilution::AST::IncludedBlock.of(node) || Evilution::AST::IncludedBlock.prepended(node)
|
|
59
|
+
return statements(hook.body) if hook
|
|
60
|
+
return [] if bare_visibility?(node) || load_call?(node)
|
|
61
|
+
|
|
62
|
+
[node]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.load_call?(node)
|
|
66
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && LOAD_METHODS.include?(node.name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.bare_visibility?(node)
|
|
70
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.arguments.nil? &&
|
|
71
|
+
VISIBILITY_KEYWORDS.include?(node.name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.node_lines(node)
|
|
75
|
+
(node.start_line..node.end_line).to_a
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.subject_lines(subject)
|
|
79
|
+
(subject.line_number..(subject.line_number + subject.source.count("\n"))).to_a
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.merge(lines)
|
|
83
|
+
lines.slice_when { |a, b| b != a + 1 }.map { |run| run.first..run.last }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :code_lines, :statements, :expand, :bare_visibility?, :load_call?, :node_lines,
|
|
87
|
+
:subject_lines, :merge
|
|
88
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# A value-object definition: `Data.define(...)` or `Struct.new(...)` called on
|
|
7
|
+
# the core class itself (`Data` or `::Data`, not `Foo::Data`), with or without
|
|
8
|
+
# a block.
|
|
9
|
+
module Evilution::AST::ValueObjectDefinition
|
|
10
|
+
DEFINERS = { Data: :define, Struct: :new }.freeze
|
|
11
|
+
|
|
12
|
+
def self.match?(node)
|
|
13
|
+
return false unless node.is_a?(Prism::CallNode) && core_constant?(node.receiver)
|
|
14
|
+
|
|
15
|
+
DEFINERS[node.receiver.name] == node.name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.core_constant?(node)
|
|
19
|
+
return true if node.is_a?(Prism::ConstantReadNode)
|
|
20
|
+
|
|
21
|
+
node.is_a?(Prism::ConstantPathNode) && node.parent.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private_class_method :core_constant?
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../baseline"
|
|
4
|
+
|
|
5
|
+
# The lines that say why a spec file was red in the baseline, shared by the
|
|
6
|
+
# warning printed during the run and the note in the final report.
|
|
7
|
+
class Evilution::Baseline::FailureFormatter
|
|
8
|
+
NO_DETAIL = "no failure detail was captured"
|
|
9
|
+
|
|
10
|
+
def call(failure)
|
|
11
|
+
lines = error_lines(failure) + failure.examples.map { |example| example_line(example) }
|
|
12
|
+
remainder = failure.example_count - failure.examples.length
|
|
13
|
+
lines << "... and #{remainder} more failing" if remainder.positive?
|
|
14
|
+
lines.empty? ? [NO_DETAIL] : lines
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def error_lines(failure)
|
|
20
|
+
failure.error ? failure.error.lines.map(&:chomp) : []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def example_line(example)
|
|
24
|
+
line = [example.id, example.description].reject(&:empty?).join(" ")
|
|
25
|
+
example.message.empty? ? line : "#{line} -- #{example.message}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../baseline"
|
|
4
|
+
|
|
5
|
+
# What a baseline run of one spec file sends back across the fork: whether it
|
|
6
|
+
# passed and, when it did not, enough to say why. A plain hash of strings and
|
|
7
|
+
# numbers, so it marshals over the pipe whatever the test framework is.
|
|
8
|
+
#
|
|
9
|
+
# The detail is bounded here, in the child: a spec file with a thousand failing
|
|
10
|
+
# examples has nothing more to say after the first few. Their ids are kept in
|
|
11
|
+
# far greater number, since a mutation run is compared against all of them.
|
|
12
|
+
module Evilution::Baseline::Report
|
|
13
|
+
MAX_EXAMPLES = 10
|
|
14
|
+
MAX_IDS = 1000
|
|
15
|
+
MAX_MESSAGE_LENGTH = 300
|
|
16
|
+
MESSAGE_LINES = 3
|
|
17
|
+
ERROR_LINES = 5
|
|
18
|
+
ELLIPSIS = "..."
|
|
19
|
+
|
|
20
|
+
# failed_ids: every failing example, named so a mutation run can recognise
|
|
21
|
+
# it again (see Integration::RSpec::ExampleIds); empty where the framework
|
|
22
|
+
# does not provide such names.
|
|
23
|
+
def self.build(passed:, failures: [], error: nil, failed_ids: [])
|
|
24
|
+
{
|
|
25
|
+
passed: passed ? true : false,
|
|
26
|
+
failure_count: failures.length,
|
|
27
|
+
failures: failures.first(MAX_EXAMPLES).map { |failure| example(failure) },
|
|
28
|
+
error: error_excerpt(error),
|
|
29
|
+
failed_ids: failed_ids.first(MAX_IDS)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A runner may still answer with a bare pass/fail.
|
|
34
|
+
def self.from(outcome)
|
|
35
|
+
outcome.is_a?(Hash) ? outcome : build(passed: outcome)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.example(failure)
|
|
39
|
+
{
|
|
40
|
+
id: failure[:id].to_s,
|
|
41
|
+
description: failure[:description].to_s,
|
|
42
|
+
message: truncate(lines_of(failure[:message]).first(MESSAGE_LINES).join(" "))
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
private_class_method :example
|
|
46
|
+
|
|
47
|
+
# Each line is cut on its own, so one long path does not cost the lines
|
|
48
|
+
# after it.
|
|
49
|
+
def self.error_excerpt(error)
|
|
50
|
+
lines = lines_of(error).first(ERROR_LINES)
|
|
51
|
+
lines.empty? ? nil : lines.map { |line| truncate(line) }.join("\n")
|
|
52
|
+
end
|
|
53
|
+
private_class_method :error_excerpt
|
|
54
|
+
|
|
55
|
+
def self.lines_of(text)
|
|
56
|
+
text.to_s.lines.map(&:strip).reject(&:empty?)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :lines_of
|
|
59
|
+
|
|
60
|
+
def self.truncate(text)
|
|
61
|
+
return text if text.length <= MAX_MESSAGE_LENGTH
|
|
62
|
+
|
|
63
|
+
"#{text[0, MAX_MESSAGE_LENGTH - ELLIPSIS.length]}#{ELLIPSIS}"
|
|
64
|
+
end
|
|
65
|
+
private_class_method :truncate
|
|
66
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../baseline"
|
|
4
|
+
|
|
5
|
+
class Evilution::Baseline
|
|
6
|
+
ExampleFailure = Data.define(:id, :description, :message)
|
|
7
|
+
|
|
8
|
+
# Why one spec file was red in the baseline: the examples that failed, or the
|
|
9
|
+
# error that stopped it before any example could -- a load error, a runner
|
|
10
|
+
# that raised, a timeout. example_count is every failing example; examples
|
|
11
|
+
# holds only the first few, for display; failed_ids names all of them the
|
|
12
|
+
# way a mutation run does, for comparison.
|
|
13
|
+
SpecFailure = Data.define(:spec_file, :examples, :example_count, :error, :failed_ids) do
|
|
14
|
+
def self.from_report(spec_file, report)
|
|
15
|
+
examples = Array(report[:failures]).map { |failure| ExampleFailure.new(**failure) }
|
|
16
|
+
new(
|
|
17
|
+
spec_file: spec_file,
|
|
18
|
+
examples: examples,
|
|
19
|
+
example_count: report[:failure_count] || examples.length,
|
|
20
|
+
error: report[:error],
|
|
21
|
+
failed_ids: Array(report[:failed_ids])
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(spec_file:, examples: [], example_count: 0, error: nil, failed_ids: [])
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
spec_file: spec_file,
|
|
32
|
+
error: error,
|
|
33
|
+
failing_examples: example_count,
|
|
34
|
+
examples: examples.map(&:to_h)
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/evilution/baseline.rb
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "spec_resolver"
|
|
4
4
|
require_relative "process_cleanup"
|
|
5
|
+
require_relative "diagnostic"
|
|
5
6
|
|
|
6
7
|
class Evilution::Baseline
|
|
7
|
-
Result = Struct.new(:failed_spec_files, :duration) do
|
|
8
|
-
def initialize(
|
|
8
|
+
Result = Struct.new(:failed_spec_files, :duration, :failures) do
|
|
9
|
+
def initialize(failed_spec_files:, duration:, failures: [])
|
|
9
10
|
super
|
|
10
11
|
freeze
|
|
11
12
|
end
|
|
@@ -13,6 +14,11 @@ class Evilution::Baseline
|
|
|
13
14
|
def failed?
|
|
14
15
|
!failed_spec_files.empty?
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
# Every example known to have failed before any mutation ran.
|
|
19
|
+
def failed_example_ids
|
|
20
|
+
failures.flat_map(&:failed_ids).to_set
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
# spec_selector: the object `run` resolves with (integration layout plus
|
|
@@ -37,18 +43,20 @@ class Evilution::Baseline
|
|
|
37
43
|
|
|
38
44
|
def call(subjects)
|
|
39
45
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
40
|
-
|
|
41
|
-
failed = Set.new
|
|
42
|
-
|
|
43
|
-
spec_files.each do |spec_file|
|
|
44
|
-
failed.add(spec_file) unless run_spec_file(spec_file)
|
|
45
|
-
end
|
|
46
|
+
failures = baseline_spec_files(subjects).filter_map { |spec_file| check_spec_file(spec_file) }
|
|
46
47
|
|
|
47
48
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
48
|
-
Result.new(failed_spec_files:
|
|
49
|
+
Result.new(failed_spec_files: failures.to_set(&:spec_file), duration: duration, failures: failures)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def run_spec_file(spec_file)
|
|
53
|
+
check_spec_file(spec_file).nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# nil when the spec file passed, otherwise why it did not. Anything that
|
|
57
|
+
# stops the baseline from getting an answer counts as failing, and is
|
|
58
|
+
# recorded as the reason.
|
|
59
|
+
def check_spec_file(spec_file)
|
|
52
60
|
raise Evilution::Error, "no baseline runner configured" unless @runner
|
|
53
61
|
|
|
54
62
|
read_io, write_io = IO.pipe
|
|
@@ -57,11 +65,11 @@ class Evilution::Baseline
|
|
|
57
65
|
read_result(read_io, pid, spec_file)
|
|
58
66
|
rescue Evilution::Error
|
|
59
67
|
raise
|
|
60
|
-
rescue StandardError
|
|
61
|
-
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
warned(SpecFailure.new(spec_file: spec_file, error: "#{e.class}: #{e.message}"))
|
|
62
70
|
ensure
|
|
63
|
-
read_io
|
|
64
|
-
write_io
|
|
71
|
+
read_io.close if read_io
|
|
72
|
+
write_io.close if write_io
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
def fork_spec_runner(spec_file, read_io, write_io)
|
|
@@ -71,32 +79,28 @@ class Evilution::Baseline
|
|
|
71
79
|
$stdout.reopen(File::NULL, "w")
|
|
72
80
|
$stderr.reopen(File::NULL, "w")
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
Marshal.dump(
|
|
82
|
+
report = child_report(runner, spec_file)
|
|
83
|
+
Marshal.dump(report, write_io)
|
|
76
84
|
write_io.close
|
|
77
|
-
exit!(passed ? 0 : 1)
|
|
85
|
+
exit!(report[:passed] ? 0 : 1)
|
|
78
86
|
end
|
|
79
87
|
end
|
|
80
88
|
|
|
81
89
|
GRACE_PERIOD = 0.5
|
|
82
90
|
|
|
83
91
|
def read_result(read_io, pid, spec_file)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
terminate_child(pid)
|
|
93
|
-
warn_timeout(spec_file)
|
|
94
|
-
false
|
|
95
|
-
end
|
|
92
|
+
return timed_out(pid, spec_file) unless read_io.wait_readable(@timeout)
|
|
93
|
+
|
|
94
|
+
data = read_io.read
|
|
95
|
+
_, status = Process.wait2(pid)
|
|
96
|
+
return warned(SpecFailure.new(spec_file: spec_file, error: unreported_error(status))) if data.empty?
|
|
97
|
+
|
|
98
|
+
report = Marshal.load(data)
|
|
99
|
+
report[:passed] ? nil : warned(SpecFailure.from_report(spec_file, report))
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
def warn_timeout(spec_file)
|
|
99
|
-
warn
|
|
103
|
+
Evilution::Diagnostic.warn("[evilution] Baseline for #{spec_file} timed out after #{@timeout}s; treating it as failing.")
|
|
100
104
|
end
|
|
101
105
|
|
|
102
106
|
def terminate_child(pid)
|
|
@@ -114,6 +118,35 @@ class Evilution::Baseline
|
|
|
114
118
|
|
|
115
119
|
private
|
|
116
120
|
|
|
121
|
+
# Runs in the child. A runner that raises -- a spec helper that cannot load,
|
|
122
|
+
# a framework that is not there -- is a failure with a reason, not a child
|
|
123
|
+
# that dies without a word.
|
|
124
|
+
def child_report(runner, spec_file)
|
|
125
|
+
Report.from(runner.call(spec_file))
|
|
126
|
+
rescue StandardError, ScriptError => e
|
|
127
|
+
Report.build(passed: false, error: "#{e.class}: #{e.message}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def timed_out(pid, spec_file)
|
|
131
|
+
terminate_child(pid)
|
|
132
|
+
warn_timeout(spec_file)
|
|
133
|
+
SpecFailure.new(spec_file: spec_file, error: "timed out after #{@timeout}s")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def unreported_error(status)
|
|
137
|
+
cause = status.signaled? ? "signal #{status.termsig}" : "exit status #{status.exitstatus}"
|
|
138
|
+
"baseline process ended without reporting (#{cause})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def warned(failure)
|
|
142
|
+
detail = FailureFormatter.new.call(failure).map { |line| " #{line}" }
|
|
143
|
+
Evilution::Diagnostic.warn(
|
|
144
|
+
["[evilution] Baseline failed for #{failure.spec_file}; " \
|
|
145
|
+
"a mutation that fails only its already-failing examples will be reported neutral.", *detail].join("\n")
|
|
146
|
+
)
|
|
147
|
+
failure
|
|
148
|
+
end
|
|
149
|
+
|
|
117
150
|
def baseline_spec_files(subjects)
|
|
118
151
|
return Array(@test_files).uniq if @test_files && !@test_files.empty?
|
|
119
152
|
|
|
@@ -137,7 +170,7 @@ class Evilution::Baseline
|
|
|
137
170
|
|
|
138
171
|
def warn_no_matching_test(file_path)
|
|
139
172
|
action = @fallback_to_full_suite ? "running full suite" : "marking its mutations unresolved"
|
|
140
|
-
warn
|
|
173
|
+
Evilution::Diagnostic.warn("[evilution] No matching test found for #{file_path}, #{action}. #{no_match_hint(file_path)}")
|
|
141
174
|
end
|
|
142
175
|
|
|
143
176
|
def no_match_hint(file_path)
|
|
@@ -150,3 +183,7 @@ class Evilution::Baseline
|
|
|
150
183
|
@fallback_to_full_suite ? "#{hint}." : "#{hint}, or --fallback-full-suite to run the whole suite."
|
|
151
184
|
end
|
|
152
185
|
end
|
|
186
|
+
|
|
187
|
+
require_relative "baseline/report"
|
|
188
|
+
require_relative "baseline/spec_failure"
|
|
189
|
+
require_relative "baseline/failure_formatter"
|