Merge pull request #19216 from senny/bin_test_runner

`bin/rails test` runner (rerun snippets, run tests by line, option documentation)
This commit is contained in:
Yves Senn 2015-03-19 14:27:01 +01:00
commit 9959e9525b
14 changed files with 411 additions and 65 deletions

@ -60,6 +60,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
# FIX: Our test suite isn't ready to run in random order yet
gem 'minitest', '< 5.3.4'
gem 'method_source'
platforms :mri_19 do
gem 'ruby-prof', '~> 0.11.2'

@ -134,6 +134,7 @@ GEM
loofah (2.0.1)
nokogiri (>= 1.5.9)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (2.4.3)
mini_portile (0.6.2)
minitest (5.3.3)
@ -260,6 +261,7 @@ DEPENDENCIES
json
kindlerb (= 0.1.1)
mail!
method_source
minitest (< 5.3.4)
mocha (~> 0.14)
mysql (>= 2.9.0)

@ -6,7 +6,8 @@
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
"r" => "runner",
"t" => "test",
}
command = ARGV.shift

@ -14,6 +14,7 @@ class CommandsTasks # :nodoc:
generate Generate new code (short-cut alias: "g")
console Start the Rails console (short-cut alias: "c")
server Start the Rails server (short-cut alias: "s")
test Run tests (short-cut alias: "t")
dbconsole Start a console for the database specified in config/database.yml
(short-cut alias: "db")
new Create a new Rails application. "rails new my_app" creates a
@ -27,7 +28,7 @@ class CommandsTasks # :nodoc:
All commands can be run with -h (or --help) for more information.
EOT
COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)
COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test)
def initialize(argv)
@argv = argv
@ -81,6 +82,10 @@ def server
end
end
def test
require_command!("test")
end
def dbconsole
require_command!("dbconsole")
Rails::DBConsole.start

@ -0,0 +1,5 @@
require "rails/test_unit/runner"
$: << File.expand_path("../../test", APP_PATH)
Rails::TestRunner.run(ARGV)

@ -2,6 +2,7 @@
# so fixtures aren't loaded into that environment
abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production?
require "rails/test_unit/minitest_plugin"
require 'active_support/testing/autorun'
require 'active_support/test_case'
require 'action_controller'
@ -9,12 +10,6 @@
require 'action_dispatch/testing/integration'
require 'rails/generators/test_case'
# Config Rails backtrace in tests.
require 'rails/backtrace_cleaner'
if ENV["BACKTRACE"].nil?
Minitest.backtrace_filter = Rails.backtrace_cleaner
end
if defined?(ActiveRecord::Base)
ActiveRecord::Migration.maintain_test_schema!

@ -0,0 +1,14 @@
require "minitest"
require "rails/test_unit/reporter"
def Minitest.plugin_rails_init(options)
self.reporter << Rails::TestUnitReporter.new(options[:io], options)
if $rails_test_runner && (method = $rails_test_runner.find_method)
options[:filter] = method
end
if !($rails_test_runner && $rails_test_runner.show_backtrace?)
Minitest.backtrace_filter = Rails.backtrace_cleaner
end
end
Minitest.extensions << 'rails'

@ -0,0 +1,22 @@
require "minitest"
module Rails
class TestUnitReporter < Minitest::StatisticsReporter
def report
return if results.empty?
io.puts
io.puts "Failed tests:"
io.puts
io.puts aggregated_results
end
def aggregated_results # :nodoc:
filtered_results = results.dup
filtered_results.reject!(&:skipped?) unless options[:verbose]
filtered_results.map do |result|
location, line = result.method(result.name).source_location
"bin/rails test #{location}:#{line}"
end.join "\n"
end
end
end

