Created
December 5, 2023 20:20
-
-
Save aks/72bd2d28a2dcc0cb386a099ce0b13f0b to your computer and use it in GitHub Desktop.
Ruby script to produce the spec files corresponding to the ruby file arguments.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# specs-for file ... | |
# | |
# Don't you just hate having to find the corresponding spec file for some files | |
# you just updated? And, you're not using Guard (for reasons)? | |
# | |
# pass all the files you touches as arguments into this script, and it will | |
# issue all the corresponding spec files. | |
# | |
# If you have "ag" installed, it will be used to find the files, otherwise, the | |
# Ruby Dir class is used (slightly slower than "ag"). | |
# | |
# Free to use, but don't pretend you wrote it. | |
# Copyright Alan K. Stebbens <[email protected]> | |
PROG = File.basename($0) | |
require 'optparse' | |
require 'pry-byebug' | |
# This class find Specs for given files. | |
# rubocop:disable Metrics/ClassLength | |
class SpecsFor | |
def usage | |
warn <<~USAGE | |
usage: $PROG [-hev] FILE .. | |
Print the specs corresponding to file, if any. FILE may include partial | |
or complete paths to a ruby file. | |
Spec files are simply passed through, while ruby code files are converted | |
into spec paths by converting "app" or "lib" directories into "spec", | |
with corresponding subdirectories. | |
Uses "ag" to find the spec files, if "ag" is installed. Otherwise, uses | |
the ruby Dir class. | |
Options | |
-d debug mode | |
-h show this help | |
-e match FILE exactly | |
-v be verbose | |
USAGE | |
exit | |
end | |
# if FILE has no path at all, | |
# 1. find it in the current directory | |
# 2. find it across the project | |
# 3. report the corresponding spec file, if any | |
attr_reader :opts, :file_args, :files, :specs | |
def exact?; opts[:exact]; end | |
def norun?; opts[:norun]; end | |
def verbose?; opts[:verbose]; end | |
def debug?; opts[:debug]; end | |
def parse_options | |
@opts = {} | |
OptionParser.new do |opt| | |
opt.banner = "Usage: #{PROG} [options] FILE ..." | |
opt.on('-h', '--help') do | |
warn opt | |
exit | |
end | |
opt.on('-d', '--debug') | |
opt.on('-n', '--norun') | |
opt.on('-e', '--exact') | |
opt.on('-v', '--verbose') | |
end.parse!(into: @opts) | |
@file_args = ARGV | |
end | |
def find_files | |
file_args.each do |file_arg| | |
if File.exist?(file_arg) | |
@files << file_arg | |
else | |
found = find_file(file_arg) | |
if found.empty? | |
warn "==> Could not find: #{file_arg}" | |
else | |
show_files(found, 'Found file') if verbose? | |
@files.append(*found) | |
end | |
end | |
end | |
end | |
def find_file(file) | |
if ag_present? | |
find_file_with_ag(file) | |
else | |
find_file_with_dir(file) | |
end | |
end | |
def ag_present? | |
@ag_present ||= !`which ag`.empty? | |
end | |
def find_file_with_ag(file) | |
dir = File.dirname(file) | |
base = File.basename(file) | |
ext = File.extname(base) | |
plain = file.count('/').zero? | |
# if the file is plain, search for it recursively | |
# if the file has a directory, with exact, do not glob the dir path | |
# if exact matching is on, do not glob the filename | |
# if there is no extension, find any .rb extension | |
file_pat = | |
if plain && exact? | |
base | |
elsif plain | |
".*#{base}" | |
elsif exact? | |
"#{dir}/#{base}" | |
else | |
".*#{dir}/.*#{base}" | |
end | |
binding.pry if debug? | |
file_pat += exact? ? '\.rb$' : '*\.rb$' unless ext | |
`ag -S -g "#{file_pat}"`.split("\n") | |
end | |
def find_file_with_dir(file) | |
dir = File.dirname(file) | |
base = File.basename(file) | |
ext = File.extname(base) | |
plain = file.count('/').zero? | |
# if the file is plain, search for it recursively | |
# if the file has a directory, with exact, do not glob the dir path | |
# if exact matching is on, do not glob the filename | |
# if there is no extension, find any .rb extension | |
binding.pry if debug? | |
file_pat = | |
if plain && exact? | |
"**/#{base}" | |
elsif plain | |
"**/*#{base}" | |
elsif exact? | |
"#{dir}/#{base}" | |
else | |
"**/#{dir}/*#{base}" | |
end | |
file_pat += exact? ? '.rb' : '*.rb' unless ext | |
Dir[file_pat] | |
end | |
def show_files(files, what = nil) | |
if files.size == 1 | |
warn "#{what} #{files.first}" | |
else | |
warn "#{what}s:" | |
warn ' ' + files.sort.join("\n ") # rubocop: disable Style/StringConcatenation | |
end | |
end | |
# look for app/**/*.rb or lib/**/*.rb and convert into spec/**/*_spec.rb | |
def find_specs | |
files.each do |file| | |
if file.end_with?('_spec.rb') | |
@specs << file | |
elsif file.match?(%r{(?:^|/)(?<dir>app|lib)/}) | |
find_spec_file(file) | |
elsif verbose? | |
warn "I don't know how to find the spec for #{file}" | |
end | |
end | |
end | |
def find_spec_file(file) | |
spec_file = file.sub(%r{(?<=/|^)(lib|app)/}, 'spec/').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 | |
def show_specs | |
puts specs.join("\n") | |
end | |
def run | |
parse_options | |
find_files | |
find_specs | |
show_specs | |
end | |
def initialize | |
@file_args = [] | |
@files = [] | |
@specs = [] | |
end | |
end | |
# rubocop:enable Metrics/ClassLength | |
SpecsFor.new.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment