Skip to content

Commit

Permalink
Use morpher predicates for filtering
Browse files Browse the repository at this point in the history
* Removes Mutant::Predicate
* Simplifies CLI builder logic
* More to come
  • Loading branch information
mbj committed Feb 2, 2014
1 parent 14c906b commit 0fe8acc
Show file tree
Hide file tree
Showing 21 changed files with 216 additions and 616 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ source 'https://rubygems.org'

gem 'mutant', path: '.'

gem 'morpher', git: 'https://github.com/mbj/morpher.git'

gemspec name: 'mutant'

gem 'devtools', git: 'https://github.com/rom-rb/devtools.git'
Expand Down
2 changes: 1 addition & 1 deletion config/flay.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
threshold: 18
total_score: 813
total_score: 811
7 changes: 1 addition & 6 deletions lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require 'diff/lcs/hunk'
require 'anima'
require 'concord'
require 'morpher'

# Library namespace
module Mutant
Expand All @@ -33,11 +34,6 @@ module Mutant
require 'mutant/constants'
require 'mutant/random'
require 'mutant/walker'
require 'mutant/predicate'
require 'mutant/predicate/attribute'
require 'mutant/predicate/whitelist'
require 'mutant/predicate/blacklist'
require 'mutant/predicate/matcher'
require 'mutant/mutator'
require 'mutant/mutation'
require 'mutant/mutation/evil'
Expand Down Expand Up @@ -123,7 +119,6 @@ module Mutant
require 'mutant/cli/classifier'
require 'mutant/cli/classifier/namespace'
require 'mutant/cli/classifier/method'
require 'mutant/cli/builder'
require 'mutant/color'
require 'mutant/differ'
require 'mutant/reporter'
Expand Down
217 changes: 151 additions & 66 deletions lib/mutant/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Mutant

# Comandline parser
class CLI
include Adamantium::Flat, Equalizer.new(:config)
include Adamantium::Flat, Equalizer.new(:config), NodeHelpers

# Error raised when CLI argv is invalid
Error = Class.new(RuntimeError)
Expand All @@ -32,6 +32,132 @@ def self.run(arguments)
EXIT_FAILURE
end

# Builder for configuration components
class Builder
include NodeHelpers

# Initalize object
#
# @return [undefined]
#
# @api private
#
def initialize
@matchers = []
@subject_ignores = []
@subject_selectors = []
end

# Add a subject ignore
#
# @param [Matcher]
#
# @return [self]
#
# @api private
#
def add_subject_ignore(matcher)
@subject_ignores << matcher
self
end

# Add a subject selector
#
# @param [#call] selector
#
# @return [self]
def add_subject_selector(selector)
@subject_selectors << selector
self
end

# Add a subject matcher
#
# @param [#call] selector
#
# @return [self]
#
# @api private
#
def add_matcher(matcher)
@matchers << matcher
self
end

def matcher
if @matchers.empty?
raise(Error, 'No patterns given')
end

matcher = Matcher::Chain.build(@matchers)

if predicate
Matcher::Filter.new(matcher, predicate)
else
matcher
end
end

private

# Return subject selector
#
# @return [#call]
# if selector is present
#
# @return [nil]
# otherwise
#
# @api private
#
def subject_selector
if @subject_selectors.any?
Morpher::Evaluator::Predicate::Or.new(@subject_selectors)
end
end

# Return predicate
#
# @return [#call]
# if filter is needed
#
# @return [nil]
# othrwise
#
# @api private
#
def predicate
if subject_selector && subject_rejector
Morpher::Evaluator::Predicate::And.new([
subject_selector,
Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
])
elsif subject_selector
subject_selector
elsif subject_rejector
Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
else
nil
end
end

# Return subject rejector
#
# @return [#call]
#
# @api private
#
def subject_rejector
rejectors = @subject_ignores.flat_map(&:to_a).map do |subject|
Morpher.evaluator(s(:eql, s(:attribute, :identification), s(:static, subject.identification)))
end

