1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/railties/lib/rails/test_unit/runner.rb
Étienne Barrié 9aac3cb1d2 Ensure test rake commands run immediately
Before this commit, Rails test Rake tasks only load the test files, and
the tests only run in an at_exit hook via minitest/autorun.

This prevents conditionally running tasks only when tests pass, or even
more simply in the right order. As a simple example, if you have:

task default: [:test, :rubocop]

The rubocop task will run after the test task loads the test files but
before the tests actually run.

This commit changes the test Rake tasks to shell out to the test runner
as a new process.

This diverges from previous behavior because now, any changes made in
the Rakefile or other code loaded by Rake won't be available to the
child process. However this brings the behavior of `rake test` closer to
the behavior of `rails test`.

Co-authored-by: Adrianna Chang <adrianna.chang@shopify.com>
2021-01-16 14:38:30 -05:00

160 lines
4.7 KiB
Ruby

# frozen_string_literal: true
require "shellwords"
require "method_source"
require "rake/file_list"
require "active_support/core_ext/module/attribute_accessors"
module Rails
module TestUnit
class Runner
mattr_reader :filters, default: []
class << self
def attach_before_load_options(opts)
opts.on("--warnings", "-w", "Run with Ruby warnings enabled") { }
opts.on("-e", "--environment ENV", "Run tests in the ENV environment") { }
end
def parse_options(argv)
# Perform manual parsing and cleanup since option parser raises on unknown options.
env_index = argv.index("--environment") || argv.index("-e")
if env_index
argv.delete_at(env_index)
environment = argv.delete_at(env_index).strip
end
ENV["RAILS_ENV"] = environment || "test"
w_index = argv.index("--warnings") || argv.index("-w")
$VERBOSE = argv.delete_at(w_index) if w_index
end
def rake_run(argv = [])
# Ensure the tests run during the Rake Task action, not when the process exits
success = system("rails", "test", *argv, *Shellwords.split(ENV["TESTOPTS"] || ""))
success || exit(false)
end
def run(argv = [])
load_tests(argv)
require "active_support/testing/autorun"
end
def load_tests(argv)
patterns = extract_filters(argv)
tests = Rake::FileList[patterns.any? ? patterns : default_test_glob]
tests.exclude(default_test_exclude_glob) if patterns.empty?
tests.to_a.each { |path| require File.expand_path(path) }
end
def compose_filter(runnable, filter)
if filters.any? { |_, lines| lines.any? }
CompositeFilter.new(runnable, filter, filters)
else
filter
end
end
private
def extract_filters(argv)
# Extract absolute and relative paths but skip -n /.*/ regexp filters.
argv.select { |arg| path_argument?(arg) && !regexp_filter?(arg) }.map do |path|
path = path.tr("\\", "/")
case
when /(:\d+)+$/.match?(path)
file, *lines = path.split(":")
filters << [ file, lines ]
file
when Dir.exist?(path)
"#{path}/**/*_test.rb"
else
filters << [ path, [] ]
path
end
end
end
def default_test_glob
ENV["DEFAULT_TEST"] || "test/**/*_test.rb"
end
def default_test_exclude_glob
ENV["DEFAULT_TEST_EXCLUDE"] || "test/{system,dummy}/**/*_test.rb"
end
def regexp_filter?(arg)
arg.start_with?("/") && arg.end_with?("/")
end
def path_argument?(arg)
%r"^[/\\]?\w+[/\\]".match?(arg)
end
end
end
class CompositeFilter # :nodoc:
attr_reader :named_filter
def initialize(runnable, filter, patterns)
@runnable = runnable
@named_filter = derive_named_filter(filter)
@filters = [ @named_filter, *derive_line_filters(patterns) ].compact
end
# minitest uses === to find matching filters.
def ===(method)
@filters.any? { |filter| filter === method }
end
private
def derive_named_filter(filter)
if filter.respond_to?(:named_filter)
filter.named_filter
elsif filter =~ %r%/(.*)/% # Regexp filtering copied from minitest.
Regexp.new $1
elsif filter.is_a?(String)
filter
end
end
def derive_line_filters(patterns)
patterns.flat_map do |file, lines|
if lines.empty?
Filter.new(@runnable, file, nil) if file
else
lines.map { |line| Filter.new(@runnable, file, line) }
end
end
end
end
class Filter # :nodoc:
def initialize(runnable, file, line)
@runnable, @file = runnable, File.expand_path(file)
@line = line.to_i if line
end
def ===(method)
return unless @runnable.method_defined?(method)
if @line
test_file, test_range = definition_for(@runnable.instance_method(method))
test_file == @file && test_range.include?(@line)
else
@runnable.instance_method(method).source_location.first == @file
end
end
private
def definition_for(method)
file, start_line = method.source_location
end_line = method.source.count("\n") + start_line - 1
return file, start_line..end_line
end
end
end
end