mutant 0.8.7 → 0.8.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +5 -0
  3. data/README.md +64 -3
  4. data/config/flay.yml +1 -1
  5. data/lib/mutant.rb +2 -0
  6. data/lib/mutant/cli.rb +1 -1
  7. data/lib/mutant/env/bootstrap.rb +36 -8
  8. data/lib/mutant/expression/method.rb +3 -5
  9. data/lib/mutant/expression/methods.rb +2 -4
  10. data/lib/mutant/expression/namespace.rb +4 -8
  11. data/lib/mutant/matcher.rb +3 -18
  12. data/lib/mutant/matcher/chain.rb +7 -13
  13. data/lib/mutant/matcher/compiler.rb +2 -13
  14. data/lib/mutant/matcher/filter.rb +6 -19
  15. data/lib/mutant/matcher/method.rb +124 -104
  16. data/lib/mutant/matcher/method/instance.rb +40 -34
  17. data/lib/mutant/matcher/method/singleton.rb +80 -61
  18. data/lib/mutant/matcher/methods.rb +19 -29
  19. data/lib/mutant/matcher/namespace.rb +22 -16
  20. data/lib/mutant/matcher/null.rb +4 -7
  21. data/lib/mutant/matcher/scope.rb +23 -13
  22. data/lib/mutant/matcher/static.rb +17 -0
  23. data/lib/mutant/mutation.rb +0 -5
  24. data/lib/mutant/reporter/cli/format.rb +2 -3
  25. data/lib/mutant/reporter/cli/printer/env_progress.rb +37 -11
  26. data/lib/mutant/reporter/cli/printer/status_progressive.rb +1 -1
  27. data/lib/mutant/scope.rb +6 -0
  28. data/lib/mutant/subject/method.rb +0 -7
  29. data/lib/mutant/subject/method/instance.rb +0 -10
  30. data/lib/mutant/subject/method/singleton.rb +0 -10
  31. data/lib/mutant/version.rb +1 -1
  32. data/lib/mutant/zombifier.rb +2 -1
  33. data/mutant-rspec.gemspec +1 -1
  34. data/spec/integration/mutant/rspec_spec.rb +1 -1
  35. data/spec/shared/method_matcher_behavior.rb +21 -14
  36. data/spec/spec_helper.rb +6 -0
  37. data/spec/unit/mutant/env/boostrap_spec.rb +88 -26
  38. data/spec/unit/mutant/env_spec.rb +0 -1
  39. data/spec/unit/mutant/expression/method_spec.rb +3 -3
  40. data/spec/unit/mutant/expression/methods_spec.rb +3 -4
  41. data/spec/unit/mutant/expression/namespace/flat_spec.rb +2 -3
  42. data/spec/unit/mutant/expression/namespace/recursive_spec.rb +2 -4
  43. data/spec/unit/mutant/matcher/chain_spec.rb +21 -29
  44. data/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +16 -13
  45. data/spec/unit/mutant/matcher/compiler_spec.rb +49 -60
  46. data/spec/unit/mutant/matcher/filter_spec.rb +15 -31
  47. data/spec/unit/mutant/matcher/method/instance_spec.rb +84 -128
  48. data/spec/unit/mutant/matcher/method/singleton_spec.rb +48 -52
  49. data/spec/unit/mutant/matcher/methods/instance_spec.rb +21 -24
  50. data/spec/unit/mutant/matcher/methods/singleton_spec.rb +18 -21
  51. data/spec/unit/mutant/matcher/namespace_spec.rb +30 -38
  52. data/spec/unit/mutant/matcher/null_spec.rb +5 -20
  53. data/spec/unit/mutant/matcher/scope_spec.rb +33 -0
  54. data/spec/unit/mutant/matcher/static_spec.rb +11 -0
  55. data/spec/unit/mutant/mutation_spec.rb +30 -10
  56. data/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb +6 -0
  57. data/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb +2 -0
  58. data/spec/unit/mutant/reporter/cli/printer/status_spec.rb +10 -0
  59. data/spec/unit/mutant/reporter/cli_spec.rb +4 -0
  60. data/spec/unit/mutant/subject/method/instance_spec.rb +0 -28
  61. data/spec/unit/mutant/subject/method/singleton_spec.rb +0 -28
  62. data/test_app/Gemfile.rspec3.4 +7 -0
  63. data/test_app/lib/test_app.rb +16 -12
  64. data/test_app/lib/test_app/literal.rb +3 -0
  65. metadata +9 -2
@@ -0,0 +1,17 @@
1
+ module Mutant
2
+ class Matcher
3
+ # Matcher returning subjects already known at its creation time
4
+ class Static
5
+ include Concord.new(:subjects)
6
+
7
+ # Call matcher
8
+ #
9
+ # @return [Enumerable<Subject>]
10
+ #
11
+ # @api private
12
+ def call(_env)
13
+ subjects
14
+ end
15
+ end # Static
16
+ end # Matcher
17
+ end # Mutant
@@ -59,15 +59,10 @@ module Mutant
59
59
 
60
60
  # Insert mutated node
61
61
  #
62
- # FIXME: Cache subject visibility in a better way! Ideally dont mutate it
63
- # implicitly. Also subject.public? should NOT be a public interface it
64
- # is a detail of method mutations.
65
- #
66
62
  # @return [self]
67
63
  #
68
64
  # @api private
69
65
  def insert
70
- subject.public?
71
66
  subject.prepare
72
67
  Loader::Eval.call(root, subject)
73
68
  self
@@ -41,9 +41,8 @@ module Mutant
41
41
  # @return [Boolean]
42
42
  #
43
43
  # @api private
44
- def tty?
45
- @tty
46
- end
44
+ alias_method :tty?, :tty
45
+ public :tty?
47
46
 
48
47
  %i[puts write].each do |name|
49
48
  define_method(name) do |*args, &block|
@@ -10,34 +10,51 @@ module Mutant
10
10
  :amount_mutations,
11
11
  :amount_mutations_alive,
12
12
  :amount_mutations_killed,
13
+ :amount_mutation_results,
13
14
  :runtime,
14
15
  :killtime,
15
16
  :overhead,
16
17
  :env
17
18
  )
18
19
 
20
+ # rubocop:disable SpaceInsideBrackets
21
+ FORMATS = IceNine.deep_freeze([
22
+ [:info, 'Subjects: %s', :amount_subjects ],
23
+ [:info, 'Mutations: %s', :amount_mutations ],
24
+ [:info, 'Results: %s', :amount_mutation_results ],
25
+ [:info, 'Kills: %s', :amount_mutations_killed ],
26
+ [:info, 'Alive: %s', :amount_mutations_alive ],
27
+ [:info, 'Runtime: %0.2fs', :runtime ],
28
+ [:info, 'Killtime: %0.2fs', :killtime ],
29
+ [:info, 'Overhead: %0.2f%%', :overhead_percent ],
30
+ [:info, 'Mutations/s: %0.2f', :mutations_per_second ],
31
+ [:status, 'Coverage: %0.2f%%', :coverage_percent ],
32
+ [:status, 'Expected: %0.2f%%', :expected_coverage_percent]
33
+ ])
34
+
19
35
  # Run printer
20
36
  #
21
37
  # @return [undefined]
22
38
  #
23
- # rubocop:disable AbcSize
24
- #
25
39
  # @api private
26
40
  def run
27
41
  visit(Config, env.config)
28
- info 'Subjects: %s', amount_subjects
29
- info 'Mutations: %s', amount_mutations
30
- info 'Kills: %s', amount_mutations_killed
31
- info 'Alive: %s', amount_mutations_alive
32
- info 'Runtime: %0.2fs', runtime
33
- info 'Killtime: %0.2fs', killtime
34
- info 'Overhead: %0.2f%%', overhead_percent
35
- status 'Coverage: %0.2f%%', coverage_percent
36
- status 'Expected: %0.2f%%', (env.config.expected_coverage * 100)
42
+ FORMATS.each do |report, format, value|
43
+ __send__(report, format, __send__(value))
44
+ end
37
45
  end
38
46
 
39
47
  private
40
48
 
49
+ # Mutations processed per second
50
+ #
51
+ # @return [Float]
52
+ #
53
+ # @api private
54
+ def mutations_per_second
55
+ amount_mutation_results / runtime
56
+ end
57
+
41
58
  # Coverage in percent
42
59
  #
43
60
  # @return [Float]
@@ -47,6 +64,15 @@ module Mutant
47
64
  coverage * 100
48
65
  end
49
66
 
67
+ # Expected coverage in percent
68
+ #
69
+ # @return [Float]
70
+ #
71
+ # @api private
72
+ def expected_coverage_percent
73
+ env.config.expected_coverage * 100
74
+ end
75
+
50
76
  # Overhead in percent
51
77
  #
52
78
  # @return [Float]
@@ -41,7 +41,7 @@ module Mutant
41
41
  #
