mutineer 0.7.0 → 0.9.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/CHANGELOG.md +49 -0
- data/README.md +39 -4
- data/lib/mutineer/baseline.rb +29 -13
- data/lib/mutineer/changed_lines.rb +29 -12
- data/lib/mutineer/cli.rb +88 -20
- data/lib/mutineer/config.rb +24 -2
- data/lib/mutineer/coverage_map.rb +86 -4
- data/lib/mutineer/isolation.rb +89 -40
- data/lib/mutineer/minitest_integration.rb +13 -9
- data/lib/mutineer/mutant_id.rb +24 -5
- data/lib/mutineer/mutation.rb +12 -6
- data/lib/mutineer/mutator_registry.rb +33 -6
- data/lib/mutineer/mutators/arithmetic.rb +8 -2
- data/lib/mutineer/mutators/base.rb +11 -3
- data/lib/mutineer/mutators/boolean_connector.rb +15 -5
- data/lib/mutineer/mutators/boolean_literal.rb +19 -6
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +7 -8
- data/lib/mutineer/mutators/condition_negation.rb +14 -5
- data/lib/mutineer/mutators/literal_mutation.rb +15 -4
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +22 -7
- data/lib/mutineer/mutators/statement_removal.rb +6 -9
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/pairing.rb +32 -13
- data/lib/mutineer/parser.rb +17 -7
- data/lib/mutineer/project.rb +73 -2
- data/lib/mutineer/reporter.rb +169 -0
- data/lib/mutineer/result.rb +100 -32
- data/lib/mutineer/runner.rb +29 -2
- data/lib/mutineer/subject.rb +10 -6
- data/lib/mutineer/test_runners/minitest.rb +5 -4
- data/lib/mutineer/test_runners/rspec.rb +10 -17
- data/lib/mutineer/test_runners.rb +9 -2
- data/lib/mutineer/version.rb +2 -1
- data/lib/mutineer/worker_pool.rb +51 -29
- data/lib/mutineer.rb +4 -0
- metadata +18 -1
|
@@ -4,20 +4,26 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Boolean connector
|
|
8
|
-
# from the actual source token (operator_loc.slice) so symbolic and keyword
|
|
9
|
-
# forms each map to their own form — never crossing && to `or`, which would
|
|
10
|
-
# change precedence and surprise the reader (KTD-2).
|
|
7
|
+
# Boolean connector mutator.
|
|
11
8
|
#
|
|
12
|
-
#
|
|
9
|
+
# Replaces symbolic and keyword connectors with their opposite form.
|
|
13
10
|
class BooleanConnector < Base
|
|
11
|
+
# Token replacements for boolean connectors.
|
|
14
12
|
REPLACEMENTS = { "&&" => "||", "||" => "&&", "and" => "or", "or" => "and" }.freeze
|
|
15
13
|
|
|
14
|
+
# Visits `and` nodes.
|
|
15
|
+
#
|
|
16
|
+
# @param node [Prism::AndNode] node to inspect.
|
|
17
|
+
# @return [void]
|
|
16
18
|
def visit_and_node(node)
|
|
17
19
|
emit(node)
|
|
18
20
|
super
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# Visits `or` nodes.
|
|
24
|
+
#
|
|
25
|
+
# @param node [Prism::OrNode] node to inspect.
|
|
26
|
+
# @return [void]
|
|
21
27
|
def visit_or_node(node)
|
|
22
28
|
emit(node)
|
|
23
29
|
super
|
|
@@ -25,6 +31,10 @@ module Mutineer
|
|
|
25
31
|
|
|
26
32
|
private
|
|
27
33
|
|
|
34
|
+
# Emits a connector mutation when the token is replaceable.
|
|
35
|
+
#
|
|
36
|
+
# @param node [Prism::Node] connector node.
|
|
37
|
+
# @return [void]
|
|
28
38
|
def emit(node)
|
|
29
39
|
loc = node.operator_loc
|
|
30
40
|
replacement = REPLACEMENTS[loc.slice]
|
|
@@ -4,24 +4,32 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
#
|
|
8
|
-
# for the family (§4), so nil is in-scope by design even though it is not
|
|
9
|
-
# strictly a boolean. true<->false, and nil->true (nil->true catches more
|
|
10
|
-
# return-value gaps than nil->false). Rewrites the whole node location;
|
|
11
|
-
# these nodes have no sub-token location.
|
|
7
|
+
# Boolean literal mutator.
|
|
12
8
|
#
|
|
13
|
-
#
|
|
9
|
+
# Mutates true/false and nil literals in the boolean_literal family.
|
|
14
10
|
class BooleanLiteral < Base
|
|
11
|
+
# Visits true literals.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::TrueNode] node to inspect.
|
|
14
|
+
# @return [void]
|
|
15
15
|
def visit_true_node(node)
|
|
16
16
|
emit(node, "false")
|
|
17
17
|
super
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Visits false literals.
|
|
21
|
+
#
|
|
22
|
+
# @param node [Prism::FalseNode] node to inspect.
|
|
23
|
+
# @return [void]
|
|
20
24
|
def visit_false_node(node)
|
|
21
25
|
emit(node, "true")
|
|
22
26
|
super
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# Visits nil literals.
|
|
30
|
+
#
|
|
31
|
+
# @param node [Prism::NilNode] node to inspect.
|
|
32
|
+
# @return [void]
|
|
25
33
|
def visit_nil_node(node)
|
|
26
34
|
emit(node, "true")
|
|
27
35
|
super
|
|
@@ -29,6 +37,11 @@ module Mutineer
|
|
|
29
37
|
|
|
30
38
|
private
|
|
31
39
|
|
|
40
|
+
# Emits a boolean-literal mutation.
|
|
41
|
+
#
|
|
42
|
+
# @param node [Prism::Node] literal node.
|
|
43
|
+
# @param replacement [String] replacement token.
|
|
44
|
+
# @return [void]
|
|
32
45
|
def emit(node, replacement)
|
|
33
46
|
loc = node.location
|
|
34
47
|
@mutations << Mutation.new(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Collection / enumerable method-name mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Swaps the method-name token of a call to its semantic opposite, one
|
|
10
|
+
# mutation per occurrence, exactly like the arithmetic/comparison operators.
|
|
11
|
+
class CollectionMethod < Base
|
|
12
|
+
# Method-name swaps. All targets are core-Ruby Enumerable/Array methods so
|
|
13
|
+
# the mutant exercises real behaviour rather than always raising.
|
|
14
|
+
#
|
|
15
|
+
# ponytail: include? -> exclude? was specced but skipped — exclude? is not
|
|
16
|
+
# core Ruby, so that mutant would always NoMethodError (a weak mutant).
|
|
17
|
+
REPLACEMENTS = {
|
|
18
|
+
map: "each", each: "map",
|
|
19
|
+
all?: "any?", any?: "all?",
|
|
20
|
+
first: "last", last: "first",
|
|
21
|
+
min: "max", max: "min",
|
|
22
|
+
select: "reject", reject: "select"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Visits call nodes and emits collection-method mutations.
|
|
26
|
+
#
|
|
27
|
+
# @param node [Prism::CallNode] call node to inspect.
|
|
28
|
+
# @return [void]
|
|
29
|
+
def visit_call_node(node)
|
|
30
|
+
replacement = REPLACEMENTS[node.name]
|
|
31
|
+
loc = node.message_loc
|
|
32
|
+
if replacement && loc
|
|
33
|
+
@mutations << Mutation.new(
|
|
34
|
+
start_offset: loc.start_offset,
|
|
35
|
+
end_offset: loc.end_offset,
|
|
36
|
+
replacement: replacement,
|
|
37
|
+
operator: :collection_method
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -4,23 +4,22 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Comparison
|
|
8
|
-
# highest-value Tier-1 family (spec §4) — exposes off-by-one and boundary
|
|
9
|
-
# gaps line coverage never catches. Rewrites the operator token
|
|
10
|
-
# (CallNode#message_loc), one mutation per occurrence.
|
|
7
|
+
# Comparison and boundary mutator.
|
|
11
8
|
#
|
|
12
|
-
#
|
|
13
|
-
# mutation-testing literature, not the mutant gem.
|
|
9
|
+
# Rewrites comparison operators one occurrence at a time.
|
|
14
10
|
class Comparison < Base
|
|
11
|
+
# Token replacements for comparison operators.
|
|
15
12
|
REPLACEMENTS = {
|
|
16
13
|
:< => "<=", :<= => "<", :> => ">=", :>= => ">", :== => "!=", :!= => "=="
|
|
17
14
|
}.freeze
|
|
18
15
|
|
|
16
|
+
# Visits call nodes and emits comparison mutations.
|
|
17
|
+
#
|
|
18
|
+
# @param node [Prism::CallNode] call node to inspect.
|
|
19
|
+
# @return [void]
|
|
19
20
|
def visit_call_node(node)
|
|
20
21
|
replacement = REPLACEMENTS[node.name]
|
|
21
22
|
loc = node.message_loc
|
|
22
|
-
# receiver guard: reject any unary call accidentally named like an
|
|
23
|
-
# operator; binary comparisons always have a receiver.
|
|
24
23
|
if replacement && loc && node.receiver
|
|
25
24
|
@mutations << Mutation.new(
|
|
26
25
|
start_offset: loc.start_offset,
|
|
@@ -4,18 +4,23 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Condition-negation
|
|
8
|
-
# ternary condition in `!( ... )` textually. Ruby ternaries parse as IfNode in
|
|
9
|
-
# Prism, so visit_if_node covers them too (R12). The standard validity re-parse
|
|
10
|
-
# downstream discards any wrap that fails to round-trip (R14).
|
|
7
|
+
# Condition-negation mutator.
|
|
11
8
|
#
|
|
12
|
-
#
|
|
9
|
+
# Wraps if/unless predicates in `!( ... )` textually.
|
|
13
10
|
class ConditionNegation < Base
|
|
11
|
+
# Visits if nodes.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::IfNode] node to inspect.
|
|
14
|
+
# @return [void]
|
|
14
15
|
def visit_if_node(node)
|
|
15
16
|
wrap(node.predicate)
|
|
16
17
|
super
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# Visits unless nodes.
|
|
21
|
+
#
|
|
22
|
+
# @param node [Prism::UnlessNode] node to inspect.
|
|
23
|
+
# @return [void]
|
|
19
24
|
def visit_unless_node(node)
|
|
20
25
|
wrap(node.predicate)
|
|
21
26
|
super
|
|
@@ -23,6 +28,10 @@ module Mutineer
|
|
|
23
28
|
|
|
24
29
|
private
|
|
25
30
|
|
|
31
|
+
# Wraps a predicate in negation.
|
|
32
|
+
#
|
|
33
|
+
# @param predicate [Prism::Node, nil] predicate node.
|
|
34
|
+
# @return [void]
|
|
26
35
|
def wrap(predicate)
|
|
27
36
|
return unless predicate
|
|
28
37
|
|
|
@@ -4,12 +4,14 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Literal
|
|
8
|
-
# three mutations (0, 1, n+1) with no-op guards for 0 and 1; strings collapse
|
|
9
|
-
# to "" unless already empty. One mutation per emitted candidate (R11).
|
|
7
|
+
# Literal mutation mutator.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
9
|
+
# Mutates integers and strings with the Tier-2 literal rules.
|
|
12
10
|
class LiteralMutation < Base
|
|
11
|
+
# Visits integer literals.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::IntegerNode] node to inspect.
|
|
14
|
+
# @return [void]
|
|
13
15
|
def visit_integer_node(node)
|
|
14
16
|
n = node.value
|
|
15
17
|
emit(node.location, "0") unless n.zero?
|
|
@@ -18,6 +20,10 @@ module Mutineer
|
|
|
18
20
|
super
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# Visits string literals.
|
|
24
|
+
#
|
|
25
|
+
# @param node [Prism::StringNode] node to inspect.
|
|
26
|
+
# @return [void]
|
|
21
27
|
def visit_string_node(node)
|
|
22
28
|
loc = node.location
|
|
23
29
|
token = @source.byteslice(loc.start_offset...loc.end_offset)
|
|
@@ -27,6 +33,11 @@ module Mutineer
|
|
|
27
33
|
|
|
28
34
|
private
|
|
29
35
|
|
|
36
|
+
# Emits a literal mutation.
|
|
37
|
+
#
|
|
38
|
+
# @param loc [Prism::Location] source location.
|
|
39
|
+
# @param replacement [String] replacement literal.
|
|
40
|
+
# @return [void]
|
|
30
41
|
def emit(loc, replacement)
|
|
31
42
|
@mutations << Mutation.new(
|
|
32
43
|
start_offset: loc.start_offset,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Regular-expression literal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Conservative, clearly-safe textual swaps inside a plain regex literal:
|
|
10
|
+
# drop a leading +^+ anchor, drop a trailing +$+ anchor, and swap the +
|
|
11
|
+
# and * quantifiers. Interpolated regexes use a different node type and are
|
|
12
|
+
# never visited. Escaped tokens (preceded by a backslash) are left alone.
|
|
13
|
+
class RegexLiteral < Base
|
|
14
|
+
# Visits regular-expression literals.
|
|
15
|
+
#
|
|
16
|
+
# @param node [Prism::RegularExpressionNode] node to inspect.
|
|
17
|
+
# @return [void]
|
|
18
|
+
def visit_regular_expression_node(node)
|
|
19
|
+
loc = node.content_loc
|
|
20
|
+
content = @source.byteslice(loc.start_offset...loc.end_offset)
|
|
21
|
+
scan(content, loc.start_offset)
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Scans the pattern bytes and emits anchor/quantifier mutations.
|
|
28
|
+
#
|
|
29
|
+
# @param content [String] regex pattern between the slashes.
|
|
30
|
+
# @param base [Integer] byte offset of the pattern start in source.
|
|
31
|
+
# @return [void]
|
|
32
|
+
def scan(content, base)
|
|
33
|
+
emit(base, base + 1, "") if content.start_with?("^")
|
|
34
|
+
emit(base + content.bytesize - 1, base + content.bytesize, "") if trailing_anchor?(content)
|
|
35
|
+
|
|
36
|
+
escaped = false
|
|
37
|
+
offset = base
|
|
38
|
+
content.each_char do |ch|
|
|
39
|
+
unless escaped
|
|
40
|
+
emit(offset, offset + 1, "*") if ch == "+"
|
|
41
|
+
emit(offset, offset + 1, "+") if ch == "*"
|
|
42
|
+
end
|
|
43
|
+
escaped = ch == "\\" && !escaped
|
|
44
|
+
offset += ch.bytesize
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether the pattern ends with an unescaped +$+ anchor.
|
|
49
|
+
#
|
|
50
|
+
# @param content [String] regex pattern.
|
|
51
|
+
# @return [Boolean] true when a trailing anchor is droppable.
|
|
52
|
+
def trailing_anchor?(content)
|
|
53
|
+
content.end_with?("$") && !content.end_with?("\\$")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Emits a regex mutation.
|
|
57
|
+
#
|
|
58
|
+
# @param start_offset [Integer] byte start.
|
|
59
|
+
# @param end_offset [Integer] byte end.
|
|
60
|
+
# @param replacement [String] replacement text.
|
|
61
|
+
# @return [void]
|
|
62
|
+
def emit(start_offset, end_offset, replacement)
|
|
63
|
+
@mutations << Mutation.new(
|
|
64
|
+
start_offset: start_offset,
|
|
65
|
+
end_offset: end_offset,
|
|
66
|
+
replacement: replacement,
|
|
67
|
+
operator: :regex
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -4,15 +4,15 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Return-
|
|
8
|
-
# 1. an explicit `return <expr>` -> `return nil`, unless the value is
|
|
9
|
-
# already nil (no-op guard).
|
|
10
|
-
# 2. a method body whose final expression is neither a ReturnNode nor a
|
|
11
|
-
# NilNode -> that expression becomes `nil`.
|
|
12
|
-
# Nested defs are their own subjects, so we never descend into them (R10).
|
|
7
|
+
# Return-nil mutator.
|
|
13
8
|
#
|
|
14
|
-
#
|
|
9
|
+
# Replaces explicit return values and final expressions with nil.
|
|
15
10
|
class ReturnNil < Base
|
|
11
|
+
# Collects return-nil mutations for a subject.
|
|
12
|
+
#
|
|
13
|
+
# @param subject [Mutineer::Subject] subject to inspect.
|
|
14
|
+
# @param source [String] full source text.
|
|
15
|
+
# @return [Array<Mutineer::Mutation>] collected mutations.
|
|
16
16
|
def mutations_for(subject, source)
|
|
17
17
|
@source = source
|
|
18
18
|
@mutations = []
|
|
@@ -24,6 +24,10 @@ module Mutineer
|
|
|
24
24
|
@mutations
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Visits return nodes.
|
|
28
|
+
#
|
|
29
|
+
# @param node [Prism::ReturnNode] node to inspect.
|
|
30
|
+
# @return [void]
|
|
27
31
|
def visit_return_node(node)
|
|
28
32
|
args = node.arguments
|
|
29
33
|
if args
|
|
@@ -37,10 +41,17 @@ module Mutineer
|
|
|
37
41
|
|
|
38
42
|
# Nested method definitions are discovered as their own subjects; do not
|
|
39
43
|
# recurse into them (prevents double-counting their statements).
|
|
44
|
+
#
|
|
45
|
+
# @param node [Prism::DefNode] nested definition node.
|
|
46
|
+
# @return [void]
|
|
40
47
|
def visit_def_node(node); end
|
|
41
48
|
|
|
42
49
|
private
|
|
43
50
|
|
|
51
|
+
# Mutates a method's final expression to nil when eligible.
|
|
52
|
+
#
|
|
53
|
+
# @param body [Prism::Node] method body node.
|
|
54
|
+
# @return [void]
|
|
44
55
|
def final_expression_nil(body)
|
|
45
56
|
return unless body.is_a?(Prism::StatementsNode)
|
|
46
57
|
|
|
@@ -50,6 +61,10 @@ module Mutineer
|
|
|
50
61
|
emit(last.location)
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# Emits a return-nil mutation.
|
|
65
|
+
#
|
|
66
|
+
# @param loc [Prism::Location] source location.
|
|
67
|
+
# @return [void]
|
|
53
68
|
def emit(loc)
|
|
54
69
|
@mutations << Mutation.new(
|
|
55
70
|
start_offset: loc.start_offset,
|
|
@@ -4,14 +4,14 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Statement-removal
|
|
8
|
-
# "nil". Tests whether the suite detects a missing side effect. The final
|
|
9
|
-
# expression is always skipped — replacing the return value with nil is the
|
|
10
|
-
# M5 return-nil operator's distinct concern (KTD-1). A body with < 2
|
|
11
|
-
# statements has no non-final statement, so it generates nothing.
|
|
7
|
+
# Statement-removal mutator.
|
|
12
8
|
#
|
|
13
|
-
#
|
|
9
|
+
# Replaces each non-final method statement with `nil`.
|
|
14
10
|
class StatementRemoval < Base
|
|
11
|
+
# Visits statement nodes and emits removals.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::StatementsNode] statement list to inspect.
|
|
14
|
+
# @return [void]
|
|
15
15
|
def visit_statements_node(node)
|
|
16
16
|
stmts = node.body
|
|
17
17
|
return if stmts.length < 2
|
|
@@ -25,9 +25,6 @@ module Mutineer
|
|
|
25
25
|
operator: :statement_removal
|
|
26
26
|
)
|
|
27
27
|
end
|
|
28
|
-
# ponytail: no super — recursing into a nested StatementsNode would
|
|
29
|
-
# re-emit removals already covered at the top level and double-count.
|
|
30
|
-
# Each subject's body is visited once.
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# String literal mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Empties a non-empty string literal and fills an empty one. Only plain
|
|
10
|
+
# quoted strings are touched; interpolated strings, heredocs and %-literals
|
|
11
|
+
# are skipped for safety (they re-parse unpredictably).
|
|
12
|
+
class StringLiteral < Base
|
|
13
|
+
# Visits string literals.
|
|
14
|
+
#
|
|
15
|
+
# @param node [Prism::StringNode] node to inspect.
|
|
16
|
+
# @return [void]
|
|
17
|
+
def visit_string_node(node)
|
|
18
|
+
# ponytail: only plain "..." / '...' quotes. opening is nil for
|
|
19
|
+
# interpolation parts and %w[] elements; heredocs/%-literals use a
|
|
20
|
+
# different opening token. Skipping them keeps mutants re-parseable.
|
|
21
|
+
if %w[" '].include?(node.opening)
|
|
22
|
+
loc = node.content_loc
|
|
23
|
+
@mutations << Mutation.new(
|
|
24
|
+
start_offset: loc.start_offset,
|
|
25
|
+
end_offset: loc.end_offset,
|
|
26
|
+
replacement: node.unescaped.empty? ? "mutineer" : "",
|
|
27
|
+
operator: :string_literal
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/mutineer/pairing.rb
CHANGED
|
@@ -4,18 +4,24 @@ module Mutineer
|
|
|
4
4
|
# Source -> test pairing by path convention (#11). Pure stdlib path logic:
|
|
5
5
|
# no Rails, no class loading, no process. Two jobs:
|
|
6
6
|
# * expand_sources — a directory argument becomes its sorted **/*.rb files.
|
|
7
|
-
# * infer_test — a source's test file by convention (app/ and lib/
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# * infer_test — a source's test file by convention (app/ and lib/
|
|
8
|
+
# sources map to test/.../_test.rb or spec/.../_spec.rb),
|
|
9
|
+
# preserving namespaced subdirectories. First EXISTING
|
|
10
|
+
# candidate wins.
|
|
10
11
|
#
|
|
11
|
-
# Independently unit-testable: every method is pure in/out over the
|
|
12
|
-
# so the pairing contract is exercised with plain fixtures, no
|
|
12
|
+
# Independently unit-testable: every method is pure in/out over the
|
|
13
|
+
# filesystem, so the pairing contract is exercised with plain fixtures, no
|
|
14
|
+
# Rails, no fork.
|
|
13
15
|
module Pairing
|
|
14
16
|
module_function
|
|
15
17
|
|
|
16
18
|
# Expand each positional source: a directory -> its sorted **/*.rb files
|
|
17
|
-
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
18
|
-
# itself. Flattened, deduped, order-stable.
|
|
19
|
+
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
20
|
+
# -> itself. Flattened, deduped, order-stable.
|
|
21
|
+
#
|
|
22
|
+
# @param args [Array<String>] source paths or directories.
|
|
23
|
+
# @param project_root [String] repository root for relative expansion.
|
|
24
|
+
# @return [Array<String>] flattened, deduped source file list.
|
|
19
25
|
def expand_sources(args, project_root:)
|
|
20
26
|
root = File.expand_path(project_root)
|
|
21
27
|
Array(args).flat_map do |arg|
|
|
@@ -30,8 +36,13 @@ module Mutineer
|
|
|
30
36
|
|
|
31
37
|
# The first EXISTING candidate test path for a source (relative to
|
|
32
38
|
# project_root), or nil. `prefer` is the resolved framework ("minitest" |
|
|
33
|
-
# "rspec"): its candidates are tried first, the other framework's as
|
|
34
|
-
# so a minitest default still finds a spec and vice-versa.
|
|
39
|
+
# "rspec"): its candidates are tried first, the other framework's as
|
|
40
|
+
# fallback, so a minitest default still finds a spec and vice-versa.
|
|
41
|
+
#
|
|
42
|
+
# @param source_rel [String] relative source path.
|
|
43
|
+
# @param project_root [String] repository root for existence checks.
|
|
44
|
+
# @param prefer [String] preferred framework name.
|
|
45
|
+
# @return [String, nil] existing test path or nil.
|
|
35
46
|
def infer_test(source_rel, project_root:, prefer: "minitest")
|
|
36
47
|
base, lib = logical_path(source_rel)
|
|
37
48
|
candidates(base, lib, prefer).find do |rel|
|
|
@@ -39,10 +50,13 @@ module Mutineer
|
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
43
|
-
# app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
|
|
44
|
-
# path minus ".rb" (still attempted). Namespaced subdirs are
|
|
45
|
-
# verbatim — structural, never constant resolution.
|
|
53
|
+
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
54
|
+
# sources. app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
|
|
55
|
+
# else -> the path minus ".rb" (still attempted). Namespaced subdirs are
|
|
56
|
+
# preserved verbatim — structural, never constant resolution.
|
|
57
|
+
#
|
|
58
|
+
# @param source_rel [String] relative source path.
|
|
59
|
+
# @return [Array(String, Boolean)] logical base path and whether it came from lib/.
|
|
46
60
|
def logical_path(source_rel)
|
|
47
61
|
no_ext = source_rel.sub(/\.rb\z/, "")
|
|
48
62
|
if no_ext.start_with?("app/")
|
|
@@ -56,6 +70,11 @@ module Mutineer
|
|
|
56
70
|
|
|
57
71
|
# Ordered candidate test paths. lib/ sources also get test/lib/... and
|
|
58
72
|
# spec/lib/... (Rails apps put lib tests under either layout).
|
|
73
|
+
#
|
|
74
|
+
# @param base [String] logical source path without extension.
|
|
75
|
+
# @param lib [Boolean] whether the source originated from lib/.
|
|
76
|
+
# @param prefer [String] preferred framework name.
|
|
77
|
+
# @return [Array<String>] candidate test paths in preferred order.
|
|
59
78
|
def candidates(base, lib, prefer)
|
|
60
79
|
minitest = ["test/#{base}_test.rb"]
|
|
61
80
|
minitest << "test/lib/#{base}_test.rb" if lib
|
data/lib/mutineer/parser.rb
CHANGED
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
|
-
# Raised only for I/O failures while reading a source file.
|
|
7
|
-
#
|
|
6
|
+
# Raised only for I/O failures while reading a source file.
|
|
7
|
+
#
|
|
8
|
+
# Prism syntax errors are NOT raised — they are in-band via ParseResult#errors.
|
|
8
9
|
class ParseError < StandardError; end
|
|
9
10
|
|
|
10
|
-
# Thin boundary around Prism.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# Thin boundary around Prism.
|
|
12
|
+
#
|
|
13
|
+
# Both methods return a Prism::ParseResult so all callers use result.value,
|
|
14
|
+
# result.source.source (raw bytes), and result.errors uniformly. No wrapping
|
|
15
|
+
# struct.
|
|
13
16
|
class Parser
|
|
14
|
-
#
|
|
17
|
+
# Parses a file with Prism.
|
|
18
|
+
#
|
|
19
|
+
# @param path [String] source file path.
|
|
20
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
21
|
+
# @raise [Mutineer::ParseError] when file I/O fails.
|
|
15
22
|
def self.parse_file(path)
|
|
16
23
|
Prism.parse_file(path)
|
|
17
24
|
rescue SystemCallError => e
|
|
18
25
|
raise ParseError, e.message
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
#
|
|
28
|
+
# Parses source text with Prism.
|
|
29
|
+
#
|
|
30
|
+
# @param source [String] source text.
|
|
31
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
22
32
|
def self.parse_string(source)
|
|
23
33
|
Prism.parse(source)
|
|
24
34
|
end
|