require 'set' module Respec class App def initialize(*args) if (i = args.index('--')) @args = args.slice!(0...i) @raw_args = args[1..-1] else @args = args @raw_args = [] end @failures_path = self.class.default_failures_path @selected_failures = false @update_failures = true process_args end attr_accessor :failures_path def command @command ||= bundler_args + ['rspec'] + generated_args + raw_args + formatter_args end def bundler_args if File.exist?(ENV['BUNDLE_GEMFILE'] || 'Gemfile') ['bundle', 'exec'] else [] end end attr_reader :generated_args, :raw_args def formatter_args if @update_failures [File.expand_path('formatter.rb', File.dirname(__FILE__))] else [] end end class << self attr_accessor :default_failures_path end self.default_failures_path = ENV['RESPEC_FAILURES'] || File.expand_path(".respec_failures") def help_only? @help_only end def help <<-EOS.gsub(/^ *\|/, '') |USAGE: respec RESPEC-ARGS ... [ -- RSPEC-ARGS ... ] | |Run rspec, recording failed examples for easy rerunning later. | |RESPEC-ARGS may consist of: | | f Rerun all failed examples | <N> Rerun only the N-th failure | <file name> Run all specs in this file | <file name:N> Run specs at line N in this file | <other> Run only examples matching this pattern | -<anything> Passed directly to rspec. | --help This! (Also 'help'.) | |Any arguments following a '--' argument are passed directly to rspec. | |More info: http://github.com/oggy/respec EOS end private def process_args args = [] files = [] pass_next_arg = false @args.each do |arg| if pass_next_arg args << arg pass_next_arg = false elsif rspec_option_that_requires_an_argument?(arg) args << arg pass_next_arg = true elsif File.exist?(arg.sub(/:\d+\z/, '')) files << arg elsif arg =~ /\A(--)?help\z/ @help_only = true elsif arg =~ /\A-/ args << arg elsif arg =~ /\AFAILURES=(.*)\z/ self.failures_path = $1 elsif arg == 'f' # failures_path could still be overridden -- delay evaluation of this. args << lambda do if File.exist?(failures_path) if failures.empty? STDERR.puts "No specs failed!" [] else @selected_failures = true failures end else warn "no fail file - ignoring 'f' argument" [] end end elsif arg =~ /\A\d+\z/ i = Integer(arg) if (failure = failures[i - 1]) args << failure @selected_failures = true @update_failures = false else warn "invalid failure: #{i} for (1..#{failures.size})" end else args << '--example' << arg.gsub(/[$]/, '\\\\\\0') end end expanded = [] args.each do |arg| if arg.respond_to?(:call) expanded.concat(arg.call) else expanded << arg end end # Since we append our formatter as a file to run, rspec won't fall back to # using 'spec' by default. Add it explicitly here. files << 'spec' if files.empty? # If we selected individual failures to rerun, don't give the files to # rspec, as those files will be run in their entirety. @generated_args = expanded @generated_args.concat(files) unless @selected_failures end def failures @failures ||= if File.exist?(failures_path) File.read(failures_path).split(/\n/) else [] end end def rspec_option_that_requires_an_argument?(arg) RSPEC_OPTIONS_THAT_REQUIRE_AN_ARGUMENT.include?(arg) end RSPEC_OPTIONS_THAT_REQUIRE_AN_ARGUMENT = %w[ -I -r --require -O --options --order --seed --failure-exit-code --drb-port -f --format --formatter -o --out -P --pattern -e --example -l --line_number -t --tag --default_path ].to_set end end