mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
9aac3cb1d2
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>
160 lines
4.7 KiB
Ruby
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
|