@ -0,0 +1,138 @@
require "ostruct"
require "optparse"
require "rake/file_list"
require "method_source"
module Rails
class TestRunner
class Options
def self.parse(args)
options = { backtrace: !ENV["BACKTRACE"].nil?, name: nil, environment: "test" }
opt_parser = ::OptionParser.new do |opts|
opts.banner = "Usage: bin/rails test [options] [file or directory]"
opts.separator ""
opts.on("-e", "--environment [ENV]",
"run tests in the ENV environment") do |env|
options[:environment] = env.strip
end
opts.separator ""
opts.separator "Filter options:"
opts.separator ""
opts.separator <<-DESC
You can run a single test by appending the line number to filename:
bin/rails test test/models/user_test.rb:27
DESC
opts.on("-n", "--name [NAME]",
"Only run tests matching NAME") do |name|
options[:name] = name
end
opts.on("-p", "--pattern [PATTERN]",
"Only run tests matching PATTERN") do |pattern|
options[:name] = "/#{pattern}/"
end
opts.separator ""
opts.separator "Output options:"
opts.on("-b", "--backtrace",
"show the complte backtrace") do
options[:backtrace] = true
end
opts.separator ""
opts.separator "Common options:"
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
opt_parser.order!(args)
options[:patterns] = []
while arg = args.shift
if (file_and_line = arg.split(':')).size > 1
options[:filename], options[:line] = file_and_line
options[:filename] = File.expand_path options[:filename]
options[:line] &&= options[:line].to_i
else
arg = arg.gsub(':', '')
if Dir.exists?("#{arg}")
options[:patterns] << File.expand_path("#{arg}/**/*_test.rb")
elsif File.file?(arg)
options[:patterns] << File.expand_path(arg)
end
end
end
options
end
end
def initialize(options = {})
@options = options
end
def self.run(arguments)
options = Rails::TestRunner::Options.parse(arguments)
Rails::TestRunner.new(options).run
end
def run
$rails_test_runner = self
ENV["RAILS_ENV"] = @options[:environment]
run_tests
end
def find_method
return @options[:name] if @options[:name]
return unless @options[:line]
method = test_methods.find do |location, test_method, start_line, end_line|
location == @options[:filename] &&
(start_line..end_line).include?(@options[:line].to_i)
end
method[1] if method
end
def show_backtrace?
@options[:backtrace]
end
def test_files
return [@options[:filename]] if @options[:filename]
if @options[:patterns] && @options[:patterns].count > 0
pattern = @options[:patterns]
else
pattern = "test/**/*_test.rb"
end
Rake::FileList[pattern]
end
private
def run_tests
test_files.to_a.each do |file|
require File.expand_path file
end
end
def test_methods
methods_map = []
suites = Minitest::Runnable.runnables.shuffle
suites.each do |suite_class|
suite_class.runnable_methods.each do |test_method|
method = suite_class.instance_method(test_method)
location = method.source_location
start_line = location.last
end_line = method.source.split("\n").size + start_line - 1
methods_map << [location.first, test_method, start_line, end_line]
end
end
methods_map
end
end
end

