/
runner.rb
204 lines (174 loc) · 6.32 KB
/
runner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# frozen_string_literal: true
require "shellwords"
require "rake/file_list"
require "active_support"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/range"
require "rails/test_unit/test_parser"
module Rails
module TestUnit
class InvalidTestError < StandardError
def initialize(path, suggestion)
super(<<~MESSAGE.squish)
Could not load test file: #{path}.
#{suggestion}
MESSAGE
end
end
class Runner
TEST_FOLDERS = [:models, :helpers, :channels, :controllers, :mailers, :integration, :jobs, :mailboxes]
PATH_ARGUMENT_PATTERN = %r"^(?!/.+/$)[.\w]*[/\\]"
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 run_from_rake(test_command, argv = [])
# Ensure the tests run during the Rake Task action, not when the process exits
success = system("rails", test_command, *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 = list_tests(patterns)
tests.to_a.each do |path|
require File.expand_path(path)
rescue LoadError => exception
all_tests = list_tests([default_test_glob])
corrections = DidYouMean::SpellChecker.new(dictionary: all_tests).correct(path)
if corrections.empty?
raise exception
end
raise InvalidTestError.new(path, DidYouMean::Formatter.message_for(corrections))
end
end
def compose_filter(runnable, filter)
filter = normalize_declarative_test_filter(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.filter_map do |path|
next unless path_argument?(path)
path = path.tr("\\", "/")
case
when /(:\d+(-\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,fixtures}/**/*_test.rb"
end
def regexp_filter?(arg)
arg.start_with?("/") && arg.end_with?("/")
end
def path_argument?(arg)
PATH_ARGUMENT_PATTERN.match?(arg)
end
def list_tests(patterns)
tests = Rake::FileList[patterns.any? ? patterns : default_test_glob]
tests.exclude(default_test_exclude_glob) if patterns.empty?
tests
end
def normalize_declarative_test_filter(filter)
if filter.is_a?(String)
if regexp_filter?(filter)
# Minitest::Spec::DSL#it does not replace whitespace in method
# names, so match unmodified method names as well.
filter = filter.gsub(/\s+/, "_").delete_suffix("/") + "|" + filter.delete_prefix("/")
elsif !filter.start_with?("test_")
filter = "test_#{filter.gsub(/\s+/, "_")}"
end
end
filter
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_or_range)
@runnable, @file = runnable, File.expand_path(file)
if line_or_range
first, last = line_or_range.split("-").map(&:to_i)
last ||= first
@line_range = Range.new(first, last)
end
end
def ===(method)
return unless @runnable.method_defined?(method)
if @line_range
test_file, test_range = definition_for(@runnable.instance_method(method))
test_file == @file && @line_range.overlaps?(test_range)
else
@runnable.instance_method(method).source_location.first == @file
end
end
private
def definition_for(method)
TestParser.definition_for(method)
end
end
end
end