Use actor based parallelization
This commit is contained in:
parent
ae7284f39a
commit
e08d3b6b80
48 changed files with 2451 additions and 1013 deletions
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
---
|
||||
threshold: 18
|
||||
total_score: 1114
|
||||
total_score: 1179
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
60
lib/mutant/actor.rb
Normal 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
48
lib/mutant/actor/actor.rb
Normal 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
35
lib/mutant/actor/env.rb
Normal 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
|
53
lib/mutant/actor/mailbox.rb
Normal file
53
lib/mutant/actor/mailbox.rb
Normal 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
|
48
lib/mutant/actor/receiver.rb
Normal file
48
lib/mutant/actor/receiver.rb
Normal 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
|
27
lib/mutant/actor/sender.rb
Normal file
27
lib/mutant/actor/sender.rb
Normal 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
|
|
@ -12,7 +12,8 @@ module Mutant
|
|||
:fail_fast,
|
||||
:jobs,
|
||||
:zombie,
|
||||
:expected_coverage
|
||||
:expected_coverage,
|
||||
:actor_env
|
||||
)
|
||||
|
||||
[:fail_fast, :zombie, :debug].each do |name|
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
168
lib/mutant/runner/master.rb
Normal 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
|
141
lib/mutant/runner/scheduler.rb
Normal file
141
lib/mutant/runner/scheduler.rb
Normal 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
|
87
lib/mutant/runner/worker.rb
Normal file
87
lib/mutant/runner/worker.rb
Normal 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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
93
spec/support/fake_actor.rb
Normal file
93
spec/support/fake_actor.rb
Normal 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
|
157
spec/support/shared_context.rb
Normal file
157
spec/support/shared_context.rb
Normal 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
|
35
spec/unit/mutant/actor/actor_spec.rb
Normal file
35
spec/unit/mutant/actor/actor_spec.rb
Normal 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
|
32
spec/unit/mutant/actor/binding_spec.rb
Normal file
32
spec/unit/mutant/actor/binding_spec.rb
Normal 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
|
49
spec/unit/mutant/actor/env_spec.rb
Normal file
49
spec/unit/mutant/actor/env_spec.rb
Normal 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
|
23
spec/unit/mutant/actor/message_spec.rb
Normal file
23
spec/unit/mutant/actor/message_spec.rb
Normal 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
|
60
spec/unit/mutant/actor/receiver_spec.rb
Normal file
60
spec/unit/mutant/actor/receiver_spec.rb
Normal 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
|
22
spec/unit/mutant/actor/sender_spec.rb
Normal file
22
spec/unit/mutant/actor/sender_spec.rb
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
33
spec/unit/mutant/mailbox_spec.rb
Normal file
33
spec/unit/mutant/mailbox_spec.rb
Normal 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
|
63
spec/unit/mutant/mutation/evil_spec.rb
Normal file
63
spec/unit/mutant/mutation/evil_spec.rb
Normal 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
|
63
spec/unit/mutant/mutation/neutral_spec.rb
Normal file
63
spec/unit/mutant/mutation/neutral_spec.rb
Normal 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
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
55
spec/unit/mutant/result/env_spec.rb
Normal file
55
spec/unit/mutant/result/env_spec.rb
Normal 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
|
19
spec/unit/mutant/result/mutation_spec.rb
Normal file
19
spec/unit/mutant/result/mutation_spec.rb
Normal 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
|
43
spec/unit/mutant/result/subject_spec.rb
Normal file
43
spec/unit/mutant/result/subject_spec.rb
Normal 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
|
|
@ -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
|
201
spec/unit/mutant/runner/master_spec.rb
Normal file
201
spec/unit/mutant/runner/master_spec.rb
Normal 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
|
162
spec/unit/mutant/runner/scheduler_spec.rb
Normal file
162
spec/unit/mutant/runner/scheduler_spec.rb
Normal 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
|
65
spec/unit/mutant/runner/worker_spec.rb
Normal file
65
spec/unit/mutant/runner/worker_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue