minitest 5.27.0 → 6.0.0.a1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,418 @@
1
+ require "prism"
2
+
3
+ module Minitest; end # :nodoc:
4
+
5
+ ##
6
+ # PathExpander helps pre-process command-line arguments expanding
7
+ # directories into their constituent files. It further helps by
8
+ # providing additional mechanisms to make specifying subsets easier
9
+ # with path subtraction and allowing for command-line arguments to be
10
+ # saved in a file.
11
+ #
12
+ # NOTE: this is NOT an options processor. It is a path processor
13
+ # (basically everything else besides options). It does provide a
14
+ # mechanism for pre-filtering cmdline options, but not with the intent
15
+ # of actually processing them in PathExpander. Use OptionParser to
16
+ # deal with options either before or after passing ARGV through
17
+ # PathExpander.
18
+
19
+ class Minitest::VendoredPathExpander
20
+ # extracted version = "2.0.0"
21
+
22
+ ##
23
+ # The args array to process.
24
+
25
+ attr_accessor :args
26
+
27
+ ##
28
+ # The glob used to expand dirs to files.
29
+
30
+ attr_accessor :glob
31
+
32
+ ##
33
+ # The path to scan if no paths are found in the initial scan.
34
+
35
+ attr_accessor :path
36
+
37
+ ##
38
+ # Create a new path expander that operates on args and expands via
39
+ # glob as necessary. Takes an optional +path+ arg to fall back on if
40
+ # no paths are found on the initial scan (see #process_args).
41
+
42
+ def initialize args, glob, path = "."
43
+ self.args = args
44
+ self.glob = glob
45
+ self.path = path
46
+ end
47
+
48
+ ##
49
+ # Takes an array of paths and returns an array of paths where all
50
+ # directories are expanded to all files found via the glob provided
51
+ # to PathExpander.
52
+ #
53
+ # Paths are normalized to not have a leading "./".
54
+
55
+ def expand_dirs_to_files *dirs
56
+ dirs.flatten.map { |p|
57
+ if File.directory? p then
58
+ Dir[File.join(p, glob)].find_all { |f| File.file? f }
59
+ else
60
+ p
61
+ end
62
+ }.flatten.sort.map { |s| s.to_s.delete_prefix "./" }
63
+ end
64
+
65
+ ##
66
+ # Process a file into more arguments. Override this to add
67
+ # additional capabilities.
68
+
69
+ def process_file path
70
+ File.readlines(path).map(&:chomp)
71
+ end
72
+
73
+ ##
74
+ # Enumerate over args passed to PathExpander and return a list of
75
+ # files and flags to process. Arguments are processed as:
76
+ #
77
+ # @file_of_args :: Read the file and append to args.
78
+ # -file_path :: Subtract path from file to be processed.
79
+ # -dir_path :: Expand and subtract paths from files to be processed.
80
+ # -not_a_path :: Add to flags to be processed.
81
+ # dir_path :: Expand and add to files to be processed.
82
+ # file_path :: Add to files to be processed.
83
+ # - :: Add "-" (stdin) to files to be processed.
84
+ #
85
+ # See expand_dirs_to_files for details on how expansion occurs.
86
+ #
87
+ # Subtraction happens last, regardless of argument ordering.
88
+ #
89
+ # If no files are found (which is not the same as having an empty
90
+ # file list after subtraction), then fall back to expanding on the
91
+ # default #path given to initialize.
92
+
93
+ def process_args
94
+ pos_files = []
95
+ neg_files = []
96
+ flags = []
97
+ clean = true
98
+
99
+ root_dir = File.expand_path "/" # needed for windows paths
100
+
101
+ args.each do |arg|
102
+ case arg
103
+ when /^@(.*)/ then # push back on, so they can have dirs/-/@ as well
104
+ clean = false
105
+ args.concat process_file $1
106
+ when "-" then
107
+ pos_files << arg
108
+ when /^-(.*)/ then
109
+ if File.exist? $1 then
110
+ clean = false
111
+ neg_files += expand_dirs_to_files($1)
112
+ else
113
+ flags << arg
114
+ end
115
+ else
116
+ root_path = File.expand_path(arg) == root_dir # eg: -n /./
117
+ if File.exist? arg and not root_path then
118
+ clean = false
119
+ pos_files += expand_dirs_to_files(arg)
120
+ else
121
+ flags << arg
122
+ end
123
+ end
124
+ end
125
+
126
+ files = pos_files - neg_files
127
+ files += expand_dirs_to_files(self.path) if files.empty? && clean
128
+
129
+ [files, flags]
130
+ end
131
+
132
+ ##
133
+ # Process over flags and treat any special ones here. Returns an
134
+ # array of the flags you haven't processed.
135
+ #
136
+ # This version does nothing. Subclass and override for
137
+ # customization.
138
+
139
+ def process_flags flags
140
+ flags
141
+ end
142
+
143
+ ##
144
+ # Top-level method processes args. If no block is given, immediately
145
+ # returns with an Enumerator for further chaining.
146
+ #
147
+ # Otherwise, it calls +pre_process+, +process_args+ and
148
+ # +process_flags+, enumerates over the files, and then calls
149
+ # +post_process+, returning self for any further chaining.
150
+ #
151
+ # Most of the time, you're going to provide a block to process files
152
+ # and do nothing more with the result. Eg:
153
+ #
154
+ # PathExpander.new(ARGV).process do |f|
155
+ # puts "./#{f}"
156
+ # end
157
+ #
158
+ # or:
159
+ #
160
+ # PathExpander.new(ARGV).process # => Enumerator
161
+
162
+ def process(&b)
163
+ return enum_for(:process) unless block_given?
164
+
165
+ pre_process
166
+
167
+ files, flags = process_args
168
+
169
+ args.replace process_flags flags
170
+
171
+ files.uniq.each(&b)
172
+
173
+ post_process
174
+
175
+ self
176
+ end
177
+
178
+ def pre_process = nil
179
+ def post_process = nil
180
+
181
+ ##
182
+ # A file filter mechanism similar to, but not as extensive as,
183
+ # .gitignore files:
184
+ #
185
+ # + If a pattern does not contain a slash, it is treated as a shell glob.
186
+ # + If a pattern ends in a slash, it matches on directories (and contents).
187
+ # + Otherwise, it matches on relative paths.
188
+ #
189
+ # File.fnmatch is used throughout, so glob patterns work for all 3 types.
190
+ #
191
+ # Takes a list of +files+ and either an io or path of +ignore+ data
192
+ # and returns a list of files left after filtering.
193
+
194
+ def filter_files files, ignore
195
+ ignore_paths = if ignore.respond_to? :read then
196
+ ignore.read
197
+ elsif File.exist? ignore then
198
+ File.read ignore
199
+ end
200
+
201
+ if ignore_paths then
202
+ nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" }
203
+ dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" }
204
+ dirs = dirs.map { |s| s.chomp "/" }
205
+
206
+ dirs.map! { |i| File.expand_path i }
207
+ globs.map! { |i| File.expand_path i }
208
+ ifiles.map! { |i| File.expand_path i }
209
+
210
+ only_paths = File::FNM_PATHNAME
211
+ files = files.reject { |f|
212
+ f = File.expand_path(f)
213
+ dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } ||
214
+ globs.any? { |i| File.fnmatch?(i, f) } ||
215
+ ifiles.any? { |i| File.fnmatch?(i, f, only_paths) }
216
+ }
217
+ end
218
+
219
+ files
220
+ end
221
+ end # VendoredPathExpander
222
+
223
+ ##
224
+ # Minitest's PathExpander to find and filter tests.
225
+
226
+ class Minitest::PathExpander < Minitest::VendoredPathExpander
227
+ attr_accessor :by_line # :nodoc:
228
+
229
+ TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
230
+
231
+ def initialize args = ARGV # :nodoc:
232
+ super args, TEST_GLOB, "test"
233
+ self.by_line = {}
234
+ end
235
+
236
+ def process_args # :nodoc:
237
+ args.reject! { |arg| # this is a good use of overriding
238
+ case arg
239
+ when /^(.*):([\d,-]+)$/ then
240
+ f, ls = $1, $2
241
+ ls = ls
242
+ .split(/,/)
243
+ .map { |l|
244
+ case l
245
+ when /^\d+$/ then
246
+ l.to_i
247
+ when /^(\d+)-(\d+)$/ then
248
+ $1.to_i..$2.to_i
249
+ else
250
+ raise "unhandled argument format: %p" % [l]
251
+ end
252
+ }
253
+ next unless File.exist? f
254
+ args << f # push path on lest it run whole dir
255
+ by_line[f] = ls
256
+ end
257
+ }
258
+
259
+ super
260
+ end
261
+
262
+ ##
263
+ # Overrides PathExpander#process_flags to filter out ruby flags
264
+ # from minitest flags. Only supports -I<paths>, -d, and -w for
265
+ # ruby.
266
+
267
+ def process_flags flags
268
+ flags.reject { |flag| # all hits are truthy, so this works out well
269
+ case flag
270
+ when /^-I(.*)/ then
271
+ $LOAD_PATH.prepend(*$1.split(/:/))
272
+ when /^-d/ then
273
+ $DEBUG = true
274
+ when /^-w/ then
275
+ $VERBOSE = true
276
+ else
277
+ false
278
+ end
279
+ }
280
+ end
281
+
282
+ ##
283
+ # Add additional arguments to args to handle path:line argument filtering
284
+
285
+ def post_process
286
+ return if by_line.empty?
287
+
288
+ tests = tests_by_class
289
+
290
+ exit! if handle_missing_tests? tests
291
+
292
+ test_res = tests_to_regexp tests
293
+ self.args << "-n" << "/#{test_res.join "|"}/"
294
+ end
295
+
296
+ ##
297
+ # Find and return all known tests as a hash of klass => [TM...]
298
+ # pairs.
299
+
300
+ def all_tests
301
+ Minitest.seed = 42 # minor hack to deal with runnable_methods shuffling
302
+ Minitest::Runnable.runnables
303
+ .to_h { |k|
304
+ ms = k.runnable_methods
305
+ .sort
306
+ .map { |m| TM.new k, m.to_sym }
307
+ .sort_by { |t| [t.path, t.line_s] }
308
+ [k, ms]
309
+ }
310
+ .reject { |k, v| v.empty? }
311
+ end
312
+
313
+ ##
314
+ # Returns a hash mapping Minitest runnable classes to TMs
315
+
316
+ def tests_by_class
317
+ all_tests
318
+ .transform_values { |ms|
319
+ ms.select { |m|
320
+ bl = by_line[m.path]
321
+ not bl or bl.any? { |l| m.include? l }
322
+ }
323
+ }
324
+ .reject { |k, v| v.empty? }
325
+ end
326
+
327
+ ##
328
+ # Converts +tests+ to an array of "klass#(methods+)" regexps to be
329
+ # used for test selection.
330
+
331
+ def tests_to_regexp tests
332
+ tests # { k1 => [Test(a), ...}
333
+ .transform_values { |tms| tms.map(&:name) } # { k1 => %w[a, b], ...}
334
+ .map { |k, ns| # [ "k1#(?:a|b)", "k2#c", ...]
335
+ if ns.size > 1 then
336
+ ns.map! { |n| Regexp.escape n }
337
+ "%s#\(?:%s\)" % [Regexp.escape(k.name), ns.join("|")]
338
+ else
339
+ "%s#%s" % [Regexp.escape(k.name), ns.first]
340
+ end
341
+ }
342
+ end
343
+
344
+ ##
345
+ # Handle the case where a line number doesn't match any known tests.
346
+ # Returns true to signal that running should stop.
347
+
348
+ def handle_missing_tests? tests
349
+ _tests = tests.values.flatten
350
+ not_found = by_line
351
+ .flat_map { |f, ls| ls.map { |l| [f, l] } }
352
+ .reject { |f, l|
353
+ _tests.any? { |t| t.path == f and t.include? l }
354
+ }
355
+
356
+ unless not_found.empty? then
357
+ by_path = all_tests.values.flatten.group_by(&:path)
358
+
359
+ puts
360
+ puts "ERROR: test(s) not found at:"
361
+ not_found.each do |f, l|
362
+ puts " %s:%s" % [f, l]
363
+ puts
364
+ puts "Did you mean?"
365
+ puts
366
+ l = l.begin if l.is_a? Range
367
+ by_path[f]
368
+ .sort_by { |m| (m.line_s - l).abs }
369
+ .first(2)
370
+ .each do |m|
371
+ puts " %-30s (dist=%+d) (%s)" % [m, m.line_s - l, m.name]
372
+ end
373
+ puts
374
+ end
375
+ true
376
+ end
377
+ end
378
+
379
+ ##
380
+ # Simple TestMethod (abbr TM) Data object.
381
+
382
+ TM = Data.define :klass, :name, :path, :lines do
383
+ def initialize klass:, name:
384
+ method = klass.instance_method name
385
+ path, line_s = method.source_location
386
+
387
+ path = path.delete_prefix "#{Dir.pwd}/"
388
+
389
+ line_e = line_s + TM.source_for(method).lines.size - 1
390
+
391
+ lines = line_s..line_e
392
+
393
+ super klass:, name:, path:, lines:
394
+ end
395
+
396
+ def self.source_for method
397
+ path, line = method.source_location
398
+ file = cache[path] ||= File.readlines(path)
399
+
400
+ ruby = +""
401
+
402
+ file[line-1..].each do |l|
403
+ ruby << l
404
+ return ruby if Prism.parse_success? ruby
405
+ end
406
+
407
+ nil
408
+ end
409
+
410
+ def self.cache = @cache ||= {}
411
+
412
+ def include?(o) = o.is_a?(Integer) ? lines.include?(o) : lines.overlap?(o)
413
+
414
+ def to_s = "%s:%d-%d" % [path, lines.begin, lines.end]
415
+
416
+ def line_s = lines.begin
417
+ end
418
+ end
@@ -1,4 +1,4 @@
1
1
  require_relative "../minitest"
