mutant 0.10.23 → 0.10.24
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/lib/mutant.rb +20 -4
- data/lib/mutant/ast/regexp.rb +37 -0
- data/lib/mutant/ast/regexp/transformer.rb +150 -0
- data/lib/mutant/ast/regexp/transformer/direct.rb +121 -0
- data/lib/mutant/ast/regexp/transformer/named_group.rb +50 -0
- data/lib/mutant/ast/regexp/transformer/options_group.rb +68 -0
- data/lib/mutant/ast/regexp/transformer/quantifier.rb +90 -0
- data/lib/mutant/ast/regexp/transformer/recursive.rb +56 -0
- data/lib/mutant/ast/regexp/transformer/root.rb +28 -0
- data/lib/mutant/ast/regexp/transformer/text.rb +58 -0
- data/lib/mutant/ast/types.rb +115 -2
- data/lib/mutant/cli/command/environment.rb +9 -3
- data/lib/mutant/config.rb +8 -54
- data/lib/mutant/config/coverage_criteria.rb +61 -0
- data/lib/mutant/expression.rb +0 -12
- data/lib/mutant/matcher.rb +2 -2
- data/lib/mutant/matcher/config.rb +25 -6
- data/lib/mutant/matcher/method.rb +2 -3
- data/lib/mutant/meta/example/dsl.rb +6 -1
- data/lib/mutant/mutator/node/kwargs.rb +2 -2
- data/lib/mutant/mutator/node/literal/regex.rb +26 -0
- data/lib/mutant/mutator/node/regexp.rb +31 -0
- data/lib/mutant/mutator/node/regexp/alternation_meta.rb +20 -0
- data/lib/mutant/mutator/node/regexp/capture_group.rb +25 -0
- data/lib/mutant/mutator/node/regexp/character_type.rb +31 -0
- data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +20 -0
- data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +20 -0
- data/lib/mutant/mutator/node/regexp/greedy_zero_or_more.rb +24 -0
- data/lib/mutant/subject.rb +1 -3
- data/lib/mutant/subject/method/instance.rb +1 -3
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +1 -2
- metadata +40 -4
- data/lib/mutant/warnings.rb +0 -106
data/lib/mutant/config.rb
CHANGED
@@ -37,54 +37,6 @@ module Mutant
|
|
37
37
|
|
38
38
|
private_constant(*constants(false))
|
39
39
|
|
40
|
-
class CoverageCriteria
|
41
|
-
include Anima.new(:process_abort, :test_result, :timeout)
|
42
|
-
|
43
|
-
EMPTY = new(
|
44
|
-
process_abort: nil,
|
45
|
-
test_result: nil,
|
46
|
-
timeout: nil
|
47
|
-
)
|
48
|
-
|
49
|
-
DEFAULT = new(
|
50
|
-
process_abort: false,
|
51
|
-
test_result: true,
|
52
|
-
timeout: false
|
53
|
-
)
|
54
|
-
|
55
|
-
TRANSFORM =
|
56
|
-
Transform::Sequence.new(
|
57
|
-
[
|
58
|
-
Transform::Hash.new(
|
59
|
-
optional: [
|
60
|
-
Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
|
61
|
-
Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
|
62
|
-
Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
|
63
|
-
],
|
64
|
-
required: []
|
65
|
-
),
|
66
|
-
Transform::Hash::Symbolize.new,
|
67
|
-
->(value) { Either::Right.new(DEFAULT.with(**value)) }
|
68
|
-
]
|
69
|
-
)
|
70
|
-
|
71
|
-
def merge(other)
|
72
|
-
self.class.new(
|
73
|
-
process_abort: overwrite(other, :process_abort),
|
74
|
-
test_result: overwrite(other, :test_result),
|
75
|
-
timeout: overwrite(other, :timeout)
|
76
|
-
)
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
def overwrite(other, attribute_name)
|
82
|
-
other_value = other.public_send(attribute_name)
|
83
|
-
|
84
|
-
other_value.nil? ? public_send(attribute_name) : other_value
|
85
|
-
end
|
86
|
-
end # CoverageCriteria
|
87
|
-
|
88
40
|
# Merge with other config
|
89
41
|
#
|
90
42
|
# @param [Config] other
|
@@ -116,13 +68,14 @@ module Mutant
|
|
116
68
|
#
|
117
69
|
# @return [Either<String,Config>]
|
118
70
|
def self.load_config_file(world)
|
119
|
-
|
120
|
-
|
71
|
+
files = CANDIDATES
|
72
|
+
.map(&world.pathname.public_method(:new))
|
73
|
+
.select(&:readable?)
|
121
74
|
|
122
75
|
if files.one?
|
123
|
-
load_contents(files.first).fmap(&
|
76
|
+
load_contents(files.first).fmap(&DEFAULT.public_method(:with))
|
124
77
|
elsif files.empty?
|
125
|
-
Either::Right.new(
|
78
|
+
Either::Right.new(DEFAULT)
|
126
79
|
else
|
127
80
|
Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
|
128
81
|
end
|
@@ -159,13 +112,14 @@ module Mutant
|
|
159
112
|
Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
|
160
113
|
Transform::Hash.new(
|
161
114
|
optional: [
|
162
|
-
Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
|
115
|
+
Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
|
163
116
|
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
|
164
117
|
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
|
165
118
|
Transform::Hash::Key.new('integration', Transform::STRING),
|
166
119
|
Transform::Hash::Key.new('jobs', Transform::INTEGER),
|
167
120
|
Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
|
168
|
-
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
|
121
|
+
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY),
|
122
|
+
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER)
|
169
123
|
],
|
170
124
|
required: []
|
171
125
|
),
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Config
|
5
|
+
# Configuration of coverge conditions
|
6
|
+
class CoverageCriteria
|
7
|
+
include Anima.new(:process_abort, :test_result, :timeout)
|
8
|
+
|
9
|
+
EMPTY = new(
|
10
|
+
process_abort: nil,
|
11
|
+
test_result: nil,
|
12
|
+
timeout: nil
|
13
|
+
)
|
14
|
+
|
15
|
+
DEFAULT = new(
|
16
|
+
process_abort: false,
|
17
|
+
test_result: true,
|
18
|
+
timeout: false
|
19
|
+
)
|
20
|
+
|
21
|
+
TRANSFORM =
|
22
|
+
Transform::Sequence.new(
|
23
|
+
[
|
24
|
+
Transform::Hash.new(
|
25
|
+
optional: [
|
26
|
+
Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
|
27
|
+
Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
|
28
|
+
Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
|
29
|
+
],
|
30
|
+
required: []
|
31
|
+
),
|
32
|
+
Transform::Hash::Symbolize.new,
|
33
|
+
->(value) { Either::Right.new(DEFAULT.with(**value)) }
|
34
|
+
]
|
35
|
+
)
|
36
|
+
|
37
|
+
# Merge coverage criteria with other instance
|
38
|
+
#
|
39
|
+
# Values from the other instance have precedence.
|
40
|
+
#
|
41
|
+
# @param [CoverageCriteria] other
|
42
|
+
#
|
43
|
+
# @return [CoverageCriteria]
|
44
|
+
def merge(other)
|
45
|
+
self.class.new(
|
46
|
+
process_abort: overwrite(other, :process_abort),
|
47
|
+
test_result: overwrite(other, :test_result),
|
48
|
+
timeout: overwrite(other, :timeout)
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def overwrite(other, attribute_name)
|
55
|
+
other_value = other.public_send(attribute_name)
|
56
|
+
|
57
|
+
other_value.nil? ? public_send(attribute_name) : other_value
|
58
|
+
end
|
59
|
+
end # CoverageCriteria
|
60
|
+
end # Config
|
61
|
+
end # Mutant
|
data/lib/mutant/expression.rb
CHANGED
@@ -4,8 +4,6 @@ module Mutant
|
|
4
4
|
|
5
5
|
# Abstract base class for match expression
|
6
6
|
class Expression
|
7
|
-
include AbstractType
|
8
|
-
|
9
7
|
fragment = /[A-Za-z][A-Za-z\d_]*/.freeze
|
10
8
|
SCOPE_NAME_PATTERN = /(?<scope_name>#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze
|
11
9
|
SCOPE_SYMBOL_PATTERN = '(?<scope_symbol>[.#])'
|
@@ -16,16 +14,6 @@ module Mutant
|
|
16
14
|
super.freeze
|
17
15
|
end
|
18
16
|
|
19
|
-
# Syntax of expression
|
20
|
-
#
|
21
|
-
# @return [Matcher]
|
22
|
-
abstract_method :matcher
|
23
|
-
|
24
|
-
# Syntax of expression
|
25
|
-
#
|
26
|
-
# @return [String]
|
27
|
-
abstract_method :syntax
|
28
|
-
|
29
17
|
# Match length with other expression
|
30
18
|
#
|
31
19
|
# @param [Expression] other
|
data/lib/mutant/matcher.rb
CHANGED
@@ -20,7 +20,7 @@ module Mutant
|
|
20
20
|
# @return [Matcher]
|
21
21
|
def self.from_config(config)
|
22
22
|
Filter.new(
|
23
|
-
Chain.new(config.
|
23
|
+
Chain.new(config.subjects.map(&:matcher)),
|
24
24
|
method(:allowed_subject?).curry.call(config)
|
25
25
|
)
|
26
26
|
end
|
@@ -42,7 +42,7 @@ module Mutant
|
|
42
42
|
#
|
43
43
|
# @return [Boolean]
|
44
44
|
def self.ignore_subject?(config, subject)
|
45
|
-
config.
|
45
|
+
config.ignore.any? do |expression|
|
46
46
|
expression.prefix?(subject.expression)
|
47
47
|
end
|
48
48
|
end
|
@@ -5,8 +5,8 @@ module Mutant
|
|
5
5
|
# Subject matcher configuration
|
6
6
|
class Config
|
7
7
|
include Adamantium, Anima.new(
|
8
|
-
:
|
9
|
-
:
|
8
|
+
:ignore,
|
9
|
+
:subjects,
|
10
10
|
:start_expressions,
|
11
11
|
:subject_filters
|
12
12
|
)
|
@@ -17,15 +17,34 @@ module Mutant
|
|
17
17
|
ENUM_DELIMITER = ','
|
18
18
|
EMPTY_ATTRIBUTES = 'empty'
|
19
19
|
PRESENTATIONS = IceNine.deep_freeze(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
ignore: :syntax,
|
21
|
+
start_expressions: :syntax,
|
22
|
+
subject_filters: :inspect,
|
23
|
+
subjects: :syntax
|
24
24
|
)
|
25
25
|
private_constant(*constants(false))
|
26
26
|
|
27
27
|
DEFAULT = new(Hash[anima.attribute_names.map { |name| [name, []] }])
|
28
28
|
|
29
|
+
expression = ->(input) { Mutant::Config::DEFAULT.expression_parser.call(input) }
|
30
|
+
|
31
|
+
expression_array = Transform::Array.new(expression)
|
32
|
+
|
33
|
+
LOADER =
|
34
|
+
Transform::Sequence.new(
|
35
|
+
[
|
36
|
+
Transform::Hash.new(
|
37
|
+
optional: [
|
38
|
+
Transform::Hash::Key.new('subjects', expression_array),
|
39
|
+
Transform::Hash::Key.new('ignore', expression_array)
|
40
|
+
],
|
41
|
+
required: []
|
42
|
+
),
|
43
|
+
Transform::Hash::Symbolize.new,
|
44
|
+
->(attributes) { Either::Right.new(DEFAULT.with(attributes)) }
|
45
|
+
]
|
46
|
+
)
|
47
|
+
|
29
48
|
# Inspection string
|
30
49
|
#
|
31
50
|
# @return [String]
|
@@ -37,7 +37,7 @@ module Mutant
|
|
37
37
|
# @return [Example]
|
38
38
|
#
|
39
39
|
# @raise [RuntimeError]
|
40
|
-
# in case example cannot be
|
40
|
+
# in case the example cannot be built
|
41
41
|
def example
|
42
42
|
fail 'source not defined' unless @source
|
43
43
|
|
@@ -82,6 +82,11 @@ module Mutant
|
|
82
82
|
mutation('self')
|
83
83
|
end
|
84
84
|
|
85
|
+
def regexp_mutations
|
86
|
+
mutation('//')
|
87
|
+
mutation('/nomatch\A/')
|
88
|
+
end
|
89
|
+
|
85
90
|
def node(input)
|
86
91
|
case input
|
87
92
|
when String
|
@@ -28,7 +28,7 @@ module Mutant
|
|
28
28
|
def emit_argument_mutations
|
29
29
|
children.each_with_index do |child, index|
|
30
30
|
Mutator.mutate(child).each do |mutant|
|
31
|
-
|
31
|
+
unless forbid_argument?(mutant)
|
32
32
|
emit_child_update(index, mutant)
|
33
33
|
end
|
34
34
|
end
|
@@ -36,7 +36,7 @@ module Mutant
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def forbid_argument?(node)
|
39
|
-
|
39
|
+
n_pair?(node) && DISALLOW.include?(node.children.first.type)
|
40
40
|
end
|
41
41
|
end # Kwargs
|
42
42
|
end # Node
|
@@ -19,6 +19,7 @@ module Mutant
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def dispatch
|
22
|
+
mutate_body
|
22
23
|
emit_singletons unless parent_node
|
23
24
|
children.each_with_index do |child, index|
|
24
25
|
mutate_child(index) unless n_str?(child)
|
@@ -27,6 +28,31 @@ module Mutant
|
|
27
28
|
emit_type(s(:str, NULL_REGEXP_SOURCE), options)
|
28
29
|
end
|
29
30
|
|
31
|
+
# NOTE: will only mutate parts of regexp body if the
|
32
|
+
# body is composed of only strings. Regular expressions
|
33
|
+
# with interpolation are skipped
|
34
|
+
def mutate_body
|
35
|
+
return unless body.all?(&method(:n_str?))
|
36
|
+
|
37
|
+
Mutator.mutate(body_ast).each do |mutation|
|
38
|
+
source = AST::Regexp.to_expression(mutation).to_s
|
39
|
+
emit_type(s(:str, source), options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def body_ast
|
44
|
+
AST::Regexp.to_ast(body_expression)
|
45
|
+
end
|
46
|
+
|
47
|
+
def body_expression
|
48
|
+
AST::Regexp.parse(body.map(&:children).join)
|
49
|
+
end
|
50
|
+
memoize :body_expression
|
51
|
+
|
52
|
+
def body
|
53
|
+
children.slice(0...-1)
|
54
|
+
end
|
55
|
+
|
30
56
|
end # Regex
|
31
57
|
end # Literal
|
32
58
|
end # Node
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutator
|
5
|
+
class Node
|
6
|
+
module Regexp
|
7
|
+
# Mutator for root expression regexp wrapper
|
8
|
+
class RootExpression < Node
|
9
|
+
handle(:regexp_root_expression)
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def dispatch
|
14
|
+
children.each_index(&method(:mutate_child))
|
15
|
+
end
|
16
|
+
end # RootExpression
|
17
|
+
|
18
|
+
# Mutator for beginning of line anchor `^`
|
19
|
+
class BeginningOfLineAnchor < Node
|
20
|
+
handle(:regexp_bol_anchor)
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def dispatch
|
25
|
+
emit(s(:regexp_bos_anchor))
|
26
|
+
end
|
27
|
+
end # BeginningOfLineAnchor
|
28
|
+
end # Regexp
|
29
|
+
end # Node
|
30
|
+
end # Mutator
|
31
|
+
end # Mutant
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutator
|
5
|
+
class Node
|
6
|
+
module Regexp
|
7
|
+
# Mutator for pipe in `/foo|bar/` regexp
|
8
|
+
class AlternationMeta < Node
|
9
|
+
handle(:regexp_alternation_meta)
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def dispatch
|
14
|
+
children.each_index(&method(:delete_child))
|
15
|
+
end
|
16
|
+
end # AlternationMeta
|
17
|
+
end # Regexp
|
18
|
+
end # Node
|
19
|
+
end # Mutator
|
20
|
+
end # Mutant
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutator
|
5
|
+
class Node
|
6
|
+
module Regexp
|
7
|
+
# Mutator for regexp capture groups, such as `/(foo)/`
|
8
|
+
class CaptureGroup < Node
|
9
|
+
handle(:regexp_capture_group)
|
10
|
+
|
11
|
+
children :group
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def dispatch
|
16
|
+
return unless group
|
17
|
+
|
18
|
+
emit(s(:regexp_passive_group, group))
|
19
|
+
emit_group_mutations
|
20
|
+
end
|
21
|
+
end # EndOfLineAnchor
|
22
|
+
end # Regexp
|
23
|
+
end # Node
|
24
|
+
end # Mutator
|
25
|
+
end # Mutant
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutator
|
5
|
+
class Node
|
6
|
+
module Regexp
|
7
|
+
# Character type mutator
|
8
|
+
class CharacterType < Node
|
9
|
+
map = {
|
10
|
+
regexp_digit_type: :regexp_nondigit_type,
|
11
|
+
regexp_hex_type: :regexp_nonhex_type,
|
12
|
+
regexp_space_type: :regexp_nonspace_type,
|
13
|
+
regexp_word_boundary_anchor: :regexp_nonword_boundary_anchor,
|
14
|
+
regexp_word_type: :regexp_nonword_type,
|
15
|
+
regexp_xgrapheme_type: :regexp_linebreak_type
|
16
|
+
}
|
17
|
+
|
18
|
+
MAP = IceNine.deep_freeze(map.merge(map.invert))
|
19
|
+
|
20
|
+
handle(*MAP.keys)
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def dispatch
|
25
|
+
emit(s(MAP.fetch(node.type)))
|
26
|
+
end
|
27
|
+
end # CharacterType
|
28
|
+
end # Regexp
|
29
|
+
end # Node
|
30
|
+
end # Mutator
|
31
|
+
end # Mutant
|