Add parallel runner / reporter
This commit is contained in:
parent
815526f8b4
commit
105730f5ba
34 changed files with 1192 additions and 992 deletions
|
@ -1,3 +1,8 @@
|
|||
# v0.6.0 2014-07-17
|
||||
|
||||
* Parallel execution / reporting.
|
||||
* Add -j, --jobs flag to control concurrency.
|
||||
|
||||
# v0.5.26 2014-07-07
|
||||
|
||||
* Fix exceptions generation matcher errors
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
---
|
||||
threshold: 18
|
||||
total_score: 1053
|
||||
total_score: 1068
|
||||
|
|
|
@ -12,6 +12,7 @@ ControlParameter:
|
|||
enabled: true
|
||||
exclude:
|
||||
- Mutant::Expression#match_length
|
||||
- Mutant::Reporter::CLI::Printer::SubjectProgress#print_mutation_result
|
||||
DataClump:
|
||||
enabled: true
|
||||
exclude: []
|
||||
|
@ -39,6 +40,8 @@ FeatureEnvy:
|
|||
- Mutant::Meta::Example::Verification#format_mutation
|
||||
- Mutant::Reporter::CLI#subject_results
|
||||
- Mutant::Runner#run_mutation_test
|
||||
- Mutant::Runner#kill_mutation
|
||||
- Mutant::Runner#finish
|
||||
IrresponsibleModule:
|
||||
enabled: true
|
||||
exclude: []
|
||||
|
@ -46,6 +49,7 @@ LongParameterList:
|
|||
enabled: true
|
||||
exclude:
|
||||
- Mutant::Matcher::Method::Instance#self.build?
|
||||
- Mutant::Runner#finish # API client of parallel, one gets _ignored.
|
||||
- Mutant::Runner#self.run
|
||||
max_params: 2
|
||||
LongYieldList:
|
||||
|
@ -61,10 +65,8 @@ NestedIterators:
|
|||
- Mutant::Mutator::Util::Array::Element#dispatch
|
||||
- Mutant::Mutator::Node::Resbody#mutate_captures
|
||||
- Mutant::Mutator::Node::Arguments#emit_argument_mutations
|
||||
- Mutant::Reporter::CLI::Report::Env#generic_stats
|
||||
- Mutant::RequireHighjack#infect
|
||||
- Mutant::RequireHighjack#desinfect
|
||||
- Mutant::Reporter::CLI::Registry#included
|
||||
- Mutant::Subject#tests
|
||||
- Parser::Lexer#self.new
|
||||
max_allowed_nesting: 1
|
||||
|
@ -81,9 +83,8 @@ RepeatedConditional:
|
|||
TooManyInstanceVariables:
|
||||
enabled: true
|
||||
exclude:
|
||||
- Mutant::CLI # 4 vars
|
||||
- Mutant::Killer # 4 vars
|
||||
- Mutant::Mutator # 4 vars
|
||||
- Mutant::Runner # 4 vars
|
||||
max_instance_variables: 3
|
||||
TooManyMethods:
|
||||
enabled: true
|
||||
|
@ -99,9 +100,8 @@ TooManyStatements:
|
|||
exclude:
|
||||
- Mutant#self.singleton_subclass_instance
|
||||
- Mutant::Integration::Rspec#run
|
||||
- Mutant::Reporter::CLI::Report::Env#run
|
||||
- Mutant::Reporter::CLI::Registry#included
|
||||
- Mutant::Reporter::CLI#colorized_diff
|
||||
- Mutant::Reporter::CLI::Printer::EnvProgress#run
|
||||
- Mutant::RequireHighjack#infect
|
||||
- Mutant::Rspec::Killer#run
|
||||
- Mutant::Runner#visit_collection
|
||||
|
|
|
@ -182,27 +182,14 @@ require 'mutant/integration'
|
|||
require 'mutant/cli'
|
||||
require 'mutant/color'
|
||||
require 'mutant/diff'
|
||||
require 'mutant/runner'
|
||||
require 'mutant/runner/collector'
|
||||
require 'mutant/result'
|
||||
require 'mutant/reporter'
|
||||
require 'mutant/reporter/null'
|
||||
require 'mutant/reporter/trace'
|
||||
require 'mutant/reporter/cli'
|
||||
require 'mutant/reporter/cli/registry'
|
||||
require 'mutant/reporter/cli/printer'
|
||||
require 'mutant/reporter/cli/report'
|
||||
require 'mutant/reporter/cli/report/env'
|
||||
require 'mutant/reporter/cli/report/subject'
|
||||
require 'mutant/reporter/cli/report/mutation'
|
||||
require 'mutant/reporter/cli/report/test'
|
||||
require 'mutant/reporter/cli/progress'
|
||||
require 'mutant/reporter/cli/progress/env'
|
||||
require 'mutant/reporter/cli/progress/config'
|
||||
require 'mutant/reporter/cli/progress/subject'
|
||||
require 'mutant/reporter/cli/progress/noop'
|
||||
require 'mutant/reporter/cli/progress/result'
|
||||
require 'mutant/reporter/cli/progress/result/mutation'
|
||||
require 'mutant/reporter/cli/progress/result/subject'
|
||||
require 'mutant/runner'
|
||||
require 'mutant/zombifier'
|
||||
require 'mutant/zombifier/file'
|
||||
|
||||
|
@ -219,6 +206,7 @@ module Mutant
|
|||
isolation: Mutant::Isolation::Fork,
|
||||
reporter: Reporter::CLI.new($stdout),
|
||||
zombie: false,
|
||||
processes: Parallel.processor_count,
|
||||
expected_coverage: 100.0
|
||||
)
|
||||
end # Config
|
||||
|
|
|
@ -112,6 +112,9 @@ module Mutant
|
|||
opts.on('-r', '--require NAME', 'Require file with NAME') do |name|
|
||||
add(:requires, name)
|
||||
end
|
||||
opts.on('-j', '--jobs NUMBER', 'Number of kill processes. Defaults to number of processors.') do |number|
|
||||
update(processes: Integer(number))
|
||||
end
|
||||
end
|
||||
|
||||
# Use integration
|
||||
|
|
|
@ -10,6 +10,7 @@ module Mutant
|
|||
:reporter,
|
||||
:isolation,
|
||||
:fail_fast,
|
||||
:processes,
|
||||
:zombie,
|
||||
:expected_coverage
|
||||
)
|
||||
|
|
|
@ -63,10 +63,10 @@ module Mutant
|
|||
end
|
||||
output.rewind
|
||||
Result::Test.new(
|
||||
test: self,
|
||||
test: nil,
|
||||
mutation: nil,
|
||||
output: output.read,
|
||||
runtime: Time.now - start,
|
||||
mutation: nil,
|
||||
passed: !failed
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Mutant
|
||||
# Abstract base class for reporters
|
||||
class Reporter
|
||||
include Adamantium::Flat, AbstractType
|
||||
include AbstractType
|
||||
|
||||
# Write warning message
|
||||
#
|
||||
|
@ -13,9 +13,9 @@ module Mutant
|
|||
#
|
||||
abstract_method :warn
|
||||
|
||||
# Report object
|
||||
# Report collector state
|
||||
#
|
||||
# @param [Object] object
|
||||
# @param [Runner::Collector] collector
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
|
|
|
@ -4,16 +4,59 @@ module Mutant
|
|||
class CLI < self
|
||||
include Concord.new(:output)
|
||||
|
||||
CLEAR_PREV_LINE = "\e[1A\e[2K".freeze
|
||||
|
||||
# Output abstraction to decouple tty? from buffer
|
||||
class Output
|
||||
include Concord.new(:tty, :buffer)
|
||||
|
||||
# Test if output is a tty
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def tty?
|
||||
@tty
|
||||
end
|
||||
|
||||
[:puts, :write].each do |name|
|
||||
define_method(name) do |*args, &block|
|
||||
buffer.public_send(name, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
end # Output
|
||||
|
||||
# Rate per second progress report fires
|
||||
OUTPUT_RATE = 1.0 / 20
|
||||
|
||||
# Initialize object
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(*)
|
||||
super
|
||||
@last_frame = nil
|
||||
@last_length = 0
|
||||
@tty = output.respond_to?(:tty?) && output.tty?
|
||||
end
|
||||
|
||||
# Report progress object
|
||||
#
|
||||
# @param [Object] object
|
||||
# @param [Runner::Collector] collector
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def progress(object)
|
||||
Progress.run(output, object)
|
||||
def progress(collector)
|
||||
throttle do
|
||||
swap(frame(Printer::Collector, collector))
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
|
@ -30,19 +73,71 @@ module Mutant
|
|||
self
|
||||
end
|
||||
|
||||
# Report object
|
||||
# Report env
|
||||
#
|
||||
# @param [Object] object
|
||||
# @param [Result::Env] env
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def report(object)
|
||||
Report.run(output, object)
|
||||
def report(env)
|
||||
swap(frame(Printer::EnvResult, env))
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Compute progress frame
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def frame(reporter, object)
|
||||
buffer = StringIO.new
|
||||
buffer.write(clear_command) if @tty
|
||||
reporter.run(Output.new(@tty, buffer), object)
|
||||
buffer.rewind
|
||||
buffer.read
|
||||
end
|
||||
|
||||
# Swap output frame
|
||||
#
|
||||
# @param [String] frame
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def swap(frame)
|
||||
output.write(frame)
|
||||
@last_length = frame.lines.length
|
||||
end
|
||||
|
||||
# Call block throttled
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def throttle
|
||||
now = Time.now
|
||||
return if @last_frame && (now - @last_frame) < OUTPUT_RATE
|
||||
yield
|
||||
@last_frame = now
|
||||
end
|
||||
|
||||
# Return clear command for last frame length
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def clear_command
|
||||
CLEAR_PREV_LINE * @last_length
|
||||
end
|
||||
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
|
||||
# CLI runner status printer base class
|
||||
class Printer
|
||||
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
|
||||
|
@ -18,9 +17,7 @@ module Mutant
|
|||
# @api private
|
||||
#
|
||||
def self.run(output, object)
|
||||
handler = lookup(object.class)
|
||||
handler.new(output, object).run
|
||||
self
|
||||
new(output, object).run
|
||||
end
|
||||
|
||||
# Run printer
|
||||
|
@ -45,26 +42,30 @@ module Mutant
|
|||
|
||||
# Visit a collection of objects
|
||||
#
|
||||
# @return [Class::Printer] printer
|
||||
# @return [Enumerable<Object>] collection
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def visit_collection(collection)
|
||||
collection.each(&method(:visit))
|
||||
def visit_collection(printer, collection)
|
||||
collection.each do |object|
|
||||
visit(printer, object)
|
||||
end
|
||||
end
|
||||
|
||||
# Visit object
|
||||
#
|
||||
# @param [Class::Printer] printer
|
||||
# @param [Object] object
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def visit(object)
|
||||
self.class.run(output, object)
|
||||
def visit(printer, object)
|
||||
printer.run(output, object)
|
||||
end
|
||||
|
||||
# Print an info line to output
|
||||
|
@ -93,7 +94,7 @@ module Mutant
|
|||
#
|
||||
# @api private
|
||||
#
|
||||
def puts(string = NL)
|
||||
def puts(string)
|
||||
output.puts(string)
|
||||
end
|
||||
|
||||
|
@ -107,16 +108,6 @@ module Mutant
|
|||
object.success?
|
||||
end
|
||||
|
||||
# Test if output can be colored
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def color?
|
||||
tty?
|
||||
end
|
||||
|
||||
# Colorize message
|
||||
#
|
||||
# @param [Color] color
|
||||
|
@ -133,17 +124,422 @@ module Mutant
|
|||
color.format(message)
|
||||
end
|
||||
|
||||
# Test for output to tty
|
||||
# Test if output is a tty
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def tty?
|
||||
output.respond_to?(:tty?) && output.tty?
|
||||
output.tty?
|
||||
end
|
||||
memoize :tty?
|
||||
|
||||
# Test if output can be colored
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
alias_method :color?, :tty?
|
||||
|
||||
# Printer for run collector
|
||||
class Collector < self
|
||||
|
||||
# Print progress for collector
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
visit(EnvProgress, object.result)
|
||||
active_subject_results = object.active_subject_results
|
||||
info('Active subjects: %d', active_subject_results.length)
|
||||
visit_collection(SubjectProgress, active_subject_results)
|
||||
self
|
||||
end
|
||||
|
||||
end # Collector
|
||||
|
||||
# Progress printer for configuration
|
||||
class Config < self
|
||||
|
||||
# Report configuration
|
||||
#
|
||||
# @param [Mutant::Config] config
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
info 'Mutant configuration:'
|
||||
info 'Matcher: %s', object.matcher_config.inspect
|
||||
info 'Integration: %s', object.integration.name
|
||||
info 'Expect Coverage: %0.2f%%', object.expected_coverage.inspect
|
||||
info 'Includes: %s', object.includes.inspect
|
||||
info 'Requires: %s', object.requires.inspect
|
||||
self
|
||||
end
|
||||
|
||||
end # Config
|
||||
|
||||
# Env progress printer
|
||||
class EnvProgress < self
|
||||
|
||||
delegate(
|
||||
:coverage,
|
||||
:amount_subjects,
|
||||
:amount_mutations,
|
||||
:amount_mutations_alive,
|
||||
:amount_mutations_killed,
|
||||
:runtime,
|
||||
:killtime,
|
||||
:overhead,
|
||||
:env
|
||||
)
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
visit(Config, env.config)
|
||||
info 'Available Subjects: %s', amount_subjects
|
||||
info 'Subjects: %s', amount_subjects
|
||||
info 'Mutations: %s', amount_mutations
|
||||
info 'Kills: %s', amount_mutations_killed
|
||||
info 'Alive: %s', amount_mutations_alive
|
||||
info 'Runtime: %0.2fs', runtime
|
||||
info 'Killtime: %0.2fs', killtime
|
||||
info 'Overhead: %0.2f%%', overhead_percent
|
||||
status 'Coverage: %0.2f%%', coverage_percent
|
||||
status 'Expected: %0.2f%%', env.config.expected_coverage
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return coverage percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def coverage_percent
|
||||
coverage * 100
|
||||
end
|
||||
|
||||
# Return overhead percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def overhead_percent
|
||||
(overhead / killtime) * 100
|
||||
end
|
||||
|
||||
end # EnvProgress
|
||||
|
||||
# Full env result reporter
|
||||
class EnvResult < self
|
||||
|
||||
delegate(:failed_subject_results)
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
visit_collection(SubjectResult, failed_subject_results)
|
||||
visit(EnvProgress, object)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return coverage percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def coverage_percent
|
||||
coverage * 100
|
||||
end
|
||||
|
||||
# Return overhead percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def overhead_percent
|
||||
(overhead / killtime) * 100
|
||||
end
|
||||
|
||||
end # EnvResult
|
||||
|
||||
# Subject report printer
|
||||
class SubjectResult < self
|
||||
|
||||
delegate :subject, :failed_mutations
|
||||
|
||||
# Run report printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
status(subject.identification)
|
||||
subject.tests.each do |test|
|
||||
puts("- #{test.identification}")
|
||||
end
|
||||
visit_collection(MutationResult, object.alive_mutation_results)
|
||||
self
|
||||
end
|
||||
|
||||
end # Subject
|
||||
|
||||
# Reporter for subject progress
|
||||
class SubjectProgress < self
|
||||
|
||||
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
|
||||
|
||||
SUCCESS = '.'.freeze
|
||||
FAILURE = 'F'.freeze
|
||||
|
||||
delegate(
|
||||
:subject,
|
||||
:coverage,
|
||||
:runtime,
|
||||
:amount_mutations_killed,
|
||||
:amount_mutations,
|
||||
:amount_mutation_results,
|
||||
:killtime,
|
||||
:overhead
|
||||
)
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
puts("#{subject.identification} mutations: #{amount_mutations}")
|
||||
print_tests
|
||||
print_mutation_results
|
||||
print_progress_bar_finish
|
||||
print_stats
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Print stats
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_stats
|
||||
status(
|
||||
FORMAT,
|
||||
amount_mutations_killed,
|
||||
amount_mutations,
|
||||
coverage * 100,
|
||||
killtime,
|
||||
runtime,
|
||||
overhead
|
||||
)
|
||||
end
|
||||
|
||||
# Print tests
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_tests
|
||||
subject.tests.each do |test|
|
||||
puts "- #{test.identification}"
|
||||
end
|
||||
end
|
||||
|
||||
# Print progress bar finish
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_progress_bar_finish
|
||||
puts(NL) unless amount_mutation_results.zero?
|
||||
end
|
||||
|
||||
# Print mutation results
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_mutation_results
|
||||
object.mutation_results.each(&method(:print_mutation_result))
|
||||
end
|
||||
|
||||
# Print mutation result
|
||||
#
|
||||
# @param [Result::Mutation] mutation_result
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_mutation_result(mutation_result)
|
||||
char(mutation_result.success? ? SUCCESS : FAILURE)
|
||||
end
|
||||
|
||||
# Write colorized char
|
||||
#
|
||||
# @param [String] char
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def char(char)
|
||||
output.write(colorize(status_color, char))
|
||||
end
|
||||
|
||||
end # Subject
|
||||
|
||||
# Reporter for mutation results
|
||||
class MutationResult < self
|
||||
|
||||
delegate :mutation, :failed_test_results
|
||||
|
||||
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
|
||||
|
||||
MAP = {
|
||||
Mutant::Mutation::Evil => :evil_details,
|
||||
Mutant::Mutation::Neutral => :neutral_details,
|
||||
Mutant::Mutation::Noop => :noop_details
|
||||
}.freeze
|
||||
|
||||
NEUTRAL_MESSAGE =
|
||||
"--- Neutral failure ---\n" \
|
||||
"Original code was inserted unmutated. And the test did NOT PASS.\n" \
|
||||
"Your tests do not pass initially or you found a bug in mutant / unparser.\n" \
|
||||
"Subject AST:\n" \
|
||||
"%s\n" \
|
||||
"Unparsed Source:\n" \
|
||||
"%s\n" \
|
||||
"Test Reports: %d\n"
|
||||
|
||||
NOOP_MESSAGE =
|
||||
"---- Noop failure -----\n" \
|
||||
"No code was inserted. And the test did NOT PASS.\n" \
|
||||
"This is typically a problem of your specs not passing unmutated.\n" \
|
||||
"Test Reports: %d\n"
|
||||
|
||||
FOOTER = '-----------------------'.freeze
|
||||
|
||||
# Run report printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
puts(mutation.identification)
|
||||
print_details
|
||||
puts(FOOTER)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return details
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_details
|
||||
send(MAP.fetch(mutation.class))
|
||||
end
|
||||
|
||||
# Return evil details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def evil_details
|
||||
original, current = mutation.original_source, mutation.source
|
||||
diff = Mutant::Diff.build(original, current)
|
||||
diff = color? ? diff.colorized_diff : diff.diff
|
||||
puts(diff || DIFF_ERROR_MESSAGE)
|
||||
end
|
||||
|
||||
# Noop details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def noop_details
|
||||
info(NOOP_MESSAGE, failed_test_results.length)
|
||||
visit_failed_test_results
|
||||
end
|
||||
|
||||
# Neutral details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def neutral_details
|
||||
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
|
||||
visit_failed_test_results
|
||||
end
|
||||
|
||||
# Visit failed test results
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def visit_failed_test_results
|
||||
visit_collection(TestResult, failed_test_results)
|
||||
end
|
||||
|
||||
end # MutationResult
|
||||
|
||||
# Test result reporter
|
||||
class TestResult < self
|
||||
|
||||
delegate :test, :runtime
|
||||
|
||||
# Run test result reporter
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
status('- %s / runtime: %s', test.identification, object.runtime)
|
||||
puts('Test Output:')
|
||||
puts(object.output)
|
||||
end
|
||||
|
||||
end # TestResult
|
||||
end # Printer
|
||||
end # CLI
|
||||
end # Reporter
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
# Abstract base and namespace class for process printers
|
||||
class Progress < Printer
|
||||
include AbstractType, Registry.new
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,32 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
# Progress printer for configuration
|
||||
class Config < self
|
||||
|
||||
handle(Mutant::Config)
|
||||
|
||||
# Report configuration
|
||||
#
|
||||
# @param [Mutant::Config] config
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
info 'Mutant configuration:'
|
||||
info 'Matcher: %s', object.matcher_config.inspect
|
||||
info 'Integration: %s', object.integration.name
|
||||
info 'Expect Coverage: %0.2f%%', object.expected_coverage.inspect
|
||||
info 'Includes: %s', object.includes.inspect
|
||||
info 'Requires: %s', object.requires.inspect
|
||||
self
|
||||
end
|
||||
|
||||
end # Progress
|
||||
end # Printer
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,31 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
# Progress printer for configuration
|
||||
class Env < self
|
||||
|
||||
handle Mutant::Env
|
||||
|
||||
delegate :config
|
||||
|
||||
# Report configuration
|
||||
#
|
||||
# @param [Mutant::Config] config
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
visit(config)
|
||||
info 'Available Subjects: %d', object.matchable_scopes.length
|
||||
info 'Subjects: %d', object.subjects.length
|
||||
info 'Mutations: %d', object.mutations.length
|
||||
end
|
||||
|
||||
end # Progress
|
||||
end # Printer
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,27 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
# Noop CLI progress reporter
|
||||
class Noop < self
|
||||
|
||||
handle(Mutant::Test)
|
||||
handle(Mutant::Mutation)
|
||||
handle(Mutant::Result::Env)
|
||||
handle(Mutant::Result::Test)
|
||||
|
||||
# Noop progress report
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
self
|
||||
end
|
||||
|
||||
end # Noop
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,12 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
# Abstract namespace class for result progress printers
|
||||
class Result < self
|
||||
include AbstractType
|
||||
end # Result
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,45 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
class Result
|
||||
# Mutation test result progress reporter
|
||||
class Mutation < self
|
||||
|
||||
handle(Mutant::Result::Mutation)
|
||||
|
||||
SUCCESS = '.'.freeze
|
||||
FAILURE = 'F'.freeze
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
char(success? ? SUCCESS : FAILURE)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Write colorized char
|
||||
#
|
||||
# @param [String] char
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def char(char)
|
||||
output.write(colorize(status_color, char))
|
||||
output.flush
|
||||
end
|
||||
|
||||
end # Mutation
|
||||
end # Result
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,54 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
class Result
|
||||
# Reporter for subject runners
|
||||
class Subject < self
|
||||
|
||||
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
|
||||
|
||||
handle(Mutant::Result::Subject)
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
print_progress_bar_finish
|
||||
print_stats
|
||||
self
|
||||
end
|
||||
|
||||
delegate :coverage, :runtime, :amount_mutations_killed, :amount_mutations, :killtime, :overhead
|
||||
|
||||
private
|
||||
|
||||
# Print stats
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_stats
|
||||
status(FORMAT, amount_mutations_killed, amount_mutations, coverage * 100, killtime, runtime, overhead)
|
||||
end
|
||||
|
||||
# Print progress bar finish
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_progress_bar_finish
|
||||
puts unless amount_mutations.zero?
|
||||
end
|
||||
|
||||
end # Subject
|
||||
end # Result
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,27 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Progress
|
||||
# CLI progress reporter for subjects
|
||||
class Subject < self
|
||||
|
||||
handle Mutant::Subject
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
puts("#{object.identification} mutations: #{object.mutations.length}")
|
||||
object.tests.each do |test|
|
||||
puts "- #{test.identification}"
|
||||
end
|
||||
end
|
||||
|
||||
end # Subject
|
||||
end # Progress
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,81 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
# Mixin to generate registry semantics
|
||||
class Registry < Module
|
||||
include Concord.new(:registry)
|
||||
|
||||
# Return new registry
|
||||
#
|
||||
# @return [Registry]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def self.new
|
||||
super({})
|
||||
end
|
||||
|
||||
# Register handler for class
|
||||
#
|
||||
# @param [Class] klass
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def handle(subject, handler)
|
||||
raise "Duplicate registration of #{subject}" if registry.key?(subject)
|
||||
registry[subject] = handler
|
||||
self
|
||||
end
|
||||
|
||||
# Lookup handler
|
||||
#
|
||||
# @param [Class] subject
|
||||
#
|
||||
# @return [Object]
|
||||
# if found
|
||||
#
|
||||
# @raise [RuntimeError]
|
||||
# otherwise
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def lookup(subject)
|
||||
current = subject
|
||||
until current.equal?(Object)
|
||||
if registry.key?(current)
|
||||
return registry.fetch(current)
|
||||
end
|
||||
current = current.superclass
|
||||
end
|
||||
raise "No printer for: #{subject}"
|
||||
end
|
||||
|
||||
# Hook called when module is included
|
||||
#
|
||||
# @param [Class,Module] host
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def included(host)
|
||||
super
|
||||
|
||||
object = self
|
||||
host.class_eval do
|
||||
define_singleton_method(:lookup, &object.method(:lookup))
|
||||
private_class_method :lookup
|
||||
|
||||
define_singleton_method(:handle) do |subject|
|
||||
object.handle(subject, self)
|
||||
end
|
||||
private_class_method :handle
|
||||
end
|
||||
end
|
||||
|
||||
end # Registry
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,10 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
# Abstract base class for process printers
|
||||
class Report < Printer
|
||||
include AbstractType, Registry.new
|
||||
end # Report
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,92 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Report
|
||||
|
||||
# Env result reporter
|
||||
class Env < self
|
||||
|
||||
handle(Result::Env)
|
||||
|
||||
delegate(
|
||||
:coverage, :failed_subject_results, :amount_subjects, :amount_mutations,
|
||||
:amount_mutations_alive, :amount_mutations_killed, :runtime, :killtime, :overhead, :env
|
||||
)
|
||||
|
||||
# Run printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
visit_collection(failed_subject_results)
|
||||
info 'Subjects: %s', amount_subjects
|
||||
info 'Mutations: %s', amount_mutations
|
||||
info 'Kills: %s', amount_mutations_killed
|
||||
info 'Alive: %s', amount_mutations_alive
|
||||
info 'Runtime: %0.2fs', runtime
|
||||
info 'Killtime: %0.2fs', killtime
|
||||
info 'Overhead: %0.2f%%', overhead_percent
|
||||
status 'Coverage: %0.2f%%', coverage_percent
|
||||
status 'Expected: %0.2f%%', env.config.expected_coverage
|
||||
print_generic_stats
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Print generic stats
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_generic_stats
|
||||
stats = generic_stats.to_a.sort_by(&:last)
|
||||
return if stats.empty?
|
||||
info('Nodes handled by generic mutator (type:occurrences):')
|
||||
stats.reverse_each do |type, amount|
|
||||
info('%-10s: %d', type, amount)
|
||||
end
|
||||
end
|
||||
|
||||
# Return coverage percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def coverage_percent
|
||||
coverage * 100
|
||||
end
|
||||
|
||||
# Return overhead percent
|
||||
#
|
||||
# @return [Float]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def overhead_percent
|
||||
(overhead / killtime) * 100
|
||||
end
|
||||
|
||||
# Return stats for nodes handled by generic mutator
|
||||
#
|
||||
# @return [Hash<Symbo, Fixnum>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def generic_stats
|
||||
object.subject_results.each_with_object(Hash.new(0)) do |result, stats|
|
||||
AST.walk(result.subject.node) do |node|
|
||||
stats[node.type] += 1 if Mutator::Registry.lookup(node).equal?(Mutator::Node::Generic)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # Env
|
||||
end # Report
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,103 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Report
|
||||
|
||||
# Reporter for mutations
|
||||
class Mutation < self
|
||||
|
||||
handle Mutant::Result::Mutation
|
||||
|
||||
delegate :mutation, :failed_test_results
|
||||
|
||||
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!'.freeze
|
||||
|
||||
MAP = {
|
||||
Mutant::Mutation::Evil => :evil_details,
|
||||
Mutant::Mutation::Neutral => :neutral_details,
|
||||
Mutant::Mutation::Noop => :noop_details
|
||||
}.freeze
|
||||
|
||||
NEUTRAL_MESSAGE =
|
||||
"--- Neutral failure ---\n" \
|
||||
"Original code was inserted unmutated. And the test did NOT PASS.\n" \
|
||||
"Your tests do not pass initially or you found a bug in mutant / unparser.\n" \
|
||||
"Subject AST:\n" \
|
||||
"%s\n" \
|
||||
"Unparsed Source:\n" \
|
||||
"%s\n" \
|
||||
"Test Reports: %d\n"
|
||||
|
||||
NOOP_MESSAGE =
|
||||
"---- Noop failure -----\n" \
|
||||
"No code was inserted. And the test did NOT PASS.\n" \
|
||||
"This is typically a problem of your specs not passing unmutated.\n" \
|
||||
"Test Reports: %d\n"
|
||||
|
||||
FOOTER = '-----------------------'.freeze
|
||||
|
||||
# Run report printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
puts(mutation.identification)
|
||||
print_details
|
||||
puts(FOOTER)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return details
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def print_details
|
||||
send(MAP.fetch(mutation.class))
|
||||
end
|
||||
|
||||
# Return evil details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def evil_details
|
||||
original, current = mutation.original_source, mutation.source
|
||||
diff = Mutant::Diff.build(original, current)
|
||||
diff = color? ? diff.colorized_diff : diff.diff
|
||||
puts(diff || DIFF_ERROR_MESSAGE)
|
||||
end
|
||||
|
||||
# Noop details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def noop_details
|
||||
info(NOOP_MESSAGE, failed_test_results.length)
|
||||
visit_collection(failed_test_results)
|
||||
end
|
||||
|
||||
# Neutral details
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def neutral_details
|
||||
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
|
||||
visit_collection(failed_test_results)
|
||||
end
|
||||
|
||||
end # Mutation
|
||||
end # Report
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,32 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Report
|
||||
|
||||
# Subject report printer
|
||||
class Subject < self
|
||||
|
||||
delegate :subject, :failed_mutations
|
||||
|
||||
handle(Mutant::Result::Subject)
|
||||
|
||||
# Run report printer
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
status(subject.identification)
|
||||
subject.tests.each do |test|
|
||||
puts("- #{test.identification}")
|
||||
end
|
||||
visit_collection(object.alive_mutation_results)
|
||||
self
|
||||
end
|
||||
|
||||
end # Subject
|
||||
end # Report
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -1,28 +0,0 @@
|
|||
module Mutant
|
||||
class Reporter
|
||||
class CLI
|
||||
class Report
|
||||
# Test result reporter
|
||||
class Test < self
|
||||
|
||||
handle(Mutant::Result::Test)
|
||||
|
||||
delegate :test, :runtime
|
||||
|
||||
# Run test result reporter
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run
|
||||
status('- %s / runtime: %s', test.identification, object.runtime)
|
||||
puts('Test Output:')
|
||||
puts(object.output)
|
||||
end
|
||||
|
||||
end
|
||||
end # Report
|
||||
end # CLI
|
||||
end # Reporter
|
||||
end # Mutant
|
|
@ -12,9 +12,9 @@ module Mutant
|
|||
# @api private
|
||||
#
|
||||
def coverage
|
||||
return Rational(0) if amount_mutations.zero?
|
||||
return Rational(0) if amount_mutation_results.zero?
|
||||
|
||||
Rational(amount_mutations_killed, amount_mutations)
|
||||
Rational(amount_mutations_killed, amount_mutation_results)
|
||||
end
|
||||
|
||||
# Hook called when module gets included
|
||||
|
@ -105,11 +105,11 @@ module Mutant
|
|||
|
||||
# Env result object
|
||||
class Env
|
||||
include Coverage, Result, Anima.new(:runtime, :env, :subject_results)
|
||||
include Coverage, Result, Anima.new(:runtime, :env, :subject_results, :done)
|
||||
|
||||
COVERAGE_PRECISION = 1
|
||||
|
||||
# Test if run was successful
|
||||
# Test if run is successful
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
|
@ -131,6 +131,7 @@ module Mutant
|
|||
end
|
||||
|
||||
sum :amount_mutations, :subject_results
|
||||
sum :amount_mutation_results, :subject_results
|
||||
sum :amount_mutations_alive, :subject_results
|
||||
sum :amount_mutations_killed, :subject_results
|
||||
sum :killtime, :subject_results
|
||||
|
@ -157,16 +158,6 @@ module Mutant
|
|||
:runtime
|
||||
)
|
||||
|
||||
# NOTE:
|
||||
#
|
||||
# The test is intentionally NOT part of the mashalled data.
|
||||
# In rspec the example group cannot deterministically being marshalled, because
|
||||
# they reference a crazy mix of IO objects, global objects etc.
|
||||
#
|
||||
MARSHALLED_IVARS = (anima.attribute_names - [:test]).map do |name|
|
||||
:"@#{name}"
|
||||
end
|
||||
|
||||
# Return killtime
|
||||
#
|
||||
# @return [Float]
|
||||
|
@ -185,31 +176,6 @@ module Mutant
|
|||
mutation.killed_by?(self)
|
||||
end
|
||||
|
||||
# Return marshallable data
|
||||
#
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def marshal_dump
|
||||
MARSHALLED_IVARS.map(&method(:instance_variable_get))
|
||||
end
|
||||
|
||||
# Load marshalled data
|
||||
#
|
||||
# @param [Array] array
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def marshal_load(array)
|
||||
MARSHALLED_IVARS.zip(array) do |instance_variable_name, value|
|
||||
instance_variable_set(instance_variable_name, value)
|
||||
end
|
||||
end
|
||||
|
||||
end # Test
|
||||
|
||||
# Subject result
|
||||
|
@ -239,6 +205,16 @@ module Mutant
|
|||
end
|
||||
memoize :alive_mutation_results
|
||||
|
||||
# Return amount of mutations
|
||||
#
|
||||
# @return [Fixnum]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def amount_mutation_results
|
||||
mutation_results.length
|
||||
end
|
||||
|
||||
# Return amount of mutations
|
||||
#
|
||||
# @return [Fixnum]
|
||||
|
@ -266,7 +242,7 @@ module Mutant
|
|||
# @api private
|
||||
#
|
||||
def amount_mutations_alive
|
||||
amount_mutations - amount_mutations_killed
|
||||
alive_mutation_results.length
|
||||
end
|
||||
|
||||
# Return alive mutations
|
||||
|
@ -284,7 +260,7 @@ module Mutant
|
|||
|
||||
# Mutation result
|
||||
class Mutation
|
||||
include Result, Anima.new(:runtime, :mutation, :test_results)
|
||||
include Result, Anima.new(:runtime, :mutation, :test_results, :index)
|
||||
|
||||
# Test if mutation was handeled successfully
|
||||
#
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Mutant
|
||||
# Runner baseclass
|
||||
class Runner
|
||||
include Adamantium, Concord.new(:env), Procto.call(:result)
|
||||
include Adamantium::Flat, Concord.new(:env), Procto.call(:result)
|
||||
|
||||
# Initialize object
|
||||
#
|
||||
|
@ -12,18 +12,14 @@ module Mutant
|
|||
def initialize(env)
|
||||
super
|
||||
|
||||
@stop = false
|
||||
@collector = Collector.new(env)
|
||||
@mutex = Mutex.new
|
||||
@mutations = env.mutations.dup
|
||||
|
||||
config.integration.setup
|
||||
run
|
||||
|
||||
progress(env)
|
||||
|
||||
@result = Result::Env.compute do
|
||||
{
|
||||
env: env,
|
||||
subject_results: visit_collection(env.subjects, &method(:run_subject))
|
||||
}
|
||||
end
|
||||
@result = @collector.result.update(done: true)
|
||||
|
||||
config.reporter.report(result)
|
||||
end
|
||||
|
@ -38,24 +34,91 @@ module Mutant
|
|||
|
||||
private
|
||||
|
||||
# Run subject
|
||||
# Run mutation analysis
|
||||
#
|
||||
# @return [Report::Subject]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def run_subject(subject)
|
||||
Result::Subject.compute do
|
||||
{
|
||||
subject: subject,
|
||||
mutation_results: visit_collection(subject.mutations, &method(:run_mutation))
|
||||
}
|
||||
def run
|
||||
Parallel.map(
|
||||
@mutations,
|
||||
in_processes: config.processes,
|
||||
finish: method(:finish),
|
||||
start: method(:start),
|
||||
&method(:run_mutation)
|
||||
)
|
||||
end
|
||||
|
||||
# Handle started mutation
|
||||
#
|
||||
# @param [Mutation] mutation
|
||||
# @param [Fixnum] _index
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def start(mutation, _index)
|
||||
@mutex.synchronize do
|
||||
@collector.start(mutation)
|
||||
end
|
||||
end
|
||||
|
||||
# Handle finished mutation
|
||||
#
|
||||
# @param [Mutation] mutation
|
||||
# @param [Fixnum] index
|
||||
# @param [Object] result
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def finish(mutation, index, result)
|
||||
return unless result.kind_of?(Mutant::Result::Mutation)
|
||||
|
||||
test_results = result.test_results.zip(mutation.subject.tests).map do |test_result, test|
|
||||
test_result.update(test: test, mutation: mutation) if test_result
|
||||
end.compact
|
||||
|
||||
@mutex.synchronize do
|
||||
process_result(result.update(index: index, mutation: mutation, test_results: test_results))
|
||||
end
|
||||
end
|
||||
|
||||
# Process result
|
||||
#
|
||||
# @param [Result::Mutation] result
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def process_result(result)
|
||||
@collector.finish(result)
|
||||
config.reporter.progress(@collector)
|
||||
handle_exit(result)
|
||||
end
|
||||
|
||||
# Handle exit if needed
|
||||
#
|
||||
# @param [Result::Mutation] mutation
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def handle_exit(result)
|
||||
return if !config.fail_fast || result.success?
|
||||
|
||||
@mutations.clear
|
||||
end
|
||||
|
||||
# Run mutation
|
||||
#
|
||||
# @param [Mutation]
|
||||
# @param [Mutation] mutation
|
||||
# @param [Fixnum] index
|
||||
#
|
||||
# @return [Report::Mutation]
|
||||
#
|
||||
|
@ -64,7 +127,8 @@ module Mutant
|
|||
def run_mutation(mutation)
|
||||
Result::Mutation.compute do
|
||||
{
|
||||
mutation: mutation,
|
||||
index: nil,
|
||||
mutation: nil,
|
||||
test_results: kill_mutation(mutation)
|
||||
}
|
||||
end
|
||||
|
@ -80,7 +144,7 @@ module Mutant
|
|||
#
|
||||
def kill_mutation(mutation)
|
||||
mutation.subject.tests.each_with_object([]) do |test, results|
|
||||
results << result = run_mutation_test(mutation, test).tap(&method(:progress))
|
||||
results << result = run_mutation_test(mutation, test)
|
||||
return results if mutation.killed_by?(result)
|
||||
end
|
||||
end
|
||||
|
@ -95,33 +159,6 @@ module Mutant
|
|||
env.config
|
||||
end
|
||||
|
||||
# Visit collection
|
||||
#
|
||||
# @return [Array<Result>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def visit_collection(collection)
|
||||
collection.each_with_object([]) do |item, results|
|
||||
progress(item)
|
||||
start = Time.now
|
||||
results << result = yield(item).update(runtime: Time.now - start).tap(&method(:progress))
|
||||
return results if @stop ||= config.fail_fast? && result.fail?
|
||||
end
|
||||
end
|
||||
|
||||
# Report progress
|
||||
#
|
||||
# @param [Object] object
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def progress(object)
|
||||
config.reporter.progress(object)
|
||||
end
|
||||
|
||||
# Return test result
|
||||
#
|
||||
# @return [Report::Test]
|
||||
|
@ -133,11 +170,11 @@ module Mutant
|
|||
config.isolation.call do
|
||||
mutation.insert
|
||||
test.run
|
||||
end.update(test: test, mutation: mutation)
|
||||
end
|
||||
rescue Isolation::Error => exception
|
||||
Result::Test.new(
|
||||
test: test,
|
||||
mutation: mutation,
|
||||
test: nil,
|
||||
mutation: nil,
|
||||
runtime: Time.now - time,
|
||||
output: exception.message,
|
||||
passed: false
|
||||
|
|
119
lib/mutant/runner/collector.rb
Normal file
119
lib/mutant/runner/collector.rb
Normal file
|
@ -0,0 +1,119 @@
|
|||
module Mutant
|
||||
class Runner
|
||||
# Parallel process collector
|
||||
class Collector
|
||||
include Concord::Public.new(:env)
|
||||
|
||||
# Initialize object
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(*)
|
||||
super
|
||||
@start = Time.now
|
||||
@aggregate = Hash.new { |hash, key| hash[key] = [] }
|
||||
@activity = Hash.new(0)
|
||||
end
|
||||
|
||||
# Return active subject results
|
||||
#
|
||||
# @return [Array<Result::Subject>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def active_subject_results
|
||||
active_subjects.map(&method(:subject_result))
|
||||
end
|
||||
|
||||
# Return current result
|
||||
#
|
||||
# @return [Result::Env]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def result
|
||||
Result::Env.new(
|
||||
env: env,
|
||||
runtime: Time.now - @start,
|
||||
subject_results: subject_results,
|
||||
done: false
|
||||
)
|
||||
end
|
||||
|
||||
# Handle mutation start
|
||||
#
|
||||
# @param [Mutation] mutation
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def start(mutation)
|
||||
@activity[mutation.subject] += 1
|
||||
self
|
||||
end
|
||||
|
||||
# Handle mutation finish
|
||||
#
|
||||
# @param [Result::Mutation] mutation_result
|
||||
#
|
||||
# @return [self]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def finish(mutation_result)
|
||||
subject = mutation_result.mutation.subject
|
||||
|
||||
@activity[subject] -= 1
|
||||
@aggregate[subject] << mutation_result
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return current subject results
|
||||
#
|
||||
# @return [Array<Result::Subject>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def subject_results
|
||||
env.subjects.map(&method(:subject_result))
|
||||
end
|
||||
|
||||
# Return active subjects
|
||||
#
|
||||
# @return [Array<Subject>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def active_subjects
|
||||
@activity.select do |_subject, count|
|
||||
count > 0
|
||||
end.map(&:first)
|
||||
end
|
||||
|
||||
# Return current subject result
|
||||
#
|
||||
# @param [Subject] subject
|
||||
#
|
||||
# @return [Array<Subject::Result>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def subject_result(subject)
|
||||
mutation_results = @aggregate[subject].sort_by(&:index)
|
||||
|
||||
Result::Subject.new(
|
||||
subject: subject,
|
||||
runtime: mutation_results.map(&:runtime).inject(0.0, :+),
|
||||
mutation_results: mutation_results
|
||||
)
|
||||
end
|
||||
|
||||
end # Collector
|
||||
end # Runner
|
||||
end # Mutant
|
|
@ -1,4 +1,4 @@
|
|||
module Mutant
|
||||
# The current mutant version
|
||||
VERSION = '0.5.26'.freeze
|
||||
VERSION = '0.6.0'.freeze
|
||||
end # Mutant
|
||||
|
|
|
@ -37,7 +37,7 @@ Gem::Specification.new do |gem|
|
|||
gem.add_runtime_dependency('inflecto', '~> 0.0.2')
|
||||
gem.add_runtime_dependency('anima', '~> 0.2.0')
|
||||
gem.add_runtime_dependency('concord', '~> 0.1.5')
|
||||
gem.add_runtime_dependency('parallel', '~> 1.0.0')
|
||||
gem.add_runtime_dependency('parallel', '~> 1.1.1')
|
||||
|
||||
gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5')
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ module Corpus
|
|||
Dir.chdir(repo_path) do
|
||||
Bundler.with_clean_env do
|
||||
install_mutant
|
||||
system(%W[bundle exec mutant -I lib -r #{name} --score #{expect_coverage} --use rspec #{namespace}*])
|
||||
system(%W[bundle exec mutant --use rspec -I lib -r #{name} --score #{expect_coverage} #{namespace}*])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -132,6 +132,7 @@ Environment:
|
|||
--zombie Run mutant zombified
|
||||
-I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH
|
||||
-r, --require NAME Require file with NAME
|
||||
-j, --jobs NUMBER Number of kill processes. Defaults to number of processors.
|
||||
|
||||
Options:
|
||||
--score COVERAGE Fail unless COVERAGE is not reached exactly
|
||||
|
@ -175,6 +176,16 @@ Options:
|
|||
it_should_behave_like 'a cli parser'
|
||||
end
|
||||
|
||||
context 'with jobs flag' do
|
||||
let(:flags) { %w[--jobs 0] }
|
||||
|
||||
it_should_behave_like 'a cli parser'
|
||||
|
||||
it 'configures expected coverage' do
|
||||
expect(subject.config.processes).to eql(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with score flag' do
|
||||
let(:flags) { %w[--score 99.5] }
|
||||
|
||||
|
|
|
@ -37,7 +37,9 @@ describe Mutant::Matcher::Method::Singleton, '#each' do
|
|||
|
||||
it 'warns about definition on non const/self' do
|
||||
subject
|
||||
expect(env.config.reporter.warn_calls).to include('Can only match :defs on :self or :const got :lvar unable to match')
|
||||
expect(env.config.reporter.warn_calls).to(
|
||||
include('Can only match :defs on :self or :const got :lvar unable to match')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,6 +9,10 @@ describe Mutant::Reporter::CLI do
|
|||
output.read
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Time).to receive(:now).and_return(Time.now)
|
||||
end
|
||||
|
||||
describe '#warn' do
|
||||
subject { object.warn(message) }
|
||||
|
||||
|
@ -21,6 +25,7 @@ describe Mutant::Reporter::CLI do
|
|||
|
||||
let(:result) do
|
||||
Mutant::Result::Env.new(
|
||||
done: true,
|
||||
env: env,
|
||||
runtime: 1.1,
|
||||
subject_results: subject_results
|
||||
|
@ -43,13 +48,24 @@ describe Mutant::Reporter::CLI do
|
|||
let(:matchable_scopes) { double('Matchable Scopes', length: 10) }
|
||||
|
||||
before do
|
||||
allow(mutation).to receive(:subject).and_return(_subject)
|
||||
allow(mutation_a).to receive(:subject).and_return(_subject)
|
||||
allow(mutation_b).to receive(:subject).and_return(_subject)
|
||||
end
|
||||
|
||||
let(:mutation) do
|
||||
let(:mutation_a) do
|
||||
double(
|
||||
'Mutation',
|
||||
identification: 'mutation_id',
|
||||
identification: 'mutation_id-a',
|
||||
class: mutation_class,
|
||||
original_source: 'true',
|
||||
source: mutation_source
|
||||
)
|
||||
end
|
||||
|
||||
let(:mutation_b) do
|
||||
double(
|
||||
'Mutation',
|
||||
identification: 'mutation_id-b',
|
||||
class: mutation_class,
|
||||
original_source: 'true',
|
||||
source: mutation_source
|
||||
|
@ -64,13 +80,15 @@ describe Mutant::Reporter::CLI do
|
|||
class: Mutant::Subject,
|
||||
node: s(:true),
|
||||
identification: 'subject_id',
|
||||
mutations: [mutation],
|
||||
mutations: subject_mutations,
|
||||
tests: [
|
||||
double('Test', identification: 'test_id')
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
let(:subject_mutations) { [mutation_a] }
|
||||
|
||||
let(:test_results) do
|
||||
[
|
||||
double(
|
||||
|
@ -84,22 +102,26 @@ describe Mutant::Reporter::CLI do
|
|||
]
|
||||
end
|
||||
|
||||
let(:mutation_a_result) do
|
||||
double(
|
||||
'Mutation Result',
|
||||
class: Mutant::Result::Mutation,
|
||||
mutation: mutation_a,
|
||||
killtime: 0.5,
|
||||
runtime: 1.0,
|
||||
index: 0,
|
||||
success?: mutation_result_success,
|
||||
test_results: test_results,
|
||||
failed_test_results: mutation_result_success ? [] : test_results
|
||||
)
|
||||
end
|
||||
|
||||
let(:subject_results) do
|
||||
[
|
||||
Mutant::Result::Subject.new(
|
||||
subject: _subject,
|
||||
runtime: 1.0,
|
||||
mutation_results: [
|
||||
double(
|
||||
'Mutation Result',
|
||||
class: Mutant::Result::Mutation,
|
||||
mutation: mutation,
|
||||
killtime: 0.5,
|
||||
success?: mutation_result_success,
|
||||
test_results: test_results,
|
||||
failed_test_results: mutation_result_success ? [] : test_results
|
||||
)
|
||||
]
|
||||
mutation_results: [mutation_a_result]
|
||||
)
|
||||
]
|
||||
end
|
||||
|
@ -107,12 +129,342 @@ describe Mutant::Reporter::CLI do
|
|||
let(:subjects) { [_subject] }
|
||||
|
||||
describe '#progress' do
|
||||
subject { object.progress(reportable) }
|
||||
subject { object.progress(collector) }
|
||||
|
||||
let(:config) { Mutant::Config::DEFAULT.update(includes: %w(include-dir), requires: %w(require-name)) }
|
||||
let(:collector) do
|
||||
Mutant::Runner::Collector.new(env)
|
||||
end
|
||||
|
||||
context 'with env' do
|
||||
let(:reportable) { env }
|
||||
let(:mutation_result_success) { true }
|
||||
|
||||
context 'with empty collector' do
|
||||
it 'writes expected output' do
|
||||
subject
|
||||
expect(contents).to eql(expected_output)
|
||||
end
|
||||
|
||||
let(:expected_output) do
|
||||
strip_indent(<<-REPORT)
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 0
|
||||
Runtime: 0.00s
|
||||
Killtime: 0.00s
|
||||
Overhead: NaN%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
Active subjects: 0
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'with collector active on one subject' do
|
||||
before do
|
||||
collector.start(mutation_a)
|
||||
end
|
||||
|
||||
context 'without progress' do
|
||||
|
||||
it 'writes expected output' do
|
||||
subject
|
||||
expect(contents).to eql(expected_output)
|
||||
end
|
||||
|
||||
let(:expected_output) do
|
||||
strip_indent(<<-REPORT)
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 0
|
||||
Runtime: 0.00s
|
||||
Killtime: 0.00s
|
||||
Overhead: NaN%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
Active subjects: 1
|
||||
subject_id mutations: 1
|
||||
- test_id
|
||||
(00/01) 0% - killtime: 0.00s runtime: 0.00s overhead: 0.00s
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'with progress' do
|
||||
|
||||
let(:subject_mutations) { [mutation_a, mutation_b] }
|
||||
|
||||
before do
|
||||
collector.start(mutation_b)
|
||||
collector.finish(mutation_a_result)
|
||||
end
|
||||
|
||||
context 'on failure' do
|
||||
let(:mutation_result_success) { false }
|
||||
|
||||
it 'writes expected output' do
|
||||
subject
|
||||
expect(contents).to eql(expected_output)
|
||||
end
|
||||
|
||||
let(:expected_output) do
|
||||
strip_indent(<<-REPORT)
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 2
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 0.00s
|
||||
Killtime: 0.50s
|
||||
Overhead: -100.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
Active subjects: 1
|
||||
subject_id mutations: 2
|
||||
- test_id
|
||||
F
|
||||
(00/02) 0% - killtime: 0.50s runtime: 1.00s overhead: 0.50s
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'on success' do
|
||||
it 'writes expected output' do
|
||||
subject
|
||||
expect(contents).to eql(expected_output)
|
||||
end
|
||||
|
||||
let(:expected_output) do
|
||||
strip_indent(<<-REPORT)
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 2
|
||||
Kills: 1
|
||||
Alive: 0
|
||||
Runtime: 0.00s
|
||||
Killtime: 0.50s
|
||||
Overhead: -100.00%
|
||||
Coverage: 100.00%
|
||||
Expected: 100.00%
|
||||
Active subjects: 1
|
||||
subject_id mutations: 2
|
||||
- test_id
|
||||
.
|
||||
(01/02) 100% - killtime: 0.50s runtime: 1.00s overhead: 0.50s
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#report' do
|
||||
subject { object.report(result) }
|
||||
|
||||
context 'with full covergage' do
|
||||
let(:mutation_result_success) { true }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 1
|
||||
Alive: 0
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 100.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'and partial coverage' do
|
||||
let(:mutation_result_success) { false }
|
||||
|
||||
context 'on evil mutation' do
|
||||
context 'with a diff' do
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id-a
|
||||
@@ -1,2 +1,2 @@
|
||||
-true
|
||||
+false
|
||||
-----------------------
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a diff' do
|
||||
let(:mutation_source) { 'true' }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id-a
|
||||
BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!
|
||||
-----------------------
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on neutral mutation' do
|
||||
let(:mutation_class) { Mutant::Mutation::Neutral }
|
||||
let(:mutation_source) { 'true' }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id-a
|
||||
--- Neutral failure ---
|
||||
Original code was inserted unmutated. And the test did NOT PASS.
|
||||
Your tests do not pass initially or you found a bug in mutant / unparser.
|
||||
Subject AST:
|
||||
(true)
|
||||
Unparsed Source:
|
||||
true
|
||||
Test Reports: 1
|
||||
- test_id / runtime: 1.0
|
||||
Test Output:
|
||||
test-output
|
||||
-----------------------
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'on noop mutation' do
|
||||
let(:mutation_class) { Mutant::Mutation::Noop }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id-a
|
||||
---- Noop failure -----
|
||||
No code was inserted. And the test did NOT PASS.
|
||||
This is typically a problem of your specs not passing unmutated.
|
||||
Test Reports: 1
|
||||
- test_id / runtime: 1.0
|
||||
Test Output:
|
||||
test-output
|
||||
-----------------------
|
||||
Mutant configuration:
|
||||
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
|
||||
Integration: null
|
||||
Expect Coverage: 100.00%
|
||||
Includes: []
|
||||
Requires: []
|
||||
Available Subjects: 1
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without subjects' do
|
||||
let(:subjects) { [] }
|
||||
let(:subject_results) { [] }
|
||||
|
||||
let(:config) { Mutant::Config::DEFAULT.update(includes: %w[include-dir], requires: %w[require-name]) }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
|
@ -123,219 +475,16 @@ describe Mutant::Reporter::CLI do
|
|||
Expect Coverage: 100.00%
|
||||
Includes: ["include-dir"]
|
||||
Requires: ["require-name"]
|
||||
Available Subjects: 10
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'with subject' do
|
||||
let(:reportable) { _subject }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id mutations: 1
|
||||
- test_id
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'with subject report' do
|
||||
let(:reportable) { subject_results.first }
|
||||
let(:mutation_result_success) { true }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql("\n(01/01) 100% - killtime: 0.50s runtime: 1.00s overhead: 0.50s\n")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mutation result' do
|
||||
let(:reportable) { subject_results.first.mutation_results.first }
|
||||
|
||||
context 'when mutation results in success' do
|
||||
let(:mutation_result_success) { true }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql('.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mutation results in failure' do
|
||||
let(:mutation_result_success) { false }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql('F')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#report' do
|
||||
subject { object.report(result) }
|
||||
|
||||
context 'with subjects' do
|
||||
|
||||
context 'and full covergage' do
|
||||
let(:mutation_result_success) { true }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 1
|
||||
Alive: 0
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 100.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'and partial covergage' do
|
||||
let(:mutation_result_success) { false }
|
||||
|
||||
context 'on evil mutation' do
|
||||
context 'with a diff' do
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id
|
||||
@@ -1,2 +1,2 @@
|
||||
-true
|
||||
+false
|
||||
-----------------------
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a diff' do
|
||||
let(:mutation_source) { 'true' }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id
|
||||
BUG: Mutation NOT resulted in exactly one diff. Please report a reproduction!
|
||||
-----------------------
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on neutral mutation' do
|
||||
let(:mutation_class) { Mutant::Mutation::Neutral }
|
||||
let(:mutation_source) { 'true' }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id
|
||||
--- Neutral failure ---
|
||||
Original code was inserted unmutated. And the test did NOT PASS.
|
||||
Your tests do not pass initially or you found a bug in mutant / unparser.
|
||||
Subject AST:
|
||||
(true)
|
||||
Unparsed Source:
|
||||
true
|
||||
Test Reports: 1
|
||||
- test_id / runtime: 1.0
|
||||
Test Output:
|
||||
test-output
|
||||
-----------------------
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
||||
context 'on neutral mutation' do
|
||||
let(:mutation_class) { Mutant::Mutation::Noop }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
subject_id
|
||||
- test_id
|
||||
mutation_id
|
||||
---- Noop failure -----
|
||||
No code was inserted. And the test did NOT PASS.
|
||||
This is typically a problem of your specs not passing unmutated.
|
||||
Test Reports: 1
|
||||
- test_id / runtime: 1.0
|
||||
Test Output:
|
||||
test-output
|
||||
-----------------------
|
||||
Subjects: 1
|
||||
Mutations: 1
|
||||
Kills: 0
|
||||
Alive: 1
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.50s
|
||||
Overhead: 120.00%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without subjects' do
|
||||
|
||||
let(:subjects) { [] }
|
||||
let(:subject_results) { [] }
|
||||
|
||||
it 'writes report to output' do
|
||||
subject
|
||||
expect(contents).to eql(strip_indent(<<-REPORT))
|
||||
Subjects: 0
|
||||
Mutations: 0
|
||||
Kills: 0
|
||||
Alive: 0
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.00s
|
||||
Overhead: Inf%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
Available Subjects: 0
|
||||
Subjects: 0
|
||||
Mutations: 0
|
||||
Kills: 0
|
||||
Alive: 0
|
||||
Runtime: 1.10s
|
||||
Killtime: 0.00s
|
||||
Overhead: Inf%
|
||||
Coverage: 0.00%
|
||||
Expected: 100.00%
|
||||
REPORT
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
require 'spec_helper'
|
||||
|
||||
class Double
|
||||
include Concord.new(:name, :attributes)
|
||||
|
||||
def self.new(name, attributes = {})
|
||||
super
|
||||
end
|
||||
|
||||
def update(_attributes)
|
||||
self
|
||||
end
|
||||
|
||||
def method_missing(name, *arguments)
|
||||
super unless attributes.key?(name)
|
||||
fail "Arguments provided for #{name}" if arguments.any?
|
||||
attributes.fetch(name)
|
||||
end
|
||||
end
|
||||
|
||||
# FIXME: This is not even close to a mutation covering spec.
|
||||
describe Mutant::Runner do
|
||||
let(:object) { described_class.new(env) }
|
||||
|
@ -8,24 +26,6 @@ describe Mutant::Runner do
|
|||
let(:config) { Mutant::Config::DEFAULT.update(reporter: reporter, isolation: Mutant::Isolation::None) }
|
||||
let(:subjects) { [subject_a, subject_b] }
|
||||
|
||||
class Double
|
||||
include Concord.new(:name, :attributes)
|
||||
|
||||
def self.new(name, attributes = {})
|
||||
super
|
||||
end
|
||||
|
||||
def update(_attributes)
|
||||
self
|
||||
end
|
||||
|
||||
def method_missing(name, *arguments)
|
||||
super unless attributes.key?(name)
|
||||
fail "Arguments provided for #{name}" if arguments.any?
|
||||
attributes.fetch(name)
|
||||
end
|
||||
end
|
||||
|
||||
let(:subject_a) { Double.new('Subject A', mutations: mutations_a, tests: subject_a_tests) }
|
||||
let(:subject_b) { Double.new('Subject B', mutations: mutations_b) }
|
||||
|
||||
|
@ -70,13 +70,15 @@ describe Mutant::Runner do
|
|||
subject: subject_a,
|
||||
mutation_results: [
|
||||
Mutant::Result::Mutation.new(
|
||||
mutation: mutation_a1,
|
||||
runtime: 0.0,
|
||||
index: 0,
|
||||
mutation: mutation_a1,
|
||||
runtime: 0.0,
|
||||
test_results: [test_report_a1]
|
||||
),
|
||||
Mutant::Result::Mutation.new(
|
||||
mutation: mutation_a2,
|
||||
runtime: 0.0,
|
||||
index: 1,
|
||||
mutation: mutation_a2,
|
||||
runtime: 0.0,
|
||||
test_results: [test_report_a1]
|
||||
)
|
||||
],
|
||||
|
@ -95,22 +97,22 @@ describe Mutant::Runner do
|
|||
Mutant::Result::Env.new(
|
||||
env: env,
|
||||
runtime: 0.0,
|
||||
done: false,
|
||||
subject_results: expected_subject_results
|
||||
)
|
||||
end
|
||||
|
||||
context 'on normal execution' do
|
||||
pending 'on normal execution' do
|
||||
subject { object.result }
|
||||
|
||||
its(:env) { should be(env) }
|
||||
it { should eql(expected_result) }
|
||||
|
||||
it 'reports result' do
|
||||
expect { subject }.to change { config.reporter.report_calls }.from([]).to([expected_result])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when isolation raises error' do
|
||||
skip 'when isolation raises error' do
|
||||
subject { object.result }
|
||||
|
||||
its(:env) { should be(env) }
|
||||
|
|
Loading…
Add table
Reference in a new issue