mutant 0.5.23 → 0.5.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +10 -0
  3. data/config/flay.yml +1 -1
  4. data/config/reek.yml +19 -19
  5. data/lib/mutant.rb +12 -39
  6. data/lib/mutant/ast.rb +5 -0
  7. data/lib/mutant/ast/meta.rb +131 -0
  8. data/lib/mutant/ast/named_children.rb +98 -0
  9. data/lib/mutant/ast/node_predicates.rb +19 -0
  10. data/lib/mutant/ast/nodes.rb +21 -0
  11. data/lib/mutant/ast/sexp.rb +34 -0
  12. data/lib/mutant/ast/types.rb +48 -0
  13. data/lib/mutant/cache.rb +3 -2
  14. data/lib/mutant/cli.rb +11 -151
  15. data/lib/mutant/config.rb +22 -2
  16. data/lib/mutant/context/scope.rb +11 -21
  17. data/lib/mutant/delegator.rb +2 -0
  18. data/lib/mutant/diff.rb +7 -3
  19. data/lib/mutant/env.rb +49 -0
  20. data/lib/mutant/expression.rb +36 -8
  21. data/lib/mutant/expression/methods.rb +62 -0
  22. data/lib/mutant/expression/namespace.rb +41 -28
  23. data/lib/mutant/{strategy.rb → integration.rb} +12 -31
  24. data/lib/mutant/isolation.rb +1 -1
  25. data/lib/mutant/matcher.rb +1 -21
  26. data/lib/mutant/matcher/builder.rb +142 -0
  27. data/lib/mutant/matcher/method.rb +3 -7
  28. data/lib/mutant/matcher/method/instance.rb +6 -5
  29. data/lib/mutant/matcher/method/singleton.rb +2 -7
  30. data/lib/mutant/matcher/methods.rb +11 -14
  31. data/lib/mutant/matcher/namespace.rb +31 -39
  32. data/lib/mutant/matcher/scope.rb +13 -2
  33. data/lib/mutant/meta.rb +0 -1
  34. data/lib/mutant/meta/example/dsl.rb +5 -1
  35. data/lib/mutant/mutator/node.rb +16 -44
  36. data/lib/mutant/mutator/node/or_asgn.rb +1 -1
  37. data/lib/mutant/mutator/node/send.rb +5 -60
  38. data/lib/mutant/mutator/node/super.rb +2 -5
  39. data/lib/mutant/mutator/registry.rb +1 -1
  40. data/lib/mutant/reporter.rb +10 -0
  41. data/lib/mutant/reporter/cli.rb +13 -0
  42. data/lib/mutant/reporter/cli/printer.rb +2 -0
  43. data/lib/mutant/reporter/cli/progress/config.rb +1 -1
  44. data/lib/mutant/reporter/cli/progress/noop.rb +2 -0
  45. data/lib/mutant/reporter/cli/registry.rb +2 -0
  46. data/lib/mutant/reporter/null.rb +12 -0
  47. data/lib/mutant/reporter/trace.rb +4 -0
  48. data/lib/mutant/require_highjack.rb +2 -2
  49. data/lib/mutant/rspec.rb +0 -0
  50. data/lib/mutant/runner.rb +2 -0
  51. data/lib/mutant/runner/config.rb +8 -8
  52. data/lib/mutant/runner/killer.rb +5 -0
  53. data/lib/mutant/runner/subject.rb +1 -1
  54. data/lib/mutant/subject.rb +8 -8
  55. data/lib/mutant/subject/method.rb +3 -2
  56. data/lib/mutant/subject/method/instance.rb +1 -1
  57. data/lib/mutant/test.rb +7 -65
  58. data/lib/mutant/test/report.rb +59 -0
  59. data/lib/mutant/version.rb +1 -1
  60. data/lib/mutant/warning_filter.rb +2 -0
  61. data/lib/mutant/zombifier.rb +3 -0
  62. data/lib/mutant/zombifier/file.rb +1 -1
  63. data/meta/or_asgn.rb +11 -0
  64. data/meta/send.rb +1 -1
  65. data/mutant-rspec.gemspec +1 -1
  66. data/mutant.gemspec +1 -1
  67. data/spec/integration/mutant/corpus_spec.rb +2 -0
  68. data/spec/integration/mutant/test_mutator_handles_types_spec.rb +2 -2
  69. data/spec/spec_helper.rb +4 -3
  70. data/spec/unit/mutant/cli_new_spec.rb +11 -11
  71. data/spec/unit/mutant/expression/methods_spec.rb +43 -0
  72. data/spec/unit/mutant/expression/namespace/flat_spec.rb +1 -1
  73. data/spec/unit/mutant/expression/namespace/recursive_spec.rb +19 -5
  74. data/spec/unit/mutant/{strategy_spec.rb → integration_spec.rb} +1 -1
  75. data/spec/unit/mutant/isolation_spec.rb +3 -1
  76. data/spec/unit/mutant/matcher/method/instance_spec.rb +5 -5
  77. data/spec/unit/mutant/matcher/method/singleton_spec.rb +8 -8
  78. data/spec/unit/mutant/matcher/methods/instance_spec.rb +5 -8
  79. data/spec/unit/mutant/matcher/methods/singleton_spec.rb +5 -5
  80. data/spec/unit/mutant/matcher/namespace_spec.rb +18 -23
  81. data/spec/unit/mutant/mutation_spec.rb +1 -1
  82. data/spec/unit/mutant/runner/config_spec.rb +4 -5
  83. data/spec/unit/mutant/runner/mutation_spec.rb +21 -21
  84. data/spec/unit/mutant/runner/subject_spec.rb +6 -6
  85. data/spec/unit/mutant/subject/method/instance_spec.rb +0 -4
  86. data/spec/unit/mutant/subject/method/singleton_spec.rb +0 -1
  87. data/spec/unit/mutant/subject_spec.rb +3 -3
  88. metadata +20 -6
  89. data/lib/mutant/node_helpers.rb +0 -52
