mutant 0.8.19 → 0.8.20

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +46 -8
  3. data/Changelog.md +4 -0
  4. data/Gemfile.lock +10 -12
  5. data/README.md +22 -250
  6. data/config/flay.yml +1 -1
  7. data/docs/concurrency.md +39 -0
  8. data/docs/known-problems.md +44 -0
  9. data/docs/limitations.md +50 -0
  10. data/docs/mutant-minitest.md +147 -0
  11. data/docs/mutant-rspec.md +62 -0
  12. data/docs/nomenclature.md +82 -0
  13. data/docs/reading-reports.md +74 -0
  14. data/lib/mutant.rb +4 -3
  15. data/lib/mutant/env.rb +2 -2
  16. data/lib/mutant/expression/namespace.rb +3 -1
  17. data/lib/mutant/runner/sink.rb +2 -2
  18. data/lib/mutant/timer.rb +21 -0
  19. data/lib/mutant/version.rb +1 -1
  20. data/mutant-minitest.gemspec +22 -0
  21. data/mutant.gemspec +4 -5
  22. data/spec/integration/mutant/corpus_spec.rb +1 -7
  23. data/spec/integration/mutant/minitest_spec.rb +10 -0
  24. data/spec/integration/mutant/rspec_spec.rb +1 -1
  25. data/spec/integrations.yml +14 -0
  26. data/spec/shared/framework_integration_behavior.rb +8 -5
  27. data/spec/support/corpus.rb +20 -14
  28. data/spec/unit/mutant/clock_monotonic_spec.rb +52 -0
  29. data/spec/unit/mutant/env_spec.rb +2 -2
  30. data/spec/unit/mutant/expression/namespace/recursive_spec.rb +1 -1
  31. data/spec/unit/mutant/integration/rspec_spec.rb +1 -1
  32. data/spec/unit/mutant/reporter/cli_spec.rb +1 -1
  33. data/spec/unit/mutant/runner/sink_spec.rb +1 -1
  34. data/test_app/Gemfile.minitest +6 -0
  35. data/test_app/{Gemfile.rspec3.6 → Gemfile.rspec3.8} +2 -2
  36. data/test_app/test/unit/test_app/literal_test.rb +16 -0
  37. metadata +23 -26
  38. data/spec/rcov.opts +0 -7
  39. data/spec/support/rb_bug.rb +0 -18
  40. data/test_app/Gemfile.rspec3.4 +0 -7
  41. data/test_app/Gemfile.rspec3.5 +0 -7
@@ -4,15 +4,15 @@ require 'abstract_type'
4
4
  require 'adamantium'
5
5
  require 'anima'
6
6
  require 'concord'
7
- require 'digest/sha1'
8
7
  require 'diff/lcs'
9
8
  require 'diff/lcs/hunk'
9
+ require 'digest/sha1'
10
+ require 'etc'
10
11
  require 'equalizer'
11
12
  require 'ice_nine'
12
13
  require 'morpher'
13
14
  require 'open3'
14
15
  require 'optparse'
15
- require 'parallel'
16
16
  require 'parser'
17
17
  require 'parser/current'
18
18
  require 'pathname'
@@ -177,6 +177,7 @@ require 'mutant/expression/method'
177
177
  require 'mutant/expression/methods'
178
178
  require 'mutant/expression/namespace'
179
179
  require 'mutant/test'
180
+ require 'mutant/timer'
180
181
  require 'mutant/integration'
181
182
  require 'mutant/selector'
182
183
  require 'mutant/selector/expression'
@@ -228,7 +229,7 @@ module Mutant
228
229
  marshal: Marshal,
229
230
  process: Process
230
231
  ),
231
- jobs: ::Parallel.processor_count,
232
+ jobs: Etc.nprocessors,
232
233
  kernel: Kernel,
233
234
  load_path: $LOAD_PATH,
234
235
  matcher: Matcher::Config::DEFAULT,
@@ -42,7 +42,7 @@ module Mutant
42
42
  #
43
43
  # rubocop:disable MethodLength
