mutiny 0.2.1 → 0.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 73bdf7c448948325e3ed64e73d07c14b1ea2788b
4
- data.tar.gz: aad759c85a854b070482db7e9d1775077df23162
3
+ metadata.gz: dd2e01d59dd079e28a3fa98029de8c55d4d5b705
4
+ data.tar.gz: 4b7b13dcbe3563b19d9bf7317f85b2fcebf0509b
5
5
  SHA512:
6
- metadata.gz: 634437f983d4893785bafe74e7b269fc41cc922f13dc4caabf79d6ffdc08d1f4e018d250865554ff1d3218e1d177f1039bd2af0bdd23c9869f2b46e1350b3d11
7
- data.tar.gz: 29d4deb64ad71594980a15ffc7e97b8214cb18cb18c37541cdef80b984a337e91c2fce0508020f707a8fedf1501a78b2ef019659b1dc15284d18ebc52f5e8bbb
6
+ metadata.gz: 8182f9ad54d073ca14bdbe0ce8d583e175551602aaa161fa3de5d1ee02f4bb7cd699f92f5a04ffdb1ea9abb07a80170e35e5045bdd604108353dd4aae84ecc10
7
+ data.tar.gz: 0bd7c7bcf5372e7fbafae2c824206957c3864e717c3aab0bf23458d5d7ec4fbade7446775dc552d27294aa8095ae9b9664d06561d33693f3e21f612d9de6b45d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mutiny (0.2.1)
4
+ mutiny (0.2.2)
5
5
  gli (~> 2.13.0)
6
6
  metamorpher (~> 0.2.2)
7
7
  parser (~> 2.2.2)
@@ -45,7 +45,7 @@ GEM
45
45
  ffi (1.9.10)
46
46
  gherkin (2.12.2)
47
47
  multi_json (~> 1.3)
48
- gli (2.13.1)
48
+ gli (2.13.2)
49
49
  ice_nine (0.11.1)
50
50
  json (1.8.3)
51
51
  memoizable (0.4.2)
@@ -110,4 +110,4 @@ DEPENDENCIES
110
110
  rubocop (~> 0.33.0)
111
111
 
112
112
  BUNDLED WITH
113
- 1.10.4
113
+ 1.10.6
data/RELEASES.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Release History
2
2
 
3
+ ## v0.2.2 (20 January 2016)
4
+ * Add rudimentary support for stillborn mutants
5
+ * Better error reporting during mutant generation
6
+ * Reduce chance that irrelevant subjects are selected for mutation
7
+
3
8
  ## v0.2.1 (20 August 2015)
