Merge branch 'spec-output'

This commit is contained in:
Markus Schirp 2014-05-23 01:49:45 +00:00
commit e7158acb17
63 changed files with 1472 additions and 1345 deletions

View file

@ -1,7 +1,7 @@
---
machine:
ruby:
version: 2.0.0
version: 2.1.2
test:
override:
- bundle exec rake ci

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 839
total_score: 812

View file

@ -10,6 +10,5 @@ ignore_subjects:
- Mutant::Reporter*
- Mutant::CLI*
- Mutant.singleton_subclass_instance
- Mutant.symbolset
# Executing this has undefined behavior with the zombifier
- Mutant.zombify

View file

@ -51,9 +51,11 @@ NestedIterators:
- Mutant#self.singleton_subclass_instance
- Mutant::CLI#parse
- Mutant::Mutator::Util::Array::Element#dispatch
- Mutant::Reporter::CLI::Printer::Config::Runner#generic_stats
- Mutant::Reporter::CLI::Report::Config#generic_stats
- Mutant::RequireHighjack#infect
- Mutant::RequireHighjack#desinfect
- Mutant::Reporter::CLI::Registry#included
- Mutant::Strategy#tests
- Parser::Lexer#self.new
max_allowed_nesting: 1
ignore_iterators: []
@ -63,6 +65,7 @@ RepeatedConditional:
enabled: true
exclude:
- Mutant::Mutator
- Mutant::Rspec::Strategy
- Mutant::Reporter::CLI
max_ifs: 1
TooManyInstanceVariables:
@ -84,12 +87,14 @@ TooManyStatements:
enabled: true
exclude:
- Mutant#self.singleton_subclass_instance
- Mutant#self.isolate
- Mutant::Rspec::Killer#run
- Mutant::Reporter::CLI#colorized_diff
- Mutant::Reporter::CLI::Printer::Config::Runner#run
- Mutant::Runner#dispatch
- Mutant::Reporter::CLI::Report::Config#run
- Mutant::Runner#visit_collection
- Mutant::Zombifier::File#self.find
- Mutant::RequireHighjack#infect
- Mutant::Reporter::CLI::Registry#included
# How mutant does CLI parsing is shit
- Mutant::CLI#parse
- Mutant::CLI#initialize

View file

@ -99,6 +99,10 @@ IndentationWidth:
EmptyLinesAroundBody:
Enabled: false
# I test this style for a while
LambdaCall:
Enabled: false
# I like my style more
AccessModifierIndentation:
Enabled: false

View file

@ -1,5 +1,7 @@
# encoding: UTF-8
require 'rspec'
require 'mutant/rspec'
require 'mutant/rspec/killer'
require 'mutant/rspec/strategy'
require 'mutant/rspec/test'

View file

@ -37,18 +37,42 @@ module Mutant
self
end
# Return a frozen set of symbols from string enumerable
IsolationError = Class.new(RuntimeError)
# Call block in isolation
#
# @param [Enumerable<String>]
# This isolation implements the fork strategy.
# Future strategies will probably use a process pool that can
# handle multiple mutation kills, in-isolation at once.
#
# @return [Set<Symbol>]
# @return [Object]
#
# @api private
#
def self.symbolset(strings)
strings.map(&:to_sym).to_set.freeze
def self.isolate(&block)
reader, writer = IO.pipe
pid = fork do
reader.close
writer.write(Marshal.dump(block.call))
end
writer.close
begin
data = Marshal.load(reader.read)
rescue ArgumentError
raise IsolationError, 'Childprocess wrote un-unmarshallable data'
end
status = Process.waitpid2(pid).last
unless status.exitstatus.zero?
raise IsolationError, "Childprocess exited with nonzero exit status: #{status.exitstatus}"
end
data
end
private_class_method :symbolset
# Define instance of subclassed superclass as constant
#
@ -78,6 +102,7 @@ end # Mutant
require 'mutant/version'
require 'mutant/cache'
require 'mutant/delegator'
require 'mutant/node_helpers'
require 'mutant/warning_filter'
require 'mutant/warning_expectation'
@ -162,27 +187,33 @@ require 'mutant/matcher/scope'
require 'mutant/matcher/filter'
require 'mutant/matcher/null'
require 'mutant/killer'
require 'mutant/killer/static'
require 'mutant/killer/forking'
require 'mutant/killer/forked'
require 'mutant/test'
require 'mutant/strategy'
require 'mutant/runner'
require 'mutant/runner/config'
require 'mutant/runner/subject'
require 'mutant/runner/mutation'
require 'mutant/runner/killer'
require 'mutant/cli'
require 'mutant/cli/classifier'
require 'mutant/cli/classifier/namespace'
require 'mutant/cli/classifier/method'
require 'mutant/color'
require 'mutant/differ'
require 'mutant/diff'
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/printer/config'
require 'mutant/reporter/cli/printer/subject'
require 'mutant/reporter/cli/printer/killer'
require 'mutant/reporter/cli/printer/mutation'
require 'mutant/reporter/cli/report'
require 'mutant/reporter/cli/report/config'
require 'mutant/reporter/cli/report/subject'
require 'mutant/reporter/cli/report/mutation'
require 'mutant/reporter/cli/progress'
require 'mutant/reporter/cli/progress/config'
require 'mutant/reporter/cli/progress/subject'
require 'mutant/reporter/cli/progress/mutation'
require 'mutant/reporter/cli/progress/noop'
require 'mutant/zombifier'
require 'mutant/zombifier/file'

View file

@ -2,23 +2,23 @@
module Mutant
symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze }
# Set of nodes that cannot be on the LHS of an assignment
NOT_ASSIGNABLE = symbolset %w(
int float str dstr class module self
)
NOT_ASSIGNABLE = symbolset.(%w(int float str dstr class module self))
# Set of op-assign types
OP_ASSIGN = symbolset %w(or_asgn and_asgn op_asgn)
OP_ASSIGN = symbolset.call(%w(or_asgn and_asgn op_asgn))
# Set of node types that are not valid when emitted standalone
NOT_STANDALONE = symbolset %w( splat restarg block_pass)
INDEX_OPERATORS = symbolset %w([] []=)
UNARY_METHOD_OPERATORS = symbolset %w(~@ +@ -@ !)
NOT_STANDALONE = symbolset.(%w( splat restarg block_pass))
INDEX_OPERATORS = symbolset.(%w([] []=))
UNARY_METHOD_OPERATORS = symbolset.(%w(~@ +@ -@ !))
# Operators ruby implementeds as methods
METHOD_OPERATORS = symbolset %w(
METHOD_OPERATORS = symbolset.(%w(
<=> === []= [] <= >= == !~ != =~ <<
>> ** * % / | ^ & < > + - ~@ +@ -@ !
)
))
BINARY_METHOD_OPERATORS = (
METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS)
@ -32,10 +32,10 @@ module Mutant
#
# not - 1.8 only, mutant does not support 1.8
#
NODE_BLACKLIST = symbolset %w(not)
NODE_BLACKLIST = symbolset.(%w(not))
# Nodes that are NOT generated by parser but used by mutant / unparser.
NODE_EXTRA = symbolset %w(empty)
NODE_EXTRA = symbolset.(%w(empty))
NODE_TYPES = ((Parser::Meta::NODE_TYPES + NODE_EXTRA) - NODE_BLACKLIST).to_set.freeze

