class Minitest::VendoredPathExpander
PathExpander helps pre-process command-line arguments expanding directories into their constituent files. It further helps by providing additional mechanisms to make specifying subsets easier with path subtraction and allowing for command-line arguments to be saved in a file.
NOTE: this is NOT an options processor. It is a path processor (basically everything else besides options). It does provide a mechanism for pre-filtering cmdline options, but not with the intent of actually processing them in PathExpander. Use OptionParser to deal with options either before or after passing ARGV through PathExpander.
Attributes
The args array to process.
The glob used to expand dirs to files.
The path to scan if no paths are found in the initial scan.
Public Class Methods
Source
# File lib/minitest/path_expander.rb, line 42 def initialize args, glob, path = "." self.args = args self.glob = glob self.path = path end
Create a new path expander that operates on args and expands via glob as necessary. Takes an optional path arg to fall back on if no paths are found on the initial scan (see process_args).
Public Instance Methods
Source
# File lib/minitest/path_expander.rb, line 55 def expand_dirs_to_files *dirs dirs.flatten.map { |p| if File.directory? p then Dir[File.join(p, glob)].find_all { |f| File.file? f } else p end }.flatten.sort.map { |s| s.to_s.delete_prefix "./" } end
Takes an array of paths and returns an array of paths where all directories are expanded to all files found via the glob provided to PathExpander.
Paths are normalized to not have a leading “./”.
Source
# File lib/minitest/path_expander.rb, line 192 def filter_files files, ignore ignore_paths = if ignore.respond_to? :read then ignore.read elsif File.exist? ignore then File.read ignore end if ignore_paths then nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" } dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" } dirs = dirs.map { |s| s.chomp "/" } dirs.map! { |i| File.expand_path i } globs.map! { |i| File.expand_path i } ifiles.map! { |i| File.expand_path i } only_paths = File::FNM_PATHNAME files = files.reject { |f| f = File.expand_path(f) dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } || globs.any? { |i| File.fnmatch?(i, f) } || ifiles.any? { |i| File.fnmatch?(i, f, only_paths) } } end files end
A file filter mechanism similar to, but not as extensive as, .gitignore files:
+ If a pattern does not contain a slash, it is treated as a shell glob. + If a pattern ends in a slash, it matches on directories (and contents). + Otherwise, it matches on relative paths.
File.fnmatch is used throughout, so glob patterns work for all 3 types.
Takes a list of files and either an io or path of ignore data and returns a list of files left after filtering.
Source
# File lib/minitest/path_expander.rb, line 177 def post_process = nil ## # A file filter mechanism similar to, but not as extensive as, # .gitignore files: # # + If a pattern does not contain a slash, it is treated as a shell glob. # + If a pattern ends in a slash, it matches on directories (and contents). # + Otherwise, it matches on relative paths. # # File.fnmatch is used throughout, so glob patterns work for all 3 types. # # Takes a list of +files+ and either an io or path of +ignore+ data # and returns a list of files left after filtering. def filter_files files, ignore ignore_paths = if ignore.respond_to? :read then ignore.read elsif File.exist? ignore then File.read ignore end if ignore_paths then nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" } dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" } dirs = dirs.map { |s| s.chomp "/" } dirs.map! { |i| File.expand_path i } globs.map! { |i| File.expand_path i } ifiles.map! { |i| File.expand_path i } only_paths = File::FNM_PATHNAME files = files.reject { |f| f = File.expand_path(f) dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } || globs.any? { |i| File.fnmatch?(i, f) } || ifiles.any? { |i| File.fnmatch?(i, f, only_paths) } } end files end end
Source
# File lib/minitest/path_expander.rb, line 176 def pre_process = nil def post_process = nil ## # A file filter mechanism similar to, but not as extensive as, # .gitignore files: # # + If a pattern does not contain a slash, it is treated as a shell glob. # + If a pattern ends in a slash, it matches on directories (and contents). # + Otherwise, it matches on relative paths. # # File.fnmatch is used throughout, so glob patterns work for all 3 types. # # Takes a list of +files+ and either an io or path of +ignore+ data # and returns a list of files left after filtering. def filter_files files, ignore ignore_paths = if ignore.respond_to? :read then ignore.read elsif File.exist? ignore then File.read ignore end if ignore_paths then nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" } dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" } dirs = dirs.map { |s| s.chomp "/" } dirs.map! { |i| File.expand_path i } globs.map! { |i| File.expand_path i } ifiles.map! { |i| File.expand_path i } only_paths = File::FNM_PATHNAME files = files.reject { |f| f = File.expand_path(f) dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } || globs.any? { |i| File.fnmatch?(i, f) } || ifiles.any? { |i| File.fnmatch?(i, f, only_paths) } } end files end end # VendoredPathExpander ## # Minitest's PathExpander to find and filter tests. class Minitest::PathExpander < Minitest::VendoredPathExpander attr_accessor :by_line # :nodoc: TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc: def initialize args = ARGV # :nodoc: super args, TEST_GLOB, "test" self.by_line = {} end def process_args # :nodoc: args.reject! { |arg| # this is a good use of overriding case arg when /^(.*):([\d,-]+)$/ then f, ls = $1, $2 ls = ls .split(/,/) .map { |l| case l when /^\d+$/ then l.to_i when /^(\d+)-(\d+)$/ then $1.to_i..$2.to_i else raise "unhandled argument format: %p" % [l] end } next unless File.exist? f args << f # push path on lest it run whole dir by_line[f] = ls end } super end ## # Overrides PathExpander#process_flags to filter out ruby flags # from minitest flags. Only supports -I<paths>, -d, and -w for # ruby. def process_flags flags flags.reject { |flag| # all hits are truthy, so this works out well case flag when /^-I(.*)/ then $LOAD_PATH.prepend(*$1.split(/:/)) when /^-d/ then $DEBUG = true when /^-w/ then $VERBOSE = true else false end } end ## # Add additional arguments to args to handle path:line argument filtering def post_process return if by_line.empty? tests = tests_by_class exit! if handle_missing_tests? tests test_res = tests_to_regexp tests self.args << "-n" << "/#{test_res.join "|"}/" end ## # Find and return all known tests as a hash of klass => [TM...] # pairs. def all_tests Minitest.seed = 42 # minor hack to deal with runnable_methods shuffling Minitest::Runnable.runnables .to_h { |k| ms = k.runnable_methods .sort .map { |m| TM.new k, m.to_sym } .sort_by { |t| [t.path, t.line_s] } [k, ms] } .reject { |k, v| v.empty? } end ## # Returns a hash mapping Minitest runnable classes to TMs def tests_by_class all_tests .transform_values { |ms| ms.select { |m| bl = by_line[m.path] not bl or bl.any? { |l| m.include? l } } } .reject { |k, v| v.empty? } end ## # Converts +tests+ to an array of "klass#(methods+)" regexps to be # used for test selection. def tests_to_regexp tests tests # { k1 => [Test(a), ...} .transform_values { |tms| tms.map(&:name) } # { k1 => %w[a, b], ...} .map { |k, ns| # [ "k1#(?:a|b)", "k2#c", ...] if ns.size > 1 then ns.map! { |n| Regexp.escape n } "%s#\(?:%s\)" % [Regexp.escape(k.name), ns.join("|")] else "%s#%s" % [Regexp.escape(k.name), ns.first] end } end ## # Handle the case where a line number doesn't match any known tests. # Returns true to signal that running should stop. def handle_missing_tests? tests _tests = tests.values.flatten not_found = by_line .flat_map { |f, ls| ls.map { |l| [f, l] } } .reject { |f, l| _tests.any? { |t| t.path == f and t.include? l } } unless not_found.empty? then by_path = all_tests.values.flatten.group_by(&:path) puts puts "ERROR: test(s) not found at:" not_found.each do |f, l| puts " %s:%s" % [f, l] puts puts "Did you mean?" puts l = l.begin if l.is_a? Range by_path[f] .sort_by { |m| (m.line_s - l).abs } .first(2) .each do |m| puts " %-30s (dist=%+d) (%s)" % [m, m.line_s - l, m.name] end puts end true end end ## # Simple TestMethod (abbr TM) Data object. TM = Data.define :klass, :name, :path, :lines do def initialize klass:, name: method = klass.instance_method name path, line_s = method.source_location path = path.delete_prefix "#{Dir.pwd}/" line_e = line_s + TM.source_for(method).lines.size - 1 lines = line_s..line_e super klass:, name:, path:, lines: end def self.source_for method path, line = method.source_location file = cache[path] ||= File.readlines(path) ruby = +"" file[line-1..].each do |l| ruby << l return ruby if Prism.parse_success? ruby end nil end def self.cache = @cache ||= {} def include?(o) = o.is_a?(Integer) ? lines.include?(o) : lines.overlap?(o) def to_s = "%s:%d-%d" % [path, lines.begin, lines.end] def line_s = lines.begin
Source
# File lib/minitest/path_expander.rb, line 160 def process(&b) return enum_for(:process) unless block_given? pre_process files, flags = process_args args.replace process_flags flags files.uniq.each(&b) post_process self end
Top-level method processes args. If no block is given, immediately returns with an Enumerator for further chaining.
Otherwise, it calls pre_process, process_args and process_flags, enumerates over the files, and then calls post_process, returning self for any further chaining.
Most of the time, you’re going to provide a block to process files and do nothing more with the result. Eg:
PathExpander.new(ARGV).process do |f| puts "./#{f}" end
or:
PathExpander.new(ARGV).process # => Enumerator
Source
# File lib/minitest/path_expander.rb, line 93 def process_args pos_files = [] neg_files = [] flags = [] clean = true args.each do |arg| case arg when /^@(.*)/ then # push back on, so they can have dirs/-/@ as well clean = false args.concat process_file $1 when "-" then pos_files << arg when /^-(.*)/ then if File.exist? $1 then clean = false neg_files += expand_dirs_to_files($1) else flags << arg end else root_path = File.expand_path(arg) == "/" # eg: -n /./ if File.exist? arg and not root_path then clean = false pos_files += expand_dirs_to_files(arg) else flags << arg end end end files = pos_files - neg_files files += expand_dirs_to_files(self.path) if files.empty? && clean [files, flags] end
Enumerate over args passed to PathExpander and return a list of files and flags to process. Arguments are processed as:
- @file_of_args
-
Read the file and append to args.
- -file_path
-
Subtract path from file to be processed.
- -dir_path
-
Expand and subtract paths from files to be processed.
- -not_a_path
-
Add to flags to be processed.
- dir_path
-
Expand and add to files to be processed.
- file_path
-
Add to files to be processed.
-
Add “-” (stdin) to files to be processed.
-
See expand_dirs_to_files for details on how expansion occurs.
Subtraction happens last, regardless of argument ordering.
If no files are found (which is not the same as having an empty file list after subtraction), then fall back to expanding on the default path given to initialize.
Source
# File lib/minitest/path_expander.rb, line 69 def process_file path File.readlines(path).map(&:chomp) end
Process a file into more arguments. Override this to add additional capabilities.
Source
# File lib/minitest/path_expander.rb, line 137 def process_flags flags flags end
Process over flags and treat any special ones here. Returns an array of the flags you haven’t processed.
This version does nothing. Subclass and override for customization.