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