49
lib/mutant/delegator.rb Normal file
View file

@ -0,0 +1,49 @@
module Mutant
module Delegator
module ClassMethods
private
# Create delegators to object
#
# @return [undefined]
#
# @api private
#
def delegate(*names)
names.each do |name|
define_delegator(name)
end
end
# Create delegator to object
#
# @param [Symbol] name
#
# @return [undefined]
#
# @api private
#
def define_delegator(name)
define_method(name) do
object.public_send(name)
end
private name
end
end # ClassMethods
# Hook called when module is included
#
# @param [Class,Module] host
#
# @api private
#
def self.included(host)
super
host.class_eval do
extend ClassMethods
end
end
end # Delegator
end # Mutant

View file

@ -2,7 +2,7 @@
module Mutant
# Class to create diffs from source code
class Differ
class Diff
include Adamantium::Flat, Concord.new(:old, :new)
# Return source diff
@ -20,7 +20,7 @@ module Mutant
when 0
nil
when 1
Diff::LCS::Hunk.new(old, new, diffs.first, max_length, 0)
::Diff::LCS::Hunk.new(old, new, diffs.first, max_length, 0)
.diff(:unified) << "\n"
else
$stderr.puts(
@ -55,7 +55,7 @@ module Mutant
# @param [String] old
# @param [String] new
#
# @return [Differ]
# @return [Diff]
#
# @api private
#
@ -85,7 +85,7 @@ module Mutant
# @api private
#
def diffs
Diff::LCS.diff(old, new)
::Diff::LCS.diff(old, new)
end
memoize :diffs
@ -118,5 +118,5 @@ module Mutant
end.format(line)
end
end # Differ
end # Diff
end # Mutant

View file

@ -1,123 +1,46 @@
# encoding: utf-8
module Mutant
# Abstract base class for mutant killers
# Mutation killer
class Killer
include Adamantium::Flat, AbstractType
include Equalizer.new(:strategy, :mutation, :killed?)
include Adamantium::Flat, Anima.new(:test, :mutation)
# Return strategy
#
# @return [Strategy]
#
# @api private
#
attr_reader :strategy
# Report object for kill results
class Report
include Anima.new(
:killer,
:test_report
)
# Return mutation to kill
#
# @return [Mutation]
#
# @api private
#
attr_reader :mutation
# Initialize killer object
#
# @param [Strategy] strategy
# @param [Mutation] mutation
#
# @return [undefined]
#
# @api private
#
def initialize(strategy, mutation)
@strategy, @mutation = strategy, mutation
@killed = run
end
# Test for kill failure
#
# @return [true]
# when killer succeeded
#
# @return [false]
# otherwise
#
# @api private
#
def success?
mutation.success?(self)
end
memoize :success?
# Test if mutant was killed
#
# @return [true]
# if mutant was killed
#
# @return [false]
# otherwise
#
# @api private
#
def killed?
@killed
end
# Return mutated source
#
# @return [String]
#
# @api private
#
def mutation_source
mutation.source
end
private
# Return subject
#
# @return [Subject]
#
# @api private
#
def subject
mutation.subject
end
# Run killer
#
# @return [true]
# when mutant was killed
#
# @return [false]
# otherwise
#
# @api private
#
abstract_method :run
# Null killer that never kills a mutation
class Null < self
private
# Run killer
# Test if kill was successful
#
# @return [true]
# when mutant was killed
#
# @return [false]
# otherwise
# @return [Boolean]
#
# @api private
#
def run
false
def success?
killer.mutation.should_fail?.equal?(test_report.failed?)
end
end # Report
# Return killer report
#
# @return [Killer::Report]
#
# @api private
#
def run
test_report = Mutant.isolate do
mutation.insert
test.run
end
Report.new(
killer: self,
test_report: test_report.update(test: test)
)
end
end # Killer
end # Mutant

View file

@ -1,46 +0,0 @@
# encoding: utf-8
module Mutant
class Killer
# Killer that executes other killer in forked environment
class Forked < self
# Initialize object
#
# @param [Killer] killer
# @param [Strategy] strategy
# @param [Mutation] mutation
#
# @api private
#
def initialize(killer, strategy, mutation)
@killer = killer
super(strategy, mutation)
end
private
# Run killer
#
# @return [true]
# if mutant was killed
#
# @return [false]
# otherwise
#
# @api private
#
def run
pid = fork do
killer = @killer.new(strategy, mutation)
exit(killer.killed? ? CLI::EXIT_SUCCESS : CLI::EXIT_FAILURE)
end
status = Process.wait2(pid).last
status.exited? && status.success?
end
end # Forked
end # Killer
end # Mutant

View file

@ -1,46 +0,0 @@
# encoding: utf-8
module Mutant
class Killer
# A killer that executes other killer in forked environemnts
class Forking < self
include Equalizer.new(:killer)
# Return killer
#
# @return [Killer]
#
# @api private
#
attr_reader :killer
# Initalize killer
#
# @param [Killer] killer
# the killer that will be used
#
# @return [undefined]
#
# @api private
#
def initialize(killer)
@killer = killer
end
# Return killer instance
#
# @param [Strategy] strategy
# @param [Mutation] mutation
#
# @return [Killer::Forked]
#
# @api private
#
def new(strategy, mutation)
Forked.new(killer, strategy, mutation)
end
end # Forking
end # Killer
end # Mutant

View file

@ -1,34 +0,0 @@
# encoding: utf-8
module Mutant
class Killer
# Abstract base class for killer with static result
class Static < self
# Return result
#
# @return [true]
# if mutation was killed
#
# @return [false]
# otherwise
#
# @api private
#
def run
self.class::RESULT
end
# Killer that is always successful
class Success < self
RESULT = true
end # Success
# Killer that always fails
class Fail < self
RESULT = false
end # Fail
end # Static
end # Killer
end # Mutant

View file

@ -6,6 +6,9 @@ module Mutant
include AbstractType, Adamantium::Flat
include Concord::Public.new(:subject, :node)
CODE_DELIMITER = "\0".freeze
CODE_RANGE = (0..4).freeze
# Return mutated root node
#
# @return [Parser::AST::Node]
@ -54,8 +57,9 @@ module Mutant
# @api private
#
def identification
"#{subject.identification}:#{code}"
"#{self.class::SYMBOL}:#{subject.identification}:#{code}"
end
memoize :identification
# Return mutation code
#
@ -64,7 +68,7 @@ module Mutant
# @api private
#
def code
sha1[0..4]
sha1[CODE_RANGE]
end
memoize :code
@ -89,6 +93,16 @@ module Mutant
subject.source
end
# Test if test should fail under mutation
#
# @return [Boolean]
#
# @api private
#
def should_fail?
self.class::SHOULD_FAIL
end
private
# Return sha1 sum of source and subject identification
@ -98,7 +112,7 @@ module Mutant
# @api private
#
def sha1
Digest::SHA1.hexdigest(subject.identification + 0.chr + source)
Digest::SHA1.hexdigest(subject.identification + CODE_DELIMITER + source)
end
memoize :sha1

View file

@ -5,16 +5,8 @@ module Mutant
# Evul mutation
class Evil < self
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"evil:#{super}"
end
memoize :identification
SHOULD_FAIL = true
SYMBOL = 'evil'.freeze
# Test if killer is successful
#

View file

@ -5,41 +5,15 @@ module Mutant
# Neutral mutation
class Neutral < self
SYMBOL = 'neutral'
SYMBOL = 'neutral'.freeze
SHOULD_FAIL = false
# Noop mutation, special case of neutral
class Noop < self
SYMBOL = 'noop'
SYMBOL = 'noop'.freeze
end
# Return identification
#
# @return [String]
#
# @api private
#
def identification
"#{self.class::SYMBOL}:#{super}"
end
memoize :identification
# Test if killer is successful
#
# @param [Killer] killer
#
# @return [true]
# if killer did NOT killed mutation
#
# @return [false]
# otherwise
#
# @api private
#
def success?(killer)
!killer.killed?
end
end # Noop
end # Neutral
end # Mutation

View file

@ -13,7 +13,7 @@ module Mutant
# @api private
#
def s(type, *children)
::Parser::AST::Node.new(type, children)
Parser::AST::Node.new(type, children)
end
module_function :s
@ -21,8 +21,6 @@ module Mutant
s(:send, s(:float, 0.0), :/, s(:float, 0.0))
INFINITY =
s(:send, s(:float, 1.0), :/, s(:float, 0.0))
NEW_OBJECT =
s(:send, s(:const, s(:cbase), :Object), :new)
NEGATIVE_INFINITY =
s(:send, s(:float, -1.0), :/, s(:float, 0.0))

View file

@ -15,5 +15,15 @@ module Mutant
#
abstract_method :report
# Report progress on object
#
# @param [Object] object
#
# @return [self]
#
# @api private
#
abstract_method :progress
end # Reporter
end # Mutant

View file

@ -4,10 +4,23 @@ module Mutant
class Reporter
# Reporter that reports in human readable format
class CLI < self
include Concord::Public.new(:output)
include Concord.new(:output)
NL = "\n".freeze
# Report progress object
#
# @param [Object] object
#
# @return [self]
#
# @api private
#
def progress(object)
Progress.run(output, object)
self
end
# Report object
#
# @param [Object] object
@ -17,7 +30,7 @@ module Mutant
# @api private
#
def report(object)
Printer.visit(object, output)
Report.run(output, object)
self
end

View file

@ -6,69 +6,19 @@ module Mutant
# CLI runner status printer base class
class Printer
include AbstractType, Adamantium::Flat, Concord.new(:object, :output)
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
REGISTRY = {}
# Create delegators to object
# Run printer on object to output
#
# @return [undefined]
# @param [IO] output
# @param [Object] object
#
# @api private
# @return [self]
#
def self.delegate(*names)
names.each do |name|
define_delegator(name)
end
end
private_class_method :delegate
# Create delegator to object
#
# @param [Symbol] name
#
# @return [undefined]
#
# @api private
#
def self.define_delegator(name)
define_method(name) do
object.public_send(name)
end
private name
end
private_class_method :define_delegator
# Registre handler for class
#
# @param [Class] klass
#
# @return [undefined]
#
# @api private
#
def self.handle(klass)
REGISTRY[klass] = self
end
# Finalize CLI reporter
#
# @return [undefined]
#
# @api private
#
def self.finalize
REGISTRY.freeze
end
# Build printer
#
# @return [Printer]
#
# @api private
#
def self.build(*args)
new(*args)
def self.run(output, object)
handler = lookup(object.class)
handler.new(output, object).run
self
end
# Run printer
@ -77,49 +27,6 @@ module Mutant
#
# @api private
#
def self.run(*args)
build(*args).run
self
end
# Visit object
#
# @param [Object] object
# @param [IO] output
#
# @return [undefined]
#
# @api private
#
def self.visit(object, output)
printer = lookup(object.class)
printer.run(object, output)
end
# Lookup printer class
#
# @param [Class] klass
#
# @return [Class:Printer]
# if found
#
# @raise [RuntimeError]
# otherwise
#
# @api private
#
def self.lookup(klass)
current = klass
until current == Object
if REGISTRY.key?(current)
return REGISTRY.fetch(current)
end
current = current.superclass
end
raise "No printer for: #{klass}"
end
private_class_method :lookup
abstract_method :run
private
@ -130,7 +37,7 @@ module Mutant
#
# @api private
#
def color
def status_color
success? ? Color::GREEN : Color::RED
end
@ -143,7 +50,7 @@ module Mutant
# @api private
#
def visit(object)
self.class.visit(object, output)
self.class.run(output, object)
end
# Print an info line to output
@ -163,7 +70,7 @@ module Mutant
# @api private
#
def status(string, *arguments)
puts(colorize(color, sprintf(string, *arguments)))
puts(colorize(status_color, sprintf(string, *arguments)))
end
# Print a line to output

View file

@ -1,154 +0,0 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Printer
# Printer for configuration
class Config < self
handle(Mutant::Config)
delegate :matcher, :strategy, :expected_coverage
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', matcher.inspect
info 'Strategy: %s', strategy.inspect
info 'Expect Coverage: %02f%%', expected_coverage.inspect
self
end
# Config results printer
class Runner < self
handle(Mutant::Runner::Config)
delegate(
:amount_kills, :amount_mutations, :amount_kils,
:coverage, :subjects, :failed_subjects, :runtime, :mutations
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
print_mutations
info 'Subjects: %s', amount_subjects
info 'Mutations: %s', amount_mutations
info 'Kills: %s', amount_kills
info 'Alive: %s', amount_alive
info 'Runtime: %0.2fs', runtime
info 'Killtime: %0.2fs', killtime
info 'Overhead: %0.2f%%', overhead
status 'Coverage: %0.2f%%', coverage
status 'Expected: %0.2f%%', object.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 stats for nodes handled by generic mutator
#
# @return [Hash<Symbo, Fixnum>]
#
# @api private
#
def generic_stats
subjects.each_with_object(Hash.new(0)) do |runner, stats|
Walker.run(runner.subject.node) do |node|
if Mutator::Registry.lookup(node) == Mutator::Node::Generic
stats[node.type] += 1
end
end
end
end
# Return amount of subjects
#
# @return [Fixnum]
#
# @api private
#
def amount_subjects
subjects.length
end
# Print mutations
#
# @return [undefined]
#
# @api private
#
def print_mutations
failed_subjects.each do |subject|
Subject::Runner::Details.run(subject, output)
end
end
# Return amount of time in killers
#
# @return [Float]
#
# @api private
#
def killtime
mutations.map(&:runtime).inject(0, :+)
end
memoize :killtime
# Return mutant overhead
#
# @return [Float]
#
# @api private
#
def overhead
return 0 if runtime.zero?
Rational(runtime - killtime, runtime) * 100
end
# Return amount of alive mutations
#
# @return [Fixnum]
#
# @api private
#
def amount_alive
object.amount_mutations - amount_kills
end
end # Runner
end # Config
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -1,103 +0,0 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Printer
# Mutation printer
class Mutation < self
handle(Runner::Mutation)
# Build printer
#
# @param [Runner::Mutation] runner
# @param [IO] output
#
# @return [Printer::Mutation]
#
# @api private
#
def self.build(runner, output)
mutation = runner.mutation
lookup(mutation.class).new(runner, output)
end
# Run mutation printer
#
# @return [undefined]
#
# @api private
#
def run
status('%s', mutation.identification)
puts(details)
end
private
# Return mutation
#
# @return [Mutation]
#
# @api private
#
def mutation
object.mutation
end
# Reporter for noop mutations
class Noop < self
handle(Mutant::Mutation::Neutral::Noop)
MESSAGE = [
'Parsed subject AST:',
'%s',
'Unparsed source:',
'%s'
].join("\n")
private
# Return details
#
# @return [String]
#
# @api private
#
def details
sprintf(
MESSAGE,
mutation.subject.node.inspect,
mutation.original_source
)
end
end # Noop
# Reporter for neutral and evil mutations
class Diff < self
handle(Mutant::Mutation::Neutral)
handle(Mutant::Mutation::Evil)
# Return diff
#
# @return [String]
#
# @api private
#
def details
original, current = mutation.original_source, mutation.source
differ = Differ.build(original, current)
color? ? differ.colorized_diff : differ.diff
end
end # Evil
end # Mutantion
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -1,150 +0,0 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Printer
# Subject results printer
class Subject < self
handle(Mutant::Subject)
# Run subject results printer
#
# @return [undefined]
#
# @api private
#
def run
info('%s', object.identification)
end
# Printer for subject runners
class Runner < self
handle(Mutant::Runner::Subject)
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
print_progress_bar_finish
print_stats
self
end
private
# Return mutation time on subject
#
# @return [Float]
#
# @api private
#
def time
mutations.map(&:runtime).inject(0, :+)
end
# Return subject
#
# @return [Subject]
#
# @api private
#
def subject
object.subject
end
FORMAT = '(%02d/%02d) %3d%% - %0.02fs'.freeze
# Print stats
#
# @return [undefned
#
# @api private
#
def print_stats
status(FORMAT, amount_kills, amount_mutations, coverage, time)
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts unless amount_mutations.zero?
end
# Return kills
#
# @return [Fixnum]
#
# @api private
#
def amount_kills
fails = object.failed_mutations
fails = fails.length
amount_mutations - fails
end
# Return amount of mutations
#
# @return [Array<Mutation>]
#
# @api private
#
def amount_mutations
mutations.length
end
# Return mutations
#
# @return [Array<Mutation>]
#
# @api private
#
def mutations
object.mutations
end
# Return suject coverage
#
# @return [Float]
#
# @api private
#
def coverage
return 0 if amount_mutations.zero?
Rational(amount_kills, amount_mutations) * 100
end
# Detailed subject printer
class Details < self
# Run subject details printer
#
# @return [undefined]
#
# @api private
#
def run
puts(subject.identification)
object.failed_mutations.each do |mutation|
visit(mutation)
end
print_stats
end
end # Details
end # Runner
end # Subject
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,12 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
# Abstract base class for process printers
class Progress < Printer
include AbstractType, Registry.new
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,32 @@
module Mutant
class Reporter
class CLI
class Progress
# Progress printer for configuration
class Config < self
handle(Mutant::Config)
delegate :matcher, :strategy, :expected_coverage
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', matcher.inspect
info 'Strategy: %s', strategy.inspect
info 'Expect Coverage: %02f%%', expected_coverage.inspect
self
end
end # Progress
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -1,14 +1,12 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Printer
class Progress
# Printer for killer results
class Killer < self
# Mutation progress reporter
class Mutation < self
handle(Mutant::Killer)
handle(Runner::Mutation)
SUCCESS = '.'.freeze
FAILURE = 'F'.freeze
@ -20,11 +18,7 @@ module Mutant
# @api private
#
def run
if success?
char(SUCCESS, Color::GREEN)
else
char(FAILURE, Color::RED)
end
char(success? ? SUCCESS : FAILURE)
end
private
@ -32,19 +26,18 @@ module Mutant
# Write colorized char
#
# @param [String] char
# @param [Color]
#
# @return [undefined]
#
# @api private
#
def char(char, color)
output.write(colorize(color, char))
def char(char)
output.write(colorize(status_color, char))
output.flush
end
end # Killer
end # Printer
end # Mutation
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

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

View file

@ -0,0 +1,118 @@
module Mutant
class Reporter
class CLI
class Progress
# Subject results printer
class Subject < self
handle(Mutant::Subject)
# Run subject results printer
#
# @return [undefined]
#
# @api private
#
def run
puts(object.identification)
end
end # Subject
# Reporter for subject runners
class SubjectRunner < self
FORMAT = '(%02d/%02d) %3d%% - %0.02fs'.freeze
handle(Mutant::Runner::Subject)
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
print_progress_bar_finish
print_stats
self
end
private
# Return mutation time on subject
#
# @return [Float]
#
# @api private
#
def time
mutations.map(&:runtime).inject(0, :+)
end
# Print stats
#
# @return [undefined]
#
# @api private
#
def print_stats
status(FORMAT, amount_kills, amount_mutations, coverage, time)
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts unless amount_mutations.zero?
end
# Return kills
#
# @return [Fixnum]
#
# @api private
#
def amount_kills
amount_mutations - object.failed_mutations.length
end
# Return amount of mutations
#
# @return [Array<Mutation>]
#
# @api private
#
def amount_mutations
mutations.length
end
# Return mutations
#
# @return [Array<Mutation>]
#
# @api private
#
def mutations
object.mutations
end
# Return subject coverage
#
# @return [Float]
#
# @api private
#
def coverage
return 0 if amount_mutations.zero?
Rational(amount_kills, amount_mutations) * 100
end
end # Runner
end # Progress
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,77 @@
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 == 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]
#
def included(host)
object = self
host.class_eval do
define_singleton_method(:lookup, &object.method(:lookup))
private_class_method :lookup
define_singleton_method(:handle) do |subject|
object.handle(subject, self)
end
private_class_method :handle
end
end
end # Registry
end # CLI
end # Reporter
end # Mutant

View file

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

View file

@ -0,0 +1,118 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Report
# Printer for configuration
class Config < self
handle(Mutant::Runner::Config)
delegate(
:amount_kills, :amount_mutations, :amount_kils,
:coverage, :subjects, :failed_subjects, :runtime, :mutations
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
failed_subjects.each(&method(:visit))
info 'Subjects: %s', amount_subjects
info 'Mutations: %s', amount_mutations
info 'Kills: %s', amount_kills
info 'Alive: %s', amount_alive
info 'Runtime: %0.2fs', runtime
info 'Killtime: %0.2fs', killtime
info 'Overhead: %0.2f%%', overhead
status 'Coverage: %0.2f%%', coverage
status 'Expected: %0.2f%%', object.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 stats for nodes handled by generic mutator
#
# @return [Hash<Symbo, Fixnum>]
#
# @api private
#
def generic_stats
subjects.each_with_object(Hash.new(0)) do |runner, stats|
Walker.run(runner.subject.node) do |node|
if Mutator::Registry.lookup(node) == Mutator::Node::Generic
stats[node.type] += 1
end
end
end
end
# Return amount of subjects
#
# @return [Fixnum]
#
# @api private
#
def amount_subjects
subjects.length
end
# Return amount of time in killers
#
# @return [Float]
#
# @api private
#
def killtime
mutations.map(&:runtime).inject(0, :+)
end
memoize :killtime
# Return mutant overhead
#
# @return [Float]
#
# @api private
#
def overhead
return 0 if runtime.zero?
Rational(runtime - killtime, runtime) * 100
end
# Return amount of alive mutations
#
# @return [Fixnum]
#
# @api private
#
def amount_alive
object.amount_mutations - amount_kills
end
end # Config
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,88 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Report
# Reporter for mutations
class Mutation < self
# Run report printer
#
# @return [self]
#
# @api private
#
def run
puts(object.identification)
puts(details)
self
end
# Reporter for noop mutations
class Noop < self
handle(Mutant::Mutation::Neutral::Noop)
MESSAGE = [
'Parsed subject AST:',
'%s',
'Unparsed source:',
'%s'
].join("\n").freeze
private
# Return details
#
# @return [self]
#
# @api private
#
def details
MESSAGE % [object.subject.node.inspect, object.original_source]
end
end # Noop
# Reporter for mutations producing a diff
class Diff < self
handle(Mutant::Mutation::Evil)
handle(Mutant::Mutation::Neutral)
private
# Run report printer
#
# @return [self]
#
# @api private
#
def details
original, current = object.original_source, object.source
diff = Mutant::Diff.build(original, current)
color? ? diff.colorized_diff : diff.diff
end
end # Diff
end # Mutation
# Subject report printer
class MutationRunner < self
handle(Mutant::Runner::Mutation)
# Run report printer
#
# @return [self]
#
# @api private
#
def run
visit(object.mutation)
end
end # Mutation
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,33 @@
# encoding: utf-8
module Mutant
class Reporter
class CLI
class Report
# Subject report printer
class Subject < self
handle(Mutant::Runner::Subject)
delegate :subject, :failed_mutations
# Run report printer
#
# @return [self]
#
# @api private
#
def run
status(subject.identification)
object.tests.each do |test|
puts("- #{test.identification}")
end
object.failed_mutations.each(&method(:visit))
self
end
end # Subject
end # Report
end # CLI
end # Reporter
end # Mutant

View file

@ -5,6 +5,7 @@ module Mutant
# Null reporter
class Null < self
include Equalizer.new
# Report object
#
@ -18,6 +19,18 @@ module Mutant
self
end
# Report progress on object
#
# @param [Object] _object
#
# @return [self]
#
# @api private
#
def progress(_object)
self
end
end # Null
end # Reporter
end # Mutant

View file

@ -0,0 +1,41 @@
module Mutant
class Reporter
# Reporter to trace report calls, used as a spec adapter
class Trace
include Concord::Public.new(:progress_calls, :report_calls)
# Return new trace reporter
#
# @return [Tracer]
#
# @api private
#
def self.new
super([], [])
end
# Report object
#
# @param [Object] object
#
# @return [self]
#
def report(object)
report_calls << object
self
end
# Report new progress on object
#
# @param [Object] object
#
# @return [self]
#
def progress(object)
progress_calls << object
self
end
end # Tracker
end # reporter
end # Mutant

View file

@ -1,7 +1,5 @@
# encoding: UTF-8
require 'rspec'
module Mutant
# Rspec integration namespace
module Rspec

View file

@ -1,104 +0,0 @@
# encoding: utf-8
module Mutant
module Rspec
# Runner for rspec tests
class Killer < Mutant::Killer
private
# Run rspec test
#
# @return [true]
# when test is NOT successful
#
# @return [false]
# otherwise
#
# @api private
#
def run
mutation.insert
if example_groups.nil? || example_groups.empty?
$stderr.puts("No rspec example groups found for: #{match_prefixes.join(', ')}")
return false
end
example_groups.each do |group|
return true unless group.run(reporter)
end
false
end
# Return match prefixes
#
# @return [Enumerble<String>]
#
# @api private
#
def match_prefixes
subject.match_prefixes
end
# Return example groups
#
# @return [Array<RSpec::Example>]
#
# @api private
#
def example_groups
match_prefixes.each do |match_expression|
example_groups = find_with(match_expression)
return example_groups unless example_groups.empty?
end
nil
end
memoize :example_groups
# Return example groups that match expression
#
# @param [String] match_expression
#
# @return [Enumerable<String>]
#
# @api private
#
def find_with(match_expression)
all_example_groups.select do |example_group|
example_group.description.start_with?(match_expression)
end
end
# Return all example groups
#
# @return [Enumerable<RSpec::Example>]
#
# @api private
#
def all_example_groups
strategy.example_groups
end
# Choose and memoize RSpec reporter
#
# @return [RSpec::Core::Reporter]
#
# @api private
#
def reporter
reporter_class = RSpec::Core::Reporter
if strategy.rspec2?
reporter_class.new
else
reporter_class.new(strategy.configuration)
end
end
memoize :reporter, freezer: :noop
end # Killer
end # Rspec
end # Mutant

View file

@ -2,13 +2,12 @@
module Mutant
module Rspec
# Rspec killer strategy
class Strategy < Mutant::Strategy
register 'rspec'
KILLER = Killer::Forking.new(Rspec::Killer)
# Setup rspec strategy
#
# @return [self]
@ -25,6 +24,34 @@ module Mutant
end
memoize :setup
# Return new reporter
#
# @api private
#
def reporter
reporter_class = RSpec::Core::Reporter
if rspec2?
reporter_class.new
else
reporter_class.new(configuration)
end
end
# Detect RSpec 2
#
# @return [true]
# when RSpec 2
#
# @return [false]
# otherwise
#
# @api private
#
def rspec2?
RSpec::Core::Version::STRING.start_with?('2.')
end
# Return configuration
#
# @return [RSpec::Core::Configuration]
@ -46,20 +73,6 @@ module Mutant
world.example_groups
end
# Detect RSpec 2
#
# @return [true]
# when RSpec 2
#
# @return [false]
# otherwise
#
# @api private
#
def rspec2?
RSpec::Core::Version::STRING.start_with?('2.')
end
private
# Return world
@ -73,6 +86,18 @@ module Mutant
end
memoize :world, freezer: :noop
# Return all available tests
#
# @return [Enumerable<Test>]
#
# @api private
#
def all_tests
example_groups.map do |example_group|
Test.new(self, example_group)
end
end
# Return options
#
# @return [RSpec::Core::ConfigurationOptions]

37
lib/mutant/rspec/test.rb Normal file
View file

@ -0,0 +1,37 @@
module Mutant
module Rspec
# Rspec test abstraction
class Test < Mutant::Test
include Concord.new(:strategy, :example_group)
PREFIX = :rspec
# Return subject identification
#
# @return [String]
#
# @api private
#
def subject_identification
example_group.description
end
memoize :subject_identification
# Run test, return report
#
# @return [String]
#
# @api private
#
def run
flag = example_group.run(strategy.reporter)
Report.new(
test: self,
output: '',
success: flag
)
end
end # Test
end # Rspec
end # Mutant

View file

@ -48,9 +48,9 @@ module Mutant
#
# @api private
#
def self.run(config, object)
def self.run(config, object, *arguments)
handler = lookup(object.class)
handler.new(config, object)
handler.new(config, object, *arguments)
end
# Return config
@ -101,16 +101,6 @@ module Mutant
(@end || Time.now) - @start
end
# Return reporter
#
# @return [Reporter]
#
# @api private
#
def reporter
config.reporter
end
# Test if runner is successful
#
# @return [true]
@ -133,7 +123,7 @@ module Mutant
#
abstract_method :run
# Return reporter
# Run reporter on object
#
# @param [Object] object
#
@ -141,20 +131,32 @@ module Mutant
#
# @api private
#
def report(object)
reporter.report(object)
def progress(object)
reporter.progress(object)
end
# Perform dispatch
# Return reporter
#
# @return [Reporter]
#
# @api private
#
def reporter
config.reporter
end
# Perform dispatch on multiple inputs
#
# @param [Enumerable<Object>] input
#
# @return [Enumerable<Runner>]
#
# @api private
#
def dispatch(input)
def visit_collection(input, *arguments)
collection = []
input.each do |object|
runner = visit(object)
runner = visit(object, *arguments)
collection << runner
@stop = runner.stop?
break if @stop
@ -170,8 +172,8 @@ module Mutant
#
# @api private
#
def visit(object)
Runner.run(config, object)
def visit(object, *arguments)
Runner.run(config, object, *arguments)
end
end # Runner

View file

@ -5,6 +5,9 @@ module Mutant
# Runner for object config
class Config < self
# The expected coverage precision
COVERAGE_PRECISION = 1
register Mutant::Config
# Run runner for object
@ -40,8 +43,6 @@ module Mutant
end
memoize :failed_subjects
COVERAGE_PRECISION = 1
# Test if run was successful
#
# @return [true]
@ -122,7 +123,7 @@ module Mutant
def run_subjects
strategy = self.strategy
strategy.setup
@subjects = dispatch(config.subjects)
@subjects = visit_collection(config.subjects)
strategy.teardown
end
@ -133,10 +134,10 @@ module Mutant
# @api private
#
def run
report(config)
progress(config)
run_subjects
@end = Time.now
report(self)
reporter.report(self)
end
end # Config

View file

@ -0,0 +1,51 @@
module Mutant
class Runner
# Killer runner
class Killer < self
include Equalizer.new(:config, :killer)
register Mutant::Killer
# Return killer
#
# @return [Killer]
#
# @api private
#
attr_reader :killer
protected :killer
# Test if killer ran successfully
#
# @return [Boolean]
#
# @api private
def success?
@report.success?
end
# Initialize object
#
# @param [Config] config
# @param [Mutation] mutation
#
# @return [undefined]
#
# @api private
#
def initialize(config, killer)
@killer = killer
super(config)
end
# Run killer
#
# @api private
#
def run
@report = killer.run
end
end # Killer
end # Runner
end # Mutant

View file

@ -4,7 +4,7 @@ module Mutant
class Runner
# Mutation runner
class Mutation < self
include Equalizer.new(:config, :mutation)
include Equalizer.new(:config, :mutation, :tests)
register Mutant::Mutation
@ -16,26 +16,28 @@ module Mutant
#
attr_reader :mutation
# Return killer instance
# Return killers
#
# @return [Killer]
#
# @api private
#
attr_reader :killer
attr_reader :killers
# Initialize object
#
# @param [Config] config
# @param [Mutation] mutation
# @param [Enumerable<Test>] tests
#
# @return [undefined]
#
# @api private
#
def initialize(config, mutation)
@mutation = mutation
def initialize(config, mutation, tests)
@mutation, @tests = mutation, tests
super(config)
@stop = config.fail_fast && !success?
end
# Test if mutation was handeled successfully
@ -49,7 +51,7 @@ module Mutant
# @api private
#
def success?
mutation.success?(killer)
killers.any?(&:success?)
end
private
@ -61,9 +63,14 @@ module Mutant
# @api private
#
def run
@killer = config.strategy.kill(mutation)
report(killer)
@stop = config.fail_fast && !killer.success?
progress(mutation)
@killers = @tests.map do |test|
Mutant::Killer.new(
mutation: mutation,
test: test
)
end.map(&method(:visit))
progress(self)
end
end # Mutation

View file

@ -63,6 +63,17 @@ module Mutant
failed_mutations.empty?
end
# Return tests used to kill mutations on this subject
#
# @return [Enumerable<Test>]
#
# @api private
#
def tests
config.strategy.tests(subject)
end
memoize :tests
private
# Perform operation
@ -72,10 +83,9 @@ module Mutant
# @api private
#
def run
subject = self.subject
report(subject)
@mutations = dispatch(subject.mutations)
report(self)
progress(subject)
@mutations = visit_collection(subject.mutations, tests)
progress(self)
end
end # Subject

View file

@ -54,39 +54,53 @@ module Mutant
self
end
# Kill mutation
# Return all available tests by strategy
#
# @param [Mutation] mutation
#
# @return [Killer]
# @return [Enumerable<Test>]
#
# @api private
#
def kill(mutation)
killer.new(self, mutation)
abstract_method :all_tests
# Return tests for mutation
#
# TODO: This logic is now centralized but still fucked.
#
# @param [Mutation] mutation
#
# @return [Enumerable<Test>]
#
# @api private
#
def tests(subject)
subject.match_prefixes.map do |match_expression|
tests = all_tests.select do |test|
test.subject_identification.start_with?(match_expression)
end
return tests if tests.any?
end
[]
end
private
# Return killer
#
# @return [Class:Killer]
#
# @api private
#
def killer
self.class::KILLER
end
# Null strategy that never kills a mutation
class Null < self
register 'null'
register('null')
KILLER = Killer::Null
# Return all tests
#
# @return [Enumerable<Test>]
#
# @api private
#
def all_tests
EMPTY_ARRAY
end
end # Null
end # Strategy
end # Mutant

View file

@ -68,7 +68,7 @@ module Mutant
#
def prepare
scope.send(:memoized_methods).instance_variable_get(:@memory).delete(name)
scope.send(:undef_method, name)
super
self
end

86
lib/mutant/test.rb Normal file
View file

@ -0,0 +1,86 @@
module Mutant
# Abstract base class for test that might kill a mutation
class Test
include AbstractType, Adamantium::Flat
# Object to report test status
class Report
include Adamantium::Flat, Anima::Update, Anima.new(
:test,
:output,
:success
)
alias_method :success?, :success
# Test if test failed
#
# @return [Boolean]
#
# @api private
#
def failed?
!success?
end
# Return marshallable data
#
# 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.
#
# @return [Array]
#
# @api private
#
def marshal_dump
[@output, @success]
end
# Load marshalled data
#
# @param [Array] arry
#
# @return [undefined]
#
# @api private
#
def marshal_load(array)
@output, @success = array
end
end # Report
# Run tests
#
# @return [Test::Result]
#
# @api private
#
abstract_method :run
# Return test identification
#
# @return [String]
#
# @api private
#
def identification
"#{self.class::PREFIX}:#{subject_identification}"
end
memoize :identification
# Return subject identification
#
# This method is used for current mutants primitive test selection.
#
# @return [String]
#
# @api private
#
abstract_method :subject_identification
end # Test
end # Mutant

View file

@ -0,0 +1,18 @@
# encoding: utf-8
require 'spec_helper'
describe 'null integration' do
let(:base_cmd) { 'bundle exec mutant -I lib --require test_app "::TestApp*"' }
around do |example|
Dir.chdir(TestApp.root) do
example.run
end
end
specify 'it allows to kill mutations' do
expect(Kernel.system(base_cmd)).to be(false)
end
end

View file

@ -10,7 +10,7 @@ describe 'rspec integration' do
around do |example|
Bundler.with_clean_env do
Dir.chdir(TestApp.root) do
Kernel.system("bundle install --gemfile=#{gemfile}")
Kernel.system("bundle install --gemfile=#{gemfile}") || fail('Bundle install failed!')
ENV['BUNDLE_GEMFILE'] = gemfile
example.run
end

View file

@ -2,11 +2,9 @@
if ENV['COVERAGE'] == 'true'
require 'simplecov'
require 'coveralls'
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
SimpleCov::Formatter::HTMLFormatter,
Coveralls::SimpleCov::Formatter
]
SimpleCov.start do

View file

@ -0,0 +1,162 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Diff do
let(:object) { described_class }
describe '.build' do
subject { object.build(old_string, new_string) }
let(:old_string) { "foo\nbar" }
let(:new_string) { "bar\nbaz" }
it { should eql(Mutant::Diff.new(%w(foo bar), %w(bar baz))) }
end
describe '.colorize_line' do
let(:object) { described_class }
subject { object.colorize_line(line) }
context 'line beginning with "+"' do
let(:line) { '+line' }
it { should eql(Mutant::Color::GREEN.format(line)) }
end
context 'line beginning with "-"' do
let(:line) { '-line' }
it { should eql(Mutant::Color::RED.format(line)) }
end
context 'line beginning in other char' do
let(:line) { ' line' }
it { should eql(line) }
end
end
describe '#diff' do
let(:object) { described_class.new(old, new) }
subject { object.diff }
context 'when there is a diff at begin of hunk' do
let(:old) { %w(foo bar) }
let(:new) { %w(baz bar) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,3 +1,3 @@
-foo
+baz
bar
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when there is a diff NOT at begin of hunk' do
let(:old) { %w(foo bar) }
let(:new) { %w(foo baz bar) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,3 +1,4 @@
foo
+baz
bar
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at begin' do
let(:old) { %w(foo bar baz boz a b c) }
let(:new) { %w(foo bar baz boz a b c other) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,8 +1,9 @@
foo
bar
baz
boz
a
b
c
+other
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at end, deleting' do
let(:old) { %w(other foo bar baz boz a b c) }
let(:new) { %w(foo bar baz boz a b c) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,9 +1,8 @@
-other
foo
bar
baz
boz
a
b
c
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at end, inserting' do
let(:old) { %w(foo bar baz boz a b c) }
let(:new) { %w(other foo bar baz boz a b c) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,8 +1,9 @@
+other
foo
bar
baz
boz
a
b
c
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when there is no diff' do
let(:old) { '' }
let(:new) { '' }
it { should be(nil) }
it_should_behave_like 'an idempotent method'
end
end
end

View file

@ -1,123 +0,0 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Differ, '#diff' do
let(:object) { described_class.new(old, new) }
subject { object.diff }
context 'when there is a diff at begin of hunk' do
let(:old) { %w(foo bar) }
let(:new) { %w(baz bar) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,3 +1,3 @@
-foo
+baz
bar
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when there is a diff NOT at begin of hunk' do
let(:old) { %w(foo bar) }
let(:new) { %w(foo baz bar) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,3 +1,4 @@
foo
+baz
bar
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at begin' do
let(:old) { %w(foo bar baz boz a b c) }
let(:new) { %w(foo bar baz boz a b c other) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,8 +1,9 @@
foo
bar
baz
boz
a
b
c
+other
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at end, deleting' do
let(:old) { %w(other foo bar baz boz a b c) }
let(:new) { %w(foo bar baz boz a b c) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,9 +1,8 @@
-other
foo
bar
baz
boz
a
b
c
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when the diff has a long context at end, inserting' do
let(:old) { %w(foo bar baz boz a b c) }
let(:new) { %w(other foo bar baz boz a b c) }
let(:expectation) do
strip_indent(<<-STR)
@@ -1,8 +1,9 @@
+other
foo
bar
baz
boz
a
b
c
STR
end
it { should eql(expectation) }
it_should_behave_like 'an idempotent method'
end
context 'when there is no diff' do
let(:old) { '' }
let(:new) { '' }
it { should be(nil) }
it_should_behave_like 'an idempotent method'
end
end

View file

@ -1,42 +0,0 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Differ do
let(:object) { described_class }
describe '.build' do
subject { object.build(old_string, new_string) }
let(:old_string) { "foo\nbar" }
let(:new_string) { "bar\nbaz" }
it { should eql(Mutant::Differ.new(%w(foo bar), %w(bar baz))) }
end
describe '.colorize_line' do
let(:object) { described_class }
subject { object.colorize_line(line) }
context 'line beginning with "+"' do
let(:line) { '+line' }
it { should eql(Mutant::Color::GREEN.format(line)) }
end
context 'line beginning with "-"' do
let(:line) { '-line' }
it { should eql(Mutant::Color::RED.format(line)) }
end
context 'line beginning in other char' do
let(:line) { ' line' }
it { should eql(line) }
end
end
end

View file

@ -1,30 +0,0 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Killer, '#success?' do
subject { object.success? }
let(:object) { class_under_test.new(strategy, mutation) }
let(:strategy) { double('Strategy') }
let(:mutation) { double('Mutation', success?: kill_state) }
let(:kill_state) { double('Kill State') }
before do
kill_state.stub(freeze: kill_state, dup: kill_state)
end
let(:class_under_test) do
Class.new(described_class) do
def run
end
end
end
it_should_behave_like 'an idempotent method'
it 'should use kill state to gather success' do
mutation.should_receive(:success?).with(object).and_return(kill_state)
should be(kill_state)
end
end

View file

@ -4,10 +4,13 @@ require 'spec_helper'
describe Mutant::Mutation do
let(:class_under_test) { Class.new(described_class) { memoize :identification } }
let(:object) { class_under_test.new(mutation_subject, Mutant::NodeHelpers::N_NIL) }
let(:mutation_subject) { double('Subject', identification: 'subject', source: 'original') }
let(:node) { double('Node') }
class TestMutation < Mutant::Mutation
SYMBOL = 'test'
end
let(:object) { TestMutation.new(mutation_subject, Mutant::NodeHelpers::N_NIL) }
let(:mutation_subject) { double('Subject', identification: 'subject', source: 'original') }
let(:node) { double('Node') }
describe '#code' do
subject { object.code }
@ -37,7 +40,7 @@ describe Mutant::Mutation do
subject { object.identification }
it { should eql('subject:8771a') }
it { should eql('test:subject:8771a') }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,11 @@
require 'spec_helper'
describe Mutant::Reporter::Null do
let(:object) { described_class.new }
describe '#report' do
subject { object.report(double('some input')) }
it_should_behave_like 'a command method'
end
end

View file

@ -1,57 +0,0 @@
# encoding: utf-8
require 'spec_helper'
require 'mutant-rspec'
describe Mutant::Rspec::Killer, '.new' do
before do
pending 'dactivated'
end
subject { object.new(strategy, mutation) }
let(:context) { double('Context') }
let(:mutation_subject) { double('Mutation Subject') }
let(:object) { described_class }
let(:mutation) do
double(
'Mutation',
subject: mutation_subject,
should_survive?: false
)
end
let(:strategy) do
double(
'Strategy',
spec_files: ['foo'],
error_stream: $stderr,
output_stream: $stdout
)
end
before do
mutation.stub(:insert)
mutation.stub(:reset)
RSpec::Core::Runner.stub(run: exit_status)
end
context 'when run exits zero' do
let(:exit_status) { 0 }
it { expect(subject.killed?).to be(false) }
it { should be_a(described_class) }
end
context 'when run exits nonzero' do
let(:exit_status) { 1 }
it { expect(subject.killed?).to be(true) }
it { should be_a(described_class) }
end
end

View file

@ -18,15 +18,14 @@ describe Mutant::Runner::Config do
)
end
let(:fail_fast) { false }
let(:expected_coverage) { 100.0 }
let(:reporter) { double('Reporter') }
let(:strategy) { double('Strategy') }
let(:subject_a) { double('Subject A') }
let(:subject_b) { double('Subject B') }
let(:fail_fast) { false }
let(:expected_coverage) { 100.0 }
let(:reporter) { Mutant::Reporter::Trace.new }
let(:strategy) { double('Strategy') }
let(:subject_a) { double('Subject A') }
let(:subject_b) { double('Subject B') }
before do
reporter.stub(report: reporter)
strategy.stub(:setup)
strategy.stub(:teardown)
Mutant::Runner.stub(:run).with(config, subject_a).and_return(runner_a)

View file

@ -1,44 +0,0 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Runner::Mutation, '#killer' do
let(:object) { described_class.run(config, mutation) }
let(:config) do
double(
'Config',
fail_fast: fail_fast,
reporter: reporter,
strategy: strategy
)
end
let(:reporter) { double('Reporter') }
let(:mutation) { double('Mutation', class: Mutant::Mutation) }
let(:strategy) { double('Strategy') }
let(:killer) { double('Killer', success?: success) }
let(:fail_fast) { false }
let(:success) { false }
subject { object.killer }
before do
reporter.stub(report: reporter)
strategy.stub(kill: killer)
end
it 'should call configuration to identify strategy' do
config.should_receive(:strategy).with(no_args).and_return(strategy)
should be(killer)
end
it 'should run killer' do
strategy.should_receive(:kill).with(mutation).and_return(killer)
should be(killer)
end
it { should be(killer) }
it_should_behave_like 'an idempotent method'
end

View file

@ -0,0 +1,102 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Runner::Mutation do
let(:object) { described_class.new(config, mutation, tests) }
let(:reporter) { double('Reporter') }
let(:mutation) { double('Mutation', class: Mutant::Mutation) }
let(:strategy) { double('Strategy') }
let(:killer_a) { Mutant::Killer.new(test: test_a, mutation: mutation) }
let(:killer_b) { Mutant::Killer.new(test: test_b, mutation: mutation) }
let(:runner_a) { double('Runner A', success?: success_a, stop?: stop_a) }
let(:runner_b) { double('Runner B', success?: success_b, stop?: stop_b) }
let(:runners) { [runner_a, runner_b] }
let(:killers) { [killer_a, killer_b] }
let(:fail_fast) { false }
let(:success_a) { true }
let(:success_b) { true }
let(:stop_a) { false }
let(:stop_b) { false }
let(:test_a) { double('test a') }
let(:test_b) { double('test b') }
let(:tests) { [test_a, test_b] }
before do
expect(Mutant::Runner).to receive(:run).with(config, killer_a).and_return(runner_a)
expect(Mutant::Runner).to receive(:run).with(config, killer_b).and_return(runner_b)
end
let(:config) do
double(
'Config',
fail_fast: fail_fast,
reporter: reporter,
strategy: strategy
)
end
before do
reporter.stub(progress: reporter)
strategy.stub(killers: killers)
end
describe '#stop?' do
subject { object.stop? }
context 'when fail fast is false' do
it { should be(false) }
end
context 'when fail fast is true' do
let(:fail_fast) { true }
context 'when all killers are successful' do
it { should be(false) }
end
context 'when one killer is NOT successful' do
let(:success_b) { false }
it { should be(false) }
end
context 'when all killer are NOT successful' do
let(:success_b) { false }
let(:success_a) { false }
it { should be(true) }
end
end
end
describe '#success?' do
subject { object.success? }
context 'when all killers are successful' do
it { should be(true) }
end
context 'when one killer is not successful' do
let(:success_b) { false }
it { should be(true) }
end
context 'when all killer are not successful' do
let(:success_a) { false }
let(:success_b) { false }
it { should be(false) }
end
end
describe '#killers' do
subject { object.killers }
it { should eql(runners) }
it_should_behave_like 'an idempotent method'
end
end

View file

@ -15,10 +15,11 @@ describe Mutant::Runner::Subject, '#success?' do
)
end
let(:reporter) { double('Reporter') }
let(:config) { double('Config', reporter: reporter) }
let(:mutation_a) { double('Mutation A') }
let(:mutation_b) { double('Mutation B') }
let(:reporter) { Mutant::Reporter::Trace.new }
let(:config) { double('Config', reporter: reporter, strategy: strategy) }
let(:mutation_a) { double('Mutation A') }
let(:mutation_b) { double('Mutation B') }
let(:strategy) { double('Strategy') }
let(:runner_a) do
double('Runner A', success?: success_a, stop?: stop_a)
@ -28,10 +29,12 @@ describe Mutant::Runner::Subject, '#success?' do
double('Runner B', success?: success_b, stop?: stop_b)
end
let(:tests) { [double('test a'), double('test b')] }
before do
reporter.stub(report: reporter)
Mutant::Runner.stub(:run).with(config, mutation_a).and_return(runner_a)
Mutant::Runner.stub(:run).with(config, mutation_b).and_return(runner_b)
expect(strategy).to receive(:tests).with(mutation_subject).and_return(tests)
expect(Mutant::Runner).to receive(:run).with(config, mutation_a, tests).and_return(runner_a)
expect(Mutant::Runner).to receive(:run).with(config, mutation_b, tests).and_return(runner_b)
end
context 'with failing mutations' do

View file

@ -38,4 +38,57 @@ describe Mutant do
expect(inspect).to be_frozen
end
end
describe '.isolate' do
let(:object) { described_class }
let(:expected_return) { :foo }
subject { object.isolate(&block) }
def redirect_stderr
$stderr = File.open('/dev/null')
end
unless ENV['COVERAGE']
context 'when block returns mashallable data, and process exists zero' do
let(:block) do
lambda do
:data_from_child_process
end
end
it { should eql(:data_from_child_process) }
end
end
context 'when block does return marshallable data' do
let(:block) do
lambda do
redirect_stderr
$stderr # not mashallable, nothing written to pipe and raised exceptions in child
end
end
it 'raises an exception' do
expect { subject }.to raise_error(Mutant::IsolationError, 'Childprocess wrote un-unmarshallable data')
end
end
context 'when block does return marshallable data, but process exits with nonzero exitstatus' do
let(:block) do
lambda do
redirect_stderr
at_exit do
raise
end
:foo
end
end
it 'raises an exception' do
expect { subject }.to raise_error(Mutant::IsolationError, 'Childprocess exited with nonzero exit status: 1')
end
end
end
end