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,121 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Mutant on ruby corpus' do
6
+
7
+ before do
8
+ pending 'Unparser is too slow on big files'
9
+ end
10
+
11
+ ROOT = Pathname.new(__FILE__).parent.parent.parent.parent
12
+
13
+ TMP = ROOT.join('tmp')
14
+
15
+ class Project
16
+ include Anima.new(:name, :repo_uri, :exclude)
17
+
18
+ # Perform verification via unparser cli
19
+ #
20
+ # @return [self]
21
+ # if successful
22
+ #
23
+ # @raise [Exception]
24
+ # otherwise
25
+ #
26
+ def verify
27
+ checkout
28
+ Pathname.glob(repo_path.join('**/*.rb')).sort.each do |path|
29
+ puts "Generating mutations for: #{path.to_s}"
30
+ node = Parser::CurrentRuby.parse(path.read)
31
+ count = 0
32
+ Mutant::Mutator::Node.each(node) do |mutant|
33
+ count += 1
34
+ if (count % 100).zero?
35
+ puts count
36
+ end
37
+ end
38
+ puts "Mutations: #{count}"
39
+ end
40
+ self
41
+ end
42
+
43
+ # Checkout repository
44
+ #
45
+ # @return [self]
46
+ #
47
+ # @api private
48
+ #
49
+ def checkout
50
+ TMP.mkdir unless TMP.directory?
51
+ if repo_path.exist?
52
+ Dir.chdir(repo_path) do
53
+ system(%w(git pull origin master))
54
+ system(%w(git clean -f -d -x))
55
+ end
56
+ else
57
+ system(%W(git clone #{repo_uri} #{repo_path}))
58
+ end
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ # Return repository path
65
+ #
66
+ # @return [Pathname]
67
+ #
68
+ # @api private
69
+ #
70
+ def repo_path
71
+ TMP.join(name)
72
+ end
73
+
74
+ # Helper method to execute system commands
75
+ #
76
+ # @param [Array<String>] arguments
77
+ #
78
+ # @api private
79
+ #
80
+ def system(arguments)
81
+ unless Kernel.system(*arguments)
82
+ if block_given?
83
+ yield
84
+ else
85
+ raise 'System command failed!'
86
+ end
87
+ end
88
+ end
89
+
90
+ LOADER = Morpher.build do
91
+ s(:block,
92
+ s(:guard, s(:primitive, Array)),
93
+ s(:map,
94
+ s(:block,
95
+ s(:guard, s(:primitive, Hash)),
96
+ s(:hash_transform,
97
+ s(:key_symbolize, :repo_uri, s(:guard, s(:primitive, String))),
98
+ s(:key_symbolize, :name, s(:guard, s(:primitive, String))),
99
+ s(:key_symbolize, :exclude, s(:map, s(:guard, s(:primitive, String))))
100
+ ),
101
+ s(:load_attribute_hash,
102
+ # NOTE: The domain param has no DSL currently!
103
+ Morpher::Evaluator::Transformer::Domain::Param.new(
104
+ Project,
105
+ [:repo_uri, :name, :exclude]
106
+ )
107
+ )
108
+ )
109
+ )
110
+ )
111
+ end
112
+
113
+ ALL = LOADER.call(YAML.load_file(ROOT.join('spec', 'integrations.yml')))
114
+ end
115
+
116
+ Project::ALL.each do |project|
117
+ specify "unparsing #{project.name}" do
118
+ project.verify
119
+ end
120
+ end
121
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Mutant, 'rspec integration' do
5
+ describe 'rspec integration' do
6
6
 
7
7
  let(:base_cmd) { 'bundle exec mutant -I lib --require test_app --use rspec' }
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Mutant do
5
+ describe do
6
6
 
7
7
  specify 'mutant should not crash for any node parser can generate' do
8
8
  Mutant::NODE_TYPES.each do |type|
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Mutant, 'as a zombie' do
6
- pending 'it allows to create zombie from mutant' do
7
- Mutant::Zombifier.run('mutant')
5
+ describe 'as a zombie' do
6
+ specify 'it allows to create zombie from mutant' do
7
+ expect { Mutant.zombify }.to change { !!defined?(Zombie) }.from(false).to(true)
8
8
  expect(Zombie.constants).to include(:Mutant)
9
9
  end
10
10
  end
@@ -0,0 +1,23 @@
1
+ ---
2
+ - name: rubyspec
3
+ repo_uri: 'https://github.com/rubyspec/rubyspec.git'
4
+ exclude:
5
+ # Binary encoded source subjected to limitations see Readme
6
+ - core/array/pack/{b,h,u}_spec.rb
7
+ - language/versions/*1.8*
8
+ - core/array/pack/shared/float.rb
9
+ - core/array/pack/shared/integer.rb
10
+ - core/array/pack/{c,m,w}_spec.rb
11
+ - core/regexp/shared/new.rb
12
+ - core/regexp/shared/quote.rb
13
+ - core/encoding/compatible_spec.rb
14
+ - core/io/readpartial_spec.rb
15
+ - core/env/element_reference_spec.rb
16
+ - core/dir/pwd_spec.rb
17
+ - core/string/casecmp_spec.rb
18
+ - core/string/unpack/{b,c,h,m,u,w}_spec.rb
19
+ - core/string/unpack/b_spec.rb
20
+ - core/string/unpack/shared/float.rb
21
+ - core/string/unpack/shared/integer.rb
22
+ - core/symbol/casecmp_spec.rb
23
+ - optional/capi/integer_spec.rb
@@ -1,52 +1,12 @@
1
- # encoding: utf-8
2
-
3
- class Subject
4
-
5
- include Equalizer.new(:source)
6
-
7
- Undefined = Object.new.freeze
8
-
9
- attr_reader :source
10
-
11
- def self.coerce(input)
12
- case input
13
- when Parser::AST::Node
14
- new(input)
15
- when String
16
- new(Parser::CurrentRuby.parse(input))
17
- else
18
- raise
19
- end
20
- end
21
-
22
- def to_s
23
- "#{@node.inspect}\n#{@source}"
24
- end
25
-
26
- def initialize(node)
27
- source = Unparser.unparse(node)
28
- @node, @source = node, source
29
- end
30
-
31
- def assert_transitive!
32
- generated = Unparser.generate(@node)
33
- parsed = Parser::CurrentRuby.parse(generated)
34
- again = Unparser.generate(parsed)
35
- unless generated == again
36
- # mostly an unparser bug!
37
- fail sprintf("Untransitive:\n%s\n---\n%s", generated, again)
38
- end
39
- self
40
- end
41
- end
1
+ # encoding: UTF-8
42
2
 
43
3
  shared_examples_for 'a mutator' do
44
- subject { object.each(node) { |item| yields << item } }
4
+ subject { object.each(node, &yields.method(:<<)) }
45
5
 
46
6
  let(:yields) { [] }
47
7
  let(:object) { described_class }
48
8
 
49
- unless instance_methods.map(&:to_s).include?('node')
9
+ unless instance_methods.include?(:node)
50
10
  let(:node) { parse(source) }
51
11
  end
52
12
 
@@ -57,42 +17,32 @@ shared_examples_for 'a mutator' do
57
17
 
58
18
  it { should be_instance_of(to_enum.class) }
59
19
 
60
- let(:expected_mutations) do
61
- mutations.map(&Subject.method(:coerce))
20
+ def coerce(input)
21
+ case input
22
+ when String
23
+ Parser::CurrentRuby.parse(input)
24
+ when Parser::AST::Node
25
+ input
26
+ else
27
+ raise
28
+ end
62
29
  end
63
30
 
64
- let(:generated_mutations) do
31
+ def normalize(node)
32
+ Unparser::Preprocessor.run(node)
65
33
  end
66
34
 
67
- it 'generates the expected mutations' do
68
-
69
- generated = subject.map { |node| Subject.new(node) }
70
-
71
- missing = expected_mutations - generated
72
- unexpected = generated - expected_mutations
73
-
74
- message = []
75
-
76
- if missing.any?
77
- message << sprintf('Missing mutations (%i):', missing.length)
78
- message.concat(missing)
79
- end
80
-
81
- if unexpected.any?
82
- message << sprintf('Unexpected mutations (%i):', unexpected.length)
83
- message.concat(unexpected)
84
- end
35
+ let(:expected_mutations) do
36
+ mutations.map(&method(:coerce)).map(&method(:normalize))
37
+ end
85
38
 
86
- if message.any?
39
+ it 'generates the expected mutations' do
40
+ generated_mutations = subject.map(&method(:normalize))
87
41
 
88
- message = sprintf(
89
- "Original:\n%s\n%s\n-----\n%s",
90
- generate(node),
91
- node.inspect,
92
- message.join("\n-----\n")
93
- )
42
+ verifier = MutationVerifier.new(node, expected_mutations, generated_mutations)
94
43
 
95
- fail message
44
+ unless verifier.success?
45
+ fail verifier.error_report
96
46
  end
97
47
  end
98
48
  end
data/spec/spec_helper.rb CHANGED
@@ -21,8 +21,10 @@ if ENV['COVERAGE'] == 'true'
21
21
  end
22
22
  end
23
23
 
24
- require 'equalizer'
24
+ require 'concord'
25
+ require 'adamantium'
25
26
  require 'devtools/spec_helper'
27
+ require 'unparser/cli'
26
28
  require 'mutant'
27
29
 
28
30
  $LOAD_PATH << File.join(TestApp.root, 'lib')
@@ -39,7 +41,7 @@ module ParserHelper
39
41
  end
40
42
 
41
43
  def parse(string)
42
- Parser::CurrentRuby.parse(string)
44
+ Unparser::Preprocessor.run(Parser::CurrentRuby.parse(string))
43
45
  end
44
46
  end
45
47
 
@@ -0,0 +1,95 @@
1
+ # encoding: UTF-8
2
+
3
+ class MutationVerifier
4
+ include Adamantium::Flat, Concord.new(:original_node, :expected, :generated)
5
+
6
+ # Test if mutation was verified successfully
7
+ #
8
+ # @return [Boolean]
9
+ #
10
+ # @api private
11
+ #
12
+ def success?
13
+ unparser.success? && missing.empty? && unexpected.empty?
14
+ end
15
+
16
+ # Return error report
17
+ #
18
+ # @return [String]
19
+ #
20
+ # @api private
21
+ #
22
+ def error_report
23
+ unless unparser.success?
24
+ return unparser.report
25
+ end
26
+ mutation_report
27
+ end
28
+
29
+ private
30
+
31
+ # Return unexpected mutationso
32
+ #
33
+ # @return [Array<Parser::AST::Node>]
34
+ #
35
+ # @api private
36
+ #
37
+ def unexpected
38
+ generated - expected
39
+ end
40
+ memoize :unexpected
41
+
42
+ # Return mutation report
43
+ #
44
+ # @return [String]
45
+ #
46
+ # @api private
47
+ #
48
+ def mutation_report
49
+ message = ['Original:', original_node.inspect]
50
+ if missing.any?
51
+ message << 'Missing mutations:'
52
+ message << missing.map(&method(:format_mutation)).join("\n-----\n")
53
+ end
54
+ if unexpected.any?
55
+ message << 'Unexpected mutations:'
56
+ message << unexpected.map(&method(:format_mutation)).join("\n-----\n")
57
+ end
58
+ message.join("\n======\n")
59
+ end
60
+
61
+ # Format mutation
62
+ #
63
+ # @return [String]
64
+ #
65
+ # @api private
66
+ #
67
+ def format_mutation(node)
68
+ [
69
+ node.inspect,
70
+ Unparser.unparse(node)
71
+ ].join("\n")
72
+ end
73
+
74
+ # Return missing mutationso
75
+ #
76
+ # @return [Array<Parser::AST::Node>]
77
+ #
78
+ # @api private
79
+ #
80
+ def missing
81
+ expected - generated
82
+ end
83
+ memoize :missing
84
+
85
+ # Return unparser verifier
86
+ #
87
+ # @return [Unparser::CLI::Source]
88
+ #
89
+ # @api private
90
+ #
91
+ def unparser
92
+ Unparser::CLI::Source::Node.new(Unparser::Preprocessor.run(original_node))
93
+ end
94
+ memoize :unparser
95
+ end # MutationVerifier
@@ -4,6 +4,20 @@ require 'spec_helper'
4
4
 
5
5
  describe Mutant::Mutator::Node::ConditionalLoop do
6
6
 
7
+ context 'with empty body' do
8
+ let(:source) { 'while true; end' }
9
+
10
+ let(:mutations) do
11
+ mutations = []
12
+ mutations << 'while true; raise; end'
13
+ mutations << 'while false; end'
14
+ mutations << 'while nil; end'
15
+ mutations << 'nil'
16
+ end
17
+
18
+ it_should_behave_like 'a mutator'
19
+ end
20
+
7
21
  context 'with while statement' do
8
22
  let(:source) { 'while true; foo; bar; end' }
9
23
 
@@ -16,6 +30,7 @@ describe Mutant::Mutator::Node::ConditionalLoop do
16
30
  mutations << 'while nil; foo; bar; end'
17
31
  mutations << 'while true; foo; nil; end'
18
32
  mutations << 'while true; nil; bar; end'
33
+ mutations << 'while true; raise; end'
19
34
  mutations << 'nil'
20
35
  end
21
36
 
@@ -34,6 +49,7 @@ describe Mutant::Mutator::Node::ConditionalLoop do
34
49
  mutations << 'until nil; foo; bar; end'
35
50
  mutations << 'until true; foo; nil; end'
36
51
  mutations << 'until true; nil; bar; end'
52
+ mutations << 'until true; raise; end'
37
53
  mutations << 'nil'
38
54
  end
39
55
 
@@ -3,16 +3,16 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Mutant::Mutator::Node::Generic, 'match_current_line' do
6
- let(:source) { 'true if //' }
6
+ let(:source) { 'true if /foo/' }
7
7
 
8
8
  let(:mutations) do
9
9
  mutations = []
10
- mutations << 'false if //'
11
- mutations << 'nil if //'
10
+ mutations << 'false if /foo/'
11
+ mutations << 'true if //'
12
+ mutations << 'nil if /foo/'
12
13
  mutations << 'true if true'
13
14
  mutations << 'true if false'
14
15
  mutations << 'true if nil'
15
- mutations << s(:if, s(:send, s(:match_current_line, s(:regexp, s(:regopt))), :!), s(:true), nil)
16
16
  mutations << 'true if /a\A/'
17
17
  mutations << 'nil'
18
18
  end
@@ -60,10 +60,13 @@ describe Mutant::Mutator::Node::NamedValue::Access, 'mutations' do
60
60
  mutants = []
61
61
  mutants << 'a = nil; nil'
62
62
  mutants << 'a = nil'
63
- mutants << 'a'
64
63
  mutants << 'a = ::Object.new; a'
65
- mutants << 'srandom = nil; a'
66
- mutants << 'nil; a'
64
+ # TODO: fix invalid AST
65
+ # These ASTs are not valid and should NOT be emitted
66
+ # Mutations of lvarasgn need to be special cased to avoid this.
67
+ mutants << s(:begin, s(:lvasgn, :srandom, s(:nil)), s(:lvar, :a))
68
+ mutants << s(:begin, s(:nil), s(:lvar, :a))
69
+ mutants << s(:lvar, :a)
67
70
  end
68
71
 
69
72
  it_should_behave_like 'a mutator'
@@ -4,8 +4,8 @@ require 'spec_helper'
4
4
 
5
5
  describe Mutant::Mutator, 'nthref' do
6
6
  context '$1' do
7
- let(:source) { '$1' }
8
- let(:mutations) { ['$2', '$0'] }
7
+ let(:source) { '$1' }
8
+ let(:mutations) { ['$2'] }
9
9
 
10
10
  it_should_behave_like 'a mutator'
11
11
  end
@@ -13,10 +13,12 @@ describe Mutant::Mutator::Node::Generic, 'op_asgn' do
13
13
  mutations << '@a.b += 2'
14
14
  mutations << '@a.b += 0'
15
15
  mutations << '@a.b += nil'
16
- mutations << '@a += 1'
17
16
  mutations << '@a.b += 5'
18
17
  mutations << 'nil.b += 1'
19
18
  mutations << 'nil'
19
+ # TODO: fix invalid AST
20
+ # This should not get emitted as invalid AST with valid unparsed source
21
+ mutations << s(:op_asgn, s(:ivar, :@a), :+, s(:int, 1))
20
22
  end
21
23
 
22
24
  before do
@@ -63,7 +63,6 @@ describe Mutant::Mutator, 'send' do
63
63
 
64
64
  let(:mutations) do
65
65
  mutations = []
66
- mutations << 'foo ||= expression'
67
66
  mutations << 'self.foo ||= nil'
68
67
  mutations << 'nil.foo ||= expression'
69
68
  mutations << 'nil'
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mutant::RequireHighjack do
4
+ let(:object) { described_class.new(target, highjacked_calls.method(:push)) }
5
+
6
+ let(:highjacked_calls) { [] }
7
+ let(:require_calls) { [] }
8
+
9
+ let(:target) do
10
+ require_calls = self.require_calls
11
+ Module.new do
12
+ define_method(:require) do |logical_name|
13
+ require_calls << logical_name
14
+ end
15
+ module_function :require
16
+ end
17
+ end
18
+
19
+ describe '#run' do
20
+ let(:block) { -> {} }
21
+ let(:logical_name) { double('Logical Name') }
22
+
23
+ subject do
24
+ object.run(&block)
25
+ end
26
+
27
+ context 'require calls before run' do
28
+ it 'does not highjack anything' do
29
+ target.require(logical_name)
30
+ expect(require_calls).to eql([logical_name])
31
+ expect(highjacked_calls).to eql([])
32
+ end
33
+ end
34
+
35
+ context 'require calls during run' do
36
+ let(:block) { -> { target.require(logical_name) } }
37
+
38
+ it 'does highjack the calls' do
39
+ expect { subject }.to change { highjacked_calls }.from([]).to([logical_name])
40
+ expect(require_calls).to eql([])
41
+ end
42
+ end
43
+
44
+ context 'require calls after run' do
45
+
46
+ it 'does not the calls anything' do
47
+ subject
48
+ target.require(logical_name)
49
+ expect(require_calls).to eql([logical_name])
50
+ expect(highjacked_calls).to eql([])
51
+ end
52
+ end
53
+ end
54
+ end
@@ -20,6 +20,12 @@ describe Mutant::Subject::Method::Instance do
20
20
 
21
21
  let(:scope) do
22
22
  Class.new do
23
+ attr_reader :bar
24
+
25
+ def initialize
26
+ @bar = :boo
27
+ end
28
+
23
29
  def foo
24
30
  end
25
31
  end
@@ -27,11 +33,36 @@ describe Mutant::Subject::Method::Instance do
27
33
 
28
34
  subject { object.prepare }
29
35
 
30
- it 'undefines method on scope' do
31
- expect { subject }.to change { scope.instance_methods.include?(:foo) }.from(true).to(false)
36
+ context 'on non initialize methods' do
37
+
38
+ it 'undefines method on scope' do
39
+ expect { subject }.to change { scope.instance_methods.include?(:foo) }.from(true).to(false)
40
+ end
41
+
42
+ it_should_behave_like 'a command method'
43
+
32
44
  end
33
45
 
34
- it_should_behave_like 'a command method'
46
+ context 'on initialize method' do
47
+
48
+ let(:node) do
49
+ s(:def, :initialize, s(:args))
50
+ end
51
+
52
+ it 'does not write warnings' do
53
+ warnings = Mutant::WarningFilter.use do
54
+ subject
55
+ end
56
+ expect(warnings).to eql([])
57
+ end
58
+
59
+ it 'undefines method on scope' do
60
+ subject
61
+ expect { scope.new }.to raise_error(NoMethodError)
62
+ end
63
+
64
+ it_should_behave_like 'a command method'
65
+ end
35
66
  end
36
67
 
37
68
  describe '#source' do