@@ -0,0 +1,19 @@
1
+ module Mutant
2
+ module AST
3
+ # Module for node predicates
4
+ module NodePredicates
5
+
6
+ Types::ALL.each do |type|
7
+ fail "method: #{type} is already defined" if instance_methods(true).include?(type)
8
+
9
+ name = "n_#{type.to_s.chomp('?')}?"
10
+
11
+ define_method(name) do |node|
12
+ node.type.equal?(type)
13
+ end
14
+ private name
15
+ end
16
+
17
+ end # NodePredicates
18
+ end # AST
19
+ end # Mutant
@@ -0,0 +1,21 @@
1
+ module Mutant
2
+ module AST
3
+ # Singleton nodes
4
+ module Nodes
5
+ extend Sexp
6
+
7
+ N_NAN = s(:send, s(:float, 0.0), :/, s(:float, 0.0))
8
+ N_INFINITY = s(:send, s(:float, 1.0), :/, s(:float, 0.0))
9
+ N_NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:float, 0.0))
10
+ N_RAISE = s(:send, nil, :raise)
11
+ N_TRUE = s(:true)
12
+ N_FALSE = s(:false)
13
+ N_NIL = s(:nil)
14
+ N_EMPTY = s(:empty)
15
+ N_SELF = s(:self)
16
+ N_ZSUPER = s(:zsuper)
17
+ N_EMPTY_SUPER = s(:super)
18
+
19
+ end # Node
20
+ end # AST
21
+ end # Mutant
@@ -0,0 +1,34 @@
1
+ module Mutant
2
+ module AST
3
+ # Mixin for node sexp syntax
4
+ module Sexp
5
+
6
+ private
7
+
8
+ # Build node
9
+ #
10
+ # @param [Symbol] type
11
+ #
12
+ # @return [Parser::AST::Node]
13
+ #
14
+ # @api private
15
+ #
16
+ def s(type, *children)
17
+ Parser::AST::Node.new(type, children)
18
+ end
19
+
20
+ # Build a negated boolean node
21
+ #
22
+ # @param [Parser::AST::Node] node
23
+ #
24
+ # @return [Parser::AST::Node]
25
+ #
26
+ # @api private
27
+ #
28
+ def n_not(node)
29
+ s(:send, node, :!)
30
+ end
31
+
32
+ end # Sexp
33
+ end # AST
34
+ end # Mutant
@@ -0,0 +1,48 @@
1
+ module Mutant
2
+ module AST
3
+ # Groups of node types
4
+ module Types
5
+ symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze }
6
+
7
+ ASSIGNABLE_VARIABLES = symbolset.(%w[ivasgn lvasgn cvasgn gvasgn])
8
+
9
+ INDEX_ASSIGN_OPERATOR = :[]=
10
+
11
+ # Set of nodes that cannot be on the LHS of an assignment
12
+ NOT_ASSIGNABLE = symbolset.(%w[int float str dstr class module self nil])
13
+
14
+ # Set of op-assign types
15
+ OP_ASSIGN = symbolset.(%w[or_asgn and_asgn op_asgn])
16
+ # Set of node types that are not valid when emitted standalone
17
+ NOT_STANDALONE = symbolset.(%w[splat restarg block_pass])
18
+ INDEX_OPERATORS = symbolset.(%w[[] []=])
19
+ UNARY_METHOD_OPERATORS = symbolset.(%w[~@ +@ -@ !])
20
+
21
+ # Operators ruby implementeds as methods
22
+ METHOD_OPERATORS = symbolset.(%w[
23
+ <=> === []= [] <= >= == !~ != =~ <<
24
+ >> ** * % / | ^ & < > + - ~@ +@ -@ !
25
+ ])
26
+
27
+ BINARY_METHOD_OPERATORS = (
28
+ METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS)
29
+ ).to_set.freeze
30
+
31
+ OPERATOR_METHODS = (
32
+ METHOD_OPERATORS + INDEX_OPERATORS + UNARY_METHOD_OPERATORS
33
+ ).to_set.freeze
34
+
35
+ # Nodes that are NOT handled by mutant.
36
+ #
37
+ # not - 1.8 only, mutant does not support 1.8
38
+ #
39
+ BLACKLIST = symbolset.(%w[not])
40
+
41
+ # Nodes that are NOT generated by parser but used by mutant / unparser.
42
+ EXTRA = symbolset.(%w[empty])
43
+
44
+ # All node types mutant handles
45
+ ALL = ((Parser::Meta::NODE_TYPES + EXTRA) - BLACKLIST).to_set.freeze
46
+ end # Types
47
+ end # AST
48
+ end # Mutant
@@ -1,8 +1,7 @@
1
1
  module Mutant