@ -1,11 +1,12 @@
require 'rake/testtask'
require 'rails/test_unit/sub_test_task'
require "rails/test_unit/runner"
task default: :test
desc "Runs all tests in test folder"
task :test do
Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task
$: << "test"
ARGV.shift if ARGV[0] == "test"
Rails::TestRunner.run(ARGV)
end
namespace :test do
@ -14,30 +15,30 @@ namespace :test do
# If used with Active Record, this task runs before the database schema is synchronized.
end
Rails::TestTask.new(:run) do |t|
t.pattern = "test/**/*_test.rb"
end
task :run => %w[test]
desc "Run tests quickly, but also reset db"
task :db => %w[db:test:prepare test]
Rails::TestTask.new(single: "test:prepare")
["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name|
Rails::TestTask.new(name => "test:prepare") do |t|
t.pattern = "test/#{name}/**/*_test.rb"
task name => "test:prepare" do
$: << "test"
Rails::TestRunner.run(["test/#{name}"])
end
end
Rails::TestTask.new(generators: "test:prepare") do |t|
t.pattern = "test/lib/generators/**/*_test.rb"
task :generators => "test:prepare" do
$: << "test"
Rails::TestRunner.run(["test/lib/generators"])
end
Rails::TestTask.new(units: "test:prepare") do |t|
t.pattern = 'test/{models,helpers,unit}/**/*_test.rb'
task :units => "test:prepare" do
$: << "test"
Rails::TestRunner.run(["test/models", "test/helpers", "test/unit"])
end
Rails::TestTask.new(functionals: "test:prepare") do |t|
t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb'
task :functionals => "test:prepare" do
$: << "test"
Rails::TestRunner.run(["test/controllers", "test/mailers", "test/functional"])
end
end

@ -7,7 +7,6 @@ class TestRunnerTest < ActiveSupport::TestCase
def setup
build_app
ENV['RAILS_ENV'] = nil
create_schema
end
@ -55,7 +54,7 @@ def test_run_models
create_test_file :models, 'foo'
create_test_file :models, 'bar'
create_test_file :controllers, 'foobar_controller'
run_test_models_command.tap do |output|
run_test_command("test/models").tap do |output|
assert_match "FooTest", output
assert_match "BarTest", output
assert_match "2 runs, 2 assertions, 0 failures", output
@ -66,7 +65,7 @@ def test_run_helpers
create_test_file :helpers, 'foo_helper'
create_test_file :helpers, 'bar_helper'
create_test_file :controllers, 'foobar_controller'
run_test_helpers_command.tap do |output|
run_test_command("test/helpers").tap do |output|
assert_match "FooHelperTest", output
assert_match "BarHelperTest", output
assert_match "2 runs, 2 assertions, 0 failures", output
@ -74,6 +73,7 @@ def test_run_helpers
end
def test_run_units
skip "we no longer have the concept of unit tests. Just different directories..."
create_test_file :models, 'foo'
create_test_file :helpers, 'bar_helper'
create_test_file :unit, 'baz_unit'
@ -90,7 +90,7 @@ def test_run_controllers
create_test_file :controllers, 'foo_controller'
create_test_file :controllers, 'bar_controller'
create_test_file :models, 'foo'
run_test_controllers_command.tap do |output|
run_test_command("test/controllers").tap do |output|
assert_match "FooControllerTest", output
assert_match "BarControllerTest", output
assert_match "2 runs, 2 assertions, 0 failures", output
@ -101,7 +101,7 @@ def test_run_mailers
create_test_file :mailers, 'foo_mailer'
create_test_file :mailers, 'bar_mailer'
create_test_file :models, 'foo'
run_test_mailers_command.tap do |output|
run_test_command("test/mailers").tap do |output|
assert_match "FooMailerTest", output
assert_match "BarMailerTest", output
assert_match "2 runs, 2 assertions, 0 failures", output
@ -112,7 +112,7 @@ def test_run_jobs
create_test_file :jobs, 'foo_job'
create_test_file :jobs, 'bar_job'
create_test_file :models, 'foo'
run_test_jobs_command.tap do |output|
run_test_command("test/jobs").tap do |output|
assert_match "FooJobTest", output
assert_match "BarJobTest", output
assert_match "2 runs, 2 assertions, 0 failures", output
@ -120,6 +120,7 @@ def test_run_jobs
end
def test_run_functionals
skip "we no longer have the concept of functional tests. Just different directories..."
create_test_file :mailers, 'foo_mailer'
create_test_file :controllers, 'bar_controller'
create_test_file :functional, 'baz_functional'
@ -135,7 +136,7 @@ def test_run_functionals
def test_run_integration
create_test_file :integration, 'foo_integration'
create_test_file :models, 'foo'
run_test_integration_command.tap do |output|
run_test_command("test/integration").tap do |output|
assert_match "FooIntegration", output
assert_match "1 runs, 1 assertions, 0 failures", output
end
@ -165,7 +166,7 @@ def test_sanae
end
RUBY
run_test_command('test/unit/chu_2_koi_test.rb test_rikka').tap do |output|
run_test_command('-n test_rikka test/unit/chu_2_koi_test.rb').tap do |output|
assert_match "Rikka", output
assert_no_match "Sanae", output
end
@ -186,7 +187,7 @@ def test_sanae
end
RUBY
run_test_command('test/unit/chu_2_koi_test.rb /rikka/').tap do |output|
run_test_command('-p rikka test/unit/chu_2_koi_test.rb').tap do |output|
assert_match "Rikka", output
assert_no_match "Sanae", output
end
@ -194,18 +195,18 @@ def test_sanae
def test_load_fixtures_when_running_test_suites
create_model_with_fixture
suites = [:models, :helpers, [:units, :unit], :controllers, :mailers,
[:functionals, :functional], :integration]
suites = [:models, :helpers, :controllers, :mailers, :integration]
suites.each do |suite, directory|
directory ||= suite
create_fixture_test directory
assert_match "3 users", run_task(["test:#{suite}"])
assert_match "3 users", run_test_command("test/#{suite}")
Dir.chdir(app_path) { FileUtils.rm_f "test/#{directory}" }
end
end
def test_run_with_model
skip "These feel a bit odd. Not sure we should keep supporting them."
create_model_with_fixture
create_fixture_test 'models', 'user'
assert_match "3 users", run_task(["test models/user"])
@ -213,6 +214,7 @@ def test_run_with_model
end
def test_run_different_environment_using_env_var
skip "no longer possible. Running tests in a different environment should be explicit"
app_file 'test/unit/env_test.rb', <<-RUBY
require 'test_helper'
@ -227,7 +229,7 @@ def test_env
assert_match "development", run_test_command('test/unit/env_test.rb')
end
def test_run_different_environment_using_e_tag
def test_run_different_environment
env = "development"
app_file 'test/unit/env_test.rb', <<-RUBY
require 'test_helper'
@ -239,7 +241,7 @@ def test_env
end
RUBY
assert_match env, run_test_command("test/unit/env_test.rb RAILS_ENV=#{env}")
assert_match env, run_test_command("-e #{env} test/unit/env_test.rb")
end
def test_generated_scaffold_works_with_rails_test
@ -248,17 +250,8 @@ def test_generated_scaffold_works_with_rails_test
end
private
def run_task(tasks)
Dir.chdir(app_path) { `bundle exec rake #{tasks.join ' '}` }
end
def run_test_command(arguments = 'test/unit/test_test.rb')
run_task ['test', arguments]
end
%w{ mailers models helpers units controllers functionals integration jobs }.each do |type|
define_method("run_test_#{type}_command") do
run_task ["test:#{type}"]
end
Dir.chdir(app_path) { `bin/rails t #{arguments}` }
end
def create_model_with_fixture

@ -65,6 +65,7 @@ def test_failure
output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" })
assert_match %r{/app/test/unit/failing_test\.rb}, output
assert_match %r{/app/test/unit/failing_test\.rb:4}, output
end
test "ruby schema migrations" do
@ -300,23 +301,7 @@ def assert_successful_test_run(name)
end
def run_test_file(name, options = {})
ruby '-Itest', "#{app_path}/test/#{name}", options.deep_merge(env: {"RAILS_ENV" => "test"})
end
def ruby(*args)
options = args.extract_options!
env = options.fetch(:env, {})
env["RUBYLIB"] = $:.join(':')
Dir.chdir(app_path) do
`#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1`
end
end
def env_string(variables)
variables.map do |key, value|
"#{key}='#{value}'"
end.join " "
Dir.chdir(app_path) { `bin/rails test "#{app_path}/test/#{name}" 2>&1` }
end
end
end

@ -0,0 +1,74 @@
require 'abstract_unit'
require 'rails/test_unit/reporter'
class TestUnitReporterTest < ActiveSupport::TestCase
class ExampleTest < Minitest::Test
def woot; end
end
setup do
@output = StringIO.new
@reporter = Rails::TestUnitReporter.new @output
end
test "prints rerun snippet to run a single failed test" do
@reporter.record(failed_test)
@reporter.report
assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string
assert_rerun_snippet_count 1
end
test "prints rerun snippet for every failed test" do
@reporter.record(failed_test)
@reporter.record(failed_test)
@reporter.record(failed_test)
@reporter.report
assert_rerun_snippet_count 3
end
test "does not print snippet for successful and skipped tests" do
@reporter.record(passing_test)
@reporter.record(skipped_test)
@reporter.report
assert_rerun_snippet_count 0
end
test "prints rerun snippet for skipped tests if run in verbose mode" do
verbose = Rails::TestUnitReporter.new @output, verbose: true
verbose.record(skipped_test)
verbose.report
assert_rerun_snippet_count 1
end
private
def assert_rerun_snippet_count(snippet_count)
assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size
end
def failed_test
ft = ExampleTest.new(:woot)
ft.failures << begin
raise Minitest::Assertion, "boo"
rescue Minitest::Assertion => e
e
end
ft
end
def passing_test
ExampleTest.new(:woot)
end
def skipped_test
st = ExampleTest.new(:woot)
st.failures << begin
raise Minitest::Skip
rescue Minitest::Assertion => e
e
end
st
end
end

@ -0,0 +1,110 @@
require 'abstract_unit'
require 'env_helpers'
require 'rails/test_unit/runner'
class TestUnitTestRunnerTest < ActiveSupport::TestCase
include EnvHelpers
setup do
@options = Rails::TestRunner::Options
end
test "shows the filtered backtrace by default" do
options = @options.parse([])
assert_not options[:backtrace]
end
test "has --backtrace (-b) option to show the full backtrace" do
options = @options.parse(["-b"])
assert options[:backtrace]
options = @options.parse(["--backtrace"])
assert options[:backtrace]
end
test "show full backtrace using BACKTRACE environment variable" do
switch_env "BACKTRACE", "true" do
options = @options.parse([])
assert options[:backtrace]
end
end
test "tests run in the test environment by default" do
options = @options.parse([])
assert_equal "test", options[:environment]
end
test "can run in a specific environment" do
options = @options.parse(["-e development"])
assert_equal "development", options[:environment]
end
test "parse the filename and line" do
file = "test/test_unit/runner_test.rb"
absolute_file = __FILE__
options = @options.parse(["#{file}:20"])
assert_equal absolute_file, options[:filename]
assert_equal 20, options[:line]
options = @options.parse(["#{file}:"])
assert_equal [absolute_file], options[:patterns]
assert_nil options[:line]
options = @options.parse([file])
assert_equal [absolute_file], options[:patterns]
assert_nil options[:line]
end
test "find_method on same file" do
options = @options.parse(["#{__FILE__}:#{__LINE__}"])
runner = Rails::TestRunner.new(options)
assert_equal "test_find_method_on_same_file", runner.find_method
end
test "find_method on a different file" do
options = @options.parse(["foobar.rb:#{__LINE__}"])
runner = Rails::TestRunner.new(options)
assert_nil runner.find_method
end
test "run all tests in a directory" do
options = @options.parse([__dir__])
assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns]
assert_nil options[:filename]
assert_nil options[:line]
end
test "run multiple folders" do
application_dir = File.expand_path("#{__dir__}/../application")
options = @options.parse([__dir__, application_dir])
assert_equal ["#{__dir__}/**/*_test.rb", "#{application_dir}/**/*_test.rb"], options[:patterns]
assert_nil options[:filename]
assert_nil options[:line]
runner = Rails::TestRunner.new(options)
assert runner.test_files.size > 0
end
test "run multiple files and run one file by line" do
line = __LINE__
options = @options.parse([__dir__, "#{__FILE__}:#{line}"])
assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns]
assert_equal __FILE__, options[:filename]
assert_equal line, options[:line]
runner = Rails::TestRunner.new(options)
assert_equal [__FILE__], runner.test_files, 'Only returns the file that running by line'
end
test "running multiple files passing line number" do
line = __LINE__
options = @options.parse(["foobar.rb:8", "#{__FILE__}:#{line}"])
assert_equal __FILE__, options[:filename], 'Returns the last file'
assert_equal line, options[:line]
end
end