42
42
  # @api private
43
43
  def object
44
- super.payload
44
+ super().payload
45
45
  end
46
46
  end # StatusProgressive
47
47
  end # Printer
@@ -0,0 +1,6 @@
1
+ module Mutant
2
+ # Class or Module bound to an exact expression
3
+ class Scope
4
+ include Concord::Public.new(:raw, :expression)
5
+ end # Scope
6
+ end # Mutant
@@ -3,13 +3,6 @@ module Mutant
3
3
  # Abstract base class for method subjects
4
4
  class Method < self
5
5
 
6
- # Test if method is public
7
- #
8
- # @return [Boolean]
9
- #
10
- # @api private
11
- abstract_method :public?
12
-
13
6
  # Method name
14
7
  #
15
8
  # @return [Expression]
@@ -7,16 +7,6 @@ module Mutant
7
7
  NAME_INDEX = 0
8
8
  SYMBOL = '#'.freeze
9
9
 
10
- # Test if method is public
11
- #
12
- # @return [Boolean]
13
- #
14
- # @api private
15
- def public?
16
- scope.public_method_defined?(name)
17
- end
18
- memoize :public?
19
-
20
10
  # Prepare subject for mutation insertion
21
11
  #
22
12
  # @return [self]
@@ -7,16 +7,6 @@ module Mutant
7
7
  NAME_INDEX = 1
8
8
  SYMBOL = '.'.freeze
9
9
 
10
- # Test if method is public
11
- #
12
- # @return [Boolean]
13
- #
14
- # @api private
15
- def public?
16
- scope.singleton_class.public_method_defined?(name)
17
- end
18
- memoize :public?
19
-
20
10
  # Prepare subject for mutation insertion
21
11
  #
22
12
  # @return [self]
@@ -1,4 +1,4 @@
1
1
  module Mutant
2
2
  # Current mutant version
3
- VERSION = '0.8.7'.freeze
3
+ VERSION = '0.8.8'.freeze
4
4
  end # Mutant
@@ -10,6 +10,7 @@ module Mutant
10
10
  :root_require,
11
11
  :pathname
12
12
  )
13
+ private(*anima.attribute_names)
13
14
 
14
15
  include AST::Sexp
15
16
 
@@ -56,7 +57,7 @@ module Mutant
56
57
  #
57
58
  # @api private
58
59
  def include?(logical_name)
59
- !@zombified.include?(logical_name) && @includes =~ logical_name
60
+ !@zombified.include?(logical_name) && includes =~ logical_name
60
61
  end
61
62
 
62
63
  # Require file in zombie namespace
data/mutant-rspec.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.extra_rdoc_files = %w[TODO LICENSE]
17
17
 
18
18
  gem.add_runtime_dependency('mutant', "~> #{gem.version}")
19
- gem.add_runtime_dependency('rspec-core', '>= 3.2.0', '< 3.4.0')
19
+ gem.add_runtime_dependency('rspec-core', '>= 3.2.0', '< 3.5.0')
20
20
 
21
21
  gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5')
22
22
  end
@@ -2,7 +2,7 @@ RSpec.describe 'rspec integration', mutant: false do
2
2
 
3
3
  let(:base_cmd) { 'bundle exec mutant -I lib --require test_app --use rspec' }
4
4
 
5
- %w[3.2 3.3].each do |version|
5
+ %w[3.2 3.3 3.4].each do |version|
6
6
  context "RSpec #{version}" do
7
7
  let(:gemfile) { "Gemfile.rspec#{version}" }
8
8
 
@@ -1,38 +1,45 @@
1
1
  RSpec.shared_examples_for 'a method matcher' do
2
-
3
- before { subject }
4
-
5
2
  let(:node) { mutation_subject.node }
6
3
  let(:context) { mutation_subject.context }
7
- let(:mutation_subject) { yields.first }
4
+ let(:mutation_subject) { subject.first }
8
5
 
9
- it 'should return one subject' do
10
- expect(yields.size).to be(1)
6
+ it 'returns one subject' do
7
+ expect(subject.size).to be(1)
11
8
  end
12
9
 
13
- it_should_behave_like 'an #each method'
14
-
15
- it 'should have correct method name' do
10
+ it 'has expected method name' do
16
11
  expect(name).to eql(method_name)
17
12
  end
18
13
 
19
- it 'should have correct line number' do
14
+ it 'has epxected line number' do
20
15
  expect(node.location.expression.line).to eql(method_line)
21
16
  end
22
17
 
23
- it 'should have correct arity' do
18
+ it 'has expected arity' do
24
19
  expect(arguments.children.length).to eql(method_arity)
25
20
  end
26
21
 
27
- it 'should have correct scope in context' do
22
+ it 'has expected scope in context' do
28
23
  expect(context.scope).to eql(scope)
29
24
  end
30
25
 
31
- it 'should have the correct source path in context' do
26
+ it 'has source path in context' do
32
27
  expect(context.source_path).to eql(source_path)
33
28
  end
34
29
 
35
- it 'should have the correct node type' do
30
+ it 'has the correct node type' do
36
31
  expect(node.type).to be(type)
37
32
  end
38
33
  end
34
+
35
+ RSpec.shared_examples_for 'skipped candidate' do
36
+ it 'does not emit matcher' do
37
+ expect(subject).to eql([])
38
+ end
39
+
40
+ it 'does warn' do
41
+ subject
42
+
43
+ expect(env.config.reporter.warn_calls).to eql(expected_warnings)
44
+ end
45
+ end
data/spec/spec_helper.rb CHANGED
@@ -43,6 +43,12 @@ module ParserHelper
43
43
  Unparser.unparse(node)
44
44
  end
45
45
 
46
+ def test_env
47
+ Fixtures::TEST_ENV.with(
48
+ config: Mutant::Config::DEFAULT.with(reporter: Mutant::Reporter::Trace.new)
49
+ )
50
+ end
51
+
46
52
  def parse(string)
47
53
  Unparser::Preprocessor.run(Parser::CurrentRuby.parse(string))
48
54
  end
@@ -1,16 +1,29 @@
1
+ # This spec is a good example for:
2
+ #
3
+ # If test look that ugly the class under test sucks.
4
+ #
5
+ # As the bootstrap needs to infect global VM state
6
+ # this is to some degree acceptable.
7
+ #
8
+ # Still the bootstrap needs to be cleaned up.
9
+ # And the change that added this warning did the groundwork.
1
10
  RSpec.describe Mutant::Env::Bootstrap do
11
+ let(:matcher_config) { Mutant::Matcher::Config::DEFAULT }
12
+ let(:integration) { instance_double(Mutant::Integration) }
13
+ let(:integration_class) { instance_double(Class) }
14
+ let(:object_space_modules) { [] }
15
+
2
16
  let(:config) do
3
17
  Mutant::Config::DEFAULT.with(
4
- jobs: 1,
5
- reporter: Mutant::Reporter::Trace.new,
6
- includes: [],
7
- requires: [],
8
- matcher: Mutant::Matcher::Config::DEFAULT
18
+ jobs: 1,
19
+ reporter: Mutant::Reporter::Trace.new,
20
+ includes: [],
21
+ requires: [],
22
+ integration: integration_class,
23
+ matcher: matcher_config
9
24
  )
10
25
  end
11
26
 
12
- let(:integration) { Mutant::Integration::Null.new(config) }
13
-
14
27
  let(:expected_env) do