2
2
  # An AST cache
3
3
  class Cache
4
- # This is explicitly empty! Ask me if you are interested in reasons :D
5
- include Equalizer.new
4
+ include Equalizer.new, Adamantium::Mutable
6
5
 
7
6
  # Initialize object
8
7
  #
@@ -16,6 +15,8 @@ module Mutant
16
15
 
17
16
  # Return node for file
18
17
  #
18
+ # @param [#to_s] path
19
+ #
19
20
  # @return [AST::Node]
20
21
  #
21
22
  # @api private
@@ -4,7 +4,7 @@ module Mutant
4
4
 
5
5
  # Comandline parser
6
6
  class CLI
7
- include Adamantium::Flat, Equalizer.new(:config), NodeHelpers
7
+ include Adamantium::Flat, Equalizer.new(:config)
8
8
 
9
9
  # Error raised when CLI argv is invalid
10
10
  Error = Class.new(RuntimeError)
@@ -30,141 +30,6 @@ module Mutant
30
30
  EXIT_FAILURE
31
31
  end
32
32
 
33
- # Builder for configuration components
34
- class Builder
35
- include NodeHelpers
36
-
37
- # Initalize object
38
- #
39
- # @return [undefined]
40
- #
41
- # @api private
42
- #
43
- def initialize
44
- @matchers = []
45
- @subject_ignores = []
46
- @subject_selectors = []
47
- end
48
-
49
- # Add a subject ignore
50
- #
51
- # @param [Matcher]
52
- #
53
- # @return [self]
54
- #
55
- # @api private
56
- #
57
- def add_subject_ignore(matcher)
58
- @subject_ignores << matcher
59
- self
60
- end
61
-
62
- # Add a subject selector
63
- #
64
- # @param [#call] selector
65
- #
66
- # @return [self]
67
- #
68
- # @api private
69
- #
70
- def add_subject_selector(selector)
71
- @subject_selectors << selector
72
- self
73
- end
74
-
75
- # Add a subject matcher
76
- #
77
- # @param [#call] selector
78
- #
79
- # @return [self]
80
- #
81
- # @api private
82
- #
83
- def add_matcher(matcher)
84
- @matchers << matcher
85
- self
86
- end
87
-
88
- # Return generated matcher
89
- #
90
- # @return [Mutant::Matcher]
91
- #
92
- # @api private
93
- #
94
- def matcher
95
- if @matchers.empty?
96
- raise(Error, 'No patterns given')
97
- end
98
-
99
- matcher = Matcher::Chain.build(@matchers)
100
-
101
- if predicate
102
- Matcher::Filter.new(matcher, predicate)
103
- else
104
- matcher
105
- end
106
- end
107
-
108
- private
109
-
110
- # Return subject selector
111
- #
112
- # @return [#call]
113
- # if selector is present
114
- #
115
- # @return [nil]
116
- # otherwise
117
- #
118
- # @api private
119
- #
120
- def subject_selector
121
- Morpher::Evaluator::Predicate::Boolean::Or.new(@subject_selectors) if @subject_selectors.any?
122
- end
123
-
124
- # Return predicate
125
- #
126
- # @return [#call]
127
- # if filter is needed
128
- #
129
- # @return [nil]
130
- # othrwise
131
- #
132
- # @api private
133
- #
134
- def predicate
135
- if subject_selector && subject_rejector
136
- Morpher::Evaluator::Predicate::Boolean::And.new([
137
- subject_selector,
138
- Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
139
- ])
140
- elsif subject_selector
141
- subject_selector
142
- elsif subject_rejector
143
- Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
144
- else
145
- nil
146
- end
147
- end
148
-
149
- # Return subject rejector
150
- #
151
- # @return [#call]
152
- # if there is a subject rejector
153
- #
154
- # @return [nil]
155
- # otherwise
156
- #
157
- # @api private
158
- #
159
- def subject_rejector
160
- rejectors = @subject_ignores.flat_map(&:to_a).map do |subject|
161
- Morpher.compile(s(:eql, s(:attribute, :identification), s(:static, subject.identification)))
162
- end
163
-
164
- Morpher::Evaluator::Predicate::Boolean::Or.new(rejectors) if rejectors.any?
165
- end
166
- end
167
-
168
33
  # Initialize objecct
