Use actor based parallelization

This commit is contained in:
Markus Schirp 2014-10-23 11:37:53 +00:00
parent ae7284f39a
commit e08d3b6b80
48 changed files with 2451 additions and 1013 deletions

View file

@ -1,3 +1,7 @@
# v0.7.0 2014-11-xx
* Use homegrown actor based parallelization
# v0.6.7 2014-11-17
* Fix duplicate neutral emit for memoized instance method subjects

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1114
total_score: 1179

View file

@ -12,7 +12,6 @@ ControlParameter:
enabled: true
exclude:
- Mutant::Expression#match_length
- Mutant::Reporter::CLI::Printer::SubjectProgress#print_mutation_result
DataClump:
enabled: true
exclude: []
@ -26,8 +25,6 @@ DuplicateMethodCall:
FeatureEnvy:
enabled: true
exclude:
# Nature of OptionParser :(
- Mutant::CLI#add_environment_options
- Mutant::Env#scope_name
- Mutant::Diff#minimized_hunks
- Mutant::Integration::Rspec#run
@ -35,15 +32,13 @@ FeatureEnvy:
- Mutant::Integration::Rspec::Rspec3#full_description
- Mutant::Matcher::Method::Instance#match?
- Mutant::Matcher::Method::Singleton#receiver?
- Mutant::Mutation::Evil#success?
- Mutant::Mutation::Neutral#success?
- Mutant::Mutator::Node#children_indices
# False positive, its a utility
- Mutant::Meta::Example::Verification#format_mutation
- Mutant::Meta::Example::Verification#format_mutation # False positive, its a utility
- Mutant::Reporter::CLI#subject_results
- Mutant::Runner#run_mutation_test
- Mutant::Runner#kill_mutation
- Mutant::Runner#finish
- Mutant::Runner::Master#stop_worker
- Mutant::Runner::Worker#run_mutation
- Mutant::Runner::Worker#handle
IrresponsibleModule:
enabled: true
exclude: []
@ -51,8 +46,6 @@ 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:
enabled: true
@ -80,41 +73,32 @@ RepeatedConditional:
enabled: true
exclude:
- Mutant::Mutator
- Mutant::Reporter::CLI
- Mutant::Meta::Example::DSL
- Mutant::Runner::Master
max_ifs: 1
TooManyInstanceVariables:
enabled: true
exclude:
- Mutant::Mutator # 4 vars
- Mutant::Runner # 4 vars
- Mutant::Runner::Master # 4 vars
- Mutant::Runner::Scheduler # 4 vars
max_instance_variables: 3
TooManyMethods:
enabled: true
exclude:
- Mutant::CLI
- Mutant::Subject
- Mutant::Mutator::Node
- Mutant::Reporter::CLI
- Mutant::Runner
- Mutant::Meta::Example::Verification
max_methods: 10
TooManyStatements:
enabled: true
exclude:
- Mutant#self.singleton_subclass_instance
- Mutant::Integration::Rspec#run
- Mutant::Isolation::Fork#self.call
- Mutant::Reporter::CLI#colorized_diff
- Mutant::Reporter::CLI::Printer::EnvProgress#run
- Mutant::Reporter::CLI::Printer::Config#run
- Mutant::RequireHighjack#infect
- Mutant::Rspec::Killer#run
- Mutant::Runner#visit_collection
- Mutant::Runner#initialize
- Mutant::Runner::Mutation#run
- Mutant::Zombifier::File#self.find
- Mutant::CLI#add_debug_options
- Mutant::CLI#add_environment_options
max_statements: 7
UncommunicativeMethodName:
@ -166,8 +150,5 @@ UtilityFunction:
- Mutant::Integration::Rspec::Rspec2#new_reporter
- Mutant::Integration::Rspec::Rspec3#full_description
- Mutant::Meta::Example::Verification#format_mutation
- Mutant::Mutation::Evil#success?
- Mutant::Mutation::Neutral#success?
- Mutant::Reporter::CLI::Format::Progressive#new_buffer
- Mutant::Runner#run_mutation_test
max_helper_calls: 0

View file

@ -100,6 +100,12 @@ require 'mutant/ast/nodes'
require 'mutant/ast/named_children'
require 'mutant/ast/node_predicates'
require 'mutant/ast/meta'
require 'mutant/actor'
require 'mutant/actor/receiver'
require 'mutant/actor/sender'
require 'mutant/actor/mailbox'
require 'mutant/actor/actor'
require 'mutant/actor/env'
require 'mutant/cache'
require 'mutant/delegator'
require 'mutant/warning_filter'
@ -195,7 +201,9 @@ require 'mutant/cli'
require 'mutant/color'
require 'mutant/diff'
require 'mutant/runner'
require 'mutant/runner/collector'
require 'mutant/runner/scheduler'
require 'mutant/runner/master'
require 'mutant/runner/worker'
require 'mutant/result'
require 'mutant/reporter'
require 'mutant/reporter/null'
@ -223,6 +231,7 @@ module Mutant
reporter: Reporter::CLI.build($stdout),
zombie: false,
jobs: Mutant.ci? ? CI_DEFAULT_PROCESSOR_COUNT : Parallel.processor_count,
actor_env: Mutant::Actor::Env.new(Thread),
expected_coverage: 100.0
)
end # Config

60
lib/mutant/actor.rb Normal file
View file

@ -0,0 +1,60 @@
module Mutant
# A minimal actor implementation
module Actor
# Error raised when actor signalling protocol is violated
class ProtocolError < RuntimeError
end # ProtocolError
# Undefined message payload
Undefined = Class.new do
INSPECT = 'Mutant::Actor::Undefined'.freeze
# Return object inspection
#
# @return [String]
#
# @api private
#
def inspect
INSPECT
end
end.new
# Message being exchanged between actors
class Message
include Concord::Public.new(:type, :payload)
# Return new message
#
# @param [Symbol] type
# @param [Object] payload
#
# @return [Message]
#
def self.new(_type, _payload = Undefined)
super
end
end # Message
# Bindin to others actors sender for simple RPC
class Binding
include Concord.new(:actor, :other)
# Send message and wait for reply
#
# @param [Symbol] type
#
# @return [Object]
#
def call(type)
other.call(Message.new(type, actor.sender))
message = actor.receiver.call
fail ProtocolError, "Expected #{type} but got #{message.type}" unless type.equal?(message.type)
message.payload
end
end # Binding
end # Actor
end # Mutant

48
lib/mutant/actor/actor.rb Normal file
View file

@ -0,0 +1,48 @@
module Mutant
module Actor
# Actor object available to acting threads
class Actor
include Concord.new(:thread, :mailbox)
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@sender = mailbox.sender(thread)
end
# Return sender to this actor
#
# @return [Sender]
#
# @api private
#
attr_reader :sender
# Return receiver for messages to this actor
#
# @return [Receiver]
#
# @api private
#
def receiver
mailbox.receiver
end
# Return binding for RPC to other actors
#
# @param [Actor::Sender] other
#
# @return [Binding]
#
def bind(other)
Binding.new(self, other)
end
end # Actor
end # Actor
end # Mutant

35
lib/mutant/actor/env.rb Normal file
View file

@ -0,0 +1,35 @@
module Mutant
module Actor
# Actor root environment
class Env
include Concord.new(:thread_root)
# Spawn a new actor executing block
#
# @return [Actor::Sender]
#
# @api private
#
def spawn
mailbox = Mailbox.new
thread = thread_root.new do
yield mailbox.actor(thread_root.current)
end
mailbox.sender(thread)
end
# Return an private actor for current thread
#
# @return [Actor::Private]
#
# @api private
#
def current
Mailbox.new.actor(thread_root.current)
end
end # Env
end # Actor
end # Mutant

View file

@ -0,0 +1,53 @@
module Mutant
module Actor
# Unbound mailbox
class Mailbox
# Initialize new unbound mailbox
#
# @return [undefined]
#
# @api private
#
def initialize
@mutex = Mutex.new
@messages = []
@receiver = Receiver.new(@mutex, @messages)
freeze
end
# Return receiver
#
# @return [Receiver]
#
# @api private
#
attr_reader :receiver
# Return actor that is able to read mailbox
#
# @param [Thread] thread
#
# @return [Actor]
#
# @api private
#
def actor(thread)
Actor.new(thread, self)
end
# Return sender to mailbox
#
# @param [Thread] thread
#
# @return [Sender]
#
# @api private
#
def sender(thread)
Sender.new(thread, @mutex, @messages)
end
end # Mailbox
end # Actor
end # Mutant

View file

@ -0,0 +1,48 @@
module Mutant
module Actor
# Receiver side of an actor
class Receiver
include Concord.new(:mutex, :mailbox)
# Receives a message, blocking
#
# @return [Object]
#
# @api private
#
def call
2.times do
message = try_blocking_receive
return message unless message.equal?(Undefined)
end
fail ProtocolError
end
private
# Try a blocking receive
#
# @return [Undefined]
# if there is no message yet
#
# @return [Object]
# if there is a message
#
# @api private
#
def try_blocking_receive
@mutex.lock
if @mailbox.empty?
@mutex.unlock
Thread.stop
Undefined
else
@mailbox.shift.tap do
@mutex.unlock
end
end
end
end # Receiver
end # Actor
end # Mutant

View file

@ -0,0 +1,27 @@
module Mutant
module Actor
# Sender for messages to acting thread
class Sender
include Concord.new(:thread, :mutex, :mailbox)
# Send a message to actor
#
# @param [Object] message
#
# @return [self]
#
# @api private
#
def call(message)
mutex.synchronize do
mailbox << message
thread.run
end
self
end
end # Sender
end # Actor
end # Mutant

View file

@ -12,7 +12,8 @@ module Mutant
:fail_fast,
:jobs,
:zombie,
:expected_coverage
:expected_coverage,
:actor_env
)
[:fail_fast, :zombie, :debug].each do |name|

View file

