mutiny 0.1.0 → 0.2.0

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: 5b0cd4975a5e102b5494dea373d982f29a4360b1
4
- data.tar.gz: 84b81be52e16ec0438d308f8b41638a664c7d939
3
+ metadata.gz: 6284685da742cd3e372fb6969484dbef17fcb022
4
+ data.tar.gz: 4390a6132c28e9c9e6f705307d290ea071f0173a
5
5
  SHA512:
6
- metadata.gz: b63df8d72301fbce3ac705294ef4821fc51f076681518b2ba7b4b0936de19a30e5a09c3890464780e6b175c9ad4b9771c4dd74f4afc9a2967a1edcf0a1dff3c3
7
- data.tar.gz: 65204290c1816ab66893e05469c9425b4fc8b7d10658dc90c32b0d079e04f3f416339ced292fc18eb855910fdbabb75147cc3c9ff9c1c81502917cd232fcd0d7
6
+ metadata.gz: a144740eaa9e502506eb0c7f07e4c95cb633b61e62b41fb932d756341c5ed920564f008f511224868f4614c782169c2effb68889a64944870e054deade9ad33c
7
+ data.tar.gz: d96ccc36e280c6df7182d7f8d2eb70f88f2dbf436ded930a1d81ec6009c778d8cc4a863a4c366c70ebb78dac2a7ccad70b5173482536209266e6598229e779c9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mutiny (0.1.0)
4
+ mutiny (0.2.0)
5
5
  gli (~> 2.13.0)
6
6
  metamorpher (~> 0.2.2)
7
7
  parser (~> 2.2.2)
@@ -14,13 +14,13 @@ GEM
14
14
  adamantium (0.2.0)
15
15
  ice_nine (~> 0.11.0)
16
16
  memoizable (~> 0.4.0)
17
- aruba (0.6.2)
17
+ aruba (0.7.4)
18
18
  childprocess (>= 0.3.6)
19
19
  cucumber (>= 1.1.1)
20
20
  rspec-expectations (>= 2.7.0)
21
- ast (2.0.0)
22
- astrolabe (1.3.0)
23
- parser (>= 2.2.0.pre.3, < 3.0)
21
+ ast (2.1.0)
22
+ astrolabe (1.3.1)
23
+ parser (~> 2.2)
24
24
  attributable (0.1.0)
25
25
  builder (3.2.2)
26
26
  childprocess (0.5.6)
@@ -30,54 +30,54 @@ GEM
30
30
  concord (0.1.5)
31
31
  adamantium (~> 0.2.0)
32
32
  equalizer (~> 0.0.9)
33
- cucumber (2.0.0)
33
+ cucumber (2.0.2)
34
34
  builder (>= 2.1.2)
35
- cucumber-core (~> 1.1.3)
35
+ cucumber-core (~> 1.2.0)
36
36
  diff-lcs (>= 1.1.3)
37
37
  gherkin (~> 2.12)
38
38
  multi_json (>= 1.7.5, < 2.0)
39
39
  multi_test (>= 0.1.2)
40
- cucumber-core (1.1.3)
40
+ cucumber-core (1.2.0)
41
41
  gherkin (~> 2.12.0)
42
42
  diff-lcs (1.2.5)
43
43
  docile (1.1.5)
44
44
  equalizer (0.0.11)
45
- ffi (1.9.8)
45
+ ffi (1.9.10)
46
46
  gherkin (2.12.2)
47
47
  multi_json (~> 1.3)
48
48
  gli (2.13.1)
49
49
  ice_nine (0.11.1)
50
- json (1.8.2)
50
+ json (1.8.3)
51
51
  memoizable (0.4.2)
52
52
  thread_safe (~> 0.3, >= 0.3.1)
53
53
  metamorpher (0.2.2)
54
54
  attributable (~> 0.1.0)
55
55
  parser (~> 2.2.2)
56
56
  unparser (~> 0.2.4)
57
- multi_json (1.11.0)
57
+ multi_json (1.11.2)
58
58
  multi_test (0.1.2)
59
- parser (2.2.2.5)
59
+ parser (2.2.2.6)
60
60
  ast (>= 1.1, < 3.0)
61
61
  powerpack (0.1.1)
62
62
  procto (0.0.2)
63
63
  rainbow (2.0.0)
64
64
  rake (10.4.2)
65
- rspec (3.2.0)
66
- rspec-core (~> 3.2.0)
67
- rspec-expectations (~> 3.2.0)
68
- rspec-mocks (~> 3.2.0)
69
- rspec-core (3.2.3)
70
- rspec-support (~> 3.2.0)
71
- rspec-expectations (3.2.1)
65
+ rspec (3.3.0)
66
+ rspec-core (~> 3.3.0)
67
+ rspec-expectations (~> 3.3.0)
68
+ rspec-mocks (~> 3.3.0)
69
+ rspec-core (3.3.2)
70
+ rspec-support (~> 3.3.0)
71
+ rspec-expectations (3.3.1)
72
72
  diff-lcs (>= 1.2.0, < 2.0)
73
- rspec-support (~> 3.2.0)
74
- rspec-mocks (3.2.1)
73
+ rspec-support (~> 3.3.0)
74
+ rspec-mocks (3.3.2)
75
75
  diff-lcs (>= 1.2.0, < 2.0)
76
- rspec-support (~> 3.2.0)
77
- rspec-support (3.2.2)
78
- rubocop (0.31.0)
76
+ rspec-support (~> 3.3.0)
77
+ rspec-support (3.3.0)
78
+ rubocop (0.33.0)
79
79
  astrolabe (~> 1.3)
80
- parser (>= 2.2.2.1, < 3.0)
80
+ parser (>= 2.2.2.5, < 3.0)
81
81
  powerpack (~> 0.1)
82
82
  rainbow (>= 1.99.1, < 3.0)
83
83
  ruby-progressbar (~> 1.4)
@@ -101,13 +101,13 @@ PLATFORMS
101
101
  ruby
102
102
 
103
103
  DEPENDENCIES
104
- aruba (~> 0.6.0)
105
- bundler (~> 1.10.2)
104
+ aruba (~> 0.7.0)
105
+ bundler (~> 1.10.3)
106
106
  codeclimate-test-reporter (~> 0.4.6)
107
107
  mutiny!
108
108
  rake (~> 10.4.2)
109
- rspec (~> 3.2.0)
110
- rubocop (~> 0.31.0)
109
+ rspec (~> 3.3.0)
110
+ rubocop (~> 0.33.0)
111
111
 
112
112
  BUNDLED WITH
113
- 1.10.2
113
+ 1.10.4
data/RELEASES.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Release History
2
2
 
3
+ ## v0.2.0 (14 August 2015)
4
+ * Implement `mutiny score` mode which performs mutation analysis
5
+
3
6
  ## v0.1.0 (5 June 2015)
4
7
  Provide support for:
5
8
  * Running mutation testing commands via the `mutiny` binary
data/bin/mutiny CHANGED
@@ -32,4 +32,12 @@ command :mutate do |c|
32
32
  end
33
33
  end
34
34
 
35
+ desc 'Calculates a mutation score for your project'
36
+ long_desc 'Calculates a mutation score for your project and displays a list of surviving mutants'
37
+ command :score do |c|
38
+ c.action do
39
+ Mutiny::Mode::Score.new(@configuration).run
40
+ end
41
+ end
42
+
35
43
  exit run(ARGV)
@@ -1,9 +1,9 @@
1
1
  module Calculator
2
2
  class Min
3
3
  def run(left, right)
4
- max = left
5
- max = right if right < left
6
- max
4
+ min = left
5
+ min = right if right < left
6
+ min
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,37 @@
1
+ require_relative "results"
2
+
3
+ module Mutiny
4
+ module Analysis
5
+ class Analyser
6
+ attr_reader :mutant_set, :integration
7
+
8
+ def initialize(mutant_set:, integration:)
9
+ @mutant_set = mutant_set
10
+ @integration = integration
11
+ end
12
+
13
+ def call
14
+ analyse_all
15
+ results
16
+ end
17
+
18
+ private
19
+
20
+ def analyse_all
21
+ mutant_set.mutants.each do |mutant|
22
+ mutant.apply
23
+ test_run = integration.test(mutant.subject)
24
+ results.add(mutant, test_run)
25
+ end
26
+ end
27
+
28
+ def results
29
+ @results ||= Results.new
30
+ end
31
+
32
+ def mutant_set
33
+ @mutant_set ||= configuration.mutator.mutants_for(environment.subjects)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ module Mutiny
2
+ module Analysis
3
+ class Results
4
+ def add(mutant, test_run)
5
+ mutants << mutant
6
+ test_runs[mutant] = test_run
7
+ killed << mutant if test_run.failed?
8
+ end
9
+
10
+ def kill_count
11
+ killed.size
12
+ end
13
+
14
+ def killed?(mutant)
15
+ killed.include?(mutant)
16
+ end
17
+
18
+ def survived?(mutant)
19
+ !killed?(mutant)
20
+ end
21
+
22
+ def test_run_for(mutant)
23
+ test_runs[mutant]
24
+ end
25
+
26
+ def mutants
27
+ @mutants ||= Mutants::MutantSet.new
28
+ end
29
+
30
+ private
31
+
32
+ def killed
33
+ @killed ||= []
34
+ end
35
+
36
+ def test_runs
37
+ @test_runs ||= {}
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,23 +3,31 @@ require "stringio"
3
3
 
4
4
  module Mutiny
5
5
  class Integration
6
- class RSpec
6
+ class RSpec < self
7
7
  # This code originally based on Markus Schirp's implementation of Mutant::Integration::Rspec
8
8
  # https://github.com/mbj/mutant/blob/master/lib/mutant/integration/rspec.rb
9
9
  class Context
10
- # NB: the --fail-fast option can be used in order to find only the first failing test
11
- # CLI_OPTIONS = %w(spec --fail-fast)
12
- CLI_OPTIONS = %w(spec)
10
+ DEFAULT_CLI_OPTIONS = %w(spec)
11
+ FAIL_FAST_CLI_OPTION = %w(--fail-fast)
13
12
 
14
13
  attr_reader :runner, :world, :configuration, :output
15
14
 
16
- def initialize
15
+ def initialize(options = {})
16
+ @options = options
17
17
  @output = StringIO.new
18
18
  @world = ::RSpec.world
19
19
  @configuration = ::RSpec.configuration
20
- @runner = ::RSpec::Core::Runner.new(::RSpec::Core::ConfigurationOptions.new(CLI_OPTIONS))
20
+ @runner = ::RSpec::Core::Runner.new(::RSpec::Core::ConfigurationOptions.new(cli_options))
21
21
  @runner.setup($stderr, @output)
22
22
  end
23
+
24
+ def cli_options
25
+ if @options.fetch(:fail_fast, false)
26
+ DEFAULT_CLI_OPTIONS + FAIL_FAST_CLI_OPTION
27
+ else
28
+ DEFAULT_CLI_OPTIONS
29
+ end
30
+ end
23
31
  end
24
32
  end
25
33
  end
@@ -3,7 +3,7 @@ require_relative "test_set"
3
3
 
4
4
  module Mutiny
5
5
  class Integration
6
- class RSpec
6
+ class RSpec < self
7
7
  # This code originally based on Markus Schirp's implementation of Mutant::Integration::Rspec
8
8
  # https://github.com/mbj/mutant/blob/master/lib/mutant/integration/rspec.rb
9
9
  class Parser
@@ -2,7 +2,7 @@ require "forwardable"
2
2
 
3
3
  module Mutiny
4
4
  class Integration
5
- class RSpec
5
+ class RSpec < self
6
6
  # This code originally based on Markus Schirp's implementation of Mutant::Integration::Rspec
7
7
  # https://github.com/mbj/mutant/blob/master/lib/mutant/integration/rspec.rb
8
8
  class Runner
@@ -23,11 +23,13 @@ module Mutiny
23
23
  private
24
24
 
25
25
  def reset
26
+ @passed_examples = []
26
27
  @failed_examples = []
27
28
  end
28
29
 
29
30
  def prepare
30
31
  filter_examples
32
+ configuration.reporter.register_listener(self, :example_passed)
31
33
  configuration.reporter.register_listener(self, :example_failed)
32
34
  end
33
35
 
@@ -35,14 +37,24 @@ module Mutiny
35
37
  start = Time.now
36
38
  runner.run_specs(world.ordered_example_groups)
37
39
  output.rewind
40
+ runtime = Time.now - start
41
+ create_test_run(output.read, runtime)
42
+ end
43
+
44
+ def create_test_run(output, runtime)
38
45
  Tests::TestRun.new(
39
- tests: @test_set,
40
- failed_tests: @test_set.subset_for_examples(@failed_examples),
41
- output: output.read,
42
- runtime: Time.now - start
46
+ tests: @test_set.generalise,
47
+ passed_tests: @test_set.subset_for_examples(@passed_examples).generalise,
48
+ failed_tests: @test_set.subset_for_examples(@failed_examples).generalise,
49
+ output: output,
50
+ runtime: runtime
43
51
  )
44
52
  end
45
53
 
54
+ def example_passed(notification)
55
+ @passed_examples << notification.example
56
+ end
57
+
46
58
  def example_failed(notification)
47
59
  @failed_examples << notification.example
48
60
  end
@@ -2,13 +2,20 @@ require_relative "../../tests"
2
2
 
3
3
  module Mutiny
4
4
  class Integration
5
- class RSpec
5
+ class RSpec < self
6
6
  class Test < Tests::Test
7
- attr_reader :example
7
+ attr_reader :example, :rest
8
8
 
9
9
  def initialize(example:, **rest)
10
10
  super(rest)
11
11
  @example = example
12
+ @rest = rest
13
+ end
14
+
15
+ # Converts to a Mutiny::Tests::Test, which is independent of
16
+ # any specific testing framework
17
+ def generalise
18
+ Mutiny::Tests::Test.new(@rest)
12
19
  end
13
20
  end
14
21
  end
@@ -2,7 +2,7 @@ require_relative "../../tests"
2
2
 
3
3
  module Mutiny
4
4
  class Integration
5
- class RSpec
5
+ class RSpec < self
6
6
  class TestSet < Tests::TestSet
7
7
  def examples
8
8
  @tests.map(&:example)
@@ -11,6 +11,12 @@ module Mutiny
11
11
  def subset_for_examples(examples)
12
12
  subset { |test| examples.include?(test.example) }
13
13
  end
14
+
15
+ # Converts to a Mutiny::Tests::TestSet, which is independent of
16
+ # any specific testing framework
17
+ def generalise
18
+ Mutiny::Tests::TestSet.new(tests.map(&:generalise))
19
+ end
14
20
  end
15
21
  end
16
22
  end
@@ -6,19 +6,19 @@ module Mutiny
6
6
  class Integration
7
7
  # This code originally based on Markus Schirp's implementation of Mutant::Integration::Rspec
8
8
  # https://github.com/mbj/mutant/blob/master/lib/mutant/integration/rspec.rb
9
- class RSpec
10
- def tests
11
- Parser.new(context).call
9
+ class RSpec < self
10
+ def tests(options = {})
11
+ Parser.new(context(options)).call
12
12
  end
13
13
 
14
- def run(test_set)
15
- Runner.new(test_set, context).call
14
+ def run(test_set, options = {})
15
+ Runner.new(test_set, context(options)).call
16
16
  end
17
17
 
18
18
  private
19
19
 
20
- def context
21
- @context ||= Context.new
20
+ def context(options = {})
21
+ Context.new(options)
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,12 @@
1
+ require_relative "isolation"
2
+
3
+ module Mutiny
4
+ class Integration
5
+ def test(subject)
6
+ Isolation.call do
7
+ test_set = tests.for(subject) # TODO: is this correctly minimal?
8
+ run(test_set, fail_fast: true)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module Mutiny
2
+ class Isolation
3
+ # An inter-process communication mechanism for sending and receiving
4
+ # (marshalled) data over an IO pipe
5
+ Pipe = Struct.new(:reader, :writer) do
6
+ def self.with(&block)
7
+ IO.pipe(binmode: true) do |reader, writer|
8
+ writer.binmode
9
+ block.call(Pipe.new(reader, writer))
10
+ end
11
+ end
12
+
13
+ def receive
14
+ writer.close
15
+ Marshal.load(reader.read)
16
+ end
17
+
18
+ def send(data)
19
+ reader.close
20
+ writer.write(Marshal.dump(data))
21
+ writer.close
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module Mutiny
2
+ class Isolation
3
+ # A mechanism for temporarily silencing a stream by redirecting
4
+ # the output to the OS's null device (e.g., /dev/null)
5
+ class Vacuum
6
+ def self.silence(stream, &block)
7
+ File.open(File::NULL, File::WRONLY) do |file|
8
+ stream.reopen(file)
9
+ block.call
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'isolation/pipe'
2
+ require_relative 'isolation/vacuum'
3
+
4
+ module Mutiny
5
+ # This code originally based on Markus Schirp's implementation of Mutant::Isolation::Fork
6
+ # https://github.com/mbj/mutant/blob/master/lib/mutant/isolation.rb
7
+ class Isolation
8
+ # Runs the given block, isolating the global state so that changes cannot
9
+ # leak out to the caller's runtime
10
+ def self.call(&block)
11
+ new(block).run_in_isolation
12
+ rescue => exception
13
+ raise Error, exception
14
+ end
15
+
16
+ attr_reader :isolated_code
17
+
18
+ def initialize(isolated_code)
19
+ @isolated_code = isolated_code
20
+ end
21
+
22
+ def run_in_isolation
23
+ Pipe.with do |comms|
24
+ begin
25
+ pid = Process.fork { run_and_send_result_via(comms) }
26
+ comms.receive # wait to receive the result form the child process
27
+ ensure
28
+ Process.waitpid(pid) if pid
29
+ end
30
+ end
31
+ end
32
+
33
+ def run_and_send_result_via(comms)
34
+ Vacuum.silence($stderr) do
35
+ result = isolated_code.call
36
+ comms.send(result) # send the result to the parent process over the pipes
37
+ end
38
+ end
39
+
40
+ Error = Class.new(RuntimeError)
41
+ end
42
+ end
@@ -51,7 +51,7 @@ module Mutiny
51
51
  end
52
52
 
53
53
  def test_set
54
- @test_set ||= configuration.integration.tests.for(environment.subjects)
54
+ @test_set ||= configuration.integration.tests.for_all(environment.subjects)
55
55
  end
56
56
 
57
57
  def test_run
@@ -0,0 +1,49 @@
1
+ require_relative "../analysis/analyser"
2
+ require_relative "../output/table"
3
+
4
+ module Mutiny
5
+ class Mode
6
+ class Score < self
7
+ def run
8
+ report "Scoring..."
9
+ report "#{mutant_set.size} mutants, #{results.kill_count} killed"
10
+ report ""
11
+ report summary
12
+ end
13
+
14
+ private
15
+
16
+ def summary
17
+ Output::Table.new.tap do |summary|
18
+ summary.add_row(summary_header)
19
+ summary.add_rows(results.mutants.ordered.map { |m| summarise(m) })
20
+ end
21
+ end
22
+
23
+ def summary_header
24
+ ["Mutant", "Status", "# Tests", "Time"]
25
+ end
26
+
27
+ def summarise(mutant)
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]
34
+ end
35
+
36
+ def results
37
+ @results ||= analyser.call
38
+ end
39
+
40
+ def analyser
41
+ Analysis::Analyser.new(mutant_set: mutant_set, integration: configuration.integration)
42
+ end
43
+
44
+ def mutant_set
45
+ @mutant_set ||= configuration.mutator.mutants_for(environment.subjects)
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/mutiny/mode.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "mode/check"
2
2
  require_relative "mode/mutate"
3
+ require_relative "mode/score"
3
4
 
4
5
  module Mutiny
5
6
  class Mode
@@ -5,21 +5,22 @@ module Mutiny
5
5
  class Mutant
6
6
  attr_reader :subject, :code
7
7
 
8
- def initialize(subject:, code:)
8
+ def initialize(subject: nil, code:)
9
9
  @subject = subject
10
10
  @code = code
11
11
  end
12
12
 
13
- def store(directory, index)
14
- filename = subject.relative_path.sub(/\.rb$/, ".#{index}.rb")
15
- path = File.join(directory, filename)
16
-
17
- FileUtils.mkdir_p(File.dirname(path))
18
- File.open(path, 'w') { |f| f.write(code) }
13
+ def apply
14
+ # Evaluate the mutanted code, overidding any existing version.
15
+ # We evaluate in the context of TOPLEVEL_BINDING as we want
16
+ # the unit to be evaluated in its usual namespace.
17
+ # rubocop:disable Eval
18
+ eval(code, TOPLEVEL_BINDING)
19
+ # rubocop:enable Eval
19
20
  end
20
21
 
21
22
  def eql?(other)
22
- is_a?(other.class) && other.subject == subject && other.code == code
23
+ other.subject == subject && other.code == code
23
24
  end
24
25
 
25
26
  alias_method "==", "eql?"
@@ -3,39 +3,44 @@ require_relative "mutant"
3
3
  module Mutiny
4
4
  module Mutants
5
5
  class MutantSet
6
- def initialize
7
- @mutants_by_subject = Hash.new([])
8
- end
9
-
10
- def add(subject, mutated_code)
11
- mutants = mutated_code.map { |code| create_mutant(subject, code) }
12
- @mutants_by_subject[subject] = @mutants_by_subject[subject] + mutants
13
- end
6
+ extend Forwardable
7
+ def_delegators :mutants, :size, :<<, :concat
14
8
 
15
- def size
16
- mutants.size
9
+ def mutants
10
+ @mutants ||= []
17
11
  end
18
12
 
19
13
  def group_by_subject
20
- @mutants_by_subject.dup
14
+ mutants.group_by(&:subject).dup
21
15
  end
22
16
 
23
- def store(mutant_directory = ".mutants")
24
- group_by_subject.each do |_, mutants|
25
- mutants.each_with_index { |mutant, index| mutant.store(mutant_directory, index) }
17
+ def ordered
18
+ group_by_subject.flat_map do |_, mutants|
19
+ mutants.map.with_index do |mutant, index|
20
+ OrderedMutant.new(mutant, index)
21
+ end
26
22
  end
27
23
  end
28
24
 
29
- protected
30
-
31
- def create_mutant(subject, code)
32
- Mutant.new(subject: subject, code: code)
25
+ def store(mutant_directory = ".mutants")
26
+ ordered.each { |m| m.store(mutant_directory) }
33
27
  end
34
28
 
35
- private
29
+ class OrderedMutant < SimpleDelegator
30
+ def initialize(mutant, number)
31
+ super(mutant)
32
+ @number = number
33
+ end
36
34
 
37
- def mutants
38
- @mutants_by_subject.values.flatten
35
+ def identifier
36
+ subject.relative_path.sub(/\.rb$/, ".#{@number}.rb")
37
+ end
38
+
39
+ def store(directory)
40
+ path = File.join(directory, identifier)
41
+ FileUtils.mkdir_p(File.dirname(path))
42
+ File.open(path, 'w') { |f| f.write(code) }
43
+ end
39
44
  end
40
45
  end
41
46
  end
@@ -14,11 +14,18 @@ module Mutiny
14
14
  MutantSet.new.tap do |mutants|
15
15
  @mutations.each do |mutation|
16
16
  subjects.each do |subject|
17
- mutants.add(subject, mutation.mutate_file(subject.path))
17
+ mutated_codes = mutation.mutate_file(subject.path)
18
+ mutants.concat(create_mutants(subject, mutated_codes))
18
19
  end
19
20
  end
20
21
  end
21
22
  end
23
+
24
+ private
25
+
26
+ def create_mutants(subject, mutated_codes)
27
+ mutated_codes.map { |code| Mutant.new(subject: subject, code: code) }
28
+ end
22
29
  end
23
30
  end
24
31
  end
@@ -0,0 +1,35 @@
1
+ module Mutiny
2
+ module Output
3
+ class Table
4
+ def initialize
5
+ @rows = []
6
+ end
7
+
8
+ def add_rows(rows)
9
+ rows.each { |r| add_row(r) }
10
+ end
11
+
12
+ def add_row(cells)
13
+ @rows << cells
14
+ end
15
+
16
+ def to_s
17
+ @rows.map { |r| row_to_s(r) }.join("\n")
18
+ end
19
+
20
+ def row_to_s(cells)
21
+ "| " +
22
+ cells.each_with_index.map { |cell, index| cell_to_s(cell, index) }.join(" | ") +
23
+ " |"
24
+ end
25
+
26
+ def cell_to_s(cell, column_index)
27
+ cell.to_s.ljust(width_for_column(column_index))
28
+ end
29
+
30
+ def width_for_column(index)
31
+ @rows.map { |r| r[index].to_s.size }.max
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,18 +1,27 @@
1
1
  module Mutiny
2
2
  module Tests
3
3
  class TestRun
4
- attr_reader :tests, :failed_tests, :output, :runtime
4
+ attr_reader :tests, :passed_tests, :failed_tests, :output, :runtime
5
5
 
6
- def initialize(tests:, failed_tests:, output:, runtime:)
6
+ def initialize(tests:, passed_tests:, failed_tests:, output:, runtime:)
7
7
  @tests = tests
8
+ @passed_tests = passed_tests
8
9
  @failed_tests = failed_tests
9
10
  @output = output
10
11
  @runtime = runtime
11
12
  end
12
13
 
14
+ def executed_count
15
+ passed_tests.size + failed_tests.size
16
+ end
17
+
13
18
  def passed?
14
19
  failed_tests.empty?
15
20
  end
21
+
22
+ def failed?
23
+ !passed?
24
+ end
16
25
  end
17
26
  end
18
27
  end
@@ -18,10 +18,14 @@ module Mutiny
18
18
  tests.map(&:location)
19
19
  end
20
20
 
21
- def for(subject_set)
21
+ def for_all(subject_set)
22
22
  subset { |test| subject_set.names.include?(test.expression) }
23
23
  end
24
24
 
25
+ def for(subject)
26
+ subset { |test| subject.name == test.expression }
27
+ end
28
+
25
29
  def subset(&block)
26
30
  self.class.new(tests.select(&block))
27
31
  end
@@ -1,3 +1,3 @@
1
1
  module Mutiny
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/mutiny.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "mutiny/integration"
1
2
  require_relative "mutiny/configuration"
2
3
  require_relative "mutiny/subjects"
3
4
  require_relative "mutiny/tests"
data/mutiny.gemspec CHANGED
@@ -23,10 +23,10 @@ Gem::Specification.new do |spec|
23
23
  spec.add_runtime_dependency "gli", "~> 2.13.0"
24
24
  spec.add_runtime_dependency "metamorpher", "~> 0.2.2"
25
25
 
26
- spec.add_development_dependency "bundler", "~> 1.10.2"
26
+ spec.add_development_dependency "bundler", "~> 1.10.3"
27
27
  spec.add_development_dependency "rake", "~> 10.4.2"
28
- spec.add_development_dependency "rspec", "~> 3.2.0"
29
- spec.add_development_dependency "aruba", "~> 0.6.0"
28
+ spec.add_development_dependency "rspec", "~> 3.3.0"
29
+ spec.add_development_dependency "aruba", "~> 0.7.0"
30
30
  spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.6"
31
- spec.add_development_dependency "rubocop", "~> 0.31.0"
31
+ spec.add_development_dependency "rubocop", "~> 0.33.0"
32
32
  end
@@ -0,0 +1,35 @@
1
+ describe "Using Mutiny to generate mutants" do
2
+ before(:each) do
3
+ cd "calculator"
4
+ run "bundle exec mutiny mutate"
5
+ run "bundle exec mutiny score"
6
+ end
7
+
8
+ it "should report a mutation score" do
9
+ expected_output = "Scoring...\n" \
10
+ "14 mutants, 12 killed\n"
11
+
12
+ expect(all_output).to include(expected_output)
13
+ end
14
+
15
+ it "should report status of mutants" do
16
+ expect(all_output).to include("calculator/min.0.rb | killed")
17
+ expect(all_output).to include("calculator/min.1.rb | killed")
18
+ expect(all_output).to include("calculator/min.2.rb | survived")
19
+ expect(all_output).to include("calculator/min.3.rb | killed")
20
+ expect(all_output).to include("calculator/min.4.rb | killed")
21
+ expect(all_output).to include("calculator/min.5.rb | killed")
22
+ expect(all_output).to include("calculator/min.6.rb | killed")
23
+ expect(all_output).to include("calculator/max.0.rb | killed")
24
+ expect(all_output).to include("calculator/max.1.rb | killed")
25
+ expect(all_output).to include("calculator/max.2.rb | killed")
26
+ expect(all_output).to include("calculator/max.3.rb | killed")
27
+ expect(all_output).to include("calculator/max.4.rb | killed")
28
+ expect(all_output).to include("calculator/max.5.rb | killed")
29
+ expect(all_output).to include("calculator/max.6.rb | survived")
30
+ end
31
+
32
+ it "should fail fast (not execute all tests for some killed mutants)" do
33
+ expect(all_output).to include("2 (of 3)")
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ require "mutiny/isolation"
2
+
3
+ module Mutiny
4
+ # This code originally based on Markus Schirp's implementation of Mutant::Isolation::Fork
5
+ # https://github.com/mbj/mutant/blob/master/spec/unit/mutant/isolation_spec.rb
6
+ describe Isolation do
7
+ subject { described_class }
8
+
9
+ before do
10
+ @initial = 1
11
+ end
12
+
13
+ describe '.run' do
14
+ it 'does isolate side effects' do
15
+ subject.call { @initial = 2 }
16
+ expect(@initial).to be(1)
17
+ end
18
+
19
+ it 'return block value' do
20
+ expect(subject.call { :foo }).to be(:foo)
21
+ end
22
+
23
+ it 'wraps exceptions' do
24
+ expect { subject.call { fail } }.to raise_error(Isolation::Error, 'marshal data too short')
25
+ end
26
+
27
+ it 'redirects $stderr of children to /dev/null' do
28
+ begin
29
+ Tempfile.open('mutiny-test') do |file|
30
+ $stderr = file
31
+ subject.call { $stderr.puts('test') }
32
+ file.rewind
33
+ expect(file.read).to eql('')
34
+ end
35
+ ensure
36
+ $stderr = STDERR
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,56 +1,18 @@
1
1
  module Mutiny
