require 'optparse' module Mutant # Comandline parser class CLI include Adamantium::Flat, Equalizer.new(:config), NodeHelpers # Error raised when CLI argv is invalid Error = Class.new(RuntimeError) EXIT_FAILURE = 1 EXIT_SUCCESS = 0 # Run cli with arguments # # @param [Array] 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] # # @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 # 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 Morpher::Evaluator::Predicate::Boolean::Or.new(@subject_selectors) if @subject_selectors.any? end # Return predicate # # @return [#call] # if filter is needed # # @return [nil] # othrwise # # @api private # def predicate if subject_selector && subject_rejector 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] # if there is a subject rejector # # @return [nil] # otherwise # # @api private # def subject_rejector rejectors = @subject_ignores.flat_map(&:to_a).map do |subject| Morpher.compile(s(:eql, s(:attribute, :identification), s(:static, subject.identification))) end Morpher::Evaluator::Predicate::Boolean::Or.new(rejectors) if rejectors.any? end end # Initialize objecct # # @param [Array] # # @return [undefined] # # @api private # def initialize(arguments = []) @builder = Builder.new @debug = @fail_fast = @zombie = false @expected_coverage = 100.0 @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, strategy: @strategy, fail_fast: @fail_fast, reporter: Reporter::CLI.new($stdout), expected_coverage: @expected_coverage ) end memoize :config private # Parse the command-line options # # @param [Array] arguments # Command-line options and arguments to be parsed. # # @raise [Error] # An error occurred while parsing the options. # # @return [undefined] # # @api private # def parse(arguments) opts = OptionParser.new do |builder| builder.banner = 'usage: mutant STRATEGY [options] PATTERN ...' builder.separator('') add_environmental_options(builder) add_mutation_options(builder) add_filter_options(builder) add_debug_options(builder) end patterns = begin opts.parse!(arguments) rescue OptionParser::ParseError => error raise(Error, error.message, error.backtrace) end parse_matchers(patterns) end # Parse matchers # # @param [Enumerable] patterns # # @return [undefined] # # @api private # def parse_matchers(patterns) patterns.each do |pattern| expression = Expression.parse(pattern) unless expression raise Error, "Invalid mutant expression: #{pattern.inspect}" end @builder.add_matcher(expression.matcher(@cache)) end end # Add environmental options # # @param [Object] opts # # @return [undefined] # # @api private # def add_environmental_options(opts) opts.separator('') opts.separator('Environment:') opts.on('--zombie', 'Run mutant zombified') do @zombie = true end.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory| $LOAD_PATH << directory end.on('-r', '--require NAME', 'Require file with NAME') do |name| require(name) end end # Use plugin # # FIXME: For now all plugins are strategies. Later they could be anything that allows "late integration". # # @param [String] name # # @return [undefined] # # @api private # def use(name) require "mutant/#{name}" @strategy = Strategy.lookup(name).new rescue LoadError $stderr.puts("Cannot load plugin: #{name.inspect}") raise end # Add options # # @param [OptionParser] opts # # @return [undefined] # # @api private # def add_mutation_options(opts) 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 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(Expression.parse(pattern).matcher(@cache)) end opts.on('--code CODE', 'Scope execution to subjects with CODE') do |code| @builder.add_subject_selector(Morpher.compile(s(:eql, s(:attribute, :code), s(:static, code)))) end end # Add debug options # # @param [OptionParser] opts # # @return [undefined] # # @api private # def add_debug_options(opts) opts.on('--fail-fast', 'Fail fast') do @fail_fast = true end.on('--version', 'Print mutants version') do puts("mutant-#{Mutant::VERSION}") Kernel.exit(0) end.on('-d', '--debug', 'Enable debugging output') do @debug = true end.on_tail('-h', '--help', 'Show this message') do puts(opts) exit end end end # CLI end # Mutant