├── .gitignore ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── LICENSE ├── README.markdown ├── Rakefile ├── bin └── respec ├── lib ├── respec.rb └── respec │ ├── app.rb │ ├── formatter.rb │ └── version.rb ├── respec.gemspec └── spec ├── integration_spec.rb ├── respec ├── app_spec.rb └── formatter_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /*.gem 2 | /Gemfile.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | bundler_args: --without dev 4 | script: bundle exec rake ci 5 | rvm: 6 | - 2.0.0 7 | - 2.1.5 8 | - 2.2.0 9 | - jruby-1.7.9 10 | - rbx-2.4.1 11 | env: 12 | - RESPEC_RSPEC_VERSION='~> 2.11.0' 13 | - RESPEC_RSPEC_VERSION='~> 2.14.0' 14 | - RESPEC_RSPEC_VERSION='~> 3' 15 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | == 1.0.0 2025-03-17 2 | 3 | * Support handling numeric ranges (e.g., '2-4') 4 | * Fix handling of square bracket suffixes ('file[1:2:3]'). 5 | 6 | == 0.9.1 2017-03-21 7 | 8 | * Fix passing some options which take arguments to rspec (e.g. -p). 9 | 10 | == 0.9.0 2015-10-10 11 | 12 | * Fail with error if an invalid number is given, so we don't run everything. 13 | 14 | == 0.8.3 2015-06-12 15 | 16 | * Write .respec_failures to the right path when a test messes up the pwd. 17 | 18 | == 0.8.2 2015-03-13 19 | 20 | * Using '1' or 'f' filters examples when used with line suffixes ('file:123'). 21 | 22 | == 0.8.1 2015-02-14 23 | 24 | * Escape printed rspec command. 25 | 26 | == 0.8.0 2015-02-02 27 | 28 | * Support RSpec 3. 29 | * Record failing example names, not locations. Requires rspec 2.11 or above. 30 | * Wait until end of test suite before updating failure file. 31 | * Use rspec binstub if present. 32 | 33 | == 0.7.0 2014-04-16 34 | 35 | * Allow setting failure file via argument. Useful under CI with parallel_tests. 36 | 37 | == 0.6.0 2014-04-09 38 | 39 | * Exit with status 0 if there are no failures left to retry. Useful under CI. 40 | 41 | == 0.5.0 2013-03-02 42 | 43 | * Fix rerunning of specs defined in non-spec files (e.g. support files). 44 | 45 | == 0.4.1 2012-11-15 46 | 47 | * Run spec directory by default, like rspec. 48 | 49 | == 0.4.0 2012-11-11 50 | 51 | * Fix further issues with custom formatter being overridden by respec. 52 | 53 | == 0.3.0 2012-11-11 54 | 55 | * Don't add progress formatter when user has selected something else. 56 | * Remove 's' option. Use "-f s" instead, or an .rspec file instead. 57 | * Pass argument to deprecated --formatter option to rspec. 58 | 59 | == 0.2.0 2012-10-31 60 | 61 | * Support rspec options that require an argument. 62 | * Write failures to current directory, not home directory. 63 | 64 | == 0.1.1 2012-09-27 65 | 66 | * Handle file name arguments with line number suffixes. 67 | 68 | == 0.1.0 2012-06-08 69 | 70 | * Remove c, d, x/X shortcuts. 71 | * Pass any argument that starts with '-' directly to rspec. 72 | 73 | == 0.0.3 2012-05-24 74 | 75 | * Add shortcuts for debugger (d) and DRb (x). 76 | 77 | == 0.0.2 2012-05-23 78 | 79 | * Run through bundler if Gemfile is present. 80 | * Don't run whole files specified when rerunning failures. 81 | 82 | == 0.0.1 2012-05-17 83 | 84 | * Hi. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | gemspec 3 | 4 | gem 'ritual' 5 | gem 'temporaries' 6 | 7 | version = ENV['RESPEC_RSPEC_VERSION'] and 8 | gem 'rspec', version 9 | 10 | group :dev do 11 | gem 'byebug' 12 | gem 'looksee' 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) George Ogata 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Respec [![Build Status](https://travis-ci.org/oggy/respec.png)](https://travis-ci.org/oggy/respec) [![Gem Version](https://badge.fury.io/rb/respec.svg)](http://badge.fury.io/rb/respec) 2 | 3 | Provides a command, `respec`, which wraps `rspec`, and records your 4 | failing examples for easy rerunning. 5 | 6 | ## How? 7 | 8 | Run your specs: 9 | 10 | respec 11 | 12 | 3 fail. Rerun just the 3 failures like this: 13 | 14 | respec f 15 | 16 | Need to debug failure #1? Pop a `debugger` (or `pry`) in your code, and rerun it 17 | like this: 18 | 19 | respec 1 20 | 21 | This will just rerun failure 1. Once it's passing, rerun the 3 failing 22 | examples again: 23 | 24 | respec f 25 | 26 | 1 is now fixed, but 2 and 3 are still failing - `respec f` will now 27 | only run failures 2 and 3 again. 28 | 29 | ## How it works 30 | 31 | All that's happening is the list of failed examples is being recorded 32 | in a file (`.respec_failures`). The `f` argument means "run these 33 | recorded failures only." A numeric argument like `1` means "just run 34 | that failure." 35 | 36 | The list of failed examples is always updated _except_ when selecting 37 | which failures to rerun with a number (more than one number can also 38 | be given, incidentally). 39 | 40 | ## Other tricks 41 | 42 | You can pass `respec` file or directory names, just like 43 | `rspec`. However, you can also just specify example names on the 44 | command line: 45 | 46 | respec 'My example name' 47 | 48 | If the argument doesn't name an existing file, it's assumed to be an 49 | example name. 50 | 51 | It'll even `bundle exec` for you automatically, or use a binstub if present. 52 | 53 | If you use git, run all specs modified since the last commit with: 54 | 55 | respec d 56 | 57 | (**d** for "git **d**iff", which this uses.) 58 | 59 | There are a few other shortcuts. `respec --help` to see them all. 60 | 61 | If you're using this on CI to [rerun your failures][junit-merge], you may want 62 | to control where the failure file is written. You can do this in one of 2 ways: 63 | 64 | Either pass a `FAILURES=...` argument: 65 | 66 | respec FAILURES=/path/to/file ... 67 | 68 | Or use the `RESPEC_FAILURES` environment variable. 69 | 70 | RESPEC_FAILURES=/path/to/file respec ... 71 | 72 | [junit-merge]: https://github.com/oggy/junit_merge 73 | 74 | ## Contributing 75 | 76 | * [Bug reports](https://github.com/oggy/respec/issues) 77 | * [Source](https://github.com/oggy/respec) 78 | * Patches: Fork on Github, send pull request. 79 | * Include tests where practical. 80 | * Leave the version alone, or bump it in a separate commit. 81 | 82 | ## Copyright 83 | 84 | Copyright (c) George Ogata. See LICENSE for details. 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'ritual' 2 | 3 | task :ci do 4 | sh 'git config --global user.name || git config --global user.name Test' 5 | sh 'git config --global user.email || git config --global user.email test@example.com' 6 | sh 'bundle exec rspec spec' 7 | end 8 | -------------------------------------------------------------------------------- /bin/respec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ROOT = File.expand_path('..', File.dirname(__FILE__)) 4 | $:.unshift "#{ROOT}/lib" 5 | require 'respec' 6 | require 'shellwords' 7 | 8 | app = Respec::App.new(*ARGV) 9 | if app.help_only? 10 | STDERR.puts app.help 11 | elsif app.error 12 | STDERR.puts app.error 13 | exit 1 14 | else 15 | STDERR.puts "++ #{app.command.shelljoin}" 16 | exec *app.command 17 | end 18 | -------------------------------------------------------------------------------- /lib/respec.rb: -------------------------------------------------------------------------------- 1 | module Respec 2 | autoload :VERSION, 'respec/version' 3 | autoload :App, 'respec/app' 4 | autoload :Formatter, 'respec/formatter' 5 | end 6 | -------------------------------------------------------------------------------- /lib/respec/app.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Respec 4 | class App 5 | def initialize(*args) 6 | if (i = args.index('--')) 7 | @args = args.slice!(0...i) 8 | @raw_args = args[1..-1] 9 | else 10 | @args = args 11 | @raw_args = [] 12 | end 13 | @failures_path = self.class.default_failures_path 14 | @update_failures = true 15 | @error = nil 16 | process_args 17 | end 18 | 19 | attr_accessor :failures_path 20 | 21 | def error 22 | command 23 | @error 24 | end 25 | 26 | def command 27 | @command ||= program_args + generated_args + raw_args + formatter_args 28 | end 29 | 30 | def program_args 31 | if File.exist?('bin/rspec') 32 | ['bin/rspec'] 33 | elsif File.exist?(ENV['BUNDLE_GEMFILE'] || 'Gemfile') 34 | ['bundle', 'exec', 'rspec'] 35 | else 36 | ['rspec'] 37 | end 38 | end 39 | 40 | attr_reader :generated_args, :raw_args 41 | 42 | def formatter_args 43 | if @update_failures 44 | [File.expand_path('formatter.rb', File.dirname(__FILE__))] 45 | else 46 | [] 47 | end 48 | end 49 | 50 | class << self 51 | attr_accessor :default_failures_path 52 | end 53 | self.default_failures_path = ENV['RESPEC_FAILURES'] || File.expand_path(".respec_failures") 54 | 55 | def help_only? 56 | @help_only 57 | end 58 | 59 | def help 60 | <<-EOS.gsub(/^ *\|/, '') 61 | |USAGE: respec RESPEC-ARGS ... [ -- RSPEC-ARGS ... ] 62 | | 63 | |Run rspec, recording failed examples for easy rerunning later. 64 | | 65 | |RESPEC-ARGS may consist of: 66 | | 67 | | d Run all spec files changed since the last git commit 68 | | f Rerun all failed examples 69 | | Rerun only the N-th failure 70 | | Run all specs in this file 71 | | Run specs at line N in this file 72 | | Run only examples matching this pattern 73 | | - Passed directly to rspec. 74 | | --help This! (Also 'help'.) 75 | | 76 | |Any arguments following a '--' argument are passed directly to rspec. 77 | | 78 | |More info: http://github.com/oggy/respec 79 | EOS 80 | end 81 | 82 | private 83 | 84 | def process_args 85 | args = [] 86 | files = [] 87 | pass_next_arg = false 88 | using_filters = false 89 | changed_only = false 90 | path_suffix_re = /(:\d+|\[\d+(?::\d+)*\])\z/ 91 | @args.each do |arg| 92 | if pass_next_arg 93 | args << arg 94 | pass_next_arg = false 95 | elsif rspec_option_that_requires_an_argument?(arg) 96 | args << arg 97 | pass_next_arg = true 98 | elsif File.exist?(arg.sub(path_suffix_re, '')) 99 | files << arg 100 | elsif arg =~ /\A(--)?help\z/ 101 | @help_only = true 102 | elsif arg =~ /\A-/ 103 | args << arg 104 | elsif arg =~ /\AFAILURES=(.*)\z/ 105 | self.failures_path = $1 106 | elsif arg == 'd' 107 | changed_only = true 108 | elsif arg == 'f' 109 | # failures_path could still be overridden -- delay evaluation of this. 110 | args << lambda do 111 | if File.exist?(failures_path) 112 | if failures.empty? 113 | STDERR.puts "No specs failed!" 114 | [] 115 | else 116 | failures.flat_map { |f| ['-e', f] } 117 | end 118 | else 119 | warn "no fail file - ignoring 'f' argument" 120 | [] 121 | end 122 | end 123 | using_filters = true 124 | elsif arg =~ /\A\d+\z/ 125 | i = Integer(arg) 126 | if (failure = failures[i - 1]) 127 | args << '-e' << failure 128 | @update_failures = false 129 | using_filters = true 130 | else 131 | @error = "invalid failure: #{i} for (1..#{failures.size})" 132 | end 133 | elsif arg =~ /\A(\d+)-(\d+)\z/ 134 | lo = Integer($1) 135 | hi = Integer($2) 136 | if lo > hi 137 | @error = "invalid range: #{lo}-#{hi}" 138 | elsif failures[lo - 1].nil? || failures[hi - 1].nil? 139 | @error = "failures out of range #{lo}-#{hi} (for 0..#{failures.size - 1})" 140 | else 141 | (lo..hi).each do |i| 142 | args << "-e" << failures[i - 1] 143 | end 144 | @update_failures = false 145 | using_filters = true 146 | end 147 | else 148 | args << '-e' << arg.gsub(/[$]/, '\\\\\\0') 149 | end 150 | end 151 | 152 | expanded = [] 153 | args.each do |arg| 154 | if arg.respond_to?(:call) 155 | expanded.concat(arg.call) 156 | else 157 | expanded << arg 158 | end 159 | end 160 | 161 | # If rerunning failures, chop off explicit line numbers, as they are 162 | # additive, and filters are subtractive. 163 | if using_filters 164 | files.map! { |f| f.sub(path_suffix_re, '') } 165 | end 166 | 167 | # Since we append our formatter as a file to run, rspec won't fall back to 168 | # using 'spec' by default. Add it explicitly here. 169 | files << 'spec' if files.empty? 170 | 171 | # Filter files only to those changed if 'd' is present. 172 | if changed_only 173 | files = changed_paths(files) 174 | end 175 | 176 | # If we selected individual failures to rerun, don't give the files to 177 | # rspec, as those files will be run in their entirety. 178 | @generated_args = expanded 179 | @generated_args.concat(files) 180 | end 181 | 182 | def changed_paths(paths) 183 | changes = `git status --short --untracked-files=all #{paths.shelljoin}` 184 | changes.lines.map do |line| 185 | path = line[3..-1].chomp 186 | path if File.exist?(path) && path =~ /\.rb\z/i 187 | end.compact.uniq 188 | end 189 | 190 | def failures 191 | @failures ||= 192 | if File.exist?(failures_path) 193 | File.read(failures_path).split(/\n/) 194 | else 195 | [] 196 | end 197 | end 198 | 199 | def rspec_option_that_requires_an_argument?(arg) 200 | RSPEC_OPTIONS_THAT_REQUIRE_AN_ARGUMENT.include?(arg) 201 | end 202 | 203 | RSPEC_OPTIONS_THAT_REQUIRE_AN_ARGUMENT = %w[ 204 | -I 205 | -r --require 206 | -O --options 207 | --order 208 | --seed 209 | --failure-exit-code 210 | --error-exit-code 211 | --drb-port 212 | -f --format --formatter 213 | -o --out 214 | --deprecation-out 215 | -p --profile 216 | -P --pattern 217 | --exclude-pattern 218 | -e --example 219 | -E --example-matches 220 | -t --tag 221 | --default-path 222 | ].to_set 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /lib/respec/formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/formatters/base_formatter' 2 | require 'rspec/core/version' 3 | 4 | module Respec 5 | def self.failures_path 6 | @failures_path ||= ENV['RESPEC_FAILURES'] || File.expand_path(".respec_failures") 7 | end 8 | 9 | if (RSpec::Core::Version::STRING.scan(/\d+/).map { |s| s.to_i } <=> [3]) < 0 10 | 11 | class Formatter < RSpec::Core::Formatters::BaseFormatter 12 | def initialize(output=nil) 13 | super(output) 14 | end 15 | 16 | def start_dump 17 | open(Respec.failures_path, 'w') do |file| 18 | @failed_examples.each do |example| 19 | file.puts example.metadata[:full_description] 20 | end 21 | end 22 | end 23 | end 24 | 25 | else 26 | 27 | class Formatter < RSpec::Core::Formatters::BaseFormatter 28 | def initialize(output=nil) 29 | @respec_failures = [] 30 | super(output) 31 | end 32 | 33 | def example_failed(notification) 34 | @respec_failures << notification.example.full_description 35 | end 36 | 37 | def start_dump(notification) 38 | open(Respec.failures_path, 'w') do |file| 39 | @respec_failures.each do |failure| 40 | file.puts failure 41 | end 42 | end 43 | end 44 | 45 | RSpec::Core::Formatters.register self, :example_failed, :start_dump 46 | end 47 | 48 | end 49 | end 50 | 51 | # We inject this here rather than on the command line, as the logic to assemble 52 | # the list of formatters is complex, and easily broken by adding a --format 53 | # option. 54 | RSpec.configure do |config| 55 | config.add_formatter 'progress' if config.formatters.empty? 56 | config.add_formatter Respec::Formatter 57 | 58 | # Memoize this before any tests run in case a test messes up the pwd. 59 | config.before(:all) { Respec.failures_path } 60 | end 61 | -------------------------------------------------------------------------------- /lib/respec/version.rb: -------------------------------------------------------------------------------- 1 | module Respec 2 | VERSION = [1, 0, 0] 3 | 4 | class << VERSION 5 | include Comparable 6 | 7 | def to_s 8 | join('.') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /respec.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('lib', File.dirname(__FILE__)) 2 | require 'respec/version' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = 'respec' 6 | gem.version = Respec::VERSION 7 | gem.authors = ['George Ogata'] 8 | gem.email = ['george.ogata@gmail.com'] 9 | gem.license = 'MIT' 10 | gem.summary = "Rerun failing RSpec examples easily." 11 | gem.homepage = 'http://github.com/oggy/respec' 12 | 13 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 14 | gem.files = `git ls-files`.split("\n") 15 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | 17 | gem.add_runtime_dependency 'rspec', '>= 2.11' 18 | end 19 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'rbconfig' 3 | 4 | describe Respec do 5 | use_temporary_directory TMP 6 | 7 | CONFIG = (Object.const_defined?(:RbConfig) ? RbConfig : Config)::CONFIG 8 | def respec(args) 9 | # Rubinius can trip here as the simulated edits to the .rb files happen quickly enough that the 10 | # .rbc will look current. Blow away the .rbx directory to ensure the source is read each time. 11 | FileUtils.rm_rf '.rbx' 12 | 13 | ruby = File.join(CONFIG['bindir'], CONFIG['ruby_install_name']) 14 | respec = "#{ROOT}/bin/respec" 15 | output = `RESPEC_FAILURES=#{TMP}/failures.txt #{ruby} -I #{ROOT}/lib #{respec} #{args} 2>&1` 16 | [$?, output] 17 | end 18 | 19 | def make_spec(params) 20 | num_failures = params[:num_failures] or 21 | raise ArgumentError, "expected :num_failures parameter" 22 | 23 | source = "describe 'test' do\n" 24 | (0...2).map do |i| 25 | if i < num_failures 26 | source << " it('#{i}') { expect(1).to == 2 }\n" 27 | else 28 | source << " it('#{i}') {}\n" 29 | end 30 | end 31 | source << "end" 32 | open(spec_path, 'w') { |f| f.puts source } 33 | end 34 | 35 | def spec_path 36 | "#{TMP}/test_spec.rb" 37 | end 38 | 39 | it "should let you rerun failing specs until they all pass" do 40 | Dir.chdir tmp do 41 | make_spec(:num_failures => 2) 42 | status, output = respec(spec_path) 43 | expect(status).to_not be_success 44 | expect(output).to include('2 examples, 2 failures') 45 | 46 | make_spec(:num_failures => 1) 47 | status, output = respec("#{spec_path} f") 48 | expect(status).to_not be_success 49 | expect(output).to include('2 examples, 1 failure') 50 | 51 | make_spec(:num_failures => 0) 52 | status, output = respec("#{spec_path} f") 53 | expect(status).to be_success 54 | expect(output).to include('1 example, 0 failures') 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/respec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', File.dirname(__FILE__)) 2 | require 'fileutils' 3 | 4 | describe Respec::App do 5 | use_temporary_directory TMP 6 | 7 | FORMATTER_PATH = File.expand_path("#{ROOT}/lib/respec/formatter.rb", File.dirname(__FILE__)) 8 | FAIL_PATH = "#{TMP}/failures.txt" 9 | 10 | Respec::App.default_failures_path = FAIL_PATH 11 | 12 | def make_failures_file(*examples) 13 | options = examples.last.is_a?(Hash) ? examples.pop : {} 14 | path = options[:path] || Respec::App.default_failures_path 15 | open path, 'w' do |file| 16 | examples.each do |example| 17 | file.puts example 18 | end 19 | end 20 | end 21 | 22 | describe "#failures_path" do 23 | it "defaults to the global default" do 24 | app = Respec::App.new 25 | expect(app.failures_path).to eq FAIL_PATH 26 | end 27 | 28 | it "can be overridden with a FAILURES= argument" do 29 | app = Respec::App.new('FAILURES=overridden.txt') 30 | expect(app.failures_path).to eq 'overridden.txt' 31 | end 32 | end 33 | 34 | describe "#program_args" do 35 | it "should run through a binstub if present" do 36 | FileUtils.mkdir "#{tmp}/bin" 37 | FileUtils.touch "#{tmp}/bin/rspec" 38 | Dir.chdir tmp do 39 | app = Respec::App.new 40 | expect(app.program_args).to eq ['bin/rspec'] 41 | end 42 | end 43 | 44 | it "should otherwise run through bundler if a Gemfile is present" do 45 | FileUtils.touch "#{tmp}/Gemfile" 46 | Dir.chdir tmp do 47 | app = Respec::App.new 48 | expect(app.program_args).to eq ['bundle', 'exec', 'rspec'] 49 | end 50 | end 51 | 52 | it "should check the BUNDLE_GEMFILE environment variable if set" do 53 | with_hash_value ENV, 'BUNDLE_GEMFILE', "#{tmp}/custom_gemfile" do 54 | FileUtils.touch "#{tmp}/custom_gemfile" 55 | Dir.chdir tmp do 56 | app = Respec::App.new 57 | expect(app.program_args).to eq ['bundle', 'exec', 'rspec'] 58 | end 59 | end 60 | end 61 | 62 | it "should not run through bundler if no Gemfile is present" do 63 | with_hash_value ENV, 'BUNDLE_GEMFILE', nil do 64 | Dir.chdir tmp do 65 | app = Respec::App.new 66 | expect(app.program_args).to eq ['rspec'] 67 | end 68 | end 69 | end 70 | end 71 | 72 | describe "#formatter_args" do 73 | it "should update the stored failures if no args are given" do 74 | app = Respec::App.new 75 | expect(app.formatter_args).to eq [FORMATTER_PATH] 76 | end 77 | 78 | it "should update the stored failures if 'f' is used" do 79 | make_failures_file 'a' 80 | app = Respec::App.new('f') 81 | expect(app.formatter_args).to eq [FORMATTER_PATH] 82 | end 83 | 84 | it "should not update the stored failures if a numeric argument is given" do 85 | make_failures_file 'a' 86 | app = Respec::App.new('1') 87 | expect(app.formatter_args).to eq [] 88 | end 89 | end 90 | 91 | describe "#generated_args" do 92 | def in_git_repo 93 | dir = "#{tmp}/repo" 94 | FileUtils.mkdir_p dir 95 | Dir.chdir(dir) do 96 | system 'git init --quiet .' 97 | yield 98 | end 99 | end 100 | 101 | def make_git_file(path, index_status, status) 102 | FileUtils.mkdir_p File.dirname(path) 103 | 104 | unless index_status == nil || index_status == :new 105 | open(path, 'w') { |f| f.print 1 } 106 | system "git add #{path.shellescape}" 107 | system "git commit --quiet --message . #{path.shellescape}" 108 | end 109 | 110 | case index_status 111 | when :new, :updated 112 | open(path, 'w') { |f| f.print 2 } 113 | when :up_to_date, nil 114 | when :removed 115 | File.delete path 116 | else 117 | raise ArgumentError, "invalid index_status: #{index_status}" 118 | end 119 | 120 | system "git add -- #{path.shellescape}" unless index_status.nil? 121 | 122 | case status 123 | when :new, :updated 124 | open(path, 'w') { |f| f.print 3 } 125 | when :up_to_date 126 | when :removed 127 | File.delete path 128 | else 129 | raise ArgumentError, "invalid status: #{status}" 130 | end 131 | end 132 | 133 | it "should pass all arguments that start with '-' to rspec" do 134 | FileUtils.touch "#{tmp}/file" 135 | app = Respec::App.new('-a', '-b', '-c', "#{tmp}/file") 136 | expect(app.generated_args).to eq ['-a', '-b', '-c', "#{tmp}/file"] 137 | end 138 | 139 | it "should pass arguments for rspec options that need them" do 140 | FileUtils.touch "#{tmp}/file" 141 | expect(Respec::App.new('-I', 'lib', '-t', 'mytag', "#{tmp}/file").generated_args).to eq ['-I', 'lib', '-t', 'mytag', "#{tmp}/file"] 142 | end 143 | 144 | it "should run all failures if 'f' is given" do 145 | make_failures_file 'a', 'b' 146 | app = Respec::App.new('f') 147 | expect(app.generated_args).to eq ['-e', 'a', '-e', 'b', 'spec'] 148 | end 149 | 150 | it "should run all new and updated files if 'd' is given" do 151 | in_git_repo do 152 | make_git_file 'a/01.rb', nil, :new 153 | 154 | make_git_file 'a/02.rb', :new, :up_to_date 155 | make_git_file 'a/03.rb', :new, :updated 156 | make_git_file 'a/04.rb', :new, :removed 157 | 158 | make_git_file 'a/05.rb', :up_to_date, :up_to_date 159 | make_git_file 'a/06.rb', :up_to_date, :updated 160 | make_git_file 'a/07.rb', :up_to_date, :removed 161 | 162 | make_git_file 'a/08.rb', :updated, :up_to_date 163 | make_git_file 'a/09.rb', :updated, :updated 164 | make_git_file 'a/10.rb', :updated, :removed 165 | 166 | make_git_file 'a/11.rb', :removed, :up_to_date 167 | make_git_file 'a/12.rb', :removed, :new 168 | 169 | app = Respec::App.new('d', 'a') 170 | expect(app.generated_args).to \ 171 | contain_exactly(*[1, 2, 3, 6, 8, 9, 12].map { |i| 'a/%02d.rb' % i}) 172 | end 173 | end 174 | 175 | it "should only include .rb files for 'd'" do 176 | in_git_repo do 177 | make_git_file 'a/1.rb', :new, :up_to_date 178 | make_git_file 'a/1.br', :new, :up_to_date 179 | 180 | app = Respec::App.new('d', 'a') 181 | expect(app.generated_args).to eq %w[a/1.rb] 182 | end 183 | end 184 | 185 | it "should not include files outside the given spec directories for 'd'" do 186 | in_git_repo do 187 | make_git_file 'a/1.rb', :new, :up_to_date 188 | make_git_file 'b/1.rb', :new, :up_to_date 189 | make_git_file 'c/1.rb', :new, :up_to_date 190 | 191 | app = Respec::App.new('d', 'a', 'b') 192 | expect(app.generated_args).to eq %w[a/1.rb b/1.rb] 193 | end 194 | end 195 | 196 | it "should filter files from the default spec directory for 'd'" do 197 | in_git_repo do 198 | make_git_file 'spec/1.rb', :new, :up_to_date 199 | make_git_file 'other/1.rb', :new, :up_to_date 200 | 201 | app = Respec::App.new('d') 202 | expect(app.generated_args).to eq %w[spec/1.rb] 203 | end 204 | end 205 | 206 | it "should pass failures with spaces in them as a single argument" do 207 | make_failures_file 'a a' 208 | app = Respec::App.new('f') 209 | expect(app.generated_args).to eq ['-e', 'a a', 'spec'] 210 | end 211 | 212 | it "should find the right failures if the failures file is overridden after the 'f'" do 213 | make_failures_file 'a', 'b', path: "#{FAIL_PATH}-overridden" 214 | app = Respec::App.new('f', "FAILURES=#{FAIL_PATH}-overridden") 215 | expect(app.generated_args).to eq ['-e', 'a', '-e', 'b', 'spec'] 216 | end 217 | 218 | it "should run the n-th failure if a numeric argument 'n' is given" do 219 | make_failures_file 'a', 'b' 220 | app = Respec::App.new('2') 221 | expect(app.generated_args).to eq ['-e', 'b', 'spec'] 222 | end 223 | 224 | it "should run the x through y-th failures if a numeric range x-y is given" do 225 | make_failures_file 'a', 'b', 'c', 'd', 'e' 226 | app = Respec::App.new('2-4') 227 | expect(app.generated_args).to eq ['-e', 'b', '-e', 'c', '-e', 'd', 'spec'] 228 | end 229 | 230 | it "should interpret existing file names as file name arguments" do 231 | FileUtils.touch "#{tmp}/existing.rb" 232 | app = Respec::App.new("#{tmp}/existing.rb") 233 | expect(app.generated_args).to eq ["#{tmp}/existing.rb"] 234 | end 235 | 236 | it "should pass existing file names with line numbers directly to rspec" do 237 | FileUtils.touch "#{tmp}/existing.rb" 238 | app = Respec::App.new("#{tmp}/existing.rb:123") 239 | expect(app.generated_args).to eq ["#{tmp}/existing.rb:123"] 240 | end 241 | 242 | it "should pass existing file names with discriminators in square brackets directly to rspec" do 243 | FileUtils.touch "#{tmp}/existing.rb" 244 | app = Respec::App.new("#{tmp}/existing.rb[1:22:333]") 245 | expect(app.generated_args).to eq ["#{tmp}/existing.rb[1:22:333]"] 246 | end 247 | 248 | it "should truncate line numbers when using numeric arguments" do 249 | make_failures_file 'a' 250 | FileUtils.touch "#{tmp}/existing.rb" 251 | app = Respec::App.new("#{tmp}/existing.rb:123", '1') 252 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/existing.rb"] 253 | end 254 | 255 | it "should truncate square brackets when using numeric arguments" do 256 | make_failures_file 'a' 257 | FileUtils.touch "#{tmp}/existing.rb" 258 | app = Respec::App.new("#{tmp}/existing.rb[1:22:333]", '1') 259 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/existing.rb"] 260 | end 261 | 262 | it "should truncate line numbers when rerunning all failures" do 263 | make_failures_file 'a' 264 | FileUtils.touch "#{tmp}/existing.rb" 265 | app = Respec::App.new("#{tmp}/existing.rb:123", 'f') 266 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/existing.rb"] 267 | end 268 | 269 | it "should truncate square brackets when rerunning all failures" do 270 | make_failures_file 'a' 271 | FileUtils.touch "#{tmp}/existing.rb" 272 | app = Respec::App.new("#{tmp}/existing.rb[1:22:333]", 'f') 273 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/existing.rb"] 274 | end 275 | 276 | it "should treat other arguments as example names" do 277 | app = Respec::App.new('a', 'b') 278 | expect(app.generated_args).to eq ['-e', 'a', '-e', 'b', 'spec'] 279 | end 280 | 281 | it "should include named files when a numeric argument is given" do 282 | FileUtils.touch "#{tmp}/FILE" 283 | make_failures_file 'a' 284 | app = Respec::App.new("#{tmp}/FILE", '1') 285 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/FILE"] 286 | end 287 | 288 | it "should include named files when an 'f' argument is given" do 289 | FileUtils.touch "#{tmp}/FILE" 290 | make_failures_file 'a' 291 | app = Respec::App.new("#{tmp}/FILE", 'f') 292 | expect(app.generated_args).to eq ['-e', 'a', "#{tmp}/FILE"] 293 | end 294 | 295 | it "should explicitly add the spec directory if no files are given or errors to rerun" do 296 | app = Respec::App.new 297 | expect(app.generated_args).to eq ['spec'] 298 | end 299 | 300 | it "should not add the spec directory if any files are given" do 301 | FileUtils.touch "#{tmp}/FILE" 302 | app = Respec::App.new("#{tmp}/FILE") 303 | expect(app.generated_args).to eq ["#{tmp}/FILE"] 304 | end 305 | 306 | it "should add the spec directory if a numeric argument is given without explicit files" do 307 | make_failures_file 'a' 308 | app = Respec::App.new('1') 309 | expect(app.generated_args).to eq ['-e', 'a', 'spec'] 310 | end 311 | 312 | it "should add the spec directory when an 'f' argument is given without explicit files" do 313 | make_failures_file 'a' 314 | app = Respec::App.new('f') 315 | expect(app.generated_args).to eq ['-e', 'a', 'spec'] 316 | end 317 | end 318 | 319 | describe "#raw_args" do 320 | it "should pass arguments after '--' directly to rspec" do 321 | app = Respec::App.new('--', '--blah') 322 | expect(app.raw_args).to eq ['--blah'] 323 | end 324 | end 325 | 326 | describe "#command" do 327 | it "should combine all the args" do 328 | Dir.chdir tmp do 329 | FileUtils.touch 'Gemfile' 330 | app = Respec::App.new('--', '-t', 'TAG') 331 | expect(app.command).to eq ['bundle', 'exec', 'rspec', 'spec', '-t', 'TAG', FORMATTER_PATH] 332 | end 333 | end 334 | end 335 | 336 | describe "#error" do 337 | it "should not be set if the arguments are valid" do 338 | make_failures_file 'a' 339 | app = Respec::App.new('1') 340 | expect(app.error).to be_nil 341 | end 342 | 343 | it "should be set if an invalid failure is given" do 344 | make_failures_file 'a', 'b' 345 | app = Respec::App.new('3') 346 | expect(app.error).to include 'invalid failure: 3 for (1..2)' 347 | end 348 | end 349 | end 350 | -------------------------------------------------------------------------------- /spec/respec/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require 'rspec/version' 3 | 4 | describe Respec::Formatter do 5 | use_temporary_directory TMP 6 | let(:formatter) { Respec::Formatter.new } 7 | 8 | def failures 9 | File.read("#{TMP}/failures.txt") 10 | end 11 | 12 | describe Respec::Formatter do 13 | if (RSpec::Core::Version::STRING.scan(/\d+/).map { |s| s.to_i } <=> [3]) < 0 14 | 15 | before { Respec.stub(failures_path: "#{TMP}/failures.txt") } 16 | 17 | def make_failing_example(description) 18 | metadata = {full_description: description} 19 | mock(RSpec::Core::Example.allocate, metadata: metadata) 20 | end 21 | 22 | it "records failed example names and dumps them at the end" do 23 | failed_examples = [make_failing_example('example 1'), make_failing_example('example 2')] 24 | formatter.instance_variable_set(:@failed_examples, failed_examples) 25 | formatter.start_dump 26 | expect(failures).to eq "example 1\nexample 2\n" 27 | end 28 | 29 | it "empties the failure file if no examples failed" do 30 | formatter.start_dump 31 | expect(failures).to eq '' 32 | end 33 | 34 | else 35 | 36 | before { allow(Respec).to receive(:failures_path).and_return("#{TMP}/failures.txt") } 37 | 38 | def make_failure_notification(description) 39 | result = RSpec::Core::Example::ExecutionResult.new 40 | example = double(RSpec::Core::Example.allocate, full_description: description, execution_result: result) 41 | RSpec::Core::Notifications::FailedExampleNotification.new(example) 42 | end 43 | 44 | it "records failed example names and dumps them at the end" do 45 | formatter.example_failed(make_failure_notification('example 1')) 46 | formatter.example_failed(make_failure_notification('example 2')) 47 | formatter.start_dump(RSpec::Core::Notifications::NullNotification) 48 | expect(failures).to eq "example 1\nexample 2\n" 49 | end 50 | 51 | it "empties the failure file if no examples failed" do 52 | formatter.start_dump(RSpec::Core::Notifications::NullNotification) 53 | expect(failures).to eq '' 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ROOT = File.expand_path('..', File.dirname(__FILE__)) 2 | TMP = "#{ROOT}/spec/tmp" 3 | 4 | $:.unshift "#{ROOT}/lib" 5 | require 'respec' 6 | require 'temporaries' 7 | --------------------------------------------------------------------------------