15
28
  Mutant::Env.new(
16
29
  cache: Mutant::Cache.new,
@@ -28,16 +41,40 @@ RSpec.describe Mutant::Env::Bootstrap do
28
41
  it { should eql(expected_env) }
29
42
  end
30
43
 
31
- let(:object_space_modules) { [] }
32
-
33
44
  before do
34
- allow(ObjectSpace).to receive(:each_object).with(Module).and_return(object_space_modules.each)
45
+ expect(integration_class).to receive(:new)
46
+ .with(config)
47
+ .and_return(integration)
48
+
49
+ expect(integration).to receive(:setup).and_return(integration)
50
+
51
+ expect(ObjectSpace).to receive(:each_object)
52
+ .with(Module)
53
+ .and_return(object_space_modules.each)
54
+ end
55
+
56
+ describe '#warn' do
57
+ let(:object) { described_class.new(config) }
58
+ let(:message) { instance_double(String) }
59
+
60
+ subject { object.warn(message) }
61
+
62
+ it 'reports a warning' do
63
+ expect { subject }
64
+ .to change { object.config.reporter.warn_calls }
65
+ .from([])
66
+ .to([message])
67
+ end
68
+
69
+ it_behaves_like 'a command method'
35
70
  end
36
71
 
37
72
  describe '.call' do
38
73
  subject { described_class.call(config) }
39
74
 
40
75
  context 'when Module#name calls result in exceptions' do
76
+ let(:object_space_modules) { [invalid_class] }
77
+
41
78
  let(:invalid_class) do
42
79
  Class.new do
43
80
  def self.name
@@ -46,8 +83,6 @@ RSpec.describe Mutant::Env::Bootstrap do
46
83
  end
47
84
  end
48
85
 
49
- let(:object_space_modules) { [invalid_class] }
50
-
51
86
  after do
52
87
  # Fix Class#name so other specs do not see this one
53
88
  class << invalid_class
@@ -59,21 +94,41 @@ RSpec.describe Mutant::Env::Bootstrap do
59
94
 
60
95
  it 'warns via reporter' do
61
96
  expected_warnings = [
62
- "Class#name from: #{invalid_class} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"
97
+ "Class#name from: #{invalid_class} raised an error: " \
98
+ "RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"
63
99
  ]
64
100
 
65
- expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
101
+ expect { subject }
102
+ .to change { config.reporter.warn_calls }
103
+ .from([])
104
+ .to(expected_warnings)
66
105
  end
67
106
 
68
107
  include_examples 'bootstrap call'
69
108
  end
70
109
 
71
- context 'when includes are present' do
110
+ context 'when requires are configured' do
111
+ let(:config) { super().with(requires: %w[foo bar]) }
112
+
113
+ before do
114
+ %w[foo bar].each do |component|
115
+ expect(Kernel).to receive(:require)
116
+ .with(component)
117
+ .and_return(true)
118
+ end
119
+ end
120
+
121
+ include_examples 'bootstrap call'
122
+ end
123
+
124
+ context 'when includes are configured' do
72
125
  let(:config) { super().with(includes: %w[foo bar]) }
73
126
 
74
127
  before do
75
128
  %w[foo bar].each do |component|
76
- expect($LOAD_PATH).to receive(:<<).with(component).and_return($LOAD_PATH)
129
+ expect($LOAD_PATH).to receive(:<<)
130
+ .with(component)
131
+ .and_return($LOAD_PATH)
77
132
  end
78
133
  end
79
134
 
@@ -81,6 +136,8 @@ RSpec.describe Mutant::Env::Bootstrap do
81
136
  end
82
137
 
83
138
  context 'when Module#name does not return a String or nil' do
139
+ let(:object_space_modules) { [invalid_class] }
140
+
84
141
  let(:invalid_class) do
85
142
  Class.new do
86
143
  def self.name
@@ -89,8 +146,6 @@ RSpec.describe Mutant::Env::Bootstrap do
89
146
  end
90
147
  end
91
148
 
92
- let(:object_space_modules) { [invalid_class] }
93
-
94
149
  after do
95
150
  # Fix Class#name so other specs do not see this one
96
151
  class << invalid_class
@@ -101,29 +156,36 @@ RSpec.describe Mutant::Env::Bootstrap do
101
156
  end
102
157
 
103
158
  it 'warns via reporter' do
104
-
105
159
  expected_warnings = [
106
160
  "Class#name from: #{invalid_class.inspect} returned Object. #{Mutant::Env::SEMANTICS_MESSAGE}"
107
161
  ]
108
162
 
109
- expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
163
+ expect { subject }
164
+ .to change { config.reporter.warn_calls }
165
+ .from([]).to(expected_warnings)
110
166
  end
111
167
 
112
168
  include_examples 'bootstrap call'
113
169
  end
114
170
 
115
171
  context 'when scope matches expression' do
116
- let(:mutations) { [double('Mutation')] }
117
- let(:subjects) { [double('Subject', mutations: mutations)] }
172
+ let(:object_space_modules) { [TestApp::Literal, TestApp::Empty] }
173
+ let(:match_expressions) { object_space_modules.map(&:name).map(&method(:parse_expression)) }
118
174
 
119
- before do
120
- expect(Mutant::Matcher::Compiler).to receive(:call).and_return(subjects)
175
+ let(:matcher_config) do
176
+ super().with(match_expressions: match_expressions)
121
177
  end
122
178
 
123
179
  let(:expected_env) do
180
+ subjects = Mutant::Matcher::Scope.new(TestApp::Literal).call(Fixtures::TEST_ENV)
181
+
124
182
  super().with(
125
- subjects: subjects,
126
- mutations: mutations
183
+ matchable_scopes: [
184
+ Mutant::Scope.new(TestApp::Empty, match_expressions.last),
185
+ Mutant::Scope.new(TestApp::Literal, match_expressions.first)
186
+ ],
187
+ subjects: subjects,
188
+ mutations: subjects.flat_map(&:mutations)
127
189
  )
128
190
  end
129
191