mutant 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Changelog.md +12 -1
  4. data/README.md +3 -0
  5. data/Rakefile +0 -9
  6. data/bin/mutant +1 -1
  7. data/config/flay.yml +1 -1
  8. data/config/mutant.yml +13 -0
  9. data/config/reek.yml +6 -2
  10. data/lib/mutant/constants.rb +0 -13
  11. data/lib/mutant/mutator/node/conditional_loop.rb +2 -1
  12. data/lib/mutant/mutator/node/generic.rb +1 -1
  13. data/lib/mutant/mutator/node/if.rb +1 -1
  14. data/lib/mutant/mutator/node/literal/regex.rb +2 -2
  15. data/lib/mutant/mutator/node/match_current_line.rb +27 -0
  16. data/lib/mutant/mutator/node/named_value/variable_assignment.rb +1 -1
  17. data/lib/mutant/mutator/node/nthref.rb +3 -1
  18. data/lib/mutant/mutator/node/rescue.rb +0 -12
  19. data/lib/mutant/mutator/node/send/attribute_assignment.rb +51 -0
  20. data/lib/mutant/mutator/node/send/index.rb +43 -0
  21. data/lib/mutant/mutator/node/send.rb +7 -37
  22. data/lib/mutant/mutator/node.rb +4 -12
  23. data/lib/mutant/mutator.rb +2 -26
  24. data/lib/mutant/node_helpers.rb +3 -3
  25. data/lib/mutant/require_highjack.rb +64 -0
  26. data/lib/mutant/subject/method/instance.rb +9 -1
  27. data/lib/mutant/version.rb +1 -1
  28. data/lib/mutant/warning_expectation.rb +40 -0
  29. data/lib/mutant/warning_filter.rb +74 -0
  30. data/lib/mutant/zombifier/file.rb +81 -0
  31. data/lib/mutant/zombifier.rb +61 -240
  32. data/lib/mutant.rb +57 -1
  33. data/lib/parser_extensions.rb +25 -0
  34. data/mutant.gemspec +4 -3
  35. data/spec/integration/mutant/corpus_spec.rb +121 -0
  36. data/spec/integration/mutant/rspec_spec.rb +1 -1
  37. data/spec/integration/mutant/test_mutator_handles_types_spec.rb +1 -1
  38. data/spec/integration/mutant/zombie_spec.rb +3 -3
  39. data/spec/integrations.yml +23 -0
  40. data/spec/shared/mutator_behavior.rb +22 -72
  41. data/spec/spec_helper.rb +4 -2
  42. data/spec/support/mutation_verifier.rb +95 -0
  43. data/spec/unit/mutant/mutator/node/conditional_loop_spec.rb +16 -0
  44. data/spec/unit/mutant/mutator/node/match_current_line_spec.rb +4 -4
  45. data/spec/unit/mutant/mutator/node/named_value/access_spec.rb +6 -3
  46. data/spec/unit/mutant/mutator/node/nthref_spec.rb +2 -2
  47. data/spec/unit/mutant/mutator/node/op_assgn_spec.rb +3 -1
  48. data/spec/unit/mutant/mutator/node/send_spec.rb +0 -1
  49. data/spec/unit/mutant/require_highjack_spec.rb +54 -0
  50. data/spec/unit/mutant/subject/method/instance_spec.rb +34 -3
  51. data/spec/unit/mutant/warning_expectation.rb +71 -0
  52. data/spec/unit/mutant/warning_filter_spec.rb +94 -0
  53. metadata +40 -9
  54. data/lib/mutant/singleton_methods.rb +0 -30