if rejectors.any?
Morpher::Evaluator::Predicate::Or.new(rejectors)
end
end
end

# Initialize objecct
#
# @param [Array<String>]
Expand All @@ -41,7 +167,7 @@ def self.run(arguments)
# @api private
#
def initialize(arguments = [])
@filters, @matchers = [], []
@builder = Builder.new
@debug = @fail_fast = @zombie = false
@expect_coverage = 100.0
@strategy = Strategy::Null.new
Expand All @@ -61,57 +187,17 @@ def config
cache: @cache,
zombie: @zombie,
debug: @debug,
matcher: matcher,
subject_predicate: @subject_predicate.output,
matcher: @builder.matcher,
strategy: @strategy,
fail_fast: @fail_fast,
reporter: reporter,
reporter: Reporter::CLI.new($stdout),
expected_coverage: @expect_coverage
)
end
memoize :config

private

# Return reporter
#
# @return [Mutant::Reporter::CLI]
#
# @api private
#
def reporter
Reporter::CLI.new($stdout)
end

# Return matcher
#
# @return [Mutant::Matcher]
#
# @raise [CLI::Error]
# raises error when matcher is not given
#
# @api private
#
def matcher
if @matchers.empty?
raise(Error, 'No matchers given')
end

Matcher::Chain.build(@matchers)
end

# Add mutation filter
#
# @param [Class<Predicate>] klass
#
# @return [undefined]
#
# @api private
#
def add_filter(klass, *arguments)
@filters << klass.new(*arguments)
end

# Parse the command-line options
#
# @param [Array<String>] arguments
Expand All @@ -126,11 +212,11 @@ def add_filter(klass, *arguments)
#
def parse(arguments)
opts = OptionParser.new do |builder|
builder.banner = 'usage: mutant STRATEGY [options] MATCHERS ...'
builder.banner = 'usage: mutant STRATEGY [options] PATTERN ...'
builder.separator('')
add_filters(builder)
add_environmental_options(builder)
add_mutation_options(builder)
add_filter_options(builder)
add_debug_options(builder)
end

Expand All @@ -155,26 +241,10 @@ def parse(arguments)
def parse_matchers(patterns)
patterns.each do |pattern|
matcher = Classifier.run(@cache, pattern)
@matchers << matcher if matcher
@builder.add_matcher(matcher)
end
end

# Add filters
#
# @param [OptionParser] parser
#
# @return [undefined]
#
# @api private
#
def add_filters(parser)
parser.separator(EMPTY_STRING)
parser.separator('Strategies:')

builder = Builder::Predicate::Subject.new(@cache, parser)
@subject_predicate = builder
end

# Add environmental options
#
# @param [Object] opts
Expand Down Expand Up @@ -222,15 +292,30 @@ def use(name)
# @api private
#
def add_mutation_options(opts)
opts.separator('')
opts.separator(EMPTY_STRING)
opts.separator('Options:')

opts.on('--score COVERAGE', 'Fail unless COVERAGE is not reached exactly') do |coverage|
@expected_coverage = Float(coverage)
end.on('--use STRATEGY', 'Use STRATEGY for killing mutations') do |runner|
use(runner)
end.on('--code FILTER', 'Adds a code filter') do |filter|
add_filter(Predicate::Attribute, :code, filter)
end
end

# Add filter options
#
# @param [OptionParser] opts
#
# @return [undefined]
#
# @api private
#
def add_filter_options(opts)
opts.on('--ignore-subject PATTERN', 'Ignore subjects that match PATTERN') do |pattern|
@builder.add_subject_ignore(Classifier.run(@cache, pattern))
end
opts.on('--code CODE', 'Scope execution to subjects with CODE') do |code|
@builder.add_subject_selector(Morpher.evaluator(s(:eql, s(:attribute, :code), s(:static, code))))
end
end

Expand Down
Loading

0 comments on commit 0fe8acc

Please sign in to comment.