169
34
  #
170
35
  # @param [Array<String>]
@@ -174,18 +39,16 @@ module Mutant
174
39
  # @api private
175
40
  #
176
41
  def initialize(arguments = [])
177
- @builder = Builder.new
42
+ @builder = Matcher::Builder.new(Env::Boot.new(Reporter::CLI.new($stderr), Cache.new))
178
43
  @debug = @fail_fast = @zombie = false
179
44
  @expected_coverage = 100.0
180
- @strategy = Strategy::Null.new
181
- @cache = Mutant::Cache.new
45
+ @integration = Integration::Null.new
182
46
  parse(arguments)
183
47
  @config = Config.new(
184
- cache: @cache,
185
48
  zombie: @zombie,
186
49
  debug: @debug,
187
50
  matcher: @builder.matcher,
188
- strategy: @strategy,
51
+ integration: @integration,
189
52
  fail_fast: @fail_fast,
190
53
  reporter: Reporter::CLI.new($stdout),
191
54
  expected_coverage: @expected_coverage
@@ -236,19 +99,16 @@ module Mutant
236
99
 
237
100
  # Parse matchers
238
101
  #
239
- # @param [Enumerable<String>] patterns
102
+ # @param [Array<String>] patterns
240
103
  #
241
104
  # @return [undefined]
242
105
  #
243
106
  # @api private
244
107
  #
245
108
  def parse_matchers(patterns)
109
+ raise Error, 'No patterns given' if patterns.empty?
246
110
  patterns.each do |pattern|
247
- expression = Expression.parse(pattern)
248
- unless expression
249
- raise Error, "Invalid mutant expression: #{pattern.inspect}"
250
- end
251
- @builder.add_matcher(expression.matcher(@cache))
111
+ @builder.add_match_expression(Expression.parse(pattern))
252
112
  end
253
113
  end
254
114
 
@@ -283,8 +143,8 @@ module Mutant
283
143
  # @api private
284
144
  #
285
145
  def use(name)
286
- require "mutant/#{name}"
287
- @strategy = Strategy.lookup(name).new
146
+ require "mutant/integration/#{name}"
147
+ @integration = Integration.lookup(name).new
288
148
  rescue LoadError
289
149
  $stderr.puts("Cannot load plugin: #{name.inspect}")
290
150
  raise
@@ -319,10 +179,10 @@ module Mutant
319
179
  #
320
180
  def add_filter_options(opts)
321
181
  opts.on('--ignore-subject PATTERN', 'Ignore subjects that match PATTERN') do |pattern|
322
- @builder.add_subject_ignore(Expression.parse(pattern).matcher(@cache))
182
+ @builder.add_subject_ignore(Expression.parse(pattern))
323
183
  end
324
184
  opts.on('--code CODE', 'Scope execution to subjects with CODE') do |code|
325
- @builder.add_subject_selector(Morpher.compile(s(:eql, s(:attribute, :code), s(:static, code))))
185
+ @builder.add_subject_selector(:code, code)
326
186
  end
327
187
  end
328
188
 
@@ -2,9 +2,8 @@ module Mutant
2
2
  # The configuration of a mutator run
3
3
  class Config
4
4
  include Adamantium::Flat, Anima.new(
5
- :cache,
6
5
  :debug,
7
- :strategy,
6
+ :integration,
8
7
  :matcher,
9
8
  :reporter,
10
9
  :fail_fast,
@@ -30,5 +29,26 @@ module Mutant
30
29
  self
31
30
  end
32
31
 
32
+ # Return tests for mutation
33
+ #
34
+ # TODO: This logic is now centralized but still fucked.
35
+ #
36
+ # @param [Mutation] mutation
37
+ #
38
+ # @return [Enumerable<Test>]
39
+ #
40
+ # @api private
41
+ #
42
+ def tests(subject)
43
+ subject.match_expressions.each do |match_expression|
44
+ tests = integration.all_tests.select do |test|
45
+ match_expression.prefix?(test.expression)
46
+ end
47
+ return tests if tests.any?
48
+ end
49
+
50
+ EMPTY_ARRAY
51
+ end
52
+
33
53
  end # Config
34
54
  end # Mutant
@@ -2,8 +2,10 @@ module Mutant
2
2
  class Context
3
3
  # Scope context for mutation (Class or Module)
4
4
  class Scope < self
5
- include Adamantium::Flat, Equalizer.new(:scope, :source_path)
6
- extend NodeHelpers
5
+ include Adamantium::Flat, Concord::Public.new(:scope, :source_path)
6
+ extend AST::Sexp
7
+
8
+ NAMESPACE_DELIMITER = '::'.freeze
7
9
 
8
10
  # Return AST wrapping mutated node
9
11
  #
@@ -41,7 +43,7 @@ module Mutant
41
43
  # @api private
42
44
  #
43
45
  def self.wrap(scope, node)
44
- name = s(:const, nil, scope.name.split('::').last.to_sym)
46
+ name = s(:const, nil, scope.name.split(NAMESPACE_DELIMITER).last.to_sym)
45
47
  case scope
46
48
  when ::Class
47
49
  s(:class, name, nil, node)
@@ -87,18 +89,18 @@ module Mutant
87
89
  scope.name
88
90
  end
89
91
 
90
- # Return match prefixes
92
+ # Return match expressions
91
93
  #
92
- # @return [Enumerable<String>]
94
+ # @return [Enumerable<Expression>]
93
95
  #
94
96
  # @api private
95
97
  #
96
- def match_prefixes
98
+ def match_expressions
97
99
  name_nesting.each_index.reverse_each.map do |index|
98
- name_nesting.take(index.succ).join('::')
100
+ Expression.parse("#{name_nesting.take(index.succ).join(NAMESPACE_DELIMITER)}*")
99
101
  end
100
102
  end
101
- memoize :match_prefixes
103
+ memoize :match_expressions
102
104
 
103
105
  # Return scope wrapped by context
104
106
  #
@@ -110,18 +112,6 @@ module Mutant
110
112
 
111
113
  private
112
114
 
113
- # Initialize object
114
- #
115
- # @param [Object] scope
116
- # @param [String] source_path
117
- #
118
- # @api private
119
- #
120
- def initialize(scope, source_path)
121
- super(source_path)
122
- @scope = scope
123
- end
124
-
125
115
  # Return nesting of names of scope
126
116
  #
127
117
  # @return [Array<String>]
@@ -129,7 +119,7 @@ module Mutant
129
119
  # @api private
130
120
  #
131
121
  def name_nesting
132
- scope.name.split('::')
122
+ scope.name.split(NAMESPACE_DELIMITER)
133
123
  end
134
124
  memoize :name_nesting
135
125