@ -3,6 +3,10 @@ module Mutant
class Env
include Adamantium::Flat, Concord::Public.new(:config, :cache)
SEMANTICS_MESSAGE =
"Fix your lib to follow normal ruby semantics!\n" \
'{Module,Class}#name should return resolvable constant name as String or nil'.freeze
# Return new env
#
# @param [Config] config
@ -86,7 +90,7 @@ module Mutant
def scope_name(scope)
scope.name
rescue => exception
warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect} fix your lib to follow normal ruby semantics!")
warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect}. #{SEMANTICS_MESSAGE}")
nil
end
@ -106,7 +110,7 @@ module Mutant
name = scope_name(scope) or return
unless name.is_a?(String)
warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect} instead String or nil. Fix your lib to follow normal ruby semantics!")
warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}")
return
end

View file

@ -50,8 +50,7 @@ module Mutant
end
writer.close
result = Marshal.load(reader.read)
result
Marshal.load(reader.read)
rescue => exception
fail Error, exception
ensure

View file

@ -7,6 +7,30 @@ module Mutant
CODE_DELIMITER = "\0".freeze
CODE_RANGE = (0..4).freeze
# Kill mutation via isolation
#
# @param [Isolation] isolation
#
# @return [Result::Mutation]
#
# @api private
#
def kill(isolation)
result = Result::Mutation.new(
index: nil,
mutation: self,
test_results: []
)
subject.tests.reduce(result) do |current, test|
return current unless current.continue?
test_result = test.kill(isolation, self)
current.update(
test_results: current.test_results.dup << test_result
)
end
end
# Insert mutated node
#
# FIXME: Cache subject visibility in a better way! Ideally dont mutate it
@ -67,17 +91,23 @@ module Mutant
subject.source
end
# Test if mutation is killed by test report
# Test if mutation is killed by test reports
#
# @param [Report::Test] test_report
# @param [Array<Report::Test>] test_reports
#
# @return [Boolean]
#
# @api private
#
def killed_by?(test_report)
self.class::SHOULD_PASS.equal?(test_report.passed)
end
abstract_singleton_method :success?
# Test if execution can be continued
#
# @return [Boolean]
#
# @api private
#
abstract_singleton_method :continue?
private
@ -105,24 +135,65 @@ module Mutant
# Evil mutation that should case mutations to fail tests
class Evil < self
SHOULD_PASS = false
SYMBOL = 'evil'.freeze
SYMBOL = 'evil'.freeze
# Test if mutation is killed by test reports
#
# @param [Array<Report::Test>] test_reports
#
# @return [Boolean]
#
# @api private
#
def self.success?(test_results)
!test_results.all?(&:passed)
end
# Test if mutation execution can be continued
#
# @return [Boolean]
#
# @api private
#
def self.continue?(test_results)
!success?(test_results)
end
end # Evil
# Neutral mutation that should not cause mutations to fail tests
class Neutral < self
SYMBOL = 'neutral'.freeze
SHOULD_PASS = true
SYMBOL = 'neutral'.freeze
# Test if mutation is killed by test reports
#
# @param [Array<Report::Test>] test_reports
#
# @return [Boolean]
#
# @api private
#
def self.success?(test_results)
test_results.any? && test_results.all?(&:passed)
end
# Test if mutation execution can be continued
#
# @return [Boolean] _test_results
#
# @api private
#
def self.continue?(_test_results)
true
end
end # Neutral
# Noop mutation, special case of neutral
class Noop < self
class Noop < Neutral
SYMBOL = 'noop'.freeze
SHOULD_PASS = true
SYMBOL = 'noop'.freeze
end # Noop

View file

@ -17,7 +17,7 @@ module Mutant
# Return progress representation
#
# @param [Runner::Collector] collector
# @param [Runner::Status] status
#
# @return [String]
#
@ -67,6 +67,18 @@ module Mutant
# Format for progressive non rewindable output
class Progressive < self
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
@seen = Set.new
super
end
# Return start representation
#
# @return [String]
@ -83,10 +95,13 @@ module Mutant
#
# @api private
#
def progress(collector)
last_mutation_result = collector.last_mutation_result
return EMPTY_STRING unless last_mutation_result
format(Printer::MutationProgressResult, last_mutation_result)
def progress(status)
current = status.env_result.subject_results.flat_map(&:mutation_results)
new = current.reject(&@seen.method(:include?))
@seen = current.to_set
new.map do |mutation_result|
format(Printer::MutationProgressResult, mutation_result)
end.join(EMPTY_STRING)
end
private
@ -109,20 +124,6 @@ module Mutant
BUFFER_FLAGS = 'a+'.freeze
# Rate per second progress report fires
OUTPUT_RATE = 1.0 / 20
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@last_frame = nil
end
# Format start
#
# @param [Env] env
@ -137,16 +138,14 @@ module Mutant
# Format progress
#
# @param [Runner::Collector] collector
# @param [Runner::Status] status
#
# @return [String]
#
# @api private
#
def progress(collector)
throttle do
format(Printer::Collector, collector)
end.to_s
def progress(status)
format(Printer::Status, status)
end
private
@ -166,18 +165,6 @@ module Mutant
buffer << tput.restore
end
# Call block throttled
#
# @return [self]
#
# @api private
#
def throttle
now = Time.now
return if @last_frame && (now - @last_frame) < OUTPUT_RATE
yield.tap { @last_frame = now }
end
end # Framed
end # Format
end # CLI

View file

@ -5,6 +5,8 @@ module Mutant
class Printer
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
delegate(:success?)
NL = "\n".freeze
# Run printer on object to output
@ -98,16 +100,6 @@ module Mutant
output.puts(string)
end
# Test if runner was successful
#
# @return [Boolean]
#
# @api private
#
def success?
object.success?
end
# Colorize message
#
# @param [Color] color
@ -142,8 +134,10 @@ module Mutant
#
alias_method :color?, :tty?
# Printer for run collector
class Collector < self
# Printer for runner status
class Status < self
delegate(:active_jobs, :env_result)
# Print progress for collector
#
@ -152,14 +146,42 @@ module Mutant
# @api private
#
def run
visit(EnvProgress, object.result)
active_subject_results = object.active_subject_results
visit(EnvProgress, object.env_result)
info('Active subjects: %d', active_subject_results.length)
visit_collection(SubjectProgress, active_subject_results)
job_status
self
end
end # Collector
private
# Print worker status
#
# @return [undefined]
#
def job_status
return if active_jobs.empty?
info('Active Jobs:')
object.active_jobs.sort_by(&:index).each do |job|
info('%d: %s', job.index, job.mutation.identification)
end
end
# Return active subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def active_subject_results
active_subjects = active_jobs.map(&:mutation).flat_map(&:subject).to_set
env_result.subject_results.select do |subject_result|
active_subjects.include?(subject_result.subject)
end
end
end # Status
# Progress printer for configuration
class Config < self
@ -408,7 +430,7 @@ module Mutant
delegate :mutation, :failed_test_results
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!'.freeze
MAP = {
Mutant::Mutation::Evil => :evil_details,
@ -509,7 +531,7 @@ module Mutant
# Test result reporter
class TestResult < self
delegate :test, :runtime
delegate :test, :runtime, :mutation
# Run test result reporter
#
@ -518,11 +540,15 @@ module Mutant
# @api private
#
def run
status('- %s / runtime: %s', test.identification, object.runtime)
status('- %s / runtime: %s', test.identification, runtime)
puts('Test Output:')
puts(object.output)
end
def success?
false
end
end # TestResult
end # Printer
end # CLI

View file

@ -54,30 +54,8 @@ module Mutant
end
memoize name
end
# Compute result tracking runtime
#
# @return [Result]
#
# @api private
#
def compute
start = Time.now
new(yield.merge(runtime: Time.now - start))
end
end # ClassMethods
# Test if operation is failing
#
# @return [Boolean]
#
# @api private
#
def fail?
!success?
end
# Return overhead
#
# @return [Float]
@ -98,7 +76,7 @@ module Mutant
#
def self.included(host)
host.class_eval do
include Adamantium::Flat, Anima::Update
include Adamantium, Anima::Update
extend ClassMethods
end
end
@ -120,6 +98,16 @@ module Mutant
end
memoize :success?
# Test if runner should continue on env
#
# @return [Boolean]
#
# @api private
#
def continue?
!env.config.fail_fast || subject_results.all?(&:success?)
end
# Return failed subject results
#
# @return [Array<Result::Subject>]
@ -130,12 +118,21 @@ module Mutant
subject_results.reject(&:success?)
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
# Return amount of mutations
#
# @return [Fixnum]
#
# @api private
#
def amount_mutations
env.mutations.length
end
# Return amount of subjects
#
# @return [Fixnum]
@ -166,23 +163,14 @@ module Mutant
#
alias_method :killtime, :runtime
# Test if mutation test result is successful
#
# @return [Boolean]
#
# @api private
#
def success?
mutation.killed_by?(self)
end
end # Test
# Subject result
class Subject
include Coverage, Result, Anima.new(:subject, :mutation_results, :runtime)
include Coverage, Result, Anima.new(:subject, :mutation_results)
sum :killtime, :mutation_results
sum :runtime, :mutation_results
# Test if subject was processed successful
#
@ -194,6 +182,16 @@ module Mutant
alive_mutation_results.empty?
end
# Test if runner should continue on subject
#
# @return [Boolean]
#
# @api private
#
def continue?
mutation_results.all?(&:success?)
end
# Return killed mutations
#
# @return [Array<Result::Mutation>]
@ -260,7 +258,20 @@ module Mutant
# Mutation result
class Mutation
include Result, Anima.new(:runtime, :mutation, :test_results, :index)
include Result, Anima.new(:mutation, :test_results, :index)
sum :runtime, :test_results
# Return failed test results
#
# @return [Array<Result::Test>]
#
# @api private
#
def failed_test_results
test_results.reject(&:passed)
end
memoize :failed_test_results
# Test if mutation was handled successfully
#
@ -269,17 +280,17 @@ module Mutant
# @api private
#
def success?
test_results.any?(&:success?)
mutation.class.success?(test_results)
end
# Return failed test results
# Test if execution on mutation can be stopped
#
# @return [Array]
# @return [Boolean]
#
# @api private
#
def failed_test_results
test_results.select(&:fail?)
def continue?
mutation.class.continue?(test_results)
end
sum :killtime, :test_results

View file

@ -3,30 +3,58 @@ module Mutant
class Runner
include Adamantium::Flat, Concord.new(:env), Procto.call(:result)
# Status of the runner execution
class Status
include Adamantium, Anima::Update, Anima.new(
:env_result,
:active_jobs,
:done
)
end # Status
# Job to push to workers
class Job
include Adamantium::Flat, Anima.new(:index, :mutation)
end # Job
# Job result object received from workers
class JobResult
include Adamantium::Flat, Anima.new(:job, :result)
end
REPORT_FREQUENCY = 20.0
REPORT_DELAY = 1 / REPORT_FREQUENCY
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(env)
def initialize(*)
super
@collector = Collector.new(env)
@mutex = Mutex.new
@mutations = env.mutations.dup
@index = 0
@continue = true
reporter.start(env)
config.integration.setup
reporter.start(env)
@master = config.actor_env.current.bind(Master.call(env))
run
status = nil
@result = @collector.result
loop do
status = current_status
break if status.done
reporter.progress(status)
Kernel.sleep(REPORT_DELAY)
end
reporter.report(result)
reporter.progress(status)
@master.call(:stop)
@result = status.env_result
reporter.report(@result)
end
# Return result
@ -39,127 +67,14 @@ module Mutant
private
# Run mutation analysis
# Return reporter
#
# @return [Report::Subject]
# @return [Reporter]
#
# @api private
#
def run
Parallel.map(
method(:next),
in_threads: config.jobs,
finish: method(:finish),
start: method(:start),
&method(:run_mutation)
)
end
# Return next mutation or stop
#
# @return [Mutation]
# in case there is a next mutation
#
# @return [Parallel::Stop]
# in case there is no next mutation or runner should stop early
#
#
# @api private
def next
@mutex.synchronize do
mutation = @mutations.at(@index)
if @continue && mutation
@index += 1
mutation
else
Parallel::Stop
end
end
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.is_a?(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)
reporter.progress(@collector)
return unless config.fail_fast && !result.success?
@continue = false
end
# Run mutation
#
# @param [Mutation] mutation
#
# @return [Report::Mutation]
#
# @api private
#
def run_mutation(mutation)
Result::Mutation.compute do
{
index: nil,
mutation: nil,
test_results: kill_mutation(mutation)
}
end
end
# Kill mutation
#
# @param [Mutation] mutation
#
# @return [Array<Result::Test>]
#
# @api private
#
def kill_mutation(mutation)
mutation.subject.tests.each_with_object([]) do |test, results|
results << result = run_mutation_test(mutation, test)
return results if mutation.killed_by?(result)
end
def reporter
env.config.reporter
end
# Return config
@ -172,36 +87,14 @@ module Mutant
env.config
end
# Return test result
# Return current status
#
# @return [Report::Test]
# @return [Status]
#
# @api private
#
def run_mutation_test(mutation, test)
time = Time.now
config.isolation.call do
mutation.insert
test.run
end
rescue Isolation::Error => exception
Result::Test.new(
test: test,
mutation: mutation,
runtime: Time.now - time,
output: exception.message,
passed: false
)
end
# Return reporter
#
# @return [Reporter]
#
# @api private
#
def reporter
config.reporter
def current_status
@master.call(:status)
end
end # Runner

View file

@ -1,133 +0,0 @@
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] = [] }
@active = Set.new
@last_mutation_result = nil
end
# Return last mutation result
#
# @return [Result::Mutation]
# if there is a last mutation result
#
# @return [nil]
# otherwise
#
# @api private
#
attr_reader :last_mutation_result
# 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
)
end
# Register mutation start
#
# @param [Mutation] mutation
#
# @return [self]
#
# @api private
#
def start(mutation)
@active << mutation
self
end
# Handle mutation finish
#
# @param [Result::Mutation] mutation_result
#
# @return [self]
#
# @api private
#
def finish(mutation_result)
@last_mutation_result = mutation_result
mutation = mutation_result.mutation
@active.delete(mutation)
@aggregate[mutation.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
@active.each_with_object(Set.new) do |mutation, subjects|
subjects << mutation.subject
end.sort_by(&:identification)
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

168
lib/mutant/runner/master.rb Normal file
View file

@ -0,0 +1,168 @@
module Mutant
class Runner
# Master actor to control workers
class Master
include Concord.new(:env, :actor)
private_class_method :new
# Run master runner component
#
# @param [Env] env
#
# @return [Actor::Sender]
#
# @api private
#
def self.call(env)
env.config.actor_env.spawn do |actor|
new(env, actor).__send__(:run)
end
end
private
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@scheduler = Scheduler.new(env)
@workers = env.config.jobs
@stop = false
@stopping = false
end
# Run work loop
#
# @return [self]
#
# @api private
#
def run
@workers.times do |id|
Worker.run(
id: id,
config: env.config,
parent: actor.sender
)
end
receive_loop
end
# Handle messages
#
# @param [Actor::Message] message
#
# @return [undefined]
#
# @api private
#
def handle(message)
type, payload = message.type, message.payload
case type
when :ready
ready_worker(payload)
when :status
handle_status(payload)
when :result
handle_result(payload)
when :stop
handle_stop(payload)
else
fail Actor::ProtocolError, "Unexpected message: #{type.inspect}"
end
end
# Run receive loop
#
# @return [undefined]
#
# @api private
#
def receive_loop
loop do
break if @workers.zero? && @stop
handle(actor.receiver.call)
end
end
# Handle status
#
# @param [Actor::Sender] sender
#
def handle_status(sender)
sender.call(Actor::Message.new(:status, @scheduler.status))
end
# Handle result
#
# @param [JobResult] job_result
#
# @return [undefined]
#
def handle_result(job_result)
return if @stopping
@scheduler.job_result(job_result)
@stopping = env.config.fail_fast && @scheduler.status.done
end
# Handle stop
#
# @param [Actor::Sender] sender
#
# @return [undefined]
#
# @api private
#
def handle_stop(sender)
@stopping = true
@stop = true
receive_loop
sender.call(Actor::Message.new(:stop))
end
# Handle ready worker
#
# @param [Actor::Sender] sender
#
# @return [undefined]
#
# @api private
#
def ready_worker(sender)
if @stopping
stop_worker(sender)
return
end
job = @scheduler.next_job
if job
sender.call(Actor::Message.new(:job, job))
else
stop_worker(sender)
end
end
# Stop worker
#
# @param [Actor::Sender] sender
#
# @return [undefined]
#
# @api private
#
def stop_worker(sender)
@workers -= 1
sender.call(Actor::Message.new(:stop))
end
end # Master
end # Runner
end # Mutant

View file

@ -0,0 +1,141 @@
module Mutant
class Runner
# Job scheduler
class Scheduler
include Concord.new(:env)
# Initialize object
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
@index = 0
@start = Time.now
@active_jobs = Set.new
@subject_results = Hash.new do |_hash, subject|
Result::Subject.new(
subject: subject,
mutation_results: []
)
end
end
# Return runner status
#
# @return [Status]
#
# @api private
#
def status
Status.new(
env_result: env_result,
done: done?,
active_jobs: @active_jobs.dup
)
end
# Return next job
#
# @return [Job]
# in case there is a next job
#
# @return [nil]
# otherwise
#
# @api private
def next_job
return unless next_mutation?
Job.new(
mutation: mutations.fetch(@index),
index: @index
).tap do |job|
@index += 1
@active_jobs << job
end
end
# Consume job result
#
# @param [JobResult] job_result
#
# @return [self]
#
# @api private
#
def job_result(job_result)
@active_jobs.delete(job_result.job)
mutation_result(job_result.result)
self
end
private
# Test if mutation run is done
#
# @return [Boolean]
#
# @api private
#
def done?
!env_result.continue? || (!next_mutation? && @active_jobs.empty?)
end
# Handle mutation finish
#
# @param [Result::Mutation] mutation_result
#
# @return [self]
#
# @api private
#
def mutation_result(mutation_result)
mutation = mutation_result.mutation
original = @subject_results[mutation.subject]
@subject_results[mutation.subject] = original.update(
mutation_results: (original.mutation_results.dup << mutation_result)
)
end
# Test if a next mutation exist
#
# @return [Boolean]
#
# @api private
#
def next_mutation?
mutations.length > @index
end
# Return mutations
#
# @return [Array<Mutation>]
#
# @api private
#
def mutations
env.mutations
end
# Return current result
#
# @return [Result::Env]
#
# @api private
#
def env_result
Result::Env.new(
env: env,
runtime: Time.now - @start,
subject_results: @subject_results.values
)
end
end # Scheduler
end # Runner
end # Mutant

View file

@ -0,0 +1,87 @@
module Mutant
class Runner
# Mutation killing worker receiving work from parent
class Worker
include Adamantium::Flat, Anima.new(:config, :id, :parent)
private_class_method :new
# Run worker
#
# @param [Hash<Symbol, Object] attributes
#
# @return [Actor::Sender]
#
# @api private
#
def self.run(attributes)
attributes.fetch(:config).actor_env.spawn do |actor|
worker = new(attributes)
worker.send(:run, actor)
end
end
private
# Worker loop
#
# @return [self]
#
# @api private
#
# rubocop:disable Lint/Loop
#
def run(actor)
begin
parent.call(Actor::Message.new(:ready, actor.sender))
end until handle(actor.receiver.call)
end
# Handle job
#
# @param [Message] message
#
# @return [Boolean]
#
def handle(message)
type, payload = message.type, message.payload
case message.type
when :job
handle_job(payload)
nil
when :stop
true
else
fail Actor::ProtocolError, "Unknown command: #{type.inspect}"
end
end
# Handle mutation
#
# @param [Job] job
#
# @return [undefined]
#
# @api private
#
def handle_job(job)
parent.call(Actor::Message.new(:result, JobResult.new(job: job, result: run_mutation(job))))
end
# Run mutation
#
# @param [Mutation] mutation
#
# @return [Report::Mutation]
#
# @api private
#
def run_mutation(job)
job.mutation.kill(config.isolation).update(
index: job.index
)
end
end # Worker
end # Runner
end # Mutant

View file

@ -14,6 +14,33 @@ module Mutant
end
memoize :identification
# Kill mutation with test under isolation
#
# @param [Isolation] isolation
# @param [Mutation] mutation
#
# @return [Report::Test]
#
# @api private
#
def kill(isolation, mutation)
time = Time.now
isolation.call do
mutation.insert
run
end.update(test: self)
rescue Isolation::Error => exception
Result::Test.new(
test: self,
mutation: mutation,
runtime: Time.now - time,
output: exception.message,
passed: false
)
end
private
# Run test, return report
#
# @return [Report]

View file

@ -12,7 +12,7 @@ if ENV['COVERAGE'] == 'true'
add_filter 'lib/mutant/zombifier'
add_filter 'lib/mutant/zombifier/*'
minimum_coverage 97.64 # TODO: raise this to 100, then mutation test
minimum_coverage 100
end
end
@ -45,8 +45,16 @@ module ParserHelper
end
end
module MessageHelper
def message(*arguments)
Mutant::Actor::Message.new(*arguments)
end
end
RSpec.configure do |config|
config.extend(SharedContext)
config.include(CompressHelper)
config.include(MessageHelper)
config.include(ParserHelper)
config.include(Mutant::AST::Sexp)
end

View file

@ -0,0 +1,93 @@
require 'mutant/actor'
# A fake actor used from specs
module FakeActor
class Expectation
include Concord::Public.new(:name, :message)
end
class MessageSequence
include Adamantium::Flat, Concord.new(:messages)
def self.new
super([])
end
def add(name, *message_arguments)
messages << Expectation.new(name, Mutant::Actor::Message.new(*message_arguments))
self
end
def sending(expectation)
raise "Unexpected send: #{expectation.inspect}" if messages.empty?
expected = messages.shift
unless expectation.eql?(expected)
raise "Got:\n#{expectation.inspect}\nExpected:\n#{expected.inspect}"
end
self
end
def receiving(name)
raise "No message to read for #{name.inspect}" if messages.empty?
expected = messages.shift
raise "Unexpected message #{expected.inspect} for #{name.inspect}" unless expected.name.eql?(name)
expected.message
end
def consumed?
messages.empty?
end
end
class Env
include Concord.new(:messages, :actor_names)
def spawn
name = @actor_names.shift
raise 'Tried to spawn actor when no name available' unless name
actor = actor(name)
yield actor if block_given?
actor.sender
end
def current
actor(:current)
end
def actor(name)
Actor.new(name, @messages)
end
end # Env
class Actor
include Concord.new(:name, :messages)
def receiver
Receiver.new(name, messages)
end
def sender
Sender.new(name, messages)
end
def bind(sender)
Mutant::Actor::Binding.new(self, sender)
end
end
class Sender
include Concord.new(:name, :messages)
def call(message)
messages.sending(Expectation.new(name, message))
end
end # Sender
class Receiver
include Concord::Public.new(:name, :messages)
def call
messages.receiving(name)
end
end
end

View file

@ -0,0 +1,157 @@
module SharedContext
def update(name, &block)
define_method(name) do
super().update(instance_eval(&block))
end
end
def messages(&block)
let(:message_sequence) do
FakeActor::MessageSequence.new.tap do |sequence|
sequence.instance_eval(&block)
end
end
end
# rubocop:disable MethodLength
def setup_shared_context
let(:env) { double('env', config: config, subjects: [subject_a], mutations: mutations) }
let(:job_a) { Mutant::Runner::Job.new(index: 0, mutation: mutation_a) }
let(:job_b) { Mutant::Runner::Job.new(index: 1, mutation: mutation_b) }
let(:job_a_result) { Mutant::Runner::JobResult.new(job: job_a, result: mutation_a_result) }
let(:job_b_result) { Mutant::Runner::JobResult.new(job: job_b, result: mutation_b_result) }
let(:mutations) { [mutation_a, mutation_b] }
let(:matchable_scopes) { double('matchable scopes', length: 10) }
let(:test_a) { double('test a', identification: 'test-a') }
let(:test_b) { double('test b', identification: 'test-b') }
let(:actor_names) { [] }
let(:message_sequence) { FakeActor::MessageSequence.new }
let(:config) do
Mutant::Config::DEFAULT.update(
actor_env: actor_env,
jobs: 1,
reporter: Mutant::Reporter::Trace.new
)
end
let(:actor_env) do
FakeActor::Env.new(message_sequence, actor_names)
end
let(:subject_a) do
double(
'subject a',
node: s(:true),
source: 'true',
tests: [test_a],
identification: 'subject-a'
)
end
before do
allow(subject_a).to receive(:mutations).and_return([mutation_a, mutation_b])
end
let(:empty_status) do
Mutant::Runner::Status.new(
active_jobs: Set.new,
env_result: env_result.update(subject_results: [], runtime: 0.0),
done: false
)
end
let(:status) do
Mutant::Runner::Status.new(
active_jobs: Set.new,
env_result: env_result,
done: true
)
end
let(:env_result) do
Mutant::Result::Env.new(
env: env,
runtime: 4.0,
subject_results: [subject_a_result]
)
end
let(:mutation_a_node) { s(:false) }
let(:mutation_b_node) { s(:nil) }
let(:mutation_b) { Mutant::Mutation::Evil.new(subject_a, mutation_b_node) }
let(:mutation_a) { Mutant::Mutation::Evil.new(subject_a, mutation_a_node) }
let(:mutation_a_result) do
Mutant::Result::Mutation.new(
index: 1,
mutation: mutation_a,
test_results: [mutation_a_test_a_result, mutation_a_test_b_result]
)
end
let(:mutation_b_result) do
Mutant::Result::Mutation.new(
index: 1,
mutation: mutation_a,
test_results: [mutation_b_test_a_result, mutation_b_test_b_result]
)
end
let(:mutation_a_test_a_result) do
Mutant::Result::Test.new(
mutation: mutation_a,
test: test_a,
passed: false,
runtime: 1.0,
output: 'mutation a test a result output'
)
end
let(:mutation_a_test_b_result) do
Mutant::Result::Test.new(
mutation: mutation_b,
test: test_b,
passed: false,
runtime: 1.0,
output: 'mutation a test b result output'
)
end
let(:mutation_b_test_a_result) do
Mutant::Result::Test.new(
mutation: mutation_b,
test: test_a,
passed: false,
runtime: 1.0,
output: 'mutation b test a result output'
)
end
let(:mutation_b_test_b_result) do
Mutant::Result::Test.new(
mutation: mutation_b,
test: test_b,
passed: false,
runtime: 1.0,
output: 'mutation b test b result output'
)
end
let(:subject_a_result) do
Mutant::Result::Subject.new(
subject: subject_a,
mutation_results: [mutation_a_result, mutation_b_result]
)
end
let(:empty_subject_a_result) do
subject_a_result.update(mutation_results: [])
end
let(:partial_subject_a_result) do
subject_a_result.update(mutation_results: [mutation_a_result])
end
end
end

View file

@ -0,0 +1,35 @@
RSpec.describe Mutant::Actor do
let(:mutex) { double('Mutex') }
let(:thread) { double('Thread') }
before do
expect(Mutex).to receive(:new).and_return(mutex)
end
describe Mutant::Actor::Actor do
let(:mailbox) { Mutant::Actor::Mailbox.new }
let(:object) { described_class.new(thread, mailbox) }
describe '#bind' do
let(:other) { double('Sender') }
subject { object.bind(other) }
it { should eql(Mutant::Actor::Binding.new(object, other)) }
end
describe '#sender' do
subject { object.sender }
it { should eql(Mutant::Actor::Sender.new(thread, mutex, [])) }
end
describe '#receiver' do
subject { object.receiver }
it 'returns receiver' do
should eql(Mutant::Actor::Receiver.new(mutex, []))
end
end
end
end

View file

@ -0,0 +1,32 @@
RSpec.describe Mutant::Actor::Binding do
let(:actor_a) { double('Actor-A', sender: sender_a, receiver: receiver_a) }
let(:sender_a) { double('Sender-A') }
let(:sender_b) { double('Sender-B') }
let(:receiver_a) { double('Receiver-A') }
let(:payload) { double('Payload') }
let(:type) { double('Type') }
let(:object) { described_class.new(actor_a, sender_b) }
describe '#call' do
subject { object.call(type) }
before do
expect(sender_b).to receive(:call).with(message(type, sender_a)).ordered
expect(receiver_a).to receive(:call).ordered.and_return(message(response_type, payload))
end
context 'when return type equals request type' do
let(:response_type) { type }
it { should be(payload) }
end
context 'when return type NOT equals request type' do
let(:response_type) { double('Other Type') }
it 'raises error' do
expect { subject }.to raise_error(Mutant::Actor::ProtocolError, "Expected #{type} but got #{response_type}")
end
end
end
end

View file

@ -0,0 +1,49 @@
RSpec.describe Mutant::Actor::Env do
let(:mutex) { double('Mutex') }
let(:thread) { double('Thread') }
let(:thread_root) { double('Thread Root') }
let(:actor) { Mutant::Actor::Actor.new(thread, mailbox) }
let(:object) { described_class.new(thread_root) }
before do
expect(Mutex).to receive(:new).and_return(mutex)
end
describe '#current' do
subject { object.current }
let!(:mailbox) { Mutant::Actor::Mailbox.new }
before do
expect(Mutant::Actor::Mailbox).to receive(:new).and_return(mailbox).ordered
expect(thread_root).to receive(:current).and_return(thread)
end
it { should eql(actor) }
end
describe '#spawn' do
subject { object.spawn(&block) }
let!(:mailbox) { Mutant::Actor::Mailbox.new }
let(:yields) { [] }
let(:block) { ->(actor) { yields << actor } }
before do
expect(Mutant::Actor::Mailbox).to receive(:new).and_return(mailbox).ordered
expect(thread_root).to receive(:new).and_yield.and_return(thread).ordered
expect(thread_root).to receive(:current).and_return(thread).ordered
end
it 'returns sender' do
should eql(actor.sender)
end
it 'yields actor' do
expect { subject }.to change { yields }.from([]).to([actor])
end
end
end

View file

@ -0,0 +1,23 @@
RSpec.describe Mutant::Actor::Message do
let(:type) { double('Type') }
let(:payload) { double('Payload') }
describe '.new' do
subject { described_class.new(*arguments) }
context 'with one argument' do
let(:arguments) { [type] }
its(:type) { should be(type) }
its(:payload) { should be(Mutant::Actor::Undefined) }
end
context 'with two arguments' do
let(:arguments) { [type, payload] }
its(:type) { should be(type) }
its(:payload) { should be(payload) }
end
end
end

View file

@ -0,0 +1,60 @@
RSpec.describe Mutant::Actor::Receiver do
let(:mailbox) { double('Mailbox') }
let(:mutex) { double('Mutex') }
let(:message) { double('Message') }
let(:object) { described_class.new(mutex, mailbox) }
describe '#call' do
subject { object.call }
context 'when mailbox contains a message' do
before do
expect(mutex).to receive(:lock).ordered
expect(mailbox).to receive(:empty?).and_return(false).ordered
expect(mailbox).to receive(:shift).and_return(message).ordered
expect(mutex).to receive(:unlock).ordered
end
it { should be(message) }
end
context 'when mailbox initially contains no message' do
before do
# 1rst failing try
expect(mutex).to receive(:lock).ordered
expect(mailbox).to receive(:empty?).and_return(true).ordered
expect(mutex).to receive(:unlock).ordered
expect(Thread).to receive(:stop).ordered
# 2nd successful try
expect(mutex).to receive(:lock).ordered
expect(mailbox).to receive(:empty?).and_return(false).ordered
expect(mailbox).to receive(:shift).and_return(message).ordered
expect(mutex).to receive(:unlock).ordered
end
it 'waits for message' do
should be(message)
end
end
context 'when mailbox contains no message but thread gets waken without message arrived' do
before do
# 1rst failing try
expect(mutex).to receive(:lock).ordered
expect(mailbox).to receive(:empty?).and_return(true).ordered
expect(mutex).to receive(:unlock).ordered
expect(Thread).to receive(:stop).ordered
# 2nd failing try
expect(mutex).to receive(:lock).ordered
expect(mailbox).to receive(:empty?).and_return(true).ordered
expect(mutex).to receive(:unlock).ordered
expect(Thread).to receive(:stop).ordered
end
it 'waits for message' do
expect { subject }.to raise_error(Mutant::Actor::ProtocolError)
end
end
end
end

View file

@ -0,0 +1,22 @@
RSpec.describe Mutant::Actor::Sender do
let(:object) { described_class.new(thread, mutex, mailbox) }
let(:thread) { double('Thread') }
let(:mutex) { double('Mutex') }
let(:mailbox) { double('Mailbox') }
let(:type) { double('Type') }
let(:payload) { double('Payload') }
let(:_message) { message(type, payload) }
describe '#call' do
subject { object.call(_message) }
before do
expect(mutex).to receive(:synchronize).ordered.and_yield
expect(mailbox).to receive(:<<).with(_message)
expect(thread).to receive(:run)
end
it_should_behave_like 'a command method'
end
end

View file

@ -12,7 +12,7 @@ RSpec.describe Mutant::Env do
end
end
expected_warnings = ["Class#name from: #{klass} raised an error: RuntimeError fix your lib to follow normal ruby semantics!"]
expected_warnings = ["Class#name from: #{klass} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
@ -33,7 +33,7 @@ RSpec.describe Mutant::Env do
end
end
expected_warnings = ["Class#name from: #{klass.inspect} returned #{Object.inspect} instead String or nil. Fix your lib to follow normal ruby semantics!"]
expected_warnings = ["Class#name from: #{klass.inspect} returned Object. #{Mutant::Env::SEMANTICS_MESSAGE}"]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)

View file

@ -0,0 +1,33 @@
RSpec.describe Mutant::Actor::Mailbox do
describe '.new' do
subject { described_class.new }
its(:frozen?) { should be(true) }
end
before do
allow(Mutex).to receive(:new).and_return(mutex)
end
let(:mutex) { double('Mutex') }
let(:object) { described_class.new }
let(:thread) { double('Thread') }
describe '#sender' do
subject { object.sender(thread) }
it { should eql(Mutant::Actor::Sender.new(thread, mutex, [])) }
end
describe '#receiver' do
subject { object.receiver }
it { should eql(Mutant::Actor::Receiver.new(mutex, [])) }
end
describe '#actor' do
subject { object.actor(thread) }
it { should eql(Mutant::Actor::Actor.new(thread, object)) }
end
end

View file

@ -0,0 +1,63 @@
RSpec.describe Mutant::Mutation::Evil do
let(:object) do
described_class.new(mutation_subject, double('node'))
end
let(:mutation_subject) { double('subject') }
describe '.continue?' do
subject { described_class.continue?(test_results) }
context 'with empty test results' do
let(:test_results) { [] }
it { should be(true) }
end
context 'with single passed test result' do
let(:test_results) { [double('test result', passed: true)] }
it { should be(true) }
end
context 'with failed test result' do
let(:test_results) { [double('test result', passed: false)] }
it { should be(false) }
end
context 'with passed test result and failed test result' do
let(:test_results) { [double('test result', passed: true), double('test result', passed: false)] }
it { should be(false) }
end
end
describe '.success?' do
subject { described_class.success?(test_results) }
context 'with empty test results' do
let(:test_results) { [] }
it { should be(false) }
end
context 'with single passed test result' do
let(:test_results) { [double('test result', passed: true)] }
it { should be(false) }
end
context 'with failed test result' do
let(:test_results) { [double('test result', passed: false)] }
it { should be(true) }
end
context 'with passed test result and failed test result' do
let(:test_results) { [double('test result', passed: true), double('test result', passed: false)] }
it { should be(true) }
end
end
end

View file

@ -0,0 +1,63 @@
RSpec.describe Mutant::Mutation::Neutral do
let(:object) do
described_class.new(mutation_subject, double('node'))
end
let(:mutation_subject) { double('subject') }
describe '.continue?' do
subject { described_class.continue?(test_results) }
context 'with empty test results' do
let(:test_results) { [] }
it { should be(true) }
end
context 'with single passed test result' do
let(:test_results) { [double('test result', passed: true)] }
it { should be(true) }
end
context 'with failed test result' do
let(:test_results) { [double('test result', passed: false)] }
it { should be(true) }
end
context 'with passed test result and failed test result' do
let(:test_results) { [double('test result', passed: true), double('test result', passed: false)] }
it { should be(true) }
end
end
describe '.success?' do
subject { described_class.success?(test_results) }
context 'with empty test results' do
let(:test_results) { [] }
it { should be(false) }
end
context 'with single passed test result' do
let(:test_results) { [double('test result', passed: true)] }
it { should be(true) }
end
context 'with failed test result' do
let(:test_results) { [double('test result', passed: false)] }
it { should be(false) }
end
context 'with passed test result and failed test result' do
let(:test_results) { [double('test result', passed: true), double('test result', passed: false)] }
it { should be(false) }
end
end
end

View file

@ -4,9 +4,36 @@ RSpec.describe Mutant::Mutation do
SYMBOL = 'test'.freeze
end
let(:object) { TestMutation.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:mutation_subject) { double('Subject', identification: 'subject', context: context, source: 'original') }
let(:context) { double('Context') }
let(:object) { TestMutation.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:context) { double('Context') }
let(:mutation_subject) do
double(
'Subject',
identification: 'subject',
context: context,
source: 'original',
tests: [test_a, test_b]
)
end
let(:test_a) { double('Test A') }
let(:test_b) { double('Test B') }
describe '#kill' do
let(:isolation) { double('Isolation') }
let(:object) { Mutant::Mutation::Evil.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:test_result_a) { double('Test Result A', passed: false) }
before do
expect(test_a).to receive(:kill).with(isolation, object).and_return(test_result_a)
end
subject { object.kill(isolation) }
it { should eql(Mutant::Result::Mutation.new(index: nil, mutation: object, test_results: [test_result_a])) }
end
describe '#code' do
subject { object.code }

View file

@ -1,4 +1,6 @@
RSpec.describe Mutant::Reporter::CLI do
setup_shared_context
let(:object) { described_class.new(output, format) }
let(:output) { StringIO.new }
@ -31,110 +33,6 @@ RSpec.describe Mutant::Reporter::CLI do
allow(Time).to receive(:now).and_return(Time.now)
end
let(:result) do
Mutant::Result::Env.new(
env: env,
runtime: 1.1,
subject_results: subject_results
)
end
let(:env) do
double(
'Env',
class: Mutant::Env,
matchable_scopes: matchable_scopes,
config: config,
subjects: subjects,
mutations: subjects.flat_map(&:mutations)
)
end
let(:config) { Mutant::Config::DEFAULT.update(jobs: 1) }
let(:mutation_class) { Mutant::Mutation::Evil }
let(:matchable_scopes) { double('Matchable Scopes', length: 10) }
before do
allow(mutation_a).to receive(:subject).and_return(_subject)
allow(mutation_b).to receive(:subject).and_return(_subject)
end
let(:mutation_a) do
double(
'Mutation',
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
)
end
let(:mutation_source) { 'false' }
let(:_subject) do
double(
'Subject',
class: Mutant::Subject,
node: s(:true),
identification: 'subject_id',
mutations: subject_mutations,
tests: [
double('Test', identification: 'test_id')
]
)
end
let(:subject_mutations) { [mutation_a] }
let(:test_results) do
[
double(
'Test Result',
class: Mutant::Result::Test,
test: _subject.tests.first,
runtime: 1.0,
output: 'test-output',
success?: mutation_result_success
)
]
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: [mutation_a_result]
)
]
end
let(:subjects) { [_subject] }
describe '.build' do
subject { described_class.build(output) }
@ -210,47 +108,36 @@ RSpec.describe Mutant::Reporter::CLI do
end
describe '#progress' do
subject { object.progress(collector) }
let(:collector) do
Mutant::Runner::Collector.new(env)
end
let(:mutation_result_success) { true }
subject { object.progress(status) }
context 'on progressive format' do
let(:format) { progressive_format }
context 'with empty collector' do
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
it_reports ''
end
context 'with last mutation present' do
before do
collector.start(mutation_a)
collector.finish(mutation_a_result)
end
update(:env_result) { { subject_results: [subject_a_result] } }
context 'when mutation is successful' do
it_reports '.'
it_reports '..'
end
context 'when mutation is NOT successful' do
let(:mutation_result_success) { false }
it_reports 'F'
update(:mutation_a_test_a_result) { { passed: true } }
update(:mutation_a_test_b_result) { { passed: true } }
it_reports 'F.'
end
end
end
context 'on framed format' do
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
let(:mutation_result_success) { true }
context 'with empty collector' do
it_reports <<-REPORT
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
@ -261,24 +148,21 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Mutations: 2
Kills: 0
Alive: 0
Runtime: 0.00s
Runtime: 4.00s
Killtime: 0.00s
Overhead: NaN%
Overhead: Inf%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
context 'with collector active on one subject' do
before do
collector.start(mutation_a)
end
context 'with scheduler active on one subject' do
context 'without progress' do
update(:status) { { active_jobs: [].to_set } }
it_reports(<<-REPORT)
Mutant configuration:
@ -290,32 +174,24 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Mutations: 2
Kills: 2
Alive: 0
Runtime: 0.00s
Killtime: 0.00s
Overhead: NaN%
Coverage: 0.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 100.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
Active subjects: 0
REPORT
end
context 'with progress' do
let(:subject_mutations) { [mutation_a, mutation_b] }
before do
collector.start(mutation_b)
collector.finish(mutation_a_result)
end
update(:status) { { active_jobs: [job_a].to_set } }
context 'on failure' do
let(:mutation_result_success) { false }
update(:mutation_a_test_a_result) { { passed: true } }
update(:mutation_a_test_b_result) { { passed: true } }
it_reports(<<-REPORT)
Mutant configuration:
@ -328,18 +204,20 @@ RSpec.describe Mutant::Reporter::CLI do
Available Subjects: 1
Subjects: 1
Mutations: 2
Kills: 0
Kills: 1
Alive: 1
Runtime: 0.00s
Killtime: 0.50s
Overhead: -100.00%
Coverage: 0.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 50.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
subject-a mutations: 2
- test-a
F.
(01/02) 50% - killtime: 4.00s runtime: 4.00s overhead: 0.00s
Active Jobs:
0: evil:subject-a:d27d2
REPORT
end
@ -355,18 +233,20 @@ RSpec.describe Mutant::Reporter::CLI do
Available Subjects: 1
Subjects: 1
Mutations: 2
Kills: 1
Kills: 2
Alive: 0
Runtime: 0.00s
Killtime: 0.50s
Overhead: -100.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.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
subject-a mutations: 2
- test-a
..
(02/02) 100% - killtime: 4.00s runtime: 4.00s overhead: 0.00s
Active Jobs:
0: evil:subject-a:d27d2
REPORT
end
end
@ -374,11 +254,9 @@ RSpec.describe Mutant::Reporter::CLI do
end
describe '#report' do
subject { object.report(result) }
subject { object.report(status.env_result) }
context 'with full coverage' do
let(:mutation_result_success) { true }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
@ -389,26 +267,27 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 100.00%
Expected: 100.00%
REPORT
end
context 'and partial coverage' do
let(:mutation_result_success) { false }
update(:mutation_a_test_a_result) { { passed: true } }
update(:mutation_a_test_b_result) { { passed: true } }
context 'on evil mutation' do
context 'with a diff' do
it_reports(<<-REPORT)
subject_id
- test_id
mutation_id-a
subject-a
- test-a
evil:subject-a:d27d2
@@ -1,2 +1,2 @@
-true
+false
@ -422,29 +301,29 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Mutations: 2
Kills: 1
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 50.00%
Expected: 100.00%
REPORT
end
context 'without a diff' do
let(:mutation_source) { 'true' }
let(:mutation_a_node) { s(:true) }
it_reports(<<-REPORT)
subject_id
- test_id
mutation_id-a
subject-a
- test-a
evil:subject-a:d5318
Original source:
true
Mutated Source:
true
BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!
BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
@ -455,26 +334,30 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Kills: 0
Mutations: 2
Kills: 1
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Coverage: 0.00%
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 50.00%
Expected: 100.00%
REPORT
end
end
context 'on neutral mutation' do
let(:mutation_class) { Mutant::Mutation::Neutral }
let(:mutation_source) { 'true' }
update(:mutation_a_test_a_result) { { passed: false } }
update(:mutation_a_test_b_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Neutral.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
subject_id
- test_id
mutation_id-a
subject-a
- test-a
neutral:subject-a:d5318
--- 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.
@ -482,10 +365,29 @@ RSpec.describe Mutant::Reporter::CLI do
(true)
Unparsed Source:
true
Test Reports: 1
- test_id / runtime: 1.0
Test Reports: 2
- test-a / runtime: 1.0
Test Output:
test-output
mutation a test a result output
- test-b / runtime: 1.0
Test Output:
mutation a test b result output
-----------------------
neutral:subject-a:d5318
--- 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: 2
- test-a / runtime: 1.0
Test Output:
mutation b test a result output
- test-b / runtime: 1.0
Test Output:
mutation b test b result output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
@ -496,31 +398,51 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Mutations: 2
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Alive: 2
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
context 'on noop mutation' do
let(:mutation_class) { Mutant::Mutation::Noop }
update(:mutation_a_test_a_result) { { passed: false } }
update(:mutation_a_test_b_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Noop.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
subject_id
- test_id
mutation_id-a
subject-a
- test-a
noop:subject-a:d5318
---- 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 Reports: 2
- test-a / runtime: 1.0
Test Output:
test-output
mutation a test a result output
- test-b / runtime: 1.0
Test Output:
mutation a test b result output
-----------------------
noop:subject-a:d5318
---- 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: 2
- test-a / runtime: 1.0
Test Output:
mutation b test a result output
- test-b / runtime: 1.0
Test Output:
mutation b test b result output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
@ -531,44 +453,17 @@ RSpec.describe Mutant::Reporter::CLI do
Requires: []
Available Subjects: 1
Subjects: 1
Mutations: 1
Mutations: 2
Kills: 0
Alive: 1
Runtime: 1.10s
Killtime: 0.50s
Overhead: 120.00%
Alive: 2
Runtime: 4.00s
Killtime: 4.00s
Overhead: 0.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
context 'without subjects' do
let(:subjects) { [] }
let(:subject_results) { [] }
let(:config) { Mutant::Config::DEFAULT.update(jobs: 1, includes: %w[include-dir], requires: %w[require-name]) }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: ["include-dir"]
Requires: ["require-name"]
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
end
end

View file

@ -0,0 +1,55 @@
RSpec.describe Mutant::Result::Env do
let(:object) do
described_class.new(
env: double('Env', config: config),
runtime: double('Runtime'),
subject_results: subject_results
)
end
let(:config) { double('Config', fail_fast: fail_fast) }
describe '#continue?' do
subject { object.continue? }
context 'config sets fail_fast flag' do
let(:fail_fast) { true }
context 'when mutation results are empty' do
let(:subject_results) { [] }
it { should be(true) }
end
context 'with failing mutation result' do
let(:subject_results) { [double('Subject Result', success?: false)] }
it { should be(false) }
end
context 'with successful mutation result' do
let(:subject_results) { [double('Subject Result', success?: true)] }
it { should be(true) }
end
context 'with failed and successful mutation result' do
let(:subject_results) do
[
double('Subject Result', success?: true),
double('Subject Result', success?: false)
]
end
it { should be(false) }
end
end
context 'config does not set fail fast flag' do
let(:fail_fast) { false }
let(:subject_results) { double('subject results') }
it { should be(true) }
end
end
end

View file

@ -0,0 +1,19 @@
RSpec.describe Mutant::Result::Mutation do
let(:object) do
described_class.new(
index: 0,
mutation: double('mutation'),
test_results: double('test results')
)
end
describe '#continue?' do
subject { object.continue? }
it 'forwards calls to mutation' do
return_value = double('return value')
expect(object.mutation.class).to receive(:continue?).with(object.test_results).and_return(return_value)
expect(subject).to be(return_value)
end
end
end

View file

@ -0,0 +1,43 @@
RSpec.describe Mutant::Result::Subject do
let(:object) do
described_class.new(
subject: mutation_subject,
mutation_results: mutation_results
)
end
let(:mutation_subject) { double('Subject') }
describe '#continue?' do
subject { object.continue? }
context 'when mutation results are empty' do
let(:mutation_results) { [] }
it { should be(true) }
end
context 'with failing mutation result' do
let(:mutation_results) { [double('Mutation Result', success?: false)] }
it { should be(false) }
end
context 'with successful mutation result' do
let(:mutation_results) { [double('Mutation Result', success?: true)] }
it { should be(true) }
end
context 'with failed and successful mutation result' do
let(:mutation_results) do
[
double('Mutation Result', success?: true),
double('Mutation Result', success?: false)
]
end
it { should be(false) }
end
end
end

View file

@ -1,198 +0,0 @@
require 'spec_helper'
describe Mutant::Runner::Collector do
let(:object) { described_class.new(env) }
before do
allow(Time).to receive(:now).and_return(Time.now)
end
let(:env) do
double(
'env',
subjects: [mutation_a.subject]
)
end
let(:mutation_a) do
double(
'mutation a',
subject: double('subject', identification: 'A')
)
end
let(:mutation_a_result) do
double(
'mutation a result',
index: 0,
runtime: 0.0,
mutation: mutation_a
)
end
let(:subject_a_result) do
Mutant::Result::Subject.new(
subject: mutation_a.subject,
runtime: 0.0,
mutation_results: [mutation_a_result]
)
end
let(:active_subject_result) do
subject_a_result.update(mutation_results: [])
end
let(:active_subject_results) do
[active_subject_result]
end
describe '.new' do
it 'initializes instance variables' do
expect(object.instance_variables).to include(:@last_mutation_result)
end
end
describe '#start' do
subject { object.start(mutation_a) }
it 'tracks the mutation as active' do
expect { subject }.to change { object.active_subject_results }.from([]).to(active_subject_results)
end
it_should_behave_like 'a command method'
end
describe '#finish' do
subject { object.finish(mutation_a_result) }
before do
object.start(mutation_a)
end
it 'removes the tracking of mutation as active' do
expect { subject }.to change { object.active_subject_results }.from(active_subject_results).to([])
end
it 'sets last mutation result' do
expect { subject }.to change { object.last_mutation_result }.from(nil).to(mutation_a_result)
end
it 'aggregates results in #result' do
subject
expect(object.result).to eql(
Mutant::Result::Env.new(
env: object.env,
runtime: 0.0,
subject_results: [subject_a_result]
)
)
end
it_should_behave_like 'a command method'
end
describe '#last_mutation_result' do
subject { object.last_mutation_result }
context 'when empty' do
it { should be(nil) }
end
context 'with partial state' do
before do
object.start(mutation_a)
end
it { should be(nil) }
end
context 'with full state' do
before do
object.start(mutation_a)
object.finish(mutation_a_result)
end
it { should be(mutation_a_result) }
end
end
describe '#active_subject_results' do
subject { object.active_subject_results }
context 'when empty' do
it { should eql([]) }
end
context 'on partial state' do
let(:mutation_b) do
double(
'mutation b',
subject: double(
'subject',
identification: 'B'
)
)
end
let(:mutation_b_result) do
double(
'mutation b result',
index: 0,
runtime: 0.0,
mutation: mutation_b
)
end
let(:subject_b_result) do
Mutant::Result::Subject.new(
subject: mutation_b.subject,
runtime: 0.0,
mutation_results: [mutation_b_result]
)
end
let(:active_subject_results) { [subject_a_result, subject_b_result] }
before do
object.start(mutation_b)
object.start(mutation_a)
end
it { should eql(active_subject_results.map { |result| result.update(mutation_results: []) }) }
end
context 'on full state' do
before do
object.start(mutation_a)
object.finish(mutation_a_result)
end
it { should eql([]) }
end
end
describe '#result' do
subject { object.result }
context 'when empty' do
it { should eql(Mutant::Result::Env.new(env: object.env, runtime: 0.0, subject_results: [active_subject_result])) }
end
context 'on partial state' do
before do
object.start(mutation_a)
end
it { should eql(Mutant::Result::Env.new(env: object.env, runtime: 0.0, subject_results: [active_subject_result])) }
end
context 'on full state' do
before do
object.start(mutation_a)
object.finish(mutation_a_result)
end
it { should eql(Mutant::Result::Env.new(env: object.env, runtime: 0.0, subject_results: [subject_a_result])) }
end
end
end

View file

@ -0,0 +1,201 @@
RSpec.describe Mutant::Runner::Master do
setup_shared_context
describe 'object initialization' do
subject { described_class.__send__(:new, env, double('actor')) }
it 'initialized instance variables' do
expect(subject.instance_variable_get(:@stop)).to be(false)
expect(subject.instance_variable_get(:@stopping)).to be(false)
end
end
describe '.call' do
let(:actor_names) { [:master, :worker_a] }
let(:worker_a) { actor_env.actor(:worker_a).sender }
let(:worker_b) { actor_env.actor(:worker_b).sender }
let(:parent) { actor_env.actor(:parent).sender }
let(:job) { double('Job') }
before do
expect(Time).to receive(:now).and_return(Time.at(0)).at_most(5).times
expect(Mutant::Runner::Worker).to receive(:run).with(
id: 0,
config: env.config,
parent: actor_env.actor(:master).sender
).and_return(worker_a)
end
subject { described_class.call(env) }
context 'jobs done before external stop' do
before do
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_a)
message_sequence.add(:master, :result, job_a_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_b)
message_sequence.add(:master, :result, job_b_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'stop by fail fast trigger first' do
update(:config) { { fail_fast: true } }
update(:mutation_b_test_a_result) { { passed: true } }
update(:mutation_b_test_b_result) { { passed: true } }
before do
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_a)
message_sequence.add(:master, :result, job_a_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_b)
message_sequence.add(:master, :result, job_b_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'stop by fail fast trigger last' do
update(:config) { { fail_fast: true } }
update(:mutation_a_test_a_result) { { passed: true } }
update(:mutation_a_test_b_result) { { passed: true } }
before do
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_a)
message_sequence.add(:master, :result, job_a_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'jobs active while external stop' do
before do
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_a)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:master, :result, job_a_result)
message_sequence.add(:master, :status, parent)
message_sequence.add(:parent, :status, empty_status.update(active_jobs: [job_a].to_set))
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'stop with pending jobs' do
before do
message_sequence.add(:master, :stop, parent)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'unhandled message received' do
before do
message_sequence.add(:master, :foo, parent)
end
it 'raises message' do
expect { subject }.to raise_error(Mutant::Actor::ProtocolError, 'Unexpected message: :foo')
end
end
context 'request status late' do
let(:expected_status) { status.update(env_result: env_result.update(runtime: 0.0)) }
before do
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_a)
message_sequence.add(:master, :result, job_a_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :job, job_b)
message_sequence.add(:master, :result, job_b_result)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:master, :status, parent)
message_sequence.add(:parent, :status, expected_status)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'request status early' do
before do
message_sequence.add(:master, :status, parent)
message_sequence.add(:parent, :status, empty_status)
message_sequence.add(:master, :stop, parent)
message_sequence.add(:master, :ready, worker_a)
message_sequence.add(:worker_a, :stop)
message_sequence.add(:parent, :stop)
end
it { should eql(actor_env.actor(:master).sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
end
end

View file

@ -0,0 +1,162 @@
require 'spec_helper'
describe Mutant::Runner::Scheduler do
let(:object) { described_class.new(env) }
before do
allow(Time).to receive(:now).and_return(Time.now)
end
setup_shared_context
let(:active_subject_a_result) do
subject_a_result.update(mutation_results: [])
end
describe '#job_result' do
subject { object.job_result(job_a_result) }
before do
expect(object.next_job).to eql(job_a)
end
it 'removes the tracking of job as active' do
expect { subject }.to change { object.status.active_jobs }.from([job_a].to_set).to(Set.new)
end
it 'aggregates results in #status' do
subject
object.job_result(job_b_result)
expect(object.status.env_result).to eql(
Mutant::Result::Env.new(
env: env,
runtime: 0.0,
subject_results: [subject_a_result]
)
)
end
it_should_behave_like 'a command method'
end
describe '#next_job' do
subject { object.next_job }
context 'when there is a next job' do
let(:mutations) { [mutation_a, mutation_b] }
it { should eql(job_a) }
it 'does not return the same job again' do
subject
expect(object.next_job).to eql(job_b)
expect(object.next_job).to be(nil)
end
it 'does record job as active' do
expect { subject }.to change { object.status.active_jobs }.from(Set.new).to([job_a].to_set)
end
end
context 'when there is no next job' do
let(:mutations) { [] }
it { should be(nil) }
end
end
describe '#status' do
subject { object.status }
context 'when empty' do
let(:expected_status) do
Mutant::Runner::Status.new(
env_result: Mutant::Result::Env.new(env: env, runtime: 0.0, subject_results: []),
active_jobs: Set.new,
done: false
)
end
it { should eql(expected_status) }
end
context 'when jobs are active' do
before do
object.next_job
object.next_job
end
let(:expected_status) do
Mutant::Runner::Status.new(
env_result: Mutant::Result::Env.new(env: env, runtime: 0.0, subject_results: []),
active_jobs: [job_a, job_b].to_set,
done: false
)
end
it { should eql(expected_status) }
end
context 'remaining jobs are active' do
before do
object.next_job
object.next_job
object.job_result(job_a_result)
end
update(:subject_a_result) { { mutation_results: [mutation_a_result] } }
let(:expected_status) do
Mutant::Runner::Status.new(
env_result: Mutant::Result::Env.new(env: env, runtime: 0.0, subject_results: [subject_a_result]),
active_jobs: [job_b].to_set,
done: false
)
end
it { should eql(expected_status) }
end
context 'under fail fast config with failed result' do
before do
object.next_job
object.next_job
object.job_result(job_a_result)
end
update(:subject_a_result) { { mutation_results: [mutation_a_result] } }
update(:mutation_a_test_a_result) { { passed: true } }
update(:mutation_a_test_b_result) { { passed: true } }
update(:config) { { fail_fast: true } }
let(:expected_status) do
Mutant::Runner::Status.new(
env_result: Mutant::Result::Env.new(env: env, runtime: 0.0, subject_results: [subject_a_result]),
active_jobs: [job_b].to_set,
done: true
)
end
it { should eql(expected_status) }
end
context 'when done' do
before do
object.next_job
object.next_job
object.status
object.job_result(job_a_result)
object.job_result(job_b_result)
end
let(:expected_status) do
Mutant::Runner::Status.new(
env_result: Mutant::Result::Env.new(env: env, runtime: 0.0, subject_results: [subject_a_result]),
active_jobs: Set.new,
done: true
)
end
it { should eql(expected_status) }
end
end
end

View file

@ -0,0 +1,65 @@
RSpec.describe Mutant::Runner::Worker do
setup_shared_context
let(:actor) { actor_env.actor(:worker) }
let(:parent) { actor_env.actor(:parent).sender }
before do
message_sequence.add(:parent, :ready, actor.sender)
end
let(:attributes) do
{
config: config,
parent: parent,
id: 1
}
end
describe '.run' do
subject { described_class.run(attributes) }
let(:actor_names) { [:worker] }
context 'when receving :job command' do
before do
expect(mutation).to receive(:kill).with(config.isolation).and_return(mutation_result).ordered
expect(mutation_result).to receive(:update).with(index: job.index).and_return(mutation_result).ordered
message_sequence.add(:worker, :job, job)
message_sequence.add(:parent, :result, job_result)
message_sequence.add(:parent, :ready, actor.sender)
message_sequence.add(:worker, :stop)
end
let(:test) { double('Test') }
let(:index) { double('Index') }
let(:test_result) { double('Test Result') }
let(:mutation) { double('Mutation') }
let(:mutation_result) { double('Mutation Result') }
let(:job_result) { Mutant::Runner::JobResult.new(job: job, result: mutation_result) }
let(:job) { Mutant::Runner::Job.new(index: index, mutation: mutation) }
it 'signals ready and status to parent' do
subject
end
it { should eql(actor.sender) }
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'when receiving unknown command' do
before do
message_sequence.add(:worker, :other)
end
it 'raises error' do
expect { subject }.to raise_error(Mutant::Actor::ProtocolError, 'Unknown command: :other')
end
end
end
end

View file

@ -1,143 +1,85 @@
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.
RSpec.describe Mutant::Runner do
let(:object) { described_class.new(env) }
setup_shared_context
let(:reporter) { Mutant::Reporter::Trace.new }
let(:config) { Mutant::Config::DEFAULT.update(reporter: reporter, isolation: Mutant::Isolation::None) }
let(:subjects) { [subject_a, subject_b] }
let(:subject_a) { Double.new('Subject A', mutations: mutations_a, tests: subject_a_tests) }
let(:subject_b) { Double.new('Subject B', mutations: mutations_b) }
let(:subject_a_tests) { [test_a1, test_a2] }
let(:env) do
subjects = self.subjects
Class.new(Mutant::Env) do
define_method(:subjects) { subjects }
end.new(config)
end
let(:mutations_a) { [mutation_a1, mutation_a2] }
let(:mutations_b) { [] }
let(:mutation_a1) { Double.new('Mutation A1') }
let(:mutation_a2) { Double.new('Mutation A2') }
let(:test_a1) { Double.new('Test A1') }
let(:test_a2) { Double.new('Test A2') }
let(:test_report_a1) { Double.new('Test Report A1') }
let(:integration) { double('Integration') }
let(:master_sender) { actor_env.spawn }
let(:runner_actor) { actor_env.actor(:runner) }
before do
allow(mutation_a1).to receive(:subject).and_return(subject_a)
allow(mutation_a1).to receive(:insert)
allow(mutation_a2).to receive(:subject).and_return(subject_a)
allow(mutation_a2).to receive(:insert)
allow(test_a1).to receive(:run).and_return(test_report_a1)
allow(mutation_a1).to receive(:killed_by?).with(test_report_a1).and_return(true)
allow(mutation_a2).to receive(:killed_by?).with(test_report_a1).and_return(true)
expect(integration).to receive(:setup).ordered
expect(Mutant::Runner::Master).to receive(:call).with(env).and_return(master_sender).ordered
end
before do
time = Time.at(0)
allow(Time).to receive(:now).and_return(time)
end
describe '.call' do
update(:config) { { integration: integration } }
let(:actor_names) { [:runner, :master] }
let(:expected_subject_results) do
[
Mutant::Result::Subject.new(
subject: subject_a,
mutation_results: [
Mutant::Result::Mutation.new(
index: 0,
mutation: mutation_a1,
runtime: 0.0,
test_results: [test_report_a1]
),
Mutant::Result::Mutation.new(
index: 1,
mutation: mutation_a2,
runtime: 0.0,
test_results: [test_report_a1]
)
],
runtime: 0.0
),
Mutant::Result::Subject.new(
subject: subject_b,
mutation_results: [],
runtime: 0.0
)
]
end
subject { described_class.call(env) }
describe '#result' do
let(:expected_result) do
Mutant::Result::Env.new(
env: env,
runtime: 0.0,
subject_results: expected_subject_results
)
end
context 'when status done gets returned immediately' do
before do
message_sequence.add(:runner, :status, actor_env.actor(:current).sender)
message_sequence.add(:current, :status, status)
message_sequence.add(:runner, :stop, actor_env.actor(:current).sender)
message_sequence.add(:current, :stop)
end
context 'on error free execution' do
subject { object.result }
it 'returns env result' do
should be(status.env_result)
end
its(:env) { should be(env) }
it 'logs start' do
expect { subject }.to change { config.reporter.start_calls }.from([]).to([env])
end
it 'reports result' do
expect { subject }.to change { config.reporter.report_calls }.from([]).to([expected_result])
it 'logs process' do
expect { subject }.to change { config.reporter.progress_calls }.from([]).to([status])
end
it 'logs result' do
expect { subject }.to change { config.reporter.report_calls }.from([]).to([status.env_result])
end
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
context 'when isolation raises error' do
subject { object.result }
its(:env) { should be(env) }
its(:subject_results) { should eql(expected_subject_results) }
it { should eql(expected_result) }
context 'when status done gets returned immediately' do
let(:incomplete_status) { status.update(done: false) }
before do
expect(Mutant::Isolation::None).to receive(:call)
.twice
.and_raise(Mutant::Isolation::Error.new('test-exception-message'))
expect(Mutant::Result::Test).to receive(:new).with(
test: test_a1,
mutation: mutation_a1,
runtime: 0.0,
output: 'test-exception-message',
passed: false
).and_return(test_report_a1)
expect(Mutant::Result::Test).to receive(:new).with(
test: test_a1,
mutation: mutation_a2,
runtime: 0.0,
output: 'test-exception-message',
passed: false
).and_return(test_report_a1)
expect(Kernel).to receive(:sleep).with(1 / 20.0).exactly(2).times.ordered
message_sequence.add(:runner, :status, actor_env.actor(:current).sender)
message_sequence.add(:current, :status, incomplete_status)
message_sequence.add(:runner, :status, actor_env.actor(:current).sender)
message_sequence.add(:current, :status, incomplete_status)
message_sequence.add(:runner, :status, actor_env.actor(:current).sender)
message_sequence.add(:current, :status, status)
message_sequence.add(:runner, :stop, actor_env.actor(:current).sender)
message_sequence.add(:current, :stop)
end
it 'returns env result' do
should be(status.env_result)
end
it 'logs start' do
expect { subject }.to change { config.reporter.start_calls }.from([]).to([env])
end
it 'logs result' do
expect { subject }.to change { config.reporter.report_calls }.from([]).to([status.env_result])
end
it 'logs process' do
expected = [incomplete_status, incomplete_status, status]
expect { subject }.to change { config.reporter.progress_calls }.from([]).to(expected)
end
it 'consumes all messages' do
expect { subject }.to change(&message_sequence.method(:consumed?)).from(false).to(true)
end
end
end
end

View file

@ -132,9 +132,21 @@ RSpec.describe Mutant::Subject::Method::Instance::Memoized do
let(:expected) do
[
Mutant::Mutation::Neutral.new(object, s(:begin, s(:def, :foo, s(:args)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))),
Mutant::Mutation::Evil.new(object, s(:begin, s(:def, :foo, s(:args), s(:send, nil, :raise)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))),
Mutant::Mutation::Evil.new(object, s(:begin, s(:def, :foo, s(:args), nil), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))),
Mutant::Mutation::Neutral.new(
object,
s(:begin,
s(:def, :foo, s(:args)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
),
Mutant::Mutation::Evil.new(
object,
s(:begin,
s(:def, :foo, s(:args), s(:send, nil, :raise)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
),
Mutant::Mutation::Evil.new(
object,
s(:begin,
s(:def, :foo, s(:args), nil), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
)
]
end

View file

@ -1,8 +1,10 @@
RSpec.describe Mutant::Test do
let(:object) { described_class.new(integration, expression) }
let(:integration) { double('Integration', name: 'test-integration') }
let(:expression) { double('Expression', syntax: 'test-syntax') }
let(:integration) { double('Integration', name: 'test-integration') }
let(:expression) { double('Expression', syntax: 'test-syntax') }
let(:report) { double('Report') }
let(:updated_report) { double('Updated Report') }
describe '#identification' do
subject { object.identification }
@ -10,14 +12,42 @@ RSpec.describe Mutant::Test do
it { should eql('test-integration:test-syntax') }
end
describe '#run' do
subject { object.run }
describe '#kill' do
let(:isolation) { Mutant::Isolation::None }
let(:mutation) { double('Mutation') }
let(:report) { double('Report') }
subject { object.kill(isolation, mutation) }
it 'runs test via integration' do
expect(integration).to receive(:run).with(object).and_return(report)
expect(subject).to be(report)
before do
expect(mutation).to receive(:insert)
end
context 'when isolation does not raise' do
before do
expect(report).to receive(:update).with(test: object).and_return(updated_report)
end
it 'runs test via integration' do
expect(integration).to receive(:run).with(object).and_return(report)
expect(subject).to be(updated_report)
end
end
context 'when isolation does raise' do
before do
allow(Time).to receive(:now).and_return(Time.at(0))
end
it 'runs test via integration' do
expect(integration).to receive(:run).with(object).and_raise(Mutant::Isolation::Error, 'fake message')
expect(subject).to eql(Mutant::Result::Test.new(
test: object,
mutation: mutation,
output: 'fake message',
passed: false,
runtime: 0.0
))
end
end
end
end