4
9
  * Fix [#1](https://github.com/mutiny/mutiny/issues/1) - "Uninitialized constant" exception for all commands
5
10
 
data/bin/mutiny CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
+ lib = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
2
5
  require "gli"
3
6
  require "mutiny"
4
7
 
5
8
  include GLI::App
6
9
 
10
+ version Mutiny::VERSION
7
11
  program_desc "A tiny mutation testing framework"
8
12
 
9
13
  flag [:l, :loads], type: Array, default_value: ['lib']
@@ -20,7 +20,7 @@ module Mutiny
20
20
  def analyse_all
21
21
  mutant_set.mutants.each do |mutant|
22
22
  mutant.apply
23
- test_run = integration.test(mutant.subject)
23
+ test_run = integration.test(mutant.subject) unless mutant.stillborn?
24
24
  results.add(mutant, test_run)
25
25
  end
26
26
  end
@@ -4,7 +4,7 @@ module Mutiny
4
4
  def add(mutant, test_run)
5
5
  mutants << mutant
6
6
  test_runs[mutant] = test_run
7
- killed << mutant if test_run.failed?
7
+ killed << mutant if mutant.stillborn? || test_run.failed?
8
8
  end
9
9
 
10
10
  def kill_count
@@ -21,5 +21,9 @@ module Mutiny
21
21
  def load_paths
22
22
  loads.map(&File.method(:expand_path))
23
23
  end
24
+
25
+ def can_load?(source_path)
26
+ load_paths.any? { |load_path| source_path.start_with?(load_path) }
27
+ end
24
28
  end
25
29
  end
@@ -26,11 +26,31 @@ module Mutiny
26
26
 
27
27
  def summarise(mutant)
28
28
  identifier = mutant.identifier
29
- status = results.survived?(mutant) ? "survived" : "killed"
30
- executed_count = results.test_run_for(mutant).executed_count
31
- total_count = results.test_run_for(mutant).tests.size
32
- runtime = results.test_run_for(mutant).runtime
33
- [identifier, status, "#{executed_count} (of #{total_count})", runtime]
29
+ status = status_for_mutant(mutant)
30
+ [identifier, status] + summarise_tests(mutant)
31
+ end
32
+
33
+ def status_for_mutant(mutant)
34
+ if mutant.stillborn?
35
+ "stillborn"
36
+ elsif results.survived?(mutant)
37
+ "survived"
38
+ else
39
+ "killed"
40
+ end
41
+ end
42
+
43
+ def summarise_tests(mutant)
44
+ if mutant.stillborn?
45
+ number_of_tests = "n/a"
46
+ runtime = "n/a"
47
+ else
48
+ executed_count = results.test_run_for(mutant).executed_count
49
+ total_count = results.test_run_for(mutant).tests.size
50
+ runtime = results.test_run_for(mutant).runtime
51
+ number_of_tests = "#{executed_count} (of #{total_count})"
52
+ end
53
+ [number_of_tests, runtime]
34
54
  end
35
55
 
36
56
  def results
@@ -3,20 +3,24 @@ require 'fileutils'
3
3
  module Mutiny
4
4
  module Mutants
5
5
  class Mutant
6
- attr_reader :subject, :code
6
+ attr_reader :subject, :code, :stillborn
7
+ alias_method :stillborn?, :stillborn
7
8
 
8
9
  def initialize(subject: nil, code:)
9
10
  @subject = subject
10
11
  @code = code
12
+ @stillborn = false
11
13
  end
12
14
 
13
15
  def apply
14
- # Evaluate the mutanted code, overidding any existing version.
16
+ # Evaluate the mutated code, overidding any existing version.
15
17
  # We evaluate in the context of TOPLEVEL_BINDING as we want
16
18
  # the unit to be evaluated in its usual namespace.
17
19
  # rubocop:disable Eval
18
20
  eval(code, TOPLEVEL_BINDING)
19
21
  # rubocop:enable Eval
22
+ rescue
23
+ @stillborn = true
20
24
  end
21
25
 
22
26
  def eql?(other)
@@ -7,8 +7,10 @@ module Mutiny
7
7
  extend Forwardable
8
8
  def_delegators :mutants, :size, :<<, :concat
9
9
 
10
- def mutants
11
- @mutants ||= []
10
+ attr_reader :mutants
11
+
12
+ def initialize(*mutants)
13
+ @mutants = mutants
12
14
  end
13
15
 
14
16
  def group_by_subject
@@ -27,6 +29,16 @@ module Mutiny
27
29
  ordered.each { |m| m.store(mutant_directory) }
28
30
  end
29
31
 
32
+ def eql?(other)
33
+ other.mutants == mutants
34
+ end
35
+
36
+ alias_method "==", "eql?"
37
+
38
+ def hash
39
+ mutants.hash
40
+ end
41
+
30
42
  class OrderedMutant < SimpleDelegator
31
43
  def initialize(mutant, number)
32
44
  super(mutant)
@@ -0,0 +1,10 @@
1
+ require "metamorpher"
2
+
3
+ module Mutiny
4
+ module Mutants
5
+ class Mutation
6
+ class Error < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -5,6 +5,10 @@ module Mutiny
5
5
  class Mutation
6
6
  include Metamorpher::Mutator
7
7
  include Metamorpher::Builders::AST
8
+
9
+ def name
10
+ self.class.name
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,8 +1,11 @@
1
1
  require_relative "mutant_set"
2
+ require_relative "mutation/error"
2
3
 
3
4
  module Mutiny
4
5
  module Mutants
5
6
  class MutationSet
7
+ attr_reader :mutations
8
+
6
9
  def initialize(*mutations)
7
10
  @mutations = mutations
8
11
  end
@@ -12,19 +15,21 @@ module Mutiny
12
15
  # several mutation operators could be matched simulatenously during a single AST traversal
13
16
  def mutate(subjects)
14
17
  MutantSet.new.tap do |mutants|
15
- @mutations.each do |mutation|
16
- subjects.each do |subject|
17
- mutated_codes = mutation.mutate_file(subject.path)
18
- mutants.concat(create_mutants(subject, mutated_codes))
19
- end
18
+ subjects.product(mutations).each do |subject, mutation|
19
+ mutants.concat(mutate_one(subject, mutation))
20
20
  end
21
21
  end
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- def create_mutants(subject, mutated_codes)
27
- mutated_codes.map { |code| Mutant.new(subject: subject, code: code) }
26
+ def mutate_one(subject, mutation)
27
+ mutation
28
+ .mutate_file(subject.path)
29
+ .map { |code| Mutant.new(subject: subject, code: code) }
30
+ rescue
31
+ msg = "Error encountered whilst mutating file at '#{subject.path}' with #{mutation.name}"
32
+ raise Mutation::Error, msg
28
33
  end
29
34
  end
30
35
  end
@@ -28,25 +28,26 @@ module Mutiny
28
28
  end
29
29
 
30
30
  def loadable?
31
- !load_path.nil?
31
+ !source_files.empty?
32
32
  end
33
33
 
34
34
  def load_path
35
35
  configuration.load_paths.detect do |load_path|
36
- source_locations.any? { |locs| locs.start_with?(load_path) }
36
+ source_files.any? { |locs| locs.start_with?(load_path) }
37
37
  end
38
38
  end
39
39
 
40
- def source_locations
40
+ def source_files
41
41
  mod.instance_methods(false)
42
42
  .map { |method_name| mod.instance_method(method_name).source_location }
43
43
  .reject(&:nil?)
44
44
  .map(&:first)
45
+ .select { |source_file| configuration.can_load?(source_file) }
45
46
  .uniq
46
47
  end
47
48
 
48
49
  def absolute_path
49
- source_locations.first
50
+ source_files.first
50
51
  end
51
52
  end
52
53
  end
@@ -1,16 +1,16 @@
1
1
  module Mutiny
2
2
  module Subjects
3
3
  class SubjectSet
4
+ include Enumerable
5
+ extend Forwardable
6
+ def_delegators :@subjects, :each, :product
7
+
4
8
  def initialize(subjects)
5
9
  @subjects = subjects
6
10
  end
7
11
 
8
12
  def names
9
- @names ||= @subjects.map(&:name).sort
10
- end
11
-
12
- def each(&block)
13
- @subjects.each(&block)
13
+ @names ||= map(&:name).sort
14
14
  end
15
15
  end
16
16
  end
@@ -1,3 +1,3 @@
1
1
  module Mutiny
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
data/lib/mutiny.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "mutiny/version"
1
2
  require_relative "mutiny/integration"
2
3
  require_relative "mutiny/configuration"
3
4
  require_relative "mutiny/subjects"
data/spec/spec_helper.rb CHANGED
@@ -25,6 +25,12 @@ RSpec.configure do |config|
25
25
  # --seed 1234
26
26
  config.order = 'random'
27
27
 
28
+ # Ensure that mocks responds only to messages that the real object would also
29
+ # respond to
30
+ config.mock_with :rspec do |mocks|
31
+ mocks.verify_doubled_constant_names = true
32
+ end
33
+
28
34
  # Isolate mutiny-under-test from its own test suite
29
35
  config.around(:example) do |example|
30
36
  RSpec::Core::Sandbox.sandboxed do
@@ -0,0 +1,78 @@
1
+ module Mutiny
2
+ module Mutants
3
+ describe MutationSet do
4
+ it "produces one mutant for each result of mutation.mutate_file" do
5
+ mutation_set = MutationSet.new(mutation_for(:code1, :code2, :code3))
6
+ subjects = subjects_for(:path1)
7
+
8
+ expected = MutantSet.new(
9
+ Mutant.new(subject: subjects.first, code: :code1),
10
+ Mutant.new(subject: subjects.first, code: :code2),
11
+ Mutant.new(subject: subjects.first, code: :code3)
12
+ )
13
+
14
+ expect(mutation_set.mutate(subjects)).to eq(expected)
15
+ end
16
+
17
+ it "produces mutants from each mutation" do
18
+ mutation_set = MutationSet.new(mutation_for(:code1), mutation_for(:code2))
19
+ subjects = subjects_for(:path1)
20
+
21
+ expected = MutantSet.new(
22
+ Mutant.new(subject: subjects.first, code: :code1),
23
+ Mutant.new(subject: subjects.first, code: :code2)
24
+ )
25
+
26
+ expect(mutation_set.mutate(subjects)).to eq(expected)
27
+ end
28
+
29
+ it "produces mutants for each subject" do
30
+ mutation_set = MutationSet.new(mutation_for(:code1, :code2), mutation_for(:code3))
31
+ subjects = subjects_for(:path1, :path2)
32
+
33
+ expected = MutantSet.new(
34
+ Mutant.new(subject: subjects.first, code: :code1),
35
+ Mutant.new(subject: subjects.first, code: :code2),
36
+ Mutant.new(subject: subjects.first, code: :code3),
37
+ Mutant.new(subject: subjects.last, code: :code1),
38
+ Mutant.new(subject: subjects.last, code: :code2),
39
+ Mutant.new(subject: subjects.last, code: :code3)
40
+ )
41
+
42
+ expect(mutation_set.mutate(subjects)).to eq(expected)
43
+ end
44
+
45
+ it "produces a mutiny error when mutation fails" do
46
+ mutation = instance_double(Mutation)
47
+ allow(mutation).to receive(:name) { "BuggyMutation" }
48
+ allow(mutation).to receive(:mutate_file).and_raise("boom")
49
+ mutation_set = MutationSet.new(mutation)
50
+ subjects = subjects_for(:path1)
51
+
52
+ expect { mutation_set.mutate(subjects) }.to raise_error do |error|
53
+ expected_message = "Error encountered whilst mutating file at 'path1' with BuggyMutation"
54
+
55
+ expect(error).to be_a(Mutation::Error)
56
+ expect(error.message).to eq(expected_message)
57
+ expect(error.cause.message).to eq("boom")
58
+ end
59
+ end
60
+
61
+ def mutation_for(*codes)
62
+ instance_double(Mutation).tap do |mutation|
63
+ allow(mutation).to receive(:mutate_file) { codes }
64
+ end
65
+ end
66
+
67
+ def subjects_for(*paths)
68
+ paths.map { |path| subject_for(path) }
69
+ end
70
+
71
+ def subject_for(path)
72
+ instance_double(Subjects::Subject).tap do |subject|
73
+ allow(subject).to receive(:path) { path }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Louis Rose
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-20 00:00:00.000000000 Z
11
+ date: 2016-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -210,6 +210,7 @@ files:
210
210
  - lib/mutiny/mutants/mutant.rb
211
211
  - lib/mutiny/mutants/mutant_set.rb
212
212
  - lib/mutiny/mutants/mutation.rb
213
+ - lib/mutiny/mutants/mutation/error.rb
213
214
  - lib/mutiny/mutants/mutation/method/binary_arithmetic_operator_replacement.rb
214
215
  - lib/mutiny/mutants/mutation/method/conditional_operator_deletion.rb
215
216
  - lib/mutiny/mutants/mutation/method/conditional_operator_insertion.rb
@@ -252,6 +253,7 @@ files:
252
253
  - spec/unit/isolation_spec.rb
253
254
  - spec/unit/mutants/mutant_set_spec.rb
254
255
  - spec/unit/mutants/mutant_spec.rb
256
+ - spec/unit/mutants/mutation_set_spec.rb
255
257
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
256
258
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
257
259
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb
@@ -306,6 +308,7 @@ test_files:
306
308
  - spec/unit/isolation_spec.rb
307
309
  - spec/unit/mutants/mutant_set_spec.rb
308
310
  - spec/unit/mutants/mutant_spec.rb
311
+ - spec/unit/mutants/mutation_set_spec.rb
309
312
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
310
313
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
311
314
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb