mutiny 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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