Class: SpecsFor
- Inherits:
-
Object
- Object
- SpecsFor
- Defined in:
- lib/specs_for.rb,
lib/specs_for/finder.rb,
lib/specs_for/runner.rb,
lib/specs_for/version.rb
Overview
Finds and runs RSpec spec files corresponding to a given set of source files. rubocop:disable Metrics/ClassLength
Defined Under Namespace
Constant Summary collapse
- PROG =
File.basename($PROGRAM_NAME)
- VALID_TAG =
/\A[a-zA-Z_][a-zA-Z0-9_:]*\z/- RELEASES =
[ ["0.1.0", "2026-03-25", "Initial release"], ["0.2.0", "2026-05-06", "Add support for tags and improved file search"], ["0.2.1", "2026-05-06", "Replace specs-for with specs_for in the url"], ["0.3.0", "2026-05-06", "Make specs-for be independent of bundler"], ["0.3.1", "2026-05-06", "Update README.md with local install info"] ]
- VERSION =
RELEASES.last.first
Instance Attribute Summary collapse
-
#file_args ⇒ Object
readonly
Returns the value of attribute file_args.
-
#files ⇒ Object
readonly
Returns the value of attribute files.
-
#specs ⇒ Object
readonly
Returns the value of attribute specs.
Instance Method Summary collapse
- #changed? ⇒ Boolean
-
#collect_spec_for(file) ⇒ Object
Converts a source file path to its spec path and appends it to @specs if the spec file exists.
- #collect_specs ⇒ Object
- #debug? ⇒ Boolean
- #exact? ⇒ Boolean
-
#extract_filename(str) ⇒ Object
Parses either a quoted path (allows spaces) or an unquoted path from the start of
str, returning [filename, remainder] or nil if nothing matched. - #filenames_from_stdin? ⇒ Boolean
-
#filter_git_status(status_output) ⇒ Object
Parses a ‘git status -s` output block and returns the relevant filenames.
-
#initialize(files: [], specs: [], opts: nil) ⇒ SpecsFor
constructor
A new instance of SpecsFor.
- #norun? ⇒ Boolean
- #opts ⇒ Object
- #parse_options(args = ARGV) ⇒ Object
- #rspec_command ⇒ Object
- #run(args = ARGV) ⇒ Object
- #run? ⇒ Boolean
- #show? ⇒ Boolean
- #tag_opts ⇒ Object
- #tags ⇒ Object
- #use_asdf? ⇒ Boolean
- #verbose? ⇒ Boolean
Constructor Details
#initialize(files: [], specs: [], opts: nil) ⇒ SpecsFor
Returns a new instance of SpecsFor.
14 15 16 17 18 19 |
# File 'lib/specs_for.rb', line 14 def initialize(files: [], specs: [], opts: nil) @file_args = [] @files = files @specs = specs @opts = opts end |
Instance Attribute Details
#file_args ⇒ Object (readonly)
Returns the value of attribute file_args.
21 22 23 |
# File 'lib/specs_for.rb', line 21 def file_args @file_args end |
#files ⇒ Object (readonly)
Returns the value of attribute files.
21 22 23 |
# File 'lib/specs_for.rb', line 21 def files @files end |
#specs ⇒ Object (readonly)
Returns the value of attribute specs.
21 22 23 |
# File 'lib/specs_for.rb', line 21 def specs @specs end |
Instance Method Details
#changed? ⇒ Boolean
30 |
# File 'lib/specs_for.rb', line 30 def changed?; opts[:changed]; end |
#collect_spec_for(file) ⇒ Object
Converts a source file path to its spec path and appends it to @specs if the spec file exists. Handles app/*/.rb, lib/*/.rb, and the component monorepo layout components/NAME/app/*/.rb / components/NAME/lib/*/.rb, preserving the component prefix.
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/specs_for.rb', line 130 def collect_spec_for(file) spec_file = file .sub(%r{(?<=/|^)((?:components/\w+/)?)(?:app|lib)/}, '\1spec/') .sub(/\.rb$/, '_spec.rb') if File.exist?(spec_file) @specs << spec_file warn "==> Found spec for #{file}" if verbose? elsif verbose? warn "==> No spec file for #{file}" end end |
#collect_specs ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/specs_for.rb', line 142 def collect_specs not_found = [] files.each do |file| if file.end_with?('_spec.rb') @specs << file elsif file.match?(%r{(?:^|/)(?:components/\w+/)?(?:app|lib)/}) collect_spec_for(file) elsif Dir.exist?(file) # skip directories else not_found << file if verbose? end end warn "==> No spec file for:\n #{not_found.join("\n ")}" if verbose? && not_found.any? @specs = @specs.uniq.sort end |
#debug? ⇒ Boolean
26 |
# File 'lib/specs_for.rb', line 26 def debug?; opts[:debug]; end |
#exact? ⇒ Boolean
23 |
# File 'lib/specs_for.rb', line 23 def exact?; opts[:exact]; end |
#extract_filename(str) ⇒ Object
Parses either a quoted path (allows spaces) or an unquoted path from the start of str, returning [filename, remainder] or nil if nothing matched.
121 122 123 124 |
# File 'lib/specs_for.rb', line 121 def extract_filename(str) match = str.match(/^\s*(?:"([^"]*)"|(\S+))\s*(.*)$/) if str [match[1] || match[2], match[3]] if match end |
#filenames_from_stdin? ⇒ Boolean
31 |
# File 'lib/specs_for.rb', line 31 def filenames_from_stdin?; opts[:filenames_from_stdin]; end |
#filter_git_status(status_output) ⇒ Object
Parses a ‘git status -s` output block and returns the relevant filenames.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/specs_for.rb', line 104 def filter_git_status(status_output) status_output.each_line(chomp: true).filter_map do |line| # git status -s uses a fixed two-char XY status field followed by a space status = line[0, 2].strip rest = line[3..] next if rest.nil? || rest.empty? case status when 'M', 'A', '??' then rest when 'R' then rest.split(' -> ', 2).last # "old -> new", take new name when 'D' then nil # deleted — skip end end end |
#norun? ⇒ Boolean
24 |
# File 'lib/specs_for.rb', line 24 def norun?; opts[:norun]; end |
#opts ⇒ Object
85 86 87 88 |
# File 'lib/specs_for.rb', line 85 def opts unless @opts @opts end |
#parse_options(args = ARGV) ⇒ Object
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 |
# File 'lib/specs_for.rb', line 33 def (args = ARGV) @opts = { tags: [] } parser = OptionParser.new do |opt| opt. = <<~BANNER Usage: #{PROG} [options] [- | FILE ...] Finds and runs RSpec specs for the given Ruby source files. Pass '-' to read filenames from STDIN. Spec files are passed through unchanged; source files are mapped to spec/ paths by replacing app/ or lib/ with spec/. Uses 'ag' for file search when available, otherwise Dir[]. Prefixes rspec with 'asdf exec' when ruby is managed by asdf. Examples: #{PROG} lib/foo.rb # run spec for one file #{PROG} -c # run specs for git-changed files #{PROG} -c -I # same, with --tag integration git diff --name-only | #{PROG} - # pipe filenames from stdin Options: BANNER opt.on('-h', '--help') { warn opt; exit } opt.on('-c', '--changed', 'Use git-changed files') opt.on('-d', '--debug', 'Debug mode (opens pry if available)') opt.on('-e', '--exact', 'Match FILE exactly') opt.on('-I', '--integration', 'Add --tag integration') { add_tag 'integration' } opt.on('-M', '--manual-integration', 'Add --tag manual_integration') { add_tag 'manual_integration' } opt.on('-n', '--norun', 'Show command, do not execute') opt.on('-r', '--run', 'Force run rspec (default unless -n or -s)') opt.on('-s', '--show', 'Print spec paths, do not run rspec') opt.on('-TNAME', '--tag NAME', 'Add --tag NAME') { |name| add_tag name } opt.on('-v', '--verbose', 'Be verbose') end parser.parse!(args, into: @opts) @opts[:filenames_from_stdin] = true if args.delete('-') @file_args = args if file_args.empty? && !filenames_from_stdin? && !changed? warn "Error: at least one FILE argument is required. Use '-' to read filenames from STDIN." warn parser exit 1 end @opts[:run] = true if !@opts[:norun] && !@opts[:show] && @opts[:run].nil? if @opts[:debug] begin require 'pry' rescue LoadError warn "debug: pry not available, continuing without it" @opts[:debug] = false end end end |
#rspec_command ⇒ Object
167 168 169 170 171 |
# File 'lib/specs_for.rb', line 167 def rspec_command argv = ['bundle', 'exec', 'rspec', *tag_argv, *specs] argv.unshift('asdf', 'exec') if use_asdf? argv end |
#run(args = ARGV) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/specs_for.rb', line 90 def run(args = ARGV) (args) collect_file_names if file_args.empty? return if changed? warn "Error: no input filenames provided. Pass FILE args or pipe at least one filename to '-'." exit 1 end collect_files collect_specs run? ? run_specs : show_specs end |
#run? ⇒ Boolean
29 |
# File 'lib/specs_for.rb', line 29 def run?; opts[:run]; end |
#show? ⇒ Boolean
27 |
# File 'lib/specs_for.rb', line 27 def show?; opts[:show]; end |
#tag_opts ⇒ Object
159 160 161 |
# File 'lib/specs_for.rb', line 159 def tag_opts tag_argv.shelljoin.strip end |
#tags ⇒ Object
28 |
# File 'lib/specs_for.rb', line 28 def ; opts[:tags]; end |
#use_asdf? ⇒ Boolean
163 164 165 |
# File 'lib/specs_for.rb', line 163 def use_asdf? ruby_path.include?('/.asdf/') end |
#verbose? ⇒ Boolean
25 |
# File 'lib/specs_for.rb', line 25 def verbose?; opts[:verbose]; end |