mutant 0.5.10 → 0.5.11

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 (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