2
2
  module Mutants
3
- class ObservableMutantSet < MutantSet
4
- def create_mutant(subject, code)
5
- MutantSpy.new(subject: subject, code: code)
6
- end
7
- end
8
-
9
- class MutantSpy < Mutant
10
- attr_reader :directory, :index
11
-
12
- def store(directory, index)
13
- @directory = directory
14
- @index = index
15
- end
16
- end
17
-
18
3
  describe MutantSet do
19
- subject(:mutant_set) { ObservableMutantSet.new }
4
+ subject(:mutant_set) { MutantSet.new }
5
+ let(:m1) { Mutant.new(subject: :min, code: :min_mutant_1) }
6
+ let(:m2) { Mutant.new(subject: :min, code: :min_mutant_2) }
7
+ let(:m3) { Mutant.new(subject: :max, code: :max_mutant_1) }
8
+ let(:m4) { Mutant.new(subject: :min, code: :min_mutant_3) }
20
9
 
21
10
  before(:each) do
22
- mutant_set.add(:min, [:min_mutant_1, :min_mutant_2])
23
- mutant_set.add(:max, [:max_mutant_1])
24
- mutant_set.add(:min, [:min_mutant_3])
25
- end
26
-
27
- it "groups mutants by subject" do
28
- groups = mutant_set.group_by_subject.to_a
29
- first = groups.first
30
- second = groups.last
31
-
32
- expect(first).to eq(mutants_for(:min, :min_mutant_1, :min_mutant_2, :min_mutant_3))
33
- expect(second).to eq(mutants_for(:max, :max_mutant_1))
34
- end
35
-
36
- it "counts mutants" do
37
- expect(mutant_set.size).to eq(4)
38
- end
39
-
40
- it "stores by delegating to mutants" do
41
- mutant_set.store(:mutant_dir)
42
-
43
- mutant_set.group_by_subject.each do |_, mutants|
44
- mutants.each_with_index do |mutant, index|
45
- expect(mutant.directory).to eq(:mutant_dir)
46
- expect(mutant.index).to eq(index)
47
- end
48
- end
11
+ mutant_set << m1 << m2 << m3 << m4
49
12
  end
50
13
 
51
- def mutants_for(subject, *code)
52
- mutants = code.map { |c| Mutant.new(subject: subject, code: c) }
53
- [subject, mutants]
14
+ it "orders mutants by subject and index" do
15
+ expect(mutant_set.ordered).to eq([m1, m2, m4, m3])
54
16
  end
55
17
  end
56
18
  end
@@ -0,0 +1,17 @@
1
+ module Mutiny
2
+ module Mutants
3
+ describe Mutant do
4
+ subject(:mutant) { Mutant.new(code: "class Hello; def run; 'Goodbye'; end; end") }
5
+
6
+ it "overrides existing code when applied" do
7
+ expect { mutant.apply }.to change { Hello.new.run }.from("Hello").to("Goodbye")
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ class Hello
14
+ def run
15
+ "Hello"
16
+ end
17
+ end
@@ -61,7 +61,7 @@ module Mutiny
61
61
 
62
62
  it "should function in the presence of anonymous modules" do
63
63
  in_sub_process do
64
- Class.new # create anonymous class
64
+ Class.new # create anonymous class
65
65
  expect(environment.subjects.names).to eq(mutatable_module_names)
66
66
  end
67
67
  end
@@ -35,31 +35,31 @@ module Mutiny
35
35
 
36
36
  context "for" do
37
37
  it "should return only those tests (whose expression) matches a subject" do
38
- subject_set = subject_set_for("Max", "Min")
38
+ subjects = subject_set_for("Max", "Min")
39
39
  test_set = test_set_for("Subtract", "Min", "Add")
40
40
 
41
- expect(test_set.for(subject_set)).to eq(test_set.subset { |t| t.expression == "Min" })
41
+ expect(test_set.for_all(subjects)).to eq(test_set.subset { |t| t.expression == "Min" })
42
42
  end
43
43
 
44
44
  it "should return multiple tests for a single subject" do
45
- subject_set = subject_set_for("Min")
45
+ subjects = subject_set_for("Min")
46
46
  test_set = test_set_for("Min", "Max", "Min", "Max", "Min")
47
47
 
48
- expect(test_set.for(subject_set)).to eq(test_set.subset { |t| t.expression == "Min" })
48
+ expect(test_set.for_all(subjects)).to eq(test_set.subset { |t| t.expression == "Min" })
49
49
  end
50
50
 
51
51
  it "should return no tests when there are no tests" do
52
- subject_set = subject_set_for("Max", "Min")
52
+ subjects = subject_set_for("Max", "Min")
53
53
  test_set = TestSet.empty
54
54
 
55
- expect(test_set.for(subject_set)).to eq(TestSet.empty)
55
+ expect(test_set.for_all(subjects)).to eq(TestSet.empty)
56
56
  end
