mutant 0.10.23 → 0.10.24
Sign up to get free protection for your applications and to get access to all the features.
- 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
|