@@ -0,0 +1,74 @@
1
+ # encoding: UTF-8
2
+
3
+ module Mutant
4
+ # Stream filter for warnings
5
+ class WarningFilter
6
+ include Equalizer.new(:target)
7
+
8
+ WARNING_PATTERN = /\A(?:.+):(?:\d+): warning: (?:.+)\n\z/
9
+
10
+ # Initialize object
11
+ #
12
+ # @param [#write] target
13
+ #
14
+ # @return [undefined]
15
+ #
16
+ # @api private
17
+ #
18
+ def initialize(target)
19
+ @target = target
20
+ @warnings = []
21
+ end
22
+
23
+ # Return filtered warnings
24
+ #
25
+ # @return [Array<String>]
26
+ #
27
+ # @api private
28
+ #
29
+ attr_reader :warnings
30
+
31
+ # Return target
32
+ #
33
+ # @return [#write] target
34
+ #
35
+ # @return [undefined]
36
+ #
37
+ # @api private
38
+ #
39
+ attr_reader :target
40
+ protected :target
41
+
42
+ # Write message to target filtering warnings
43
+ #
44
+ # @param [String] message
45
+ #
46
+ # @return [self]
47
+ #
48
+ def write(message)
49
+ if WARNING_PATTERN =~ message
50
+ warnings << message
51
+ else
52
+ target.write(message)
53
+ end
54
+
55
+ self
56
+ end
57
+
58
+ # Use warning filter during block execution
59
+ #
60
+ # @return [Array<String>]
61
+ #
62
+ # @api private
63
+ #
64
+ def self.use
65
+ original = $stderr
66
+ $stderr = filter = new(original)
67
+ yield
68
+ filter.warnings
69
+ ensure
70
+ $stderr = original
71
+ end
72
+
73
+ end # WarningFilter
74
+ end # Mutant
@@ -0,0 +1,81 @@
1
+ module Mutant
2
+ class Zombifier
3
+ # File containing source beeing zombified
4
+ class File
5
+ include NodeHelpers, Adamantium::Flat, Concord::Public.new(:path)
6
+
7
+ # Zombify contents of file
8
+ #
9
+ # @return [self]
10
+ #
11
+ # @api private
12
+ #
13
+ def zombify(namespace)
14
+ $stderr.puts("Zombifying #{path.to_s}")
15
+ eval(
16
+ Unparser.unparse(namespaced_node(namespace)),
17
+ TOPLEVEL_BINDING,
18
+ path.to_s
19
+ )
20
+ self
21
+ end
22
+
23
+ # Find file by logical path
24
+ #
25
+ # @param [String] logical_name
26
+ #
27
+ # @return [File]
28
+ # if found
29
+ #
30
+ # @return [nil]
31
+ # otherwise
32
+ #
33
+ # @api private
34
+ #
35
+ def self.find(logical_name)
36
+ file_name =
37
+ case ::File.extname(logical_name)
38
+ when '.so'
39
+ return
40
+ when '.rb'
41
+ logical_name
42
+ else
43
+ "#{logical_name}.rb"
44
+ end
45
+
46
+ $LOAD_PATH.each do |path|
47
+ path = Pathname.new(path).join(file_name)
48
+ return new(path) if path.file?
49
+ end
50
+
51
+ $stderr.puts "Cannot find file #{file_name} in $LOAD_PATH"
52
+ nil
53
+ end
54
+
55
+ private
56
+
57
+ # Return node
58
+ #
59
+ # @return [Parser::AST::Node]
60
+ #
61
+ # @api private
62
+ #
63
+ def node
64
+ Parser::CurrentRuby.parse(path.read)
65
+ end
66
+
67
+ # Return namespaced root
68
+ #
69
+ # @param [Symbol] namespace
70
+ #
71
+ # @return [Parser::AST::Node]
72
+ #
73
+ # @api private
74
+ #
75
+ def namespaced_node(namespace)
76
+ s(:module, s(:const, nil, namespace), node)
77
+ end
78
+
79
+ end # File
80
+ end # Zombifier
81
+ end # Mutant
@@ -2,264 +2,85 @@
2
2
 
3
3
  module Mutant
4
4
  # Zombifier namespace
5
- module Zombifier
5
+ class Zombifier
6
+ include Adamantium::Flat, Concord.new(:namespace)
7
+
8
+ # Excluded into zombification
9
+ includes = %w(
10
+ mutant
11
+ morpher
12
+ adamantium
13
+ equalizer
14
+ anima
15
+ concord
16
+ )
17
+
18
+ INCLUDES = %r(\A#{Regexp.union(includes)}(?:/.*)?\z).freeze
19
+
20
+ # Initialize object
21
+ #
22
+ # @param [Symbol] namespace
23
+ #
24
+ # @return [undefined]
25
+ #
26
+ # @api private
27
+ #
28
+ def initialize(namespace)
29
+ @zombified = Set.new
30
+ @highjack = RequireHighjack.new(Kernel, method(:require))
31
+ super(namespace)
32
+ end
6
33
 
7
- # Excluded from zombification, reasons
34
+ # Perform zombification of target library
8
35
  #
9
- # * Relies dynamic require, zombifier does not know how to recurse (racc)
10
- # * Unparser bug (optparse)
11
- # * Toplevel reference/cbase nodes in code (rspec)
12
- # * Creates useless toplevel modules that get vendored under ::Zombie (set)
36
+ # @param [String] logical_name
37
+ # @param [Symbol] namespace
13
38
  #
14
- IGNORE = %w(
15
- set
16
- rspec
17
- diff/lcs
18
- diff/lcs/hunk
19
- unparser
20
- parser
21
- parser/all
22
- parser/current
23
- racc/parser
24
- optparse
25
- ).to_set
39
+ # @api private
40
+ #
41
+ def self.run(logical_name, namespace)
42
+ new(namespace).run(logical_name)
43
+ end
26
44
 
27
- # Perform self zombification
45
+ # Run zombifier
28
46
  #
29
- # @return [self]
47
+ # @param [String] logical_name
48
+ #
49
+ # @return [undefined]
30
50
  #
31
51
  # @api private
32
52
  #
33
- def self.zombify
34
- run('mutant')
35
- self
53
+ def run(logical_name)
54
+ @highjack.infect
55
+ require(logical_name)
36
56
  end
37
57
 
38
- # Zombify gem
58
+ # Test if logical name is subjected to zombification
59
+ #
60
+ # @param [String]
61
+ #
62
+ # @api private
39
63
  #
40
- # @param [String] name
64
+ def include?(logical_name)
65
+ !@zombified.include?(logical_name) && INCLUDES =~ logical_name
66
+ end
67
+
68
+ # Require file in zombie namespace
69
+ #
70
+ # @param [String] logical_name
41
71
  #
42
72
  # @return [self]
43
73
  #
44
74
  # @api private
45
75
  #
46
- def self.run(name)
47
- Gem.new(name).zombify
76
+ def require(logical_name)
77
+ @highjack.original.call(logical_name)
78
+ return unless include?(logical_name)
79
+ @zombified << logical_name
80
+ file = File.find(logical_name)
81
+ file.zombify(namespace) if file
48
82
  self
49
83
  end
50
84
 
51
- # Zombifier subject, compatible with mutants loader
52
- class Subject < Mutant::Subject
53
- include NodeHelpers
54
-
55
- # Return new object
56
- #
57
- # @param [File]
58
- #
59
- # @return [Subject]
60
- #
61
- # @api private
62
- #
63
- def self.new(file)
64
- super(file, file.node)
65
- end
66
-
67
- # Perform zombification on subject
68
- #
69
- # @return [self]
70
- #
71
- # @api private
72
- #
73
- def zombify
74
- $stderr.puts("Zombifying #{context.source_path}")
75
- Loader::Eval.call(zombified_root, self)
76
- self
77
- end
78
- memoize :zombify
79
-
80
- private
81
-
82
- # Return zombified root
83
- #
84
- # @return [Parser::AST::Node]
85
- #
86
- # @api private
87
- #
88
- def zombified_root
89
- s(:module, s(:const, nil, :Zombie), node)
90
- end
91
-
92
- end # Subject
93
-
94
- # File containing source beeing zombified
95
- class File
96
- include Adamantium::Flat, Concord::Public.new(:source_path)
97
-
98
- CACHE = {}
99
-
100
- # Zombify contents of file
101
- #
102
- # @return [self]
103
- #
104
- # @api private
105
- #
106
- def zombify
107
- subject.zombify
108
- required_paths.each do |path|
109
- file = File.find(path)
110
- next unless file
111
- file.zombify
112
- end
113
- self
114
- end
115
- memoize :zombify
116
-
117
- # Find file
118
- #
119
- # @param [String] logical_name
120
- #
121
- # @return [File]
122
- # if found
123
- #
124
- # @raise [RuntimeError]
125
- # if file cannot be found
126
- #
127
- # @api private
128
- #
129
- def self.find(logical_name)
130
- return if IGNORE.include?(logical_name)
131
- CACHE.fetch(logical_name) do
132
- CACHE[logical_name] = find_uncached(logical_name)
133
- end
134
- end
135
-
136
- # Find file without cache
137
- #
138
- # @param [String] logical_name
139
- #
140
- # @return [File]
141
- # if found
142
- #
143
- # @return [nil]
144
- # otherwise
145
- #
146
- # @api private
147
- #
148
- def self.find_uncached(logical_name)
149
- file_name =
150
- if logical_name.end_with?('.rb')
151
- logical_name
152
- else
153
- "#{logical_name}.rb"
154
- end
155
-
156
- $LOAD_PATH.each do |path|
157
- path = Pathname.new(path).join(file_name)
158
- if path.file?
159
- return new(path)
160
- end
161
- end
162
-
163
- $stderr.puts "Cannot find file #{file_name} in $LOAD_PATH"
164
- nil
165
- end
166
-
167
- # Return subject
168
- #
169
- # @return [Subject]
170
- #
171
- # @api private
172
- #
173
- def subject
174
- Subject.new(self)
175
- end
176
- memoize :subject
177
-
178
- # Return node
179
- #
180
- # @return [Parser::AST::Node]
181
- #
182
- # @api private
183
- #
184
- def node
185
- Parser::CurrentRuby.parse(::File.read(source_path))
186
- end
187
- memoize :node
188
-
189
- RECEIVER_INDEX = 0
190
- SELECTOR_INDEX = 1
191
- ARGUMENT_INDEX = 2..-1.freeze
192
-
193
- # Return required paths
194
- #
195
- # @return [Enumerable<String>]
196
- #
197
- # @api private
198
- #
199
- def required_paths
200
- require_nodes.map do |node|
201
- arguments = node.children[ARGUMENT_INDEX]
202
- unless arguments.length == 1
203
- raise "Require node with not exactly one argument: #{node}"
204
- end
205
- argument = arguments.first
206
- unless argument.type == :str
207
- raise "Require argument is not a literal string: #{argument}"
208
- end
209
- argument.children.first
210
- end
211
- end
212
- memoize :required_paths
213
-
214
- private
215
-
216
- # Return require nodes
217
- #
218
- # @return [Enumerable<Parser::AST::Node>]
219
- #
220
- # @api private
221
- #
222
- def require_nodes
223
- children = node.type == :begin ? node.children : [node]
224
- children.select do |node|
225
- children = node.children
226
- node.type == :send &&
227
- children.at(RECEIVER_INDEX).nil? &&
228
- children.at(SELECTOR_INDEX) == :require
229
- end
230
- end
231
-
232
- end # File
233
-
234
- # Gem beeing zombified
235
- class Gem
236
- include Adamantium::Flat, Concord.new(:name)
237
-
238
- # Return subjects
239
- #
240
- # @return [Enumerable<Subject>]
241
- #
242
- # @api private
243
- #
244
- def zombify
245
- root_file.zombify
246
- end
247
- memoize :zombify
248
-
249
- private
250
-
251
- # Return root souce file
252
- #
253
- # @return [File]
254
- #
255
- # @api private
256
- #
257
- def root_file
258
- File.find(name) or raise 'No root file!'
259
- end
260
- memoize :root_file
261
-
262
- end # Gem
263
-
264
85
  end # Zombifier
265
86
  end # Mutant
data/lib/mutant.rb CHANGED
@@ -11,6 +11,7 @@ require 'digest/sha1'
11
11
  require 'inflecto'
12
12
  require 'parser'
13
13
  require 'parser/current'
14
+ require 'parser_extensions'
14
15
  require 'unparser'
15
16
  require 'ice_nine'
16
17
  require 'diff/lcs'
@@ -25,15 +26,66 @@ module Mutant
25
26
  EMPTY_STRING = ''.freeze
26
27
  # The frozen empty array used within mutant
27
28
  EMPTY_ARRAY = [].freeze
29
+
30
+ # Perform self zombification
31
+ #
32
+ # @return [self]
33
+ #
34
+ # @api private
35
+ #
36
+ def self.zombify
37
+ Zombifier.run('mutant', :Zombie)
38
+ self
39
+ end
40
+
41
+ # Return a frozen set of symbols from string enumerable
42
+ #
43
+ # @param [Enumerable<String>]
44
+ #
45
+ # @return [Set<Symbol>]
46
+ #
47
+ # @api private
48
+ #
49
+ def self.symbolset(strings)
50
+ strings.map(&:to_sym).to_set.freeze
51
+ end
52
+ private_class_method :symbolset
53
+
54
+ # Define instance of subclassed superclass as constant
55
+ #
56
+ # @param [Class] superclass
57
+ # @param [Symbol] name
58
+ #
59
+ # @return [self]
60
+ #
61
+ # @api private
62
+ #
63
+ def self.singleton_subclass_instance(name, superclass, &block)
64
+ klass = Class.new(superclass) do
65
+ def inspect
66
+ self.class.name
67
+ end
68
+
69
+ define_singleton_method(:name) do
70
+ "#{superclass.name}::#{name}".freeze
71
+ end
72
+ end
73
+ klass.class_eval(&block)
74
+ superclass.const_set(name, klass.new)
75
+ self
76
+ end
77
+
28
78
  end # Mutant
29
79
 
30
80
  require 'mutant/version'
31
81
  require 'mutant/cache'
32
82
  require 'mutant/node_helpers'
33
- require 'mutant/singleton_methods'
83
+ require 'mutant/warning_filter'
84
+ require 'mutant/warning_expectation'
34
85
  require 'mutant/constants'
35
86
  require 'mutant/random'
36
87
  require 'mutant/walker'
88
+ require 'mutant/require_highjack'
37
89
  require 'mutant/mutator'
38
90
  require 'mutant/mutation'
39
91
  require 'mutant/mutation/evil'
@@ -77,6 +129,8 @@ require 'mutant/mutator/node/zsuper'
77
129
  require 'mutant/mutator/node/restarg'
78
130
  require 'mutant/mutator/node/send'
79
131
  require 'mutant/mutator/node/send/binary'
132
+ require 'mutant/mutator/node/send/attribute_assignment'
133
+ require 'mutant/mutator/node/send/index'
80
134
  require 'mutant/mutator/node/when'
81
135
  require 'mutant/mutator/node/define'
82
136
  require 'mutant/mutator/node/mlhs'
@@ -89,6 +143,7 @@ require 'mutant/mutator/node/case'
89
143
  require 'mutant/mutator/node/splat'
90
144
  require 'mutant/mutator/node/resbody'
91
145
  require 'mutant/mutator/node/rescue'
146
+ require 'mutant/mutator/node/match_current_line'
92
147
  require 'mutant/config'
93
148
  require 'mutant/loader'
94
149
  require 'mutant/context'
@@ -132,3 +187,4 @@ require 'mutant/reporter/cli/printer/subject'
132
187
  require 'mutant/reporter/cli/printer/killer'
133
188
  require 'mutant/reporter/cli/printer/mutation'
134
189
  require 'mutant/zombifier'
190
+ require 'mutant/zombifier/file'
@@ -0,0 +1,25 @@
1
+ # Monkeypatch to silence warnings in parser
2
+ #
3
+ # Will be removed once https://github.com/whitequark/parser/issues/145 is solved.
4
+
5
+ # Parser namespace
6
+ module Parser
7
+ # Monkeypatched lexer
8
+ class Lexer
9
+
10
+ # Return new lexer
11
+ #
12
+ # @return [Lexer]
13
+ #
14
+ # @api private
15
+ #
16
+ def self.new(*arguments)
17
+ super.tap do |instance|
18
+ instance.instance_eval do
19
+ @force_utf32 = false
20
+ end
21
+ end
22
+ end
23
+
24
+ end # Lexer
25
+ end # Parser
data/mutant.gemspec CHANGED
@@ -24,18 +24,19 @@ Gem::Specification.new do |gem|
24
24
  gem.required_ruby_version = '>= 1.9.3'
25
25
 
26
26
  gem.add_runtime_dependency('parser', '~> 2.1')
27
+ gem.add_runtime_dependency('ast', '~> 2.0')
27
28
  gem.add_runtime_dependency('diff-lcs', '~> 1.2')
28
- gem.add_runtime_dependency('morpher', '~> 0.2.1')
29
+ gem.add_runtime_dependency('morpher', '~> 0.2.3')
29
30
  gem.add_runtime_dependency('procto', '~> 0.0.2')
30
31
  gem.add_runtime_dependency('abstract_type', '~> 0.0.7')
31
- gem.add_runtime_dependency('unparser', '~> 0.1.10')
32
+ gem.add_runtime_dependency('unparser', '~> 0.1.12')
32
33
  gem.add_runtime_dependency('ice_nine', '~> 0.11.0')
33
34
  gem.add_runtime_dependency('adamantium', '~> 0.2.0')
34
35
  gem.add_runtime_dependency('memoizable', '~> 0.4.2')
35
36
  gem.add_runtime_dependency('equalizer', '~> 0.0.9')
36
37
  gem.add_runtime_dependency('inflecto', '~> 0.0.2')
37
38
  gem.add_runtime_dependency('anima', '~> 0.2.0')
38
- gem.add_runtime_dependency('concord', '~> 0.1.4')
39
+ gem.add_runtime_dependency('concord', '~> 0.1.5')
39
40
 
40
41
  gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5')
41
42
  end