free_mutant/lib/mutant/cli.rb

354 lines
7.9 KiB
Ruby
Raw Normal View History

# encoding: utf-8
require 'optparse'
2012-08-28 12:57:39 -04:00
module Mutant
# Comandline parser
class CLI
include Adamantium::Flat, Equalizer.new(:config), NodeHelpers
2012-08-28 12:57:39 -04:00
2013-06-13 13:10:34 -04:00
# Error raised when CLI argv is invalid
Error = Class.new(RuntimeError)
EXIT_FAILURE = 1
EXIT_SUCCESS = 0
# Run cli with arguments
#
# @param [Array<String>] arguments
#
# @return [Fixnum]
# the exit status
#
# @api private
#
def self.run(arguments)
config = new(arguments).config
runner = Runner::Config.run(config)
runner.success? ? EXIT_SUCCESS : EXIT_FAILURE
rescue Error => exception
$stderr.puts(exception.message)
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]
2014-03-30 11:08:22 -04:00
#
# @api private
#
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
2014-03-30 11:08:22 -04:00
# Return generated matcher
#
# @return [Mutant::Matcher]
#
# @api private
#
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?
2014-03-08 19:00:46 -05:00
Morpher::Evaluator::Predicate::Boolean::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
2014-03-08 19:00:46 -05:00
Morpher::Evaluator::Predicate::Boolean::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|
2014-03-08 19:00:46 -05:00
Morpher.compile(s(:eql, s(:attribute, :identification), s(:static, subject.identification)))
end
if rejectors.any?
2014-03-08 19:00:46 -05:00
Morpher::Evaluator::Predicate::Boolean::Or.new(rejectors)
end
end
end
# Initialize objecct
#
# @param [Array<String>]
#
# @return [undefined]
#
# @api private
#
def initialize(arguments = [])
@builder = Builder.new
@debug = @fail_fast = @zombie = false
2014-03-06 10:21:44 -05:00
@expected_coverage = 100.0
2014-01-17 18:25:16 -05:00
@strategy = Strategy::Null.new
@cache = Mutant::Cache.new
parse(arguments)
config # trigger lazyness now
end
# Return config
#
# @return [Config]
#
# @api private
#
def config
Config.new(
cache: @cache,
zombie: @zombie,
debug: @debug,
matcher: @builder.matcher,
2014-01-17 18:25:16 -05:00
strategy: @strategy,
fail_fast: @fail_fast,
reporter: Reporter::CLI.new($stdout),
2014-03-06 10:21:44 -05:00
expected_coverage: @expected_coverage
)
end
memoize :config
private
2013-06-15 10:37:43 -04:00
# Parse the command-line options
#
# @param [Array<String>] arguments
# Command-line options and arguments to be parsed.
#
# @raise [Error]
# An error occurred while parsing the options.
#
2013-06-15 10:37:43 -04:00
# @return [undefined]
#
# @api private
#
def parse(arguments)
2013-07-28 13:58:53 -04:00
opts = OptionParser.new do |builder|
builder.banner = 'usage: mutant STRATEGY [options] PATTERN ...'
builder.separator('')
2013-08-04 17:43:57 -04:00
add_environmental_options(builder)
2014-01-18 18:27:56 -05:00
add_mutation_options(builder)
add_filter_options(builder)
2014-01-18 18:27:56 -05:00
add_debug_options(builder)
end
patterns =
2013-06-21 11:21:04 -04:00
begin
opts.parse!(arguments)
rescue OptionParser::ParseError => error
raise(Error, error.message, error.backtrace)
end
parse_matchers(patterns)
end
# Parse matchers
#
# @param [Enumerable<String>] patterns
#
2013-06-21 21:47:23 -04:00
# @return [undefined]
#
# @api private
#
def parse_matchers(patterns)
patterns.each do |pattern|
2013-09-11 16:06:32 -04:00
matcher = Classifier.run(@cache, pattern)
@builder.add_matcher(matcher)
end
end
2013-06-14 14:54:02 -04:00
2013-08-04 17:03:39 -04:00
# Add environmental options
#
# @param [Object] opts
#
# @return [undefined]
#
# @api private
#
def add_environmental_options(opts)
2014-01-18 18:27:56 -05:00
opts.separator('')
opts.separator('Environment:')
2013-08-04 17:03:39 -04:00
opts.on('--zombie', 'Run mutant zombified') do
@zombie = true
end.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
2013-08-04 17:03:39 -04:00
$LOAD_PATH << directory
2013-09-15 07:56:26 -04:00
end.on('-r', '--require NAME', 'Require file with NAME') do |name|
require(name)
2013-08-04 17:03:39 -04:00
end
end
2014-01-17 18:25:16 -05:00
# Use plugin
2014-01-17 18:15:42 -05:00
#
2014-01-17 18:25:16 -05:00
# FIXME: For now all plugins are strategies. Later they could be anything that allows "late integration".
#
# @param [String] name
2014-01-17 18:15:42 -05:00
#
# @return [undefined]
#
# @api private
#
2014-01-17 18:25:16 -05:00
def use(name)
2014-05-27 11:12:36 -04:00
require "mutant/#{name}"
2014-01-17 18:15:42 -05:00
@strategy = Strategy.lookup(name).new
rescue LoadError
2014-01-17 18:25:16 -05:00
$stderr.puts("Cannot load plugin: #{name.inspect}")
2014-01-17 18:15:42 -05:00
raise
end
2013-06-21 11:21:04 -04:00
# Add options
#
2014-01-18 18:27:56 -05:00
# @param [OptionParser] opts
2013-06-21 11:21:04 -04:00
#
2013-06-21 21:47:23 -04:00
# @return [undefined]
#
2013-06-21 11:21:04 -04:00
# @api private
#
2014-01-18 18:27:56 -05:00
def add_mutation_options(opts)
opts.separator(EMPTY_STRING)
2014-01-18 18:27:56 -05:00
opts.separator('Options:')
2013-06-21 11:21:04 -04:00
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|
2014-01-17 18:25:16 -05:00
use(runner)
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|
2014-03-08 19:00:46 -05:00
@builder.add_subject_selector(Morpher.compile(s(:eql, s(:attribute, :code), s(:static, code))))
2014-01-18 18:27:56 -05:00
end
end
# Add debug options
#
# @param [OptionParser] opts
#
# @return [undefined]
#
# @api private
#
def add_debug_options(opts)
opts.on('--fail-fast', 'Fail fast') do
2013-08-04 17:03:39 -04:00
@fail_fast = true
2014-05-27 11:12:36 -04:00
end.on('--version', 'Print mutants version') do
2014-01-18 18:27:56 -05:00
puts("mutant-#{Mutant::VERSION}")
Kernel.exit(0)
end.on('-d', '--debug', 'Enable debugging output') do
2013-08-04 17:03:39 -04:00
@debug = true
2013-06-21 11:21:04 -04:00
end.on_tail('-h', '--help', 'Show this message') do
2013-08-04 17:03:39 -04:00
puts(opts)
2013-06-21 11:21:04 -04:00
exit
end
end
2013-06-14 14:54:02 -04:00
end # CLI
end # Mutant