mutant 0.8.7 → 0.8.8

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