2
2
 
3
- Minitest.load_plugins
3
+ Minitest.load :pride
4
4
  Minitest::PrideIO.pride!
@@ -0,0 +1,45 @@
1
+ require "drb"
2
+ require "tmpdir"
3
+ require_relative "../minitest"
4
+
5
+ class Minitest::Server
6
+ VERSION = "1.0.9"
7
+
8
+ TOPDIR = Dir.pwd + "/"
9
+
10
+ def self.path pid = $$
11
+ "drbunix:#{Dir.tmpdir}/minitest.#{pid}"
12
+ end
13
+
14
+ def self.run client
15
+ DRb.start_service path, new(client)
16
+ end
17
+
18
+ def self.stop
19
+ DRb.stop_service
20
+ end
21
+
22
+ attr_accessor :client
23
+
24
+ def initialize client
25
+ self.client = client
26
+ end
27
+
28
+ def quit
29
+ self.class.stop
30
+ end
31
+
32
+ def start
33
+ client.minitest_start
34
+ end
35
+
36
+ def result file, klass, method, fails, assertions, time
37
+ file = file.sub(/^#{TOPDIR}/, "")
38
+
39
+ client.minitest_result file, klass, method, fails, assertions, time
40
+ end
41
+
42
+ def report
43
+ # do nothing
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ require_relative "../minitest"
2
+
3
+ module Minitest
4
+ @server = false
5
+
6
+ def self.plugin_server_options opts, options # :nodoc:
7
+ opts.on "--server=pid", Integer, "Connect to minitest server w/ pid." do |s|
8
+ @server = s
9
+ end
10
+ end
11
+
12
+ def self.plugin_server_init options
13
+ if @server then
14
+ require_relative "server"
15
+ self.reporter << Minitest::ServerReporter.new(@server)
16
+ end
17
+ end
18
+ end
19
+
20
+ class Minitest::ServerReporter < Minitest::AbstractReporter
21
+ def initialize pid
22
+ uri = Minitest::Server.path(pid)
23
+ @mt_server = DRbObject.new_with_uri uri
24
+ super()
25
+ end
26
+
27
+ def start
28
+ @mt_server.start
29
+ end
30
+
31
+ def record result
32
+ r = result
33
+ c = r.class
34
+
35
+ case r
36
+ when Minitest::Result then
37
+ file, = r.source_location
38
+ cn = r.klass
39
+ else
40
+ # TODO: remove? when is this used?
41
+ file, = r.method(r.name).source_location
42
+ cn = c.name
43
+ end
44
+
45
+ sanitize r.failures
46
+
47
+ @mt_server.result file, cn, r.name, r.failures, r.assertions, r.time
48
+ end
49
+
50
+ def sanitize failures
51
+ failures.map! { |e|
52
+ case e
53
+ when Minitest::UnexpectedError then
54
+ # embedded exception might not be able to be marshaled.
55
+ bt = e.error.backtrace
56
+
57
+ ex = RuntimeError.new(e.error.message)
58
+ e.error = ex
59
+ ex.set_backtrace bt
60
+
61
+ e = Minitest::UnexpectedError.new ex # ugh. some rails plugin. ugh.
62
+
63
+ if ex.instance_variables.include? :@bindings then # web-console is Evil
64
+ ex.instance_variable_set :@bindings, nil
65
+ e.instance_variable_set :@bindings, nil
66
+ end
67
+ when Minitest::Skip then
68
+ # do nothing
69
+ when Minitest::Assertion then
70
+ bt = e.backtrace
71
+ e = e.class.new(e.message)
72
+ e.set_backtrace bt
73
+ else
74
+ warn "Unhandled exception type: #{e.class}\n\n#{e.inspect}"
75
+ end
76
+
77
+ e
78
+ }
79
+ end
80
+
81
+ def report
82
+ @mt_server.report
83
+ end
84
+ end
data/lib/minitest/spec.rb CHANGED
@@ -4,24 +4,12 @@ class Module # :nodoc:
4
4
  def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
5
5
  block = dont_flip == :block
6
6
  dont_flip = false if block
7
- target_obj = block ? "_{obj.method}" : "_(obj)"
8
7
 
9
8
  # https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html
10
9
  # Drop this when we can drop ruby 2.6 (aka after rails 6.1 EOL, ~2024-06)
11
10
  kw_extra = "ruby2_keywords %p" % [new_name] if respond_to? :ruby2_keywords, true
12
11
 
13
- # warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
14
12
  self.class_eval <<-EOM, __FILE__, __LINE__ + 1
15
- def #{new_name} *args
16
- where = Minitest.filter_backtrace(caller).first
17
- where = where.split(/:in /, 2).first # clean up noise
18
- Kernel.warn "DEPRECATED: global use of #{new_name} from #\{where}. Use #{target_obj}.#{new_name} instead. This will fail in Minitest 6."
19
- Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
20
- end
21
- #{kw_extra}
22
- EOM
23
-
24
- Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
25
13
  def #{new_name} *args
26
14
  raise "Calling ##{new_name} outside of test." unless ctx
27
15
  case
@@ -110,15 +98,6 @@ end
110
98
 
111
99
  class Minitest::Spec < Minitest::Test
112
100
 
113
- def self.current # :nodoc:
114
- Thread.current[:current_spec]
115
- end
116
-
117
- def initialize name # :nodoc:
118
- super
119
- Thread.current[:current_spec] = self
120
- end
121
-
122
101
  ##
123
102
  # Oh look! A Minitest::Spec::DSL module! Eat your heart out DHH.
124
103
 
@@ -313,9 +292,6 @@ class Minitest::Spec < Minitest::Test
313
292
  # straight-expectation methods (on Object) because it stores its
314
293
  # test context, bypassing our hacky use of thread-local variables.
315
294
  #
316
- # NOTE: At some point, the methods on Object will be deprecated
317
- # and then removed.
318
- #
319
295
  # It is also aliased to #value and #expect for your aesthetic
320
296
  # pleasure:
321
297
  #
@@ -329,11 +305,6 @@ class Minitest::Spec < Minitest::Test
329
305
 
330
306
  alias value _
331
307
  alias expect _
332
-
333
- def before_setup # :nodoc:
334
- super
335
- Thread.current[:current_spec] = self
336
- end
337
308
  end
338
309
 
339
310
  def self.extended obj # :nodoc:
@@ -348,6 +319,6 @@ end
348
319
 
349
320
  require_relative "expectations"
350
321
 
351
- class Object # :nodoc:
352
- include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
322
+ class Minitest::Expectation
323
+ include Minitest::Expectations
353
324
  end