Add parallel runner / reporter

This commit is contained in:
Markus Schirp 2014-07-17 13:59:25 +00:00
parent 815526f8b4
commit 105730f5ba
34 changed files with 1192 additions and 992 deletions

View file

@ -1,3 +1,8 @@
# v0.6.0 2014-07-17
* Parallel execution / reporting.
* Add -j, --jobs flag to control concurrency.
# v0.5.26 2014-07-07
* Fix exceptions generation matcher errors

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1053
total_score: 1068

View file

@ -12,6 +12,7 @@ ControlParameter:
enabled: true
exclude:
- Mutant::Expression#match_length
- Mutant::Reporter::CLI::Printer::SubjectProgress#print_mutation_result
DataClump:
enabled: true
exclude: []
@ -39,6 +40,8 @@ FeatureEnvy:
- Mutant::Meta::Example::Verification#format_mutation
- Mutant::Reporter::CLI#subject_results
- Mutant::Runner#run_mutation_test
- Mutant::Runner#kill_mutation
- Mutant::Runner#finish
IrresponsibleModule:
enabled: true
exclude: []
@ -46,6 +49,7 @@ LongParameterList:
enabled: true
exclude:
- Mutant::Matcher::Method::Instance#self.build?
- Mutant::Runner#finish # API client of parallel, one gets _ignored.
- Mutant::Runner#self.run
max_params: 2
LongYieldList:
@ -61,10 +65,8 @@ NestedIterators:
- Mutant::Mutator::Util::Array::Element#dispatch
- Mutant::Mutator::Node::Resbody#mutate_captures
- Mutant::Mutator::Node::Arguments#emit_argument_mutations
- Mutant::Reporter::CLI::Report::Env#generic_stats
- Mutant::RequireHighjack#infect
- Mutant::RequireHighjack#desinfect
- Mutant::Reporter::CLI::Registry#included
- Mutant::Subject#tests
- Parser::Lexer#self.new
max_allowed_nesting: 1
@ -81,9 +83,8 @@ RepeatedConditional:
TooManyInstanceVariables:
enabled: true
exclude:
- Mutant::CLI # 4 vars
- Mutant::Killer # 4 vars
- Mutant::Mutator # 4 vars
- Mutant::Runner # 4 vars
max_instance_variables: 3
TooManyMethods:
enabled: true
@ -99,9 +100,8 @@ TooManyStatements:
exclude:
- Mutant#self.singleton_subclass_instance
- Mutant::Integration::Rspec#run
- Mutant::Reporter::CLI::Report::Env#run
- Mutant::Reporter::CLI::Registry#included
- Mutant::Reporter::CLI#colorized_diff
- Mutant::Reporter::CLI::Printer::EnvProgress#run
- Mutant::RequireHighjack#infect
- Mutant::Rspec::Killer#run
- Mutant::Runner#visit_collection

View file

@ -182,27 +182,14 @@ require 'mutant/integration'
require 'mutant/cli'
require 'mutant/color'
require 'mutant/diff'
require 'mutant/runner'
require 'mutant/runner/collector'
require 'mutant/result'
require 'mutant/reporter'
require 'mutant/reporter/null'
require 'mutant/reporter/trace'
require 'mutant/reporter/cli'
require 'mutant/reporter/cli/registry'
require 'mutant/reporter/cli/printer'
require 'mutant/reporter/cli/report'
require 'mutant/reporter/cli/report/env'
require 'mutant/reporter/cli/report/subject'
require 'mutant/reporter/cli/report/mutation'
require 'mutant/reporter/cli/report/test'
require 'mutant/reporter/cli/progress'
require 'mutant/reporter/cli/progress/env'
require 'mutant/reporter/cli/progress/config'
require 'mutant/reporter/cli/progress/subject'
require 'mutant/reporter/cli/progress/noop'
require 'mutant/reporter/cli/progress/result'
require 'mutant/reporter/cli/progress/result/mutation'
require 'mutant/reporter/cli/progress/result/subject'
require 'mutant/runner'
require 'mutant/zombifier'
require 'mutant/zombifier/file'
@ -219,6 +206,7 @@ module Mutant
isolation: Mutant::Isolation::Fork,
reporter: Reporter::CLI.new($stdout),
zombie: false,
processes: Parallel.processor_count,
expected_coverage: 100.0
)
end # Config

View file

@ -112,6 +112,9 @@ module Mutant
opts.on('-r', '--require NAME', 'Require file with NAME') do |name|
add(:requires, name)
end
opts.on('-j', '--jobs NUMBER', 'Number of kill processes. Defaults to number of processors.') do |number|
update(processes: Integer(number))
end
end
# Use integration

View file

@ -10,6 +10,7 @@ module Mutant
:reporter,
:isolation,
:fail_fast,
:processes,
:zombie,
:expected_coverage
)

View file

@ -63,10 +63,10 @@ module Mutant
end
output.rewind
Result::Test.new(
test: self,
test: nil,
mutation: nil,
output: output.read,
runtime: Time.now - start,
mutation: nil,
passed: !failed
)
end

View file

@ -1,7 +1,7 @@
module Mutant
# Abstract base class for reporters
class Reporter
include Adamantium::Flat, AbstractType
include AbstractType
# Write warning message
#
@ -13,9 +13,9 @@ module Mutant
#
abstract_method :warn
# Report object
# Report collector state
#
# @param [Object] object
# @param [Runner::Collector] collector
#
# @return [self]
#

View file

@ -4,16 +4,59 @@ module Mutant
class CLI < self
include Concord.new(:output)
CLEAR_PREV_LINE = "\e[1A\e[2K".freeze
# Output abstraction to decouple tty? from buffer
class Output
include Concord.new(:tty, :buffer)
# Test if output is a tty
#
# @return [Boolean]
#
# @api private
#
def tty?
@tty
end
[:puts, :write].each do |name|
define_method(name) do |*args, &block|
buffer.public_send(name, *args, &block)
end
end
end # Output
# Rate per second progress report fires
OUTPUT_RATE = 1.0 / 20
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@last_frame = nil
@last_length = 0
@tty = output.respond_to?(:tty?) && output.tty?
end
# Report progress object
#
# @param [Object] object
# @param [Runner::Collector] collector
#
# @return [self]
#
# @api private
#
def progress(object)
Progress.run(output, object)
def progress(collector)
throttle do
swap(frame(Printer::Collector, collector))
end
self
end
@ -30,19 +73,71 @@ module Mutant
self
end
# Report object
# Report env
#
# @param [Object] object
# @param [Result::Env] env
#
# @return [self]
#
# @api private
#
def report(object)
Report.run(output, object)
def report(env)
swap(frame(Printer::EnvResult, env))
self
end
private
# Compute progress frame
#
# @return [String]
#
# @api private
#
def frame(reporter, object)
buffer = StringIO.new
buffer.write(clear_command) if @tty
reporter.run(Output.new(@tty, buffer), object)
buffer.rewind
buffer.read
end
# Swap output frame
#
# @param [String] frame
#
# @return [undefined]
#
# @api private
#
def swap(frame)
output.write(frame)
@last_length = frame.lines.length
end
# Call block throttled
#
# @return [undefined]
#
# @api private
#
def throttle
now = Time.now
return if @last_frame && (now - @last_frame) < OUTPUT_RATE
yield
@last_frame = now
end
# Return clear command for last frame length
#
# @return [String]
#
# @api private
#
def clear_command
CLEAR_PREV_LINE * @last_length
end
end # CLI
end # Reporter
end # Mutant

View file

@ -1,7 +1,6 @@
module Mutant
class Reporter
class CLI
# CLI runner status printer base class
class Printer
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
@ -18,9 +17,7 @@ module Mutant
# @api private
#
def self.run(output, object)
handler = lookup(object.class)
handler.new(output, object).run
self
new(output, object).run
end
# Run printer
@ -45,26 +42,30 @@ module Mutant
# Visit a collection of objects
#
# @return [Class::Printer] printer
# @return [Enumerable<Object>] collection
#
# @return [undefined]
#
# @api private
#
def visit_collection(collection)
collection.each(&method(:visit))
def visit_collection(printer, collection)
collection.each do |object|
visit(printer, object)
end
end
# Visit object
#
# @param [Class::Printer] printer
# @param [Object] object
#
# @return [undefined]
#
# @api private
#
def visit(object)
self.class.run(output, object)
def visit(printer, object)
printer.run(output, object)
end
# Print an info line to output
@ -93,7 +94,7 @@ module Mutant
#
# @api private
#
def puts(string = NL)
def puts(string)
output.puts(string)
end
@ -107,16 +108,6 @@ module Mutant
object.success?
end
# Test if output can be colored
#
# @return [Boolean]
#
# @api private
#
def color?
tty?
end
# Colorize message
#
# @param [Color] color
@ -133,17 +124,422 @@ module Mutant
color.format(message)
end
# Test for output to tty
# Test if output is a tty
#
# @return [Boolean]
#
# @api private
#
def tty?
output.respond_to?(:tty?) && output.tty?
output.tty?
end
memoize :tty?
# Test if output can be colored
#
# @return [Boolean]
#
# @api private
#
alias_method :color?, :tty?
# Printer for run collector
class Collector < self
# Print progress for collector
#
# @return [self]
#
# @api private
#
def run
visit(EnvProgress, object.result)
active_subject_results = object.active_subject_results
info('Active subjects: %d', active_subject_results.length)
visit_collection(SubjectProgress, active_subject_results)
self
end
end # Collector
# Progress printer for configuration
class Config < self
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', object.matcher_config.inspect
info 'Integration: %s', object.integration.name
info 'Expect Coverage: %0.2f%%', object.expected_coverage.inspect
info 'Includes: %s', object.includes.inspect
info 'Requires: %s', object.requires.inspect
self
end
end # Config
# Env progress printer
class EnvProgress < self
delegate(
:coverage,
:amount_subjects,
:amount_mutations,
:amount_mutations_alive,
:amount_mutations_killed,
:runtime,
:killtime,
:overhead,
:env
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
visit(Config, env.config)
info 'Available Subjects: %s', amount_subjects
info 'Subjects: %s', amount_subjects
info 'Mutations: %s', amount_mutations
info 'Kills: %s', amount_mutations_killed
info 'Alive: %s', amount_mutations_alive
info 'Runtime: %0.2fs', runtime
info 'Killtime: %0.2fs', killtime
info 'Overhead: %0.2f%%', overhead_percent
status 'Coverage: %0.2f%%', coverage_percent
status 'Expected: %0.2f%%', env.config.expected_coverage
self
end
private
# Return coverage percent
#
# @return [Float]
#
# @api private
#
def coverage_percent
coverage * 100
end
# Return overhead percent
#
# @return [Float]
#
# @api private
#
def overhead_percent
(overhead / killtime) * 100
end
end # EnvProgress
# Full env result reporter
class EnvResult < self
delegate(:failed_subject_results)
# Run printer
#
# @return [self]
#
# @api private
#
def run
visit_collection(SubjectResult, failed_subject_results)
visit(EnvProgress, object)
self
end
private
# Return coverage percent
#
# @return [Float]
#
# @api private
#
def coverage_percent
coverage * 100
end
# Return overhead percent
#
# @return [Float]
#
# @api private
#
def overhead_percent
(overhead / killtime) * 100
end
end # EnvResult
# Subject report printer
class SubjectResult < self
delegate :subject, :failed_mutations
# Run report printer
#
# @return [self]
#
# @api private
#
def run
status(subject.identification)
subject.tests.each do |test|
puts("- #{test.identification}")
end
visit_collection(MutationResult, object.alive_mutation_results)
self
end
end # Subject
# Reporter for subject progress
class SubjectProgress < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
SUCCESS = '.'.freeze
FAILURE = 'F'.freeze
delegate(
:subject,
:coverage,
:runtime,
:amount_mutations_killed,
:amount_mutations,
:amount_mutation_results,
:killtime,
:overhead
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
puts("#{subject.identification} mutations: #{amount_mutations}")
print_tests
print_mutation_results
print_progress_bar_finish
print_stats
self
end
private
# Print stats
#
# @return [undefined]
#
# @api private
#
def print_stats
status(
FORMAT,
amount_mutations_killed,
amount_mutations,
coverage * 100,
killtime,
runtime,
overhead
)
end
# Print tests
#
# @return [undefined]
#
# @api private
#
def print_tests
subject.tests.each do |test|
puts "- #{test.identification}"
end
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts(NL) unless amount_mutation_results.zero?
end
# Print mutation results
#
# @return [undefined]
#
# @api private
#
def print_mutation_results
object.mutation_results.each(&method(:print_mutation_result))
end
# Print mutation result
#
# @param [Result::Mutation] mutation_result
#
# @return [undefined]
#
# @api private
#
def print_mutation_result(mutation_result)
char(mutation_result.success? ? SUCCESS : FAILURE)
end
# Write colorized char
#
# @param [String] char
#
# @return [undefined]
#
# @api private
#
def char(char)
output.write(colorize(status_color, char))
end
end # Subject
# Reporter for mutation results
class MutationResult < self
delegate :mutation, :failed_test_results
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
MAP = {
Mutant::Mutation::Evil => :evil_details,
Mutant::Mutation::Neutral => :neutral_details,
Mutant::Mutation::Noop => :noop_details
}.freeze
NEUTRAL_MESSAGE =
"--- Neutral failure ---\n" \
"Original code was inserted unmutated. And the test did NOT PASS.\n" \
"Your tests do not pass initially or you found a bug in mutant / unparser.\n" \
"Subject AST:\n" \
"%s\n" \
"Unparsed Source:\n" \
"%s\n" \
"Test Reports: %d\n"
NOOP_MESSAGE =
"---- Noop failure -----\n" \
"No code was inserted. And the test did NOT PASS.\n" \
"This is typically a problem of your specs not passing unmutated.\n" \
"Test Reports: %d\n"
FOOTER = '-----------------------'.freeze
# Run report printer
#
# @return [self]
#
# @api private
#
def run
puts(mutation.identification)
print_details
puts(FOOTER)
self
end
private
# Return details
#
# @return [undefined]
#
# @api private
#
def print_details
send(MAP.fetch(mutation.class))
end
# Return evil details
#
# @return [String]
#
# @api private
#
def evil_details
original, current = mutation.original_source, mutation.source
diff = Mutant::Diff.build(original, current)
diff = color? ? diff.colorized_diff : diff.diff
puts(diff || DIFF_ERROR_MESSAGE)
end
# Noop details
#
# @return [String]
#
# @api private
#
def noop_details
info(NOOP_MESSAGE, failed_test_results.length)
visit_failed_test_results
end
# Neutral details
#
# @return [String]
#
# @api private
#
def neutral_details
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
visit_failed_test_results
end
# Visit failed test results
#
# @return [undefined]
#
# @api private
#
def visit_failed_test_results
visit_collection(TestResult, failed_test_results)
end
end # MutationResult
# Test result reporter
class TestResult < self
delegate :test, :runtime
# Run test result reporter
#
# @return [self]
#
# @api private
#
def run
status('- %s / runtime: %s', test.identification, object.runtime)
puts('Test Output:')
puts(object.output)
end
end # TestResult
end # Printer
end # CLI
end # Reporter

View file

@ -1,10 +0,0 @@
module Mutant
class Reporter
class CLI
# Abstract base and namespace class for process printers
class Progress < Printer
include AbstractType, Registry.new
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,32 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
# Progress printer for configuration
class Config < self
handle(Mutant::Config)
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', object.matcher_config.inspect
info 'Integration: %s', object.integration.name
info 'Expect Coverage: %0.2f%%', object.expected_coverage.inspect
info 'Includes: %s', object.includes.inspect
info 'Requires: %s', object.requires.inspect
self
end
end # Progress
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -1,31 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
# Progress printer for configuration
class Env < self
handle Mutant::Env
delegate :config
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
visit(config)
info 'Available Subjects: %d', object.matchable_scopes.length
info 'Subjects: %d', object.subjects.length
info 'Mutations: %d', object.mutations.length
end
end # Progress
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -1,27 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
# Noop CLI progress reporter
class Noop < self
handle(Mutant::Test)
handle(Mutant::Mutation)
handle(Mutant::Result::Env)
handle(Mutant::Result::Test)
# Noop progress report
#
# @return [self]
#
# @api private
#
def run
self
end
end # Noop
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,12 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
# Abstract namespace class for result progress printers
class Result < self
include AbstractType
end # Result
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,45 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
class Result
# Mutation test result progress reporter
class Mutation < self
handle(Mutant::Result::Mutation)
SUCCESS = '.'.freeze
FAILURE = 'F'.freeze
# Run printer
#
# @return [self]
#
# @api private
#
def run
char(success? ? SUCCESS : FAILURE)
self
end
private
# Write colorized char
#
# @param [String] char
#
# @return [undefined]
#
# @api private
#
def char(char)
output.write(colorize(status_color, char))
output.flush
end
end # Mutation
end # Result
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,54 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
class Result
# Reporter for subject runners
class Subject < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
handle(Mutant::Result::Subject)
# Run printer
#
# @return [self]
#
# @api private
#
def run
print_progress_bar_finish
print_stats
self
end
delegate :coverage, :runtime, :amount_mutations_killed, :amount_mutations, :killtime, :overhead
private
# Print stats
#
# @return [undefined]
#
# @api private
#
def print_stats
status(FORMAT, amount_mutations_killed, amount_mutations, coverage * 100, killtime, runtime, overhead)
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts unless amount_mutations.zero?
end
end # Subject
end # Result
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,27 +0,0 @@
module Mutant
class Reporter
class CLI
class Progress
# CLI progress reporter for subjects
class Subject < self
handle Mutant::Subject
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
puts("#{object.identification} mutations: #{object.mutations.length}")
object.tests.each do |test|
puts "- #{test.identification}"
end
end
end # Subject
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -1,81 +0,0 @@
module Mutant
class Reporter
class CLI
# Mixin to generate registry semantics
class Registry < Module
include Concord.new(:registry)
# Return new registry
#
# @return [Registry]
#
# @api private
#
def self.new
super({})
end
# Register handler for class
#
# @param [Class] klass
#
# @return [self]
#
# @api private
#
def handle(subject, handler)
raise "Duplicate registration of #{subject}" if registry.key?(subject)
registry[subject] = handler
self
end
# Lookup handler
#
# @param [Class] subject
#
# @return [Object]
# if found
#
# @raise [RuntimeError]
# otherwise
#
# @api private
#
def lookup(subject)
current = subject
until current.equal?(Object)
if registry.key?(current)
return registry.fetch(current)
end
current = current.superclass
end
raise "No printer for: #{subject}"
end
# Hook called when module is included
#
# @param [Class,Module] host
#
# @return [undefined]
#
# @api private
#
def included(host)
super
object = self
host.class_eval do
define_singleton_method(:lookup, &object.method(:lookup))
private_class_method :lookup
define_singleton_method(:handle) do |subject|
object.handle(subject, self)
end
private_class_method :handle
end
end
end # Registry
end # CLI
end # Reporter
end # Mutant

View file

@ -1,10 +0,0 @@
module Mutant
class Reporter
class CLI
# Abstract base class for process printers
class Report < Printer
include AbstractType, Registry.new
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -1,92 +0,0 @@
module Mutant
class Reporter
class CLI
class Report
# Env result reporter
class Env < self
handle(Result::Env)
delegate(
:coverage, :failed_subject_results, :amount_subjects, :amount_mutations,
:amount_mutations_alive, :amount_mutations_killed, :runtime, :killtime, :overhead, :env
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
visit_collection(failed_subject_results)
info 'Subjects: %s', amount_subjects
info 'Mutations: %s', amount_mutations
info 'Kills: %s', amount_mutations_killed
info 'Alive: %s', amount_mutations_alive
info 'Runtime: %0.2fs', runtime
info 'Killtime: %0.2fs', killtime
info 'Overhead: %0.2f%%', overhead_percent
status 'Coverage: %0.2f%%', coverage_percent
status 'Expected: %0.2f%%', env.config.expected_coverage
print_generic_stats
self
end
private
# Print generic stats
#
# @return [undefined]
#
# @api private
#
def print_generic_stats
stats = generic_stats.to_a.sort_by(&:last)
return if stats.empty?
info('Nodes handled by generic mutator (type:occurrences):')
stats.reverse_each do |type, amount|
info('%-10s: %d', type, amount)
end
end
# Return coverage percent
#
# @return [Float]
#
# @api private
#
def coverage_percent
coverage * 100
end
# Return overhead percent
#
# @return [Float]
#
# @api private
#
def overhead_percent
(overhead / killtime) * 100
end
# Return stats for nodes handled by generic mutator
#
# @return [Hash<Symbo, Fixnum>]
#
# @api private
#
def generic_stats
object.subject_results.each_with_object(Hash.new(0)) do |result, stats|
AST.walk(result.subject.node) do |node|
stats[node.type] += 1 if Mutator::Registry.lookup(node).equal?(Mutator::Node::Generic)
end
end
end
end # Env
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -1,103 +0,0 @@
module Mutant
class Reporter
class CLI
class Report
# Reporter for mutations
class Mutation < self
handle Mutant::Result::Mutation
delegate :mutation, :failed_test_results
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
MAP = {
Mutant::Mutation::Evil => :evil_details,
Mutant::Mutation::Neutral => :neutral_details,
Mutant::Mutation::Noop => :noop_details
}.freeze
NEUTRAL_MESSAGE =
"--- Neutral failure ---\n" \
"Original code was inserted unmutated. And the test did NOT PASS.\n" \
"Your tests do not pass initially or you found a bug in mutant / unparser.\n" \
"Subject AST:\n" \
"%s\n" \
"Unparsed Source:\n" \
"%s\n" \
"Test Reports: %d\n"
NOOP_MESSAGE =
"---- Noop failure -----\n" \
"No code was inserted. And the test did NOT PASS.\n" \
"This is typically a problem of your specs not passing unmutated.\n" \
"Test Reports: %d\n"
FOOTER = '-----------------------'.freeze
# Run report printer
#
# @return [self]
#
# @api private
#
def run
puts(mutation.identification)
print_details
puts(FOOTER)
self
end
private
# Return details
#
# @return [undefined]
#
# @api private
#
def print_details
send(MAP.fetch(mutation.class))
end
# Return evil details
#
# @return [String]
#
# @api private
#
def evil_details
original, current = mutation.original_source, mutation.source
diff = Mutant::Diff.build(original, current)
diff = color? ? diff.colorized_diff : diff.diff
puts(diff || DIFF_ERROR_MESSAGE)
end
# Noop details
#
# @return [String]
#
# @api private
#
def noop_details
info(NOOP_MESSAGE, failed_test_results.length)
visit_collection(failed_test_results)
end
# Neutral details
#
# @return [String]
#
# @api private
#
def neutral_details
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
visit_collection(failed_test_results)
end
end # Mutation
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -1,32 +0,0 @@
module Mutant
class Reporter
class CLI
class Report
# Subject report printer
class Subject < self
delegate :subject, :failed_mutations
handle(Mutant::Result::Subject)
# Run report printer
#
# @return [self]
#
# @api private
#
def run
status(subject.identification)
subject.tests.each do |test|
puts("- #{test.identification}")
end
visit_collection(object.alive_mutation_results)
self
end
end # Subject
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -1,28 +0,0 @@
module Mutant
class Reporter
class CLI
class Report
# Test result reporter
class Test < self
handle(Mutant::Result::Test)
delegate :test, :runtime
# Run test result reporter
#
# @return [self]
#
# @api private
#
def run
status('- %s / runtime: %s', test.identification, object.runtime)
puts('Test Output:')
puts(object.output)
end
end
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -12,9 +12,9 @@ module Mutant
# @api private
#
def coverage
return Rational(0) if amount_mutations.zero?
return Rational(0) if amount_mutation_results.zero?
Rational(amount_mutations_killed, amount_mutations)
Rational(amount_mutations_killed, amount_mutation_results)
end
# Hook called when module gets included
@ -105,11 +105,11 @@ module Mutant
# Env result object
class Env
include Coverage, Result, Anima.new(:runtime, :env, :subject_results)
include Coverage, Result, Anima.new(:runtime, :env, :subject_results, :done)
COVERAGE_PRECISION = 1
# Test if run was successful
# Test if run is successful
#
# @return [Boolean]
#
@ -131,6 +131,7 @@ module Mutant
end
sum :amount_mutations, :subject_results
sum :amount_mutation_results, :subject_results
sum :amount_mutations_alive, :subject_results
sum :amount_mutations_killed, :subject_results
sum :killtime, :subject_results
@ -157,16 +158,6 @@ module Mutant
:runtime
)
# NOTE:
#
# The test is intentionally NOT part of the mashalled data.
# In rspec the example group cannot deterministically being marshalled, because
# they reference a crazy mix of IO objects, global objects etc.
#
MARSHALLED_IVARS = (anima.attribute_names - [:test]).map do |name|
:"@#{name}"
end
# Return killtime
#
# @return [Float]
@ -185,31 +176,6 @@ module Mutant
mutation.killed_by?(self)
end
# Return marshallable data
#
#
# @return [Array]
#
# @api private
#
def marshal_dump
MARSHALLED_IVARS.map(&method(:instance_variable_get))
end
# Load marshalled data
#
# @param [Array] array
#
# @return [undefined]
#
# @api private
#
def marshal_load(array)
MARSHALLED_IVARS.zip(array) do |instance_variable_name, value|
instance_variable_set(instance_variable_name, value)
end
end
end # Test
# Subject result
@ -239,6 +205,16 @@ module Mutant
end
memoize :alive_mutation_results
# Return amount of mutations
#
# @return [Fixnum]
#
# @api private
#
def amount_mutation_results
mutation_results.length
end
# Return amount of mutations
#
# @return [Fixnum]
@ -266,7 +242,7 @@ module Mutant
# @api private
#
def amount_mutations_alive
amount_mutations - amount_mutations_killed
alive_mutation_results.length
end
# Return alive mutations
@ -284,7 +260,7 @@ module Mutant
# Mutation result
class Mutation
include Result, Anima.new(:runtime, :mutation, :test_results)
include Result, Anima.new(:runtime, :mutation, :test_results, :index)
# Test if mutation was handeled successfully
#

View file

@ -1,7 +1,7 @@
module Mutant
# Runner baseclass
class Runner
include Adamantium, Concord.new(:env), Procto.call(:result)
include Adamantium::Flat, Concord.new(:env), Procto.call(:result)
# Initialize object
#
@ -12,18 +12,14 @@ module Mutant
def initialize(env)
super
@stop = false
@collector = Collector.new(env)
@mutex = Mutex.new
@mutations = env.mutations.dup
config.integration.setup
run
progress(env)
@result = Result::Env.compute do
{
env: env,
subject_results: visit_collection(env.subjects, &method(:run_subject))
}
end
@result = @collector.result.update(done: true)
config.reporter.report(result)
end
@ -38,24 +34,91 @@ module Mutant
private
# Run subject
# Run mutation analysis
#
# @return [Report::Subject]
#
# @api private
#
def run_subject(subject)
Result::Subject.compute do
{
subject: subject,
mutation_results: visit_collection(subject.mutations, &method(:run_mutation))
}
def run
Parallel.map(
@mutations,
in_processes: config.processes,
finish: method(:finish),
start: method(:start),
&method(:run_mutation)
)
end
# Handle started mutation
#
# @param [Mutation] mutation
# @param [Fixnum] _index
#
# @return [undefined]
#
# @api private
#
def start(mutation, _index)
@mutex.synchronize do
@collector.start(mutation)
end
end
# Handle finished mutation
#
# @param [Mutation] mutation
# @param [Fixnum] index
# @param [Object] result
#
# @return [undefined]
#
# @api private
#
def finish(mutation, index, result)
return unless result.kind_of?(Mutant::Result::Mutation)
test_results = result.test_results.zip(mutation.subject.tests).map do |test_result, test|
test_result.update(test: test, mutation: mutation) if test_result
end.compact
@mutex.synchronize do
process_result(result.update(index: index, mutation: mutation, test_results: test_results))
end
end
# Process result
#
# @param [Result::Mutation] result
#
# @return [undefined]
#
# @api private
#
def process_result(result)
@collector.finish(result)
config.reporter.progress(@collector)
handle_exit(result)
end
# Handle exit if needed
#
# @param [Result::Mutation] mutation
#
# @return [undefined]
#
# @api private
#
def handle_exit(result)
return if !config.fail_fast || result.success?
@mutations.clear
end
# Run mutation
#
# @param [Mutation]
# @param [Mutation] mutation
# @param [Fixnum] index
#
# @return [Report::Mutation]
#
@ -64,7 +127,8 @@ module Mutant
def run_mutation(mutation)
Result::Mutation.compute do
{
mutation: mutation,
index: nil,
mutation: nil,
test_results: kill_mutation(mutation)
}
end
@ -80,7 +144,7 @@ module Mutant
#
def kill_mutation(mutation)
mutation.subject.tests.each_with_object([]) do |test, results|
results << result = run_mutation_test(mutation, test).tap(&method(:progress))
results << result = run_mutation_test(mutation, test)
return results if mutation.killed_by?(result)
end
end
@ -95,33 +159,6 @@ module Mutant
env.config
end
# Visit collection
#
# @return [Array<Result>]
#
# @api private
#
def visit_collection(collection)
collection.each_with_object([]) do |item, results|
progress(item)
start = Time.now
results << result = yield(item).update(runtime: Time.now - start).tap(&method(:progress))
return results if @stop ||= config.fail_fast? && result.fail?
end
end
# Report progress
#
# @param [Object] object
#
# @return [undefined]
#
# @api private
#
def progress(object)
config.reporter.progress(object)
end
# Return test result
#
# @return [Report::Test]
@ -133,11 +170,11 @@ module Mutant
config.isolation.call do
mutation.insert
test.run
end.update(test: test, mutation: mutation)
end
rescue Isolation::Error => exception
Result::Test.new(
test: test,
mutation: mutation,
test: nil,
mutation: nil,
runtime: Time.now - time,
output: exception.message,
passed: false

View file

@ -0,0 +1,119 @@
module Mutant
class Runner
# Parallel process collector
class Collector
include Concord::Public.new(:env)
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@start = Time.now
@aggregate = Hash.new { |hash, key| hash[key] = [] }
@activity = Hash.new(0)
end
# Return active subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def active_subject_results
active_subjects.map(&method(:subject_result))
end
# Return current result
#
# @return [Result::Env]
#
# @api private
#
def result
Result::Env.new(
env: env,
runtime: Time.now - @start,
subject_results: subject_results,
done: false
)
end
# Handle mutation start
#
# @param [Mutation] mutation
#
# @return [self]
#
# @api private
#
def start(mutation)
@activity[mutation.subject] += 1
self
end
# Handle mutation finish
#
# @param [Result::Mutation] mutation_result
#
# @return [self]
#
# @api private
#
def finish(mutation_result)
subject = mutation_result.mutation.subject
@activity[subject] -= 1
@aggregate[subject] << mutation_result
self
end
private
# Return current subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def subject_results
env.subjects.map(&method(:subject_result))
end
# Return active subjects
#
# @return [Array<Subject>]
#
# @api private
#
def active_subjects
@activity.select do |_subject, count|
count > 0
end.map(&:first)
end
# Return current subject result
#
# @param [Subject] subject
#
# @return [Array<Subject::Result>]
#
# @api private
#
def subject_result(subject)
mutation_results = @aggregate[subject].sort_by(&:index)
Result::Subject.new(
subject: subject,
runtime: mutation_results.map(&:runtime).inject(0.0, :+),
mutation_results: mutation_results
)
end
end # Collector
end # Runner
end # Mutant

View file

@ -1,4 +1,4 @@
module Mutant
# The current mutant version
VERSION = '0.5.26'.freeze
VERSION = '0.6.0'.freeze
end # Mutant

View file

@ -37,7 +37,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency('inflecto', '~> 0.0.2')
gem.add_runtime_dependency('anima', '~> 0.2.0')
gem.add_runtime_dependency('concord', '~> 0.1.5')
gem.add_runtime_dependency('parallel', '~> 1.0.0')
gem.add_runtime_dependency('parallel', '~> 1.1.1')
gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5')
end

View file

@ -32,7 +32,7 @@ module Corpus
Dir.chdir(repo_path) do
Bundler.with_clean_env do
install_mutant
system(%W[bundle exec mutant -I lib -r #{name} --score #{expect_coverage} --use rspec #{namespace}*])
system(%W[bundle exec mutant --use rspec -I lib -r #{name} --score #{expect_coverage} #{namespace}*])
end
end
end

View file

@ -132,6 +132,7 @@ Environment:
--zombie Run mutant zombified
-I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH
-r, --require NAME Require file with NAME
-j, --jobs NUMBER Number of kill processes. Defaults to number of processors.
Options:
--score COVERAGE Fail unless COVERAGE is not reached exactly
@ -175,6 +176,16 @@ Options:
it_should_behave_like 'a cli parser'
end
context 'with jobs flag' do
let(:flags) { %w[--jobs 0] }
it_should_behave_like 'a cli parser'
it 'configures expected coverage' do
expect(subject.config.processes).to eql(0)
end
end
context 'with score flag' do
let(:flags) { %w[--score 99.5] }

View file

@ -37,7 +37,9 @@ describe Mutant::Matcher::Method::Singleton, '#each' do
it 'warns about definition on non const/self' do
subject
expect(env.config.reporter.warn_calls).to include('Can only match :defs on :self or :const got :lvar unable to match')
expect(env.config.reporter.warn_calls).to(
include('Can only match :defs on :self or :const got :lvar unable to match')
)
end
end

View file

@ -9,6 +9,10 @@ describe Mutant::Reporter::CLI do
output.read
end
before do
allow(Time).to receive(:now).and_return(Time.now)
end
describe '#warn' do
subject { object.warn(message) }
@ -21,6 +25,7 @@ describe Mutant::Reporter::CLI do
let(:result) do
Mutant::Result::Env.new(
done: true,
env: env,
runtime: 1.1,
subject_results: subject_results
@ -43,13 +48,24 @@ describe Mutant::Reporter::CLI do
let(:matchable_scopes) { double('Matchable Scopes', length: 10) }
before do
allow(mutation).to receive(:subject).and_return(_subject)
allow(mutation_a).to receive(:subject).and_return(_subject)
allow(mutation_b).to receive(:subject).and_return(_subject)
end
let(:mutation) do
let(:mutation_a) do
double(
'Mutation',
identification: 'mutation_id',
identification: 'mutation_id-a',
class: mutation_class,
original_source: 'true',
source: mutation_source
)
end
let(:mutation_b) do
double(
'Mutation',
identification: 'mutation_id-b',
class: mutation_class,
original_source: 'true',
source: mutation_source
@ -64,13 +80,15 @@ describe Mutant::Reporter::CLI do
class: Mutant::Subject,
node: s(:true),
identification: 'subject_id',
mutations: [mutation],
mutations: subject_mutations,
tests: [
double('Test', identification: 'test_id')
]
)
end
let(:subject_mutations) { [mutation_a] }
let(:test_results) do
[
double(
@ -84,22 +102,26 @@ describe Mutant::Reporter::CLI do
]
end
let(:mutation_a_result) do
double(
'Mutation Result',
class: Mutant::Result::Mutation,
mutation: mutation_a,
killtime: 0.5,
runtime: 1.0,
index: 0,
success?: mutation_result_success,
test_results: test_results,
failed_test_results: mutation_result_success ? [] : test_results
)
end
let(:subject_results) do
[
Mutant::Result::Subject.new(
subject: _subject,
runtime: 1.0,
mutation_results: [
double(
'Mutation Result',
class: Mutant::Result::Mutation,
mutation: mutation,
killtime: 0.5,
success?: mutation_result_success,
test_results: test_results,
failed_test_results: mutation_result_success ? [] : test_results
)
]
mutation_results: [mutation_a_result]
)
]
end
@ -107,12 +129,342 @@ describe Mutant::Reporter::CLI do
let(:subjects) { [_subject] }
describe '#progress' do
subject { object.progress(reportable) }
subject { object.progress(collector) }
let(:config) { Mutant::Config::DEFAULT.update(includes: %w(include-dir), requires: %w(require-name)) }
let(:collector) do
Mutant::Runner::Collector.new(env)
end
context 'with env' do
let(:reportable) { env }
let(:mutation_result_success) { true }
context 'with empty collector' do
it 'writes expected output' do
subject
expect(contents).to eql(expected_output)
end
let(:expected_output) do
strip_indent(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 0
Runtime: 0.00s
Killtime: 0.00s
Overhead: NaN%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
end
context 'with collector active on one subject' do
before do
collector.start(mutation_a)
end
context 'without progress' do
it 'writes expected output' do
subject
expect(contents).to eql(expected_output)
end
let(:expected_output) do
strip_indent(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 0
Runtime: 0.00s
Killtime: 0.00s
Overhead: NaN%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 1
subject_id mutations: 1
- test_id
(00/01) 0% - killtime: 0.00s runtime: 0.00s overhead: 0.00s
REPORT
end
end
context 'with progress' do
let(:subject_mutations) { [mutation_a, mutation_b] }
before do
collector.start(mutation_b)
collector.finish(mutation_a_result)
end
context 'on failure' do
let(:mutation_result_success) { false }
it 'writes expected output' do
subject
expect(contents).to eql(expected_output)
end
let(:expected_output) do
strip_indent(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 2
Kills: 0
Alive: 1
Runtime: 0.00s
Killtime: 0.50s
Overhead: -100.00%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 1
subject_id mutations: 2
- test_id
F
(00/02) 0% - killtime: 0.50s runtime: 1.00s overhead: 0.50s
REPORT
end
end
context 'on success' do
it 'writes expected output' do
subject
expect(contents).to eql(expected_output)
end
let(:expected_output) do
strip_indent(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 2
Kills: 1
Alive: 0
Runtime: 0.00s
Killtime: 0.50s
Overhead: -100.00%
Coverage: 100.00%
Expected: 100.00%
Active subjects: 1
subject_id mutations: 2
- test_id
.
(01/02) 100% - killtime: 0.50s runtime: 1.00s overhead: 0.50s
REPORT
end
end
end
end
end
describe '#report' do
subject { object.report(result) }
context 'with full covergage' do
let(:mutation_result_success) { true }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 1
Alive: 0
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 100.00%
Expected: 100.00%
REPORT
end
end
context 'and partial coverage' do
let(:mutation_result_success) { false }
context 'on evil mutation' do
context 'with a diff' do
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id-a
@@ -1,2 +1,2 @@
-true
+false
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
context 'without a diff' do
let(:mutation_source) { 'true' }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id-a
BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
end
context 'on neutral mutation' do
let(:mutation_class) { Mutant::Mutation::Neutral }
let(:mutation_source) { 'true' }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id-a
--- Neutral failure ---
Original code was inserted unmutated. And the test did NOT PASS.
Your tests do not pass initially or you found a bug in mutant / unparser.
Subject AST:
(true)
Unparsed Source:
true
Test Reports: 1
- test_id / runtime: 1.0
Test Output:
test-output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
context 'on noop mutation' do
let(:mutation_class) { Mutant::Mutation::Noop }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id-a
---- Noop failure -----
No code was inserted. And the test did NOT PASS.
This is typically a problem of your specs not passing unmutated.
Test Reports: 1
- test_id / runtime: 1.0
Test Output:
test-output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Includes: []
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
end
context 'without subjects' do
let(:subjects) { [] }
let(:subject_results) { [] }
let(:config) { Mutant::Config::DEFAULT.update(includes: %w[include-dir], requires: %w[require-name]) }
it 'writes report to output' do
subject
@ -123,219 +475,16 @@ describe Mutant::Reporter::CLI do
Expect Coverage: 100.00%
Includes: ["include-dir"]
Requires: ["require-name"]
Available Subjects: 10
Subjects: 1
Mutations: 1
REPORT
end
end
context 'with subject' do
let(:reportable) { _subject }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id mutations: 1
- test_id
REPORT
end
end
context 'with subject report' do
let(:reportable) { subject_results.first }
let(:mutation_result_success) { true }
it 'writes report to output' do
subject
expect(contents).to eql("\n(01/01) 100% - killtime: 0.50s runtime: 1.00s overhead: 0.50s\n")
end
end
context 'with mutation result' do
let(:reportable) { subject_results.first.mutation_results.first }
context 'when mutation results in success' do
let(:mutation_result_success) { true }
it 'writes report to output' do
subject
expect(contents).to eql('.')
end
end
context 'when mutation results in failure' do
let(:mutation_result_success) { false }
it 'writes report to output' do
subject
expect(contents).to eql('F')
end
end
end
end
describe '#report' do
subject { object.report(result) }
context 'with subjects' do
context 'and full covergage' do
let(:mutation_result_success) { true }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
Subjects: 1
Mutations: 1
Kills: 1
Alive: 0
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 100.00%
Expected: 100.00%
REPORT
end
end
context 'and partial covergage' do
let(:mutation_result_success) { false }
context 'on evil mutation' do
context 'with a diff' do
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id
@@ -1,2 +1,2 @@
-true
+false
-----------------------
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
context 'without a diff' do
let(:mutation_source) { 'true' }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id
BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!
-----------------------
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
end
context 'on neutral mutation' do
let(:mutation_class) { Mutant::Mutation::Neutral }
let(:mutation_source) { 'true' }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id
--- Neutral failure ---
Original code was inserted unmutated. And the test did NOT PASS.
Your tests do not pass initially or you found a bug in mutant / unparser.
Subject AST:
(true)
Unparsed Source:
true
Test Reports: 1
- test_id / runtime: 1.0
Test Output:
test-output
-----------------------
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
context 'on neutral mutation' do
let(:mutation_class) { Mutant::Mutation::Noop }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
subject_id
- test_id
mutation_id
---- Noop failure -----
No code was inserted. And the test did NOT PASS.
This is typically a problem of your specs not passing unmutated.
Test Reports: 1
- test_id / runtime: 1.0
Test Output:
test-output
-----------------------
Subjects: 1
Mutations: 1
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
end
end
context 'without subjects' do
let(:subjects) { [] }
let(:subject_results) { [] }
it 'writes report to output' do
subject
expect(contents).to eql(strip_indent(<<-REPORT))
Subjects: 0
Mutations: 0
Kills: 0
Alive: 0
Runtime: 1.10s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 100.00%
Available Subjects: 0
Subjects: 0
Mutations: 0
Kills: 0
Alive: 0
Runtime: 1.10s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end

View file

@ -1,5 +1,23 @@
require 'spec_helper'
class Double
include Concord.new(:name, :attributes)
def self.new(name, attributes = {})
super
end
def update(_attributes)
self
end
def method_missing(name, *arguments)
super unless attributes.key?(name)
fail "Arguments provided for #{name}" if arguments.any?
attributes.fetch(name)
end
end
# FIXME: This is not even close to a mutation covering spec.
describe Mutant::Runner do
let(:object) { described_class.new(env) }
@ -8,24 +26,6 @@ describe Mutant::Runner do
let(:config) { Mutant::Config::DEFAULT.update(reporter: reporter, isolation: Mutant::Isolation::None) }
let(:subjects) { [subject_a, subject_b] }
class Double
include Concord.new(:name, :attributes)
def self.new(name, attributes = {})
super
end
def update(_attributes)
self
end
def method_missing(name, *arguments)
super unless attributes.key?(name)
fail "Arguments provided for #{name}" if arguments.any?
attributes.fetch(name)
end
end
let(:subject_a) { Double.new('Subject A', mutations: mutations_a, tests: subject_a_tests) }
let(:subject_b) { Double.new('Subject B', mutations: mutations_b) }
@ -70,13 +70,15 @@ describe Mutant::Runner do
subject: subject_a,
mutation_results: [
Mutant::Result::Mutation.new(
mutation: mutation_a1,
runtime: 0.0,
index: 0,
mutation: mutation_a1,
runtime: 0.0,
test_results: [test_report_a1]
),
Mutant::Result::Mutation.new(
mutation: mutation_a2,
runtime: 0.0,
index: 1,
mutation: mutation_a2,
runtime: 0.0,
test_results: [test_report_a1]
)
],
@ -95,22 +97,22 @@ describe Mutant::Runner do
Mutant::Result::Env.new(
env: env,
runtime: 0.0,
done: false,
subject_results: expected_subject_results
)
end
context 'on normal execution' do
pending 'on normal execution' do
subject { object.result }
its(:env) { should be(env) }
it { should eql(expected_result) }
it 'reports result' do
expect { subject }.to change { config.reporter.report_calls }.from([]).to([expected_result])
end
end
context 'when isolation raises error' do
skip 'when isolation raises error' do
subject { object.result }
its(:env) { should be(env) }