57
57
 
58
58
  it "should return no tests when there are no relevant subjects" do
59
- subject_set = subject_set_for("Max", "Min")
59
+ subjects = subject_set_for("Max", "Min")
60
60
  test_set = test_set_for("Subtract", "Add")
61
61
 
62
- expect(test_set.for(subject_set)).to eq(TestSet.empty)
62
+ expect(test_set.for_all(subjects)).to eq(TestSet.empty)
63
63
  end
64
64
 
65
65
  def subject_set_for(*names)
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.1.0
4
+ version: 0.2.0
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-06-05 00:00:00.000000000 Z
11
+ date: 2015-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.10.2
75
+ version: 1.10.3
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 1.10.2
82
+ version: 1.10.3
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,28 +100,28 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 3.2.0
103
+ version: 3.3.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 3.2.0
110
+ version: 3.3.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: aruba
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 0.6.0
117
+ version: 0.7.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 0.6.0
124
+ version: 0.7.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: codeclimate-test-reporter
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 0.31.0
145
+ version: 0.33.0
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 0.31.0
152
+ version: 0.33.0
153
153
  description: A tiny and experimental mutation testing framework for exploring research
154
154
  ideas.
155
155
  email:
@@ -189,16 +189,23 @@ files:
189
189
  - examples/untested_calculator/lib/calculator/max.rb
190
190
  - examples/untested_calculator/spec/spec_helper.rb
191
191
  - lib/mutiny.rb
192
+ - lib/mutiny/analysis/analyser.rb
193
+ - lib/mutiny/analysis/results.rb
192
194
  - lib/mutiny/configuration.rb
195
+ - lib/mutiny/integration.rb
193
196
  - lib/mutiny/integration/rspec.rb
194
197
  - lib/mutiny/integration/rspec/context.rb
195
198
  - lib/mutiny/integration/rspec/parser.rb
196
199
  - lib/mutiny/integration/rspec/runner.rb
197
200
  - lib/mutiny/integration/rspec/test.rb
198
201
  - lib/mutiny/integration/rspec/test_set.rb
202
+ - lib/mutiny/isolation.rb
203
+ - lib/mutiny/isolation/pipe.rb
204
+ - lib/mutiny/isolation/vacuum.rb
199
205
  - lib/mutiny/mode.rb
200
206
  - lib/mutiny/mode/check.rb
201
207
  - lib/mutiny/mode/mutate.rb
208
+ - lib/mutiny/mode/score.rb
202
209
  - lib/mutiny/mutants/mutant.rb
203
210
  - lib/mutiny/mutants/mutant_set.rb
204
211
  - lib/mutiny/mutants/mutation.rb
@@ -218,6 +225,7 @@ files:
218
225
  - lib/mutiny/mutants/mutation/method/unary_arithmetic_operator_insertion.rb
219
226
  - lib/mutiny/mutants/mutation_set.rb
220
227
  - lib/mutiny/mutants/ruby.rb
228
+ - lib/mutiny/output/table.rb
221
229
  - lib/mutiny/pattern.rb
222
230
  - lib/mutiny/reporter/stdout.rb
223
231
  - lib/mutiny/subjects.rb
@@ -233,13 +241,16 @@ files:
233
241
  - mutiny.gemspec
234
242
  - spec/integration/check_spec.rb
235
243
  - spec/integration/mutate_spec.rb
244
+ - spec/integration/score_spec.rb
236
245
  - spec/spec_helper.rb
237
246
  - spec/support/aruba.rb
238
247
  - spec/support/in_example_project.rb
239
248
  - spec/support/shared_examples/shared_examples_for_an_operator_replacement_mutation.rb
240
249
  - spec/unit/integration/rspec/parser_spec.rb
241
250
  - spec/unit/integration/rspec/runner_spec.rb
251
+ - spec/unit/isolation_spec.rb
242
252
  - spec/unit/mutants/mutant_set_spec.rb
253
+ - spec/unit/mutants/mutant_spec.rb
243
254
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
244
255
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
245
256
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb
@@ -284,13 +295,16 @@ summary: A tiny mutation testing framework for Ruby
284
295
  test_files:
285
296
  - spec/integration/check_spec.rb
286
297
  - spec/integration/mutate_spec.rb
298
+ - spec/integration/score_spec.rb
287
299
  - spec/spec_helper.rb
288
300
  - spec/support/aruba.rb
289
301
  - spec/support/in_example_project.rb
290
302
  - spec/support/shared_examples/shared_examples_for_an_operator_replacement_mutation.rb
291
303
  - spec/unit/integration/rspec/parser_spec.rb
292
304
  - spec/unit/integration/rspec/runner_spec.rb
305
+ - spec/unit/isolation_spec.rb
293
306
  - spec/unit/mutants/mutant_set_spec.rb
307
+ - spec/unit/mutants/mutant_spec.rb
294
308
  - spec/unit/mutants/mutations/method/binary_operator_replacement_spec.rb
295
309
  - spec/unit/mutants/mutations/method/conditional_operator_deletion_spec.rb
296
310
  - spec/unit/mutants/mutations/method/conditional_operator_insertion_spec.rb