mutant 0.10.20 → 0.10.25

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant.rb +23 -4
  3. data/lib/mutant/ast/meta/send.rb +0 -1
  4. data/lib/mutant/ast/regexp.rb +37 -0
  5. data/lib/mutant/ast/regexp/transformer.rb +150 -0
  6. data/lib/mutant/ast/regexp/transformer/direct.rb +121 -0
  7. data/lib/mutant/ast/regexp/transformer/named_group.rb +50 -0
  8. data/lib/mutant/ast/regexp/transformer/options_group.rb +68 -0
  9. data/lib/mutant/ast/regexp/transformer/quantifier.rb +90 -0
  10. data/lib/mutant/ast/regexp/transformer/recursive.rb +56 -0
  11. data/lib/mutant/ast/regexp/transformer/root.rb +28 -0
  12. data/lib/mutant/ast/regexp/transformer/text.rb +58 -0
  13. data/lib/mutant/ast/types.rb +115 -11
  14. data/lib/mutant/cli/command/environment.rb +9 -3
  15. data/lib/mutant/config.rb +8 -54
  16. data/lib/mutant/config/coverage_criteria.rb +61 -0
  17. data/lib/mutant/env.rb +8 -6
  18. data/lib/mutant/expression.rb +0 -12
  19. data/lib/mutant/expression/namespace.rb +1 -1
  20. data/lib/mutant/isolation/fork.rb +1 -1
  21. data/lib/mutant/loader.rb +1 -1
  22. data/lib/mutant/matcher.rb +2 -2
  23. data/lib/mutant/matcher/config.rb +27 -6
  24. data/lib/mutant/matcher/method.rb +2 -3
  25. data/lib/mutant/matcher/method/metaclass.rb +1 -1
  26. data/lib/mutant/meta/example/dsl.rb +6 -1
  27. data/lib/mutant/mutator/node/arguments.rb +0 -2
  28. data/lib/mutant/mutator/node/block.rb +5 -1
  29. data/lib/mutant/mutator/node/dynamic_literal.rb +1 -1
  30. data/lib/mutant/mutator/node/kwargs.rb +44 -0
  31. data/lib/mutant/mutator/node/literal/regex.rb +26 -0
  32. data/lib/mutant/mutator/node/literal/symbol.rb +0 -2
  33. data/lib/mutant/mutator/node/regexp.rb +20 -0
  34. data/lib/mutant/mutator/node/regexp/alternation_meta.rb +20 -0
  35. data/lib/mutant/mutator/node/regexp/beginning_of_line_anchor.rb +20 -0
  36. data/lib/mutant/mutator/node/regexp/capture_group.rb +25 -0
  37. data/lib/mutant/mutator/node/regexp/character_type.rb +31 -0
  38. data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +20 -0
  39. data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +20 -0
  40. data/lib/mutant/mutator/node/regexp/zero_or_more.rb +34 -0
  41. data/lib/mutant/mutator/node/sclass.rb +1 -1
  42. data/lib/mutant/mutator/node/send.rb +36 -19
  43. data/lib/mutant/parallel.rb +43 -28
  44. data/lib/mutant/parallel/driver.rb +9 -3
  45. data/lib/mutant/parallel/worker.rb +60 -2
  46. data/lib/mutant/pipe.rb +94 -0
  47. data/lib/mutant/reporter/cli/printer/env.rb +1 -1
  48. data/lib/mutant/reporter/cli/printer/isolation_result.rb +1 -6
  49. data/lib/mutant/result.rb +8 -0
  50. data/lib/mutant/runner.rb +7 -10
  51. data/lib/mutant/runner/sink.rb +12 -2
  52. data/lib/mutant/subject.rb +1 -3
  53. data/lib/mutant/subject/method/instance.rb +2 -4
  54. data/lib/mutant/timer.rb +2 -4
  55. data/lib/mutant/transform.rb +25 -2
  56. data/lib/mutant/version.rb +1 -1
  57. data/lib/mutant/world.rb +1 -2
  58. metadata +48 -9
  59. data/lib/mutant/warnings.rb +0 -106
@@ -29,13 +29,19 @@ module Mutant
29
29
  end
30
30
 
31
31
  def expand(file_config)
32
+ if @config.matcher.subjects.any?
33
+ file_config = file_config.with(
34
+ matcher: file_config.matcher.with(subjects: [])
35
+ )
36
+ end
37
+
32
38
  @config = Config.env.merge(file_config).merge(@config).expand_defaults
33
39
  end
34
40
 
35
41
  def parse_remaining_arguments(arguments)
36
42
  Mutant.traverse(@config.expression_parser, arguments)
37
- .fmap do |match_expressions|
38
- matcher(match_expressions: match_expressions)
43
+ .fmap do |expressions|
44
+ matcher(subjects: expressions)
39
45
  self
40
46
  end
41
47
  end
@@ -82,7 +88,7 @@ module Mutant
82
88
  parser.separator('Matcher:')
83
89
 
84
90
  parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
85
- add_matcher(:ignore_expressions, @config.expression_parser.call(pattern).from_right)
91
+ add_matcher(:ignore, @config.expression_parser.call(pattern).from_right)
86
92
  end
87
93
  parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
88
94
  add_matcher(:start_expressions, @config.expression_parser.call(pattern).from_right)
@@ -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
- config = DEFAULT
120
- files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
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(&config.public_method(:with))
76
+ load_contents(files.first).fmap(&DEFAULT.public_method(:with))
124
77
  elsif files.empty?
125
- Either::Right.new(config)
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
@@ -43,19 +43,21 @@ module Mutant
43
43
  end
44
44
  # rubocop:enable Metrics/MethodLength
45
45
 
46
- # Kill mutation
46
+ # Cover mutation with specific index
47
47
  #
48
- # @param [Mutation] mutation
48
+ # @param [Integer] mutation_index
49
49
  #
50
- # @return [Result::Mutation]
51
- def kill(mutation)
50
+ # @return [Result::MutationIndex]
51
+ def cover_index(mutation_index)
52
+ mutation = mutations.fetch(mutation_index)
53
+
52
54
  start = timer.now
53
55
 
54
56
  tests = selections.fetch(mutation.subject)
55
57
 
56
- Result::Mutation.new(
58
+ Result::MutationIndex.new(
57
59
  isolation_result: run_mutation_tests(mutation, tests),
58
- mutation: mutation,
60
+ mutation_index: mutation_index,
59
61
  runtime: timer.now - start
60
62
  )
61
63
  end
@@ -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
@@ -17,7 +17,7 @@ module Mutant
17
17
  def initialize(*)
18
18
  super
19
19
 
20
- @syntax = "#{scope_name}*"
20
+ @syntax = "#{scope_name}*".freeze # rubocop:disable Style/RedundantFreeze
21
21
 
22
22
  @recursion_pattern = Regexp.union(
23
23
  /\A#{scope_name}\z/,
@@ -230,7 +230,7 @@ module Mutant
230
230
 
231
231
  end # Child
232
232
 
233
- private_constant(*(constants(false) - %i[ChildError ForkError]))
233
+ private_constant(*constants(false))
234
234
 
235
235
  # Call block in isolation
236
236
  #
@@ -18,7 +18,7 @@ module Mutant
18
18
 
19
19
  # Vale returned on MRI detecting void value expressions
20
20
  class VoidValue < self
21
- end # voidValue
21
+ end # VoidValue
22
22
  end # Result
23
23
 
24
24
  # Call loader
@@ -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.match_expressions.map(&:matcher)),
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.ignore_expressions.any? do |expression|
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
- :ignore_expressions,
9
- :match_expressions,
8
+ :ignore,
9
+ :subjects,
10
10
  :start_expressions,
11
11
  :subject_filters
12
12
  )
@@ -17,15 +17,36 @@ module Mutant
17
17
  ENUM_DELIMITER = ','
18
18
  EMPTY_ATTRIBUTES = 'empty'