44
44
  def run_mutation_tests(mutation)
45
- start = Time.now
45
+ start = Timer.now
46
46
  tests = selector.call(mutation.subject)
47
47
 
48
48
  config.isolation.call do
@@ -53,7 +53,7 @@ module Mutant
53
53
  Result::Test.new(
54
54
  output: error.message,
55
55
  passed: false,
56
- runtime: Time.now - start,
56
+ runtime: Timer.now - start,
57
57
  tests: tests
58
58
  )
59
59
  end
@@ -44,7 +44,9 @@ module Mutant
44
44
  #
45
45
  # @return [Integer]
46
46
  def match_length(expression)
47
- if @recursion_pattern.match?(expression.syntax)
47
+ if eql?(expression)
48
+ syntax.length
49
+ elsif @recursion_pattern.match?(expression.syntax)
48
50
  scope_name.length
49
51
  else
50
52
  0
@@ -10,7 +10,7 @@ module Mutant
10
10
  # @return [undefined]
11
11
  def initialize(*)
12
12
  super
13
- @start = Time.now
13
+ @start = Timer.now
14
14
  @subject_results = {}
15
15
  end
16
16
 
@@ -20,7 +20,7 @@ module Mutant
20
20
  def status
21
21
  Result::Env.new(
22
22
  env: env,
23
- runtime: Time.now - @start,
23
+ runtime: Timer.now - @start,
24
24
  subject_results: @subject_results.values
25
25
  )
26
26
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module Timer
5
+ # Monotonic elapsed time of block execution
6
+ #
7
+ # @return [Float]
8
+ def self.elapsed
9
+ start = now
10
+ yield
11
+ now - start
12
+ end
13
+
14
+ # The now monotonic time
15
+ #
16
+ # @return [Float]
17
+ def self.now
18
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
20
+ end # Timer
21
+ end # Mutant
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutant
4
4
  # Current mutant version
5
- VERSION = '0.8.19'.freeze
5
+ VERSION = '0.8.20'.freeze
6
6
  end # Mutant
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('lib/mutant/version', __dir__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'mutant-minitest'
7
+ gem.version = Mutant::VERSION.dup
8
+ gem.authors = ['Markus Schirp']
9
+ gem.email = %w[mbj@schirp-dso.com]
10
+ gem.description = 'Minitest integration for mutant'
11
+ gem.summary = gem.description
12
+ gem.homepage = 'https://github.com/mbj/mutant'
13
+ gem.license = 'MIT'
14
+
15
+ gem.require_paths = %w[lib]
16
+ gem.files = `git ls-files -- lib/mutant/{minitest,/integration/minitest.rb}`.split("\n")
17
+ gem.test_files = `git ls-files -- spec/integration/mutant/minitest.rb`.split("\n")
18
+ gem.extra_rdoc_files = %w[LICENSE]
19
+
20
+ gem.add_runtime_dependency('minitest', '~> 5.11')
21
+ gem.add_runtime_dependency('mutant', "~> #{gem.version}")
22
+ end
@@ -14,9 +14,9 @@ Gem::Specification.new do |gem|
14
14
 
15
15
  gem.require_paths = %w[lib]
16
16
 
17
- mutant_integration_files = `git ls-files -- lib/mutant/integration/*.rb`.split("\n")
17
+ exclusion = `git ls-files -- lib/mutant/{minitest,integration}`.split("\n")
18
18
 
19
- gem.files = `git ls-files`.split("\n") - mutant_integration_files
19
+ gem.files = `git ls-files`.split("\n") - exclusion
20
20
  gem.test_files = `git ls-files -- spec/{unit,integration}`.split("\n")
21
21
  gem.extra_rdoc_files = %w[LICENSE]
22
22
  gem.executables = %w[mutant]
@@ -33,13 +33,12 @@ Gem::Specification.new do |gem|
33
33
  gem.add_runtime_dependency('ice_nine', '~> 0.11.1')
34
34
  gem.add_runtime_dependency('memoizable', '~> 0.4.2')
35
35
  gem.add_runtime_dependency('morpher', '~> 0.2.6')
36
- gem.add_runtime_dependency('parallel', '~> 1.3')
37
36
  gem.add_runtime_dependency('parser', '~> 2.5.1')
38
37
  gem.add_runtime_dependency('procto', '~> 0.0.2')
39
38
  gem.add_runtime_dependency('regexp_parser', '~> 1.2')
40
- gem.add_runtime_dependency('unparser', '~> 0.2.5')
39
+ gem.add_runtime_dependency('unparser', '~> 0.3.0')
41
40
 
42
41
  gem.add_development_dependency('bundler', '~> 1.10')
43
42
  gem.add_development_dependency('devtools', '~> 0.1.21')
44
- gem.add_development_dependency('ffi', '~> 1.9.6')
43
+ gem.add_development_dependency('parallel', '~> 1.3')
45
44
  end
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe 'Mutant on ruby corpus', mutant: false do
4
-
5
- before do
6
- skip 'Corpus test is deactivated on < 2.1' if RUBY_VERSION < '2.1'
7
- skip 'Corpus test is deactivated on RBX' if RUBY_ENGINE.eql?('rbx')
8
- end
9
-
10
4
  MutantSpec::Corpus::Project::ALL.select(&:mutation_generation).each do |project|
11
5
  specify "#{project.name} does not fail on mutation generation" do
12
6
  project.verify_mutation_generation
@@ -14,7 +8,7 @@ RSpec.describe 'Mutant on ruby corpus', mutant: false do
14
8
  end
15
9
 
16
10
  MutantSpec::Corpus::Project::ALL.select(&:mutation_coverage).each do |project|
17
- specify "#{project.name} does have expected mutation coverage" do
11
+ specify "#{project.name} (#{project.integration}) does have expected mutation coverage" do
18
12
  project.verify_mutation_coverage
19
13
  end
20
14
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'minitest integration', mutant: false do
4
+
5
+ let(:base_cmd) { 'bundle exec mutant -I test -I lib --require test_app --use minitest' }
6
+
7
+ let(:gemfile) { 'Gemfile.minitest' }
8
+
9
+ it_behaves_like 'framework integration'
10
+ end
@@ -4,7 +4,7 @@ RSpec.describe 'rspec integration', mutant: false do
4
4
 
5
5
  let(:base_cmd) { 'bundle exec mutant -I lib --require test_app --use rspec' }
6
6
 
7
- %w[3.4 3.5 3.6 3.7].each do |version|
7
+ %w[3.7 3.8].each do |version|
8
8
  context "RSpec #{version}" do
9
9
  let(:gemfile) { "Gemfile.rspec#{version}" }
10
10
 
@@ -3,6 +3,7 @@
3
3
  namespace: Rubyspec
4
4
  repo_uri: 'https://github.com/ruby/rubyspec.git'
5
5
  repo_ref: 'origin/master'
6
+ integration: mspec
6
7
  ruby_glob_pattern: '**/*_spec.rb'
7
8
  mutation_coverage: false
8
9
  mutation_generation: true
@@ -24,6 +25,7 @@
24
25
  namespace: Regexp
25
26
  repo_uri: 'https://github.com/ammar/regexp_parser.git'
26
27
  repo_ref: 'v1.2.0'
28
+ integration: rspec
27
29
  ruby_glob_pattern: '**/*.rb'
28
30
  mutation_coverage: false
29
31
  mutation_generation: true
@@ -34,6 +36,17 @@
34
36
  repo_uri: 'https://github.com/mbj/auom.git'
35
37
  repo_ref: 'origin/master'
36
38
  ruby_glob_pattern: '**/*.rb'
39
+ integration: rspec
40
+ mutation_coverage: true
41
+ mutation_generation: true
42
+ expected_errors: {}
43
+ exclude: []
44
+ - name: auom
45
+ namespace: AUOM
46
+ repo_uri: 'https://github.com/mbj/auom.git'
47
+ repo_ref: 'origin/master'
48
+ ruby_glob_pattern: '**/*.rb'
49
+ integration: minitest
37
50
  mutation_coverage: true
38
51
  mutation_generation: true
39
52
  expected_errors: {}
@@ -43,6 +56,7 @@
43
56
  repo_uri: 'https://github.com/dkubb/axiom.git'
44
57
  repo_ref: 'origin/master'
45
58
  ruby_glob_pattern: '**/*.rb'
59
+ integration: rspec
46
60
  mutation_coverage: false
47
61
  mutation_generation: true
48
62
  expected_errors: {}
@@ -6,22 +6,25 @@ RSpec.shared_examples_for 'framework integration' do
6
6
  end
7
7
 
8
8
  around do |example|
9
- Dir.chdir(TestApp.root) do
10
- Kernel.system('bundle', 'install', '--gemfile', gemfile) || fail('Bundle install failed!')
11
- example.run
9
+ Bundler.with_clean_env do
10
+ Dir.chdir(TestApp.root) do
11
+ Kernel.system('bundle', 'install', '--gemfile', gemfile) || fail('Bundle install failed!')
12
+ example.run
13
+ end
12
14
  end
13
15
  end
14
16
 
15
17
  specify 'it allows to kill mutations' do
16
- expect(Kernel.system("#{base_cmd} TestApp::Literal#string")).to be(true)
18
+ expect(system_with_gemfile("#{base_cmd} TestApp::Literal#string")).to be(true)
17
19
  end
18
20
 
19
21
  specify 'it allows to exclude mutations' do
20
22
  cli = <<-CMD.split("\n").join(' ')
21
23
  #{base_cmd}
24
+ --ignore-subject TestApp::Literal#uncovered_string
25
+ --
22
26
  TestApp::Literal#string
23
27
  TestApp::Literal#uncovered_string
24
- --ignore-subject TestApp::Literal#uncovered_string
25
28
  CMD
26
29
  expect(system_with_gemfile(cli)).to be(true)
27
30
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'morpher'
4
3
  require 'anima'
4
+ require 'morpher'
5
5
  require 'mutant'
6
+ require 'parallel'
6
7
 
7
8
  # @api private
8
9
  module MutantSpec
@@ -36,6 +37,7 @@ module MutantSpec
36
37
  :expected_errors,
37
38
  :mutation_coverage,
38
39
  :mutation_generation,
40
+ :integration,
39
41
  :name,
40
42
  :namespace,
41
43
  :repo_uri,
@@ -53,16 +55,18 @@ module MutantSpec
53
55
  def verify_mutation_coverage
54
56
  checkout
55
57
  Dir.chdir(repo_path) do
56
- install_mutant
57
- system(
58
- %W[
59
- bundle exec mutant
60
- --use rspec
61
- --include lib
62
- --require #{name}
63
- #{namespace}*
64
- ]
65
- )
58
+ Bundler.with_clean_env do
59
+ install_mutant
60
+ system(
61
+ %W[
62
+ bundle exec mutant
63
+ --use #{integration}
64
+ --include lib
65
+ --require #{name}
66
+ #{namespace}*
67
+ ]
68
+ )
69
+ end
66
70
  end
67
71
  end
68
72
 
@@ -75,7 +79,7 @@ module MutantSpec
75
79
  # otherwise
76
80
  def verify_mutation_generation
77
81
  checkout
78
- start = Time.now
82
+ start = Mutant::Timer.now
79
83
 
80
84
  options = {
81
85
  finish: method(:finish),
@@ -86,7 +90,7 @@ module MutantSpec
86
90
  total = Parallel.map(effective_ruby_paths, options, &method(:count_mutations_and_check_errors))
87
91
  .inject(DEFAULT_MUTATION_COUNT, :+)
88
92
 
89
- took = Time.now - start
93
+ took = Mutant::Timer.now - start
90
94
  puts MUTATION_GENERATION_MESSAGE % [total, took, total / took]
91
95
  self
92
96
  end
@@ -163,6 +167,7 @@ module MutantSpec
163
167
  repo_path.join('Gemfile').open('a') do |file|
164
168
  file << "gem 'mutant', path: '#{relative}'\n"
165
169
  file << "gem 'mutant-rspec', path: '#{relative}'\n"
170
+ file << "gem 'mutant-minitest', path: '#{relative}'\n"
166
171
  file << "eval_gemfile File.expand_path('#{relative.join('Gemfile.shared')}')\n"
167
172
  end
168
173
  lockfile = repo_path.join('Gemfile.lock')
@@ -188,7 +193,7 @@ module MutantSpec
188
193
  if ENV.key?('CI')
189
194
  CIRCLE_CI_CONTAINER_PROCESSES
190
195
  else
191
- Parallel.processor_count
196
+ Etc.nprocessors
192
197
  end
193
198
  end
194
199
 
@@ -312,6 +317,7 @@ module MutantSpec
312
317
  s(:key_symbolize, :ruby_glob_pattern, s(:guard, s(:primitive, String))),
313
318
  s(:key_symbolize, :name, s(:guard, s(:primitive, String))),
314
319
  s(:key_symbolize, :namespace, s(:guard, s(:primitive, String))),
320
+ s(:key_symbolize, :integration, s(:guard, s(:primitive, String))),
315
321
  s(:key_symbolize, :mutation_coverage,
316
322
  s(:guard, s(:or, s(:primitive, TrueClass), s(:primitive, FalseClass)))),
317
323
  s(:key_symbolize, :mutation_generation,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Mutant::Timer do
4
+ let(:events) { [] }
5
+
6
+ let(:times) { [1.0, 2.0] }
7
+
8
+ before do
9
+ allow(Process).to receive(:clock_gettime) do |argument|
10
+ expect(argument).to be(Process::CLOCK_MONOTONIC)
11
+
12
+ events << :clock_gettime
13
+
14
+ times.fetch(events.count(:clock_gettime).pred)
15
+ end
16
+ end
17
+
18
+ describe '.elapsed' do
19
+ def apply
20
+ described_class.elapsed { events << :yield }
21
+ end
22
+
23
+ it 'executes events in expected sequence' do
24
+ expect { apply }
25
+ .to change(events, :to_a)
26
+ .from([])
27
+ .to(%i[clock_gettime yield clock_gettime])
28
+ end
29
+
30
+ it 'returns elapsed time' do
31
+ expect(apply).to be(1.0)
32
+ end
33
+ end
34
+
35
+ describe '.now' do
36
+ def apply
37
+ described_class.now
38
+ end
39
+
40
+ it 'returns current monotonic time' do
41
+ expect(apply).to be(1.0)
42
+ expect(apply).to be(2.0)
43
+ end
44
+
45
+ it 'calls expected system API' do
46
+ expect { apply }
47
+ .to change(events, :to_a)
48
+ .from([])
49
+ .to(%i[clock_gettime])
50
+ end
51
+ end
52
+ end
@@ -57,7 +57,7 @@ RSpec.describe Mutant::Env do
57
57
  .with(mutation_subject)
58
58
  .and_return(tests)
59
59
 
60
- allow(Time).to receive_messages(now: Time.at(0))
60
+ allow(Mutant::Timer).to receive(:now).and_return(2.0, 3.0)
61
61
  end
62
62
 
63
63
  context 'when isolation does not raise error' do
@@ -91,7 +91,7 @@ RSpec.describe Mutant::Env do
91
91
  Mutant::Result::Test.new(
92
92
  output: 'test-error',
93
93
  passed: false,
94
- runtime: 0.0,
94
+ runtime: 1.0,
95
95
  tests: tests
96
96
  )
97
97
  end
@@ -22,7 +22,7 @@ RSpec.describe Mutant::Expression::Namespace::Recursive do
22
22
  context 'when other is an equivalent expression' do
23
23
  let(:other) { parse_expression(object.syntax) }
24
24
 
25
- it { should be(0) }
25
+ it { should be(object.syntax.length) }
26
26
  end
27
27
 
28
28
  context 'when other expression describes a shorter prefix' do