mutant 0.10.20 → 0.10.25

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