19
19
  PRESENTATIONS = IceNine.deep_freeze(
20
- ignore_expressions: :syntax,
21
- match_expressions: :syntax,
22
- start_expressions: :syntax,
23
- subject_filters: :inspect
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 = Transform::Block.capture(:expression) do |input|
30
+ Mutant::Config::DEFAULT.expression_parser.call(input)
31
+ end
32
+
33
+ expression_array = Transform::Array.new(expression)
34
+
35
+ LOADER =
36
+ Transform::Sequence.new(
37
+ [
38
+ Transform::Hash.new(
39
+ optional: [
40
+ Transform::Hash::Key.new('subjects', expression_array),
41
+ Transform::Hash::Key.new('ignore', expression_array)
42
+ ],
43
+ required: []
44
+ ),
45
+ Transform::Hash::Symbolize.new,
46
+ ->(attributes) { Either::Right.new(DEFAULT.with(attributes)) }
47
+ ]
48
+ )
49
+
29
50
  # Inspection string
30
51
  #
31
52
  # @return [String]
@@ -87,9 +87,8 @@ module Mutant
87
87
  node = matched_node_path.last || return
88
88
 
89
89
  self.class::SUBJECT_CLASS.new(
90
- context: context,
91
- node: node,
92
- warnings: env.world.warnings
90
+ context: context,
91
+ node: node
93
92
  )
94
93
  end
95
94
  memoize :subject
@@ -80,7 +80,7 @@ module Mutant
80
80
  end # Evaluator
81
81
 
82
82
  private_constant(*constants(false))
83
- end # Singleton
83
+ end # Metaclass
84
84
  end # Method
85
85
  end # Matcher
86
86
  end # Mutant
@@ -37,7 +37,7 @@ module Mutant
37
37
  # @return [Example]
38
38
  #
39
39
  # @raise [RuntimeError]
40
- # in case example cannot be build
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
@@ -8,8 +8,6 @@ module Mutant
8
8
 
9
9
  handle(:args)
10
10
 
11
- PROCARG = %i[restarg mlhs].freeze
12
-
13
11
  private
14
12
 
15
13
  def dispatch
@@ -21,7 +21,7 @@ module Mutant
21
21
  end
22
22
 
23
23
  def mutate_body
24
- emit_body(nil)
24
+ emit_body(nil) unless unconditional_loop?
25
25
  emit_body(N_RAISE)
26
26
 
27
27
  return unless body
@@ -31,6 +31,10 @@ module Mutant
31
31
  mutate_body_receiver
32
32
  end
33
33
 
34
+ def unconditional_loop?
35
+ send.eql?(s(:send, nil, :loop))
36
+ end
37
+
34
38
  def body_has_control?
35
39
  AST.find_last_path(body) do |node|
36
40
  n_break?(node) || n_next?(node)
@@ -18,7 +18,7 @@ module Mutant
18
18
  end
19
19
  end
20
20
 
21
- end # Dstr
21
+ end # DynamicLiteral
22
22
  end # Node
23
23
  end # Mutator
24
24
  end # Mutant
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ # Mutator for kwargs node
7
+ class Kwargs < self
8
+
9
+ DISALLOW = %i[nil self].freeze
10
+
11
+ private_constant(*constants(false))
12
+
13
+ handle(:kwargs)
14
+
15
+ private
16
+
17
+ def dispatch
18
+ emit_argument_presence
19
+ emit_argument_mutations
20
+ end
21
+
22
+ def emit_argument_presence
23
+ Util::Array::Presence.call(children).each do |children|
24
+ emit_type(*children) unless children.empty?
25
+ end
26
+ end
27
+
28
+ def emit_argument_mutations
29
+ children.each_with_index do |child, index|
30
+ Mutator.mutate(child).each do |mutant|
31
+ unless forbid_argument?(mutant)
32
+ emit_child_update(index, mutant)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def forbid_argument?(node)
39
+ n_pair?(node) && DISALLOW.include?(node.children.first.type)
40
+ end
41
+ end # Kwargs
42
+ end # Node
43
+ end # Mutator
44
+ end # Mutant