specs_for 0.2.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +8 -0
- data/bin/specs-for +1 -0
- data/bin/specs_for +7 -0
- data/lib/specs_for/finder.rb +81 -0
- data/lib/specs_for/runner.rb +109 -0
- data/lib/specs_for/version.rb +9 -0
- data/lib/specs_for.rb +302 -0
- data/sig/specs_for.rbs +31 -0
- metadata +213 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 49cbe4e021b76ac0fcf642d68e4047a591c1618b47e97360822d7a7fc8f691c0
|
|
4
|
+
data.tar.gz: e82d6767e86e83dc4e2b97a9de51121e497a64cf70744dad1e4ea4c256083391
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 54b8e0df1127883a36f8b87c5ec57aa60e86a290f08fbddb413cf7061fd00a8ebffc9f090a2600a440afadf957f2bc5b30f38ba865e59105bf0211ee83d266be
|
|
7
|
+
data.tar.gz: 4e1409fce1fceba6b3763cce5c824c0fc298bf64cf0cae6cd784f27800ea984982f2abd71119db95875a51e914639f5f28ff20939eb42cea92d2a30f94be76c0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2025-05-06
|
|
9
|
+
|
|
10
|
+
### Updates:
|
|
11
|
+
|
|
12
|
+
- improved command parser
|
|
13
|
+
- support -c --changed files
|
|
14
|
+
- add better/more CLI specs
|
|
15
|
+
- add AI context
|
|
16
|
+
|
|
17
|
+
## [1.0.0] - 2026-03-25
|
|
18
|
+
|
|
19
|
+
### Added:
|
|
20
|
+
|
|
21
|
+
- Initial release of the `specs-for` gem.
|
|
22
|
+
- `bin/specs-for` CLI executable.
|
|
23
|
+
- `SpecsFor::Finder` — maps source files to their spec file counterparts.
|
|
24
|
+
- `SpecsFor::Runner` — parses CLI options, discovers git-changed files when no
|
|
25
|
+
explicit arguments are given, and executes RSpec.
|
|
26
|
+
- RSpec test suite for `Finder` and `Runner`.
|
|
27
|
+
- GitHub Actions CI workflow (runs RSpec on every push/PR to `main`).
|
|
28
|
+
- GitHub Actions Release workflow (runs tests then publishes gem on version tag push).
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file gives repository-specific guidance for working on `specs-for`.
|
|
4
|
+
|
|
5
|
+
## Project Purpose
|
|
6
|
+
|
|
7
|
+
`specs-for` is a Ruby gem that selects and runs RSpec files for provided input paths.
|
|
8
|
+
|
|
9
|
+
Current behavior highlights:
|
|
10
|
+
- Accepts one or more explicit file/directory arguments.
|
|
11
|
+
- Supports `-` to read filenames from `STDIN`.
|
|
12
|
+
- Supports `-c/--changed` to use files from `git status -s`.
|
|
13
|
+
- Uses Git ignore rules when handling changed files (`git check-ignore`), so ignored files (for example `vendor/`) are not considered.
|
|
14
|
+
- Does not run `rspec` when there is no effective input.
|
|
15
|
+
|
|
16
|
+
## Useful Commands
|
|
17
|
+
|
|
18
|
+
Setup:
|
|
19
|
+
- `bin/setup`
|
|
20
|
+
|
|
21
|
+
Run specs:
|
|
22
|
+
- `bundle exec rspec`
|
|
23
|
+
- `bundle exec rspec spec/specs_for_spec.rb`
|
|
24
|
+
- `bundle exec rake`
|
|
25
|
+
|
|
26
|
+
Run the CLI:
|
|
27
|
+
- `bin/specs-for lib/specs_for.rb`
|
|
28
|
+
- `git diff --name-only | bin/specs-for -`
|
|
29
|
+
- `bin/specs-for -c` # only run specs on changed files
|
|
30
|
+
|
|
31
|
+
## Repository Layout
|
|
32
|
+
|
|
33
|
+
- `lib/specs_for.rb` - main CLI implementation
|
|
34
|
+
- `lib/specs_for/version.rb` - gem version
|
|
35
|
+
- `bin/specs-for` and `bin/specs_for` - executable entrypoints
|
|
36
|
+
- `spec/specs_for_spec.rb` - primary behavior specs
|
|
37
|
+
- `spec/spec_helper.rb` - RSpec configuration
|
|
38
|
+
- `specs_for.gemspec` - gemspec
|
|
39
|
+
|
|
40
|
+
## Coding Conventions For This Repo
|
|
41
|
+
|
|
42
|
+
- Prefer small, focused changes and keep behavior backwards-compatible unless explicitly changing semantics.
|
|
43
|
+
- Add or update specs for any behavior change in `lib/specs_for.rb`.
|
|
44
|
+
- Keep CLI error paths explicit and testable (`SystemExit` with expected status).
|
|
45
|
+
- Prefer Git as the source of truth for ignore behavior rather than re-implementing `.gitignore` parsing.
|
|
46
|
+
- Keep output noise low in tests by stubbing `warn` when asserting error exits.
|
|
47
|
+
|
|
48
|
+
## Notes For AI Assistants
|
|
49
|
+
|
|
50
|
+
- This is a gem project, not a Rails app; avoid Rails-specific conventions unless intentionally introduced.
|
|
51
|
+
- Respect existing command style (`bundle exec ...`, `bin/...` wrappers).
|
|
52
|
+
- Do not assume optional tooling (`ag`, `pry`) is available; code already handles fallbacks.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan Stebbens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SpecsFor
|
|
2
|
+
|
|
3
|
+
This ruby gem is a CLI (Command Line Interface) tool for developers to easily
|
|
4
|
+
find and run `rspec` on the named files or discovered changed files.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bundle add 'specs_for'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If bundler is not being used to manage dependencies, install the gem by
|
|
15
|
+
executing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install specs_for
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Command line
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Run specs for specific files
|
|
27
|
+
specs-for lib/my_class.rb app/models/user.rb
|
|
28
|
+
|
|
29
|
+
# Run specs for all git-changed files
|
|
30
|
+
specs-for -c
|
|
31
|
+
|
|
32
|
+
# Run specs for changed files with an RSpec tag
|
|
33
|
+
specs-for -c -I # --tag integration
|
|
34
|
+
specs-for -c -T smoke # --tag smoke
|
|
35
|
+
|
|
36
|
+
# Read filenames from stdin
|
|
37
|
+
git diff --name-only HEAD~1 | specs-for -
|
|
38
|
+
|
|
39
|
+
# Show which spec files would run, without running them
|
|
40
|
+
specs-for -s lib/my_class.rb
|
|
41
|
+
|
|
42
|
+
# Dry-run: print the rspec command without executing it
|
|
43
|
+
specs-for -n lib/my_class.rb
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Path mapping
|
|
47
|
+
|
|
48
|
+
Source files are mapped to spec files by replacing the first `app/` or `lib/`
|
|
49
|
+
segment with `spec/` and appending `_spec` before `.rb`. Component monorepo
|
|
50
|
+
paths preserve the component prefix.
|
|
51
|
+
|
|
52
|
+
| Source file | Spec file |
|
|
53
|
+
| ------------------------------ | ------------------------------------ |
|
|
54
|
+
| `lib/foo/bar.rb` | `spec/foo/bar_spec.rb` |
|
|
55
|
+
| `app/models/user.rb` | `spec/models/user_spec.rb` |
|
|
56
|
+
| `components/mycomp/lib/foo.rb` | `components/mycomp/spec/foo_spec.rb` |
|
|
57
|
+
|
|
58
|
+
Spec files passed as arguments are forwarded to rspec unchanged.
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
| Flag | Long form | Description |
|
|
63
|
+
| --------- | ---------------------- | --------------------------------------- |
|
|
64
|
+
| `-c` | `--changed` | Use git-changed files (`git status -s`) |
|
|
65
|
+
| `-e` | `--exact` | Match FILE exactly (no fuzzy search) |
|
|
66
|
+
| `-I` | `--integration` | Add `--tag integration` |
|
|
67
|
+
| `-M` | `--manual-integration` | Add `--tag manual_integration` |
|
|
68
|
+
| `-T NAME` | `--tag NAME` | Add `--tag NAME` |
|
|
69
|
+
| `-n` | `--norun` | Print command without running |
|
|
70
|
+
| `-s` | `--show` | Print spec paths without running rspec |
|
|
71
|
+
| `-v` | `--verbose` | Verbose output |
|
|
72
|
+
| `-` | | Read filenames from STDIN |
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
77
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
|
78
|
+
prompt that will allow you to experiment.
|
|
79
|
+
|
|
80
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
81
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
82
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
83
|
+
git commits and the created tag, and push the `.gem` file to
|
|
84
|
+
[rubygems.org](https://rubygems.org).
|
|
85
|
+
|
|
86
|
+
## Contributing
|
|
87
|
+
|
|
88
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
89
|
+
https://github.com/aks/specs_for.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
The gem is available as open source under the terms of the [MIT
|
|
94
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/specs-for
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
specs_for
|
data/bin/specs_for
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SpecsFor
|
|
4
|
+
# Finder maps source files to their corresponding spec files.
|
|
5
|
+
class Finder
|
|
6
|
+
SPEC_SUFFIX = "_spec.rb"
|
|
7
|
+
|
|
8
|
+
# Default search paths for spec files, in priority order.
|
|
9
|
+
SPEC_DIRS = %w[spec test].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :spec_dirs
|
|
12
|
+
|
|
13
|
+
def initialize(spec_dirs: SPEC_DIRS)
|
|
14
|
+
@spec_dirs = Array(spec_dirs)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Given a list of file paths, return the unique set of existing spec files
|
|
18
|
+
# that correspond to those paths.
|
|
19
|
+
#
|
|
20
|
+
# @param files [Array<String>] source (or spec) file paths
|
|
21
|
+
# @return [Array<String>] sorted list of discovered spec file paths
|
|
22
|
+
def find(files)
|
|
23
|
+
files.flat_map { |file| candidates_for(file) }
|
|
24
|
+
.uniq
|
|
25
|
+
.select { |f| File.exist?(f) }
|
|
26
|
+
.sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Compute candidate spec paths for a single file.
|
|
30
|
+
#
|
|
31
|
+
# @param file [String]
|
|
32
|
+
# @return [Array<String>]
|
|
33
|
+
def candidates_for(file)
|
|
34
|
+
return [file] if spec_file?(file)
|
|
35
|
+
|
|
36
|
+
spec_dirs.flat_map { |dir| spec_paths_in(file, dir) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return true when +file+ already looks like a spec file.
|
|
40
|
+
def spec_file?(file)
|
|
41
|
+
file.end_with?(SPEC_SUFFIX)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Build candidate spec paths by replacing a source prefix with +spec_dir+.
|
|
47
|
+
#
|
|
48
|
+
# Examples
|
|
49
|
+
# spec_paths_in("lib/foo/bar.rb", "spec")
|
|
50
|
+
# #=> ["spec/foo/bar_spec.rb", "spec/lib/foo/bar_spec.rb"]
|
|
51
|
+
def spec_paths_in(file, spec_dir)
|
|
52
|
+
paths = []
|
|
53
|
+
|
|
54
|
+
# Strip a leading source directory component (lib, app, src …) when
|
|
55
|
+
# present, and map into the spec directory.
|
|
56
|
+
parts = file.split(File::SEPARATOR)
|
|
57
|
+
if parts.length > 1 && source_dir?(parts.first)
|
|
58
|
+
inner = File.join(parts[1..])
|
|
59
|
+
paths << File.join(spec_dir, spec_name(inner))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Always add a flat mapping: spec_dir/<basename>_spec.rb
|
|
63
|
+
paths << File.join(spec_dir, spec_name(File.basename(file)))
|
|
64
|
+
|
|
65
|
+
# And a full-path mapping keeping all path components.
|
|
66
|
+
paths << File.join(spec_dir, spec_name(file)) unless file.start_with?(spec_dir)
|
|
67
|
+
|
|
68
|
+
paths.uniq
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert a plain Ruby filename to its _spec.rb counterpart.
|
|
72
|
+
def spec_name(filename)
|
|
73
|
+
filename.sub(/\.rb\z/, SPEC_SUFFIX)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Heuristic: treat common source directory names as "source dirs".
|
|
77
|
+
def source_dir?(name)
|
|
78
|
+
%w[lib app src].include?(name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "finder"
|
|
5
|
+
|
|
6
|
+
class SpecsFor
|
|
7
|
+
# Runner parses CLI arguments, discovers spec files, and executes RSpec.
|
|
8
|
+
class Runner
|
|
9
|
+
attr_reader :files, :options
|
|
10
|
+
|
|
11
|
+
DEFAULT_OPTIONS = {
|
|
12
|
+
verbose: false,
|
|
13
|
+
dry_run: false,
|
|
14
|
+
rspec_options: [],
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(argv = ARGV)
|
|
18
|
+
@argv = argv.dup
|
|
19
|
+
@options = DEFAULT_OPTIONS.dup
|
|
20
|
+
@files = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Entry-point: parse args, find specs, run rspec.
|
|
24
|
+
# Returns the exit status (Integer).
|
|
25
|
+
def run
|
|
26
|
+
parse_options
|
|
27
|
+
resolve_files
|
|
28
|
+
spec_files = find_spec_files
|
|
29
|
+
|
|
30
|
+
if spec_files.empty?
|
|
31
|
+
warn "specs-for: no spec files found"
|
|
32
|
+
return 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
execute_rspec(spec_files)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_options
|
|
41
|
+
parser = OptionParser.new do |opts|
|
|
42
|
+
opts.banner = "Usage: specs-for [options] [file ...]"
|
|
43
|
+
|
|
44
|
+
opts.on("-n", "--dry-run", "Print spec files without running RSpec") do
|
|
45
|
+
@options[:dry_run] = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
opts.on("-v", "--verbose", "Show extra output") do
|
|
49
|
+
@options[:verbose] = true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
opts.on("--rspec-opts OPTS", "Extra options forwarded to RSpec") do |o|
|
|
53
|
+
@options[:rspec_options] = o.split
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
|
57
|
+
puts opts
|
|
58
|
+
exit 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.on_tail("--version", "Show version") do
|
|
62
|
+
require_relative "version"
|
|
63
|
+
puts SpecsFor::VERSION
|
|
64
|
+
exit 0
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Separate specs-for flags from positional file arguments.
|
|
69
|
+
@files = parser.parse!(@argv)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# When no explicit files are given, fall back to git-changed files.
|
|
73
|
+
def resolve_files
|
|
74
|
+
return unless @files.empty?
|
|
75
|
+
|
|
76
|
+
@files = git_changed_files
|
|
77
|
+
if @options[:verbose]
|
|
78
|
+
puts "specs-for: discovered #{@files.length} changed file(s) from git"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_spec_files
|
|
83
|
+
finder = Finder.new
|
|
84
|
+
specs = finder.find(@files)
|
|
85
|
+
if @options[:verbose]
|
|
86
|
+
puts "specs-for: found spec files:"
|
|
87
|
+
specs.each { |s| puts " #{s}" }
|
|
88
|
+
end
|
|
89
|
+
specs
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def execute_rspec(spec_files)
|
|
93
|
+
cmd = ["rspec"] + @options[:rspec_options] + spec_files
|
|
94
|
+
if @options[:dry_run] || @options[:verbose]
|
|
95
|
+
puts "specs-for: #{cmd.join(' ')}"
|
|
96
|
+
end
|
|
97
|
+
return 0 if @options[:dry_run]
|
|
98
|
+
|
|
99
|
+
system(*cmd) ? 0 : 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Return files changed (added, modified) relative to HEAD using git.
|
|
103
|
+
def git_changed_files
|
|
104
|
+
staged = `git diff --name-only --diff-filter=ACM HEAD 2>/dev/null`.split("\n")
|
|
105
|
+
unstaged = `git diff --name-only --diff-filter=ACM 2>/dev/null`.split("\n")
|
|
106
|
+
(staged + unstaged).uniq.select { |f| f.end_with?(".rb") }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/specs_for.rb
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "specs_for/version"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
|
|
8
|
+
# Finds and runs RSpec spec files corresponding to a given set of source files.
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class SpecsFor
|
|
11
|
+
PROG = File.basename($PROGRAM_NAME)
|
|
12
|
+
VALID_TAG = /\A[a-zA-Z_][a-zA-Z0-9_:]*\z/
|
|
13
|
+
|
|
14
|
+
def initialize(files: [], specs: [], opts: nil)
|
|
15
|
+
@file_args = []
|
|
16
|
+
@files = files
|
|
17
|
+
@specs = specs
|
|
18
|
+
@opts = opts
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :file_args, :files, :specs
|
|
22
|
+
|
|
23
|
+
def exact?; opts[:exact]; end
|
|
24
|
+
def norun?; opts[:norun]; end
|
|
25
|
+
def verbose?; opts[:verbose]; end
|
|
26
|
+
def debug?; opts[:debug]; end
|
|
27
|
+
def show?; opts[:show]; end
|
|
28
|
+
def tags; opts[:tags]; end
|
|
29
|
+
def run?; opts[:run]; end
|
|
30
|
+
def changed?; opts[:changed]; end
|
|
31
|
+
def filenames_from_stdin?; opts[:filenames_from_stdin]; end
|
|
32
|
+
|
|
33
|
+
def parse_options(args = ARGV)
|
|
34
|
+
@opts = { tags: [] }
|
|
35
|
+
parser = OptionParser.new do |opt|
|
|
36
|
+
opt.banner = <<~BANNER
|
|
37
|
+
Usage: #{PROG} [options] [- | FILE ...]
|
|
38
|
+
|
|
39
|
+
Finds and runs RSpec specs for the given Ruby source files. Pass '-' to
|
|
40
|
+
read filenames from STDIN. Spec files are passed through unchanged; source
|
|
41
|
+
files are mapped to spec/ paths by replacing app/ or lib/ with spec/.
|
|
42
|
+
|
|
43
|
+
Uses 'ag' for file search when available, otherwise Dir[].
|
|
44
|
+
Prefixes rspec with 'asdf exec' when ruby is managed by asdf.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
#{PROG} lib/foo.rb # run spec for one file
|
|
48
|
+
#{PROG} -c # run specs for git-changed files
|
|
49
|
+
#{PROG} -c -I # same, with --tag integration
|
|
50
|
+
git diff --name-only | #{PROG} - # pipe filenames from stdin
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
BANNER
|
|
54
|
+
opt.on('-h', '--help') { warn opt; exit }
|
|
55
|
+
opt.on('-c', '--changed', 'Use git-changed files')
|
|
56
|
+
opt.on('-d', '--debug', 'Debug mode (opens pry if available)')
|
|
57
|
+
opt.on('-e', '--exact', 'Match FILE exactly')
|
|
58
|
+
opt.on('-I', '--integration', 'Add --tag integration') { add_tag 'integration' }
|
|
59
|
+
opt.on('-M', '--manual-integration', 'Add --tag manual_integration') { add_tag 'manual_integration' }
|
|
60
|
+
opt.on('-n', '--norun', 'Show command, do not execute')
|
|
61
|
+
opt.on('-r', '--run', 'Force run rspec (default unless -n or -s)')
|
|
62
|
+
opt.on('-s', '--show', 'Print spec paths, do not run rspec')
|
|
63
|
+
opt.on('-TNAME', '--tag NAME', 'Add --tag NAME') { |name| add_tag name }
|
|
64
|
+
opt.on('-v', '--verbose', 'Be verbose')
|
|
65
|
+
end
|
|
66
|
+
parser.parse!(args, into: @opts)
|
|
67
|
+
@opts[:filenames_from_stdin] = true if args.delete('-')
|
|
68
|
+
@file_args = args
|
|
69
|
+
if file_args.empty? && !filenames_from_stdin? && !changed?
|
|
70
|
+
warn "Error: at least one FILE argument is required. Use '-' to read filenames from STDIN."
|
|
71
|
+
warn parser
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
@opts[:run] = true if !@opts[:norun] && !@opts[:show] && @opts[:run].nil?
|
|
75
|
+
if @opts[:debug]
|
|
76
|
+
begin
|
|
77
|
+
require 'pry'
|
|
78
|
+
rescue LoadError
|
|
79
|
+
warn "debug: pry not available, continuing without it"
|
|
80
|
+
@opts[:debug] = false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def opts
|
|
86
|
+
parse_options unless @opts
|
|
87
|
+
@opts
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run(args = ARGV)
|
|
91
|
+
parse_options(args)
|
|
92
|
+
collect_file_names
|
|
93
|
+
if file_args.empty?
|
|
94
|
+
return if changed?
|
|
95
|
+
warn "Error: no input filenames provided. Pass FILE args or pipe at least one filename to '-'."
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
collect_files
|
|
99
|
+
collect_specs
|
|
100
|
+
run? ? run_specs : show_specs
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parses a `git status -s` output block and returns the relevant filenames.
|
|
104
|
+
def filter_git_status(status_output)
|
|
105
|
+
status_output.each_line(chomp: true).filter_map do |line|
|
|
106
|
+
# git status -s uses a fixed two-char XY status field followed by a space
|
|
107
|
+
status = line[0, 2].strip
|
|
108
|
+
rest = line[3..]
|
|
109
|
+
next if rest.nil? || rest.empty?
|
|
110
|
+
|
|
111
|
+
case status
|
|
112
|
+
when 'M', 'A', '??' then rest
|
|
113
|
+
when 'R' then rest.split(' -> ', 2).last # "old -> new", take new name
|
|
114
|
+
when 'D' then nil # deleted — skip
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Parses either a quoted path (allows spaces) or an unquoted path from the
|
|
120
|
+
# start of +str+, returning [filename, remainder] or nil if nothing matched.
|
|
121
|
+
def extract_filename(str)
|
|
122
|
+
match = str.match(/^\s*(?:"([^"]*)"|(\S+))\s*(.*)$/) if str
|
|
123
|
+
[match[1] || match[2], match[3]] if match
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Converts a source file path to its spec path and appends it to @specs if
|
|
127
|
+
# the spec file exists. Handles app/**/*.rb, lib/**/*.rb, and the component
|
|
128
|
+
# monorepo layout components/NAME/app/**/*.rb / components/NAME/lib/**/*.rb,
|
|
129
|
+
# preserving the component prefix.
|
|
130
|
+
def collect_spec_for(file)
|
|
131
|
+
spec_file = file
|
|
132
|
+
.sub(%r{(?<=/|^)((?:components/\w+/)?)(?:app|lib)/}, '\1spec/')
|
|
133
|
+
.sub(/\.rb$/, '_spec.rb')
|
|
134
|
+
if File.exist?(spec_file)
|
|
135
|
+
@specs << spec_file
|
|
136
|
+
warn "==> Found spec for #{file}" if verbose?
|
|
137
|
+
elsif verbose?
|
|
138
|
+
warn "==> No spec file for #{file}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def collect_specs
|
|
143
|
+
not_found = []
|
|
144
|
+
files.each do |file|
|
|
145
|
+
if file.end_with?('_spec.rb')
|
|
146
|
+
@specs << file
|
|
147
|
+
elsif file.match?(%r{(?:^|/)(?:components/\w+/)?(?:app|lib)/})
|
|
148
|
+
collect_spec_for(file)
|
|
149
|
+
elsif Dir.exist?(file)
|
|
150
|
+
# skip directories
|
|
151
|
+
else
|
|
152
|
+
not_found << file if verbose?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
warn "==> No spec file for:\n #{not_found.join("\n ")}" if verbose? && not_found.any?
|
|
156
|
+
@specs = @specs.uniq.sort
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def tag_opts
|
|
160
|
+
tag_argv.shelljoin.strip
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def use_asdf?
|
|
164
|
+
ruby_path.include?('/.asdf/')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def rspec_command
|
|
168
|
+
argv = ['bundle', 'exec', 'rspec', *tag_argv, *specs]
|
|
169
|
+
argv.unshift('asdf', 'exec') if use_asdf?
|
|
170
|
+
argv
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def add_tag(name)
|
|
176
|
+
unless name.match?(VALID_TAG)
|
|
177
|
+
warn "Invalid tag name ignored: #{name.inspect}"
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
@opts[:tags] << name unless @opts[:tags].include?(name)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def collect_file_names
|
|
184
|
+
file_args.concat(local_changed_filenames) if changed?
|
|
185
|
+
file_args.concat(read_filenames_from_stdin) if filenames_from_stdin?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def collect_files(args = file_args)
|
|
189
|
+
args.each do |file_arg|
|
|
190
|
+
if Dir.exist?(file_arg)
|
|
191
|
+
collect_files(Dir.children(file_arg).map { |f| File.join(file_arg, f) })
|
|
192
|
+
elsif File.exist?(file_arg)
|
|
193
|
+
@files << file_arg
|
|
194
|
+
else
|
|
195
|
+
found = find_file(file_arg)
|
|
196
|
+
if found.empty?
|
|
197
|
+
warn "==> Could not find: #{file_arg}"
|
|
198
|
+
else
|
|
199
|
+
show_files(found, 'Found file') if verbose?
|
|
200
|
+
@files.concat(found)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def find_file(file)
|
|
207
|
+
ag_present? ? find_file_with_ag(file) : find_file_with_dir(file)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def ag_present?
|
|
211
|
+
return @ag_present unless @ag_present.nil?
|
|
212
|
+
@ag_present = system('which', 'ag', out: IO::NULL, err: IO::NULL)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def build_file_pattern(file, mode:)
|
|
216
|
+
dir = File.dirname(file)
|
|
217
|
+
base = File.basename(file)
|
|
218
|
+
ext = File.extname(base)
|
|
219
|
+
plain = file.count('/').zero?
|
|
220
|
+
|
|
221
|
+
pat =
|
|
222
|
+
if plain && exact?
|
|
223
|
+
mode == :regex ? base : "**/#{base}"
|
|
224
|
+
elsif plain
|
|
225
|
+
mode == :regex ? ".*#{base}" : "**/*#{base}"
|
|
226
|
+
elsif exact?
|
|
227
|
+
mode == :regex ? "(?:^|/)#{dir}/#{base}$" : "**/#{dir}/#{base}"
|
|
228
|
+
else
|
|
229
|
+
mode == :regex ? ".*#{dir}/.*#{base}" : "**/#{dir}/*#{base}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
return pat if ext
|
|
233
|
+
|
|
234
|
+
pat + (exact? ? (mode == :regex ? '\\.rb$' : '.rb')
|
|
235
|
+
: (mode == :regex ? '*\\.rb$' : '*.rb'))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def find_file_with_ag(file)
|
|
239
|
+
binding.pry if debug? # rubocop:disable Lint/Debugger
|
|
240
|
+
stdout, = Open3.capture2('ag', '-S', '-g', build_file_pattern(file, mode: :regex))
|
|
241
|
+
stdout.split("\n")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def find_file_with_dir(file)
|
|
245
|
+
binding.pry if debug? # rubocop:disable Lint/Debugger
|
|
246
|
+
Dir[build_file_pattern(file, mode: :glob)]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def show_files(files, label = nil)
|
|
250
|
+
if files.size == 1
|
|
251
|
+
warn "#{label} #{files.first}"
|
|
252
|
+
else
|
|
253
|
+
warn "#{label}s:"
|
|
254
|
+
warn " #{files.sort.join("\n ")}"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def show_specs
|
|
259
|
+
puts specs.join("\n")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def run_specs
|
|
263
|
+
argv = rspec_command
|
|
264
|
+
if norun?
|
|
265
|
+
warn "(norun) #{argv.shelljoin}"
|
|
266
|
+
else
|
|
267
|
+
warn "--> #{argv.shelljoin}"
|
|
268
|
+
system(*argv)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def tag_argv
|
|
273
|
+
tags.compact.flat_map { |tag| ['--tag', tag] }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def ruby_path
|
|
277
|
+
@ruby_path ||= Open3.capture2('which', 'ruby').first.chomp
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def local_changed_filenames
|
|
281
|
+
stdout, = Open3.capture2('git', 'status', '-s')
|
|
282
|
+
filter_git_status(stdout).reject { |file| ignored_by_git?(file) }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def ignored_by_git?(file)
|
|
286
|
+
system('git', 'check-ignore', '-q', '--', file, out: IO::NULL, err: IO::NULL)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def read_filenames_from_stdin
|
|
290
|
+
$stdin.each_line.flat_map do |line|
|
|
291
|
+
filenames = []
|
|
292
|
+
loop do
|
|
293
|
+
result = extract_filename(line)
|
|
294
|
+
break unless result
|
|
295
|
+
filename, line = result
|
|
296
|
+
filenames << filename
|
|
297
|
+
end
|
|
298
|
+
filenames
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
# rubocop:enable Metrics/ClassLength
|
data/sig/specs_for.rbs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class SpecsFor
|
|
2
|
+
VERSION: String
|
|
3
|
+
VALID_TAG: Regexp
|
|
4
|
+
|
|
5
|
+
attr_reader file_args: Array[String]
|
|
6
|
+
attr_reader files: Array[String]
|
|
7
|
+
attr_reader specs: Array[String]
|
|
8
|
+
|
|
9
|
+
def initialize: (?files: Array[String], ?specs: Array[String], ?opts: Hash[Symbol, untyped]?) -> void
|
|
10
|
+
def run: (?Array[String] args) -> void
|
|
11
|
+
def parse_options: (?Array[String] args) -> void
|
|
12
|
+
def opts: () -> Hash[Symbol, untyped]
|
|
13
|
+
|
|
14
|
+
def exact?: () -> bool?
|
|
15
|
+
def norun?: () -> bool?
|
|
16
|
+
def verbose?: () -> bool?
|
|
17
|
+
def debug?: () -> bool?
|
|
18
|
+
def show?: () -> bool?
|
|
19
|
+
def tags: () -> Array[String]
|
|
20
|
+
def run?: () -> bool?
|
|
21
|
+
def changed?: () -> bool?
|
|
22
|
+
def filenames_from_stdin?: () -> bool?
|
|
23
|
+
|
|
24
|
+
def filter_git_status: (String status_output) -> Array[String]
|
|
25
|
+
def extract_filename: (String? str) -> [String, String]?
|
|
26
|
+
def collect_spec_for: (String file) -> void
|
|
27
|
+
def collect_specs: () -> void
|
|
28
|
+
def tag_opts: () -> String
|
|
29
|
+
def use_asdf?: () -> bool
|
|
30
|
+
def rspec_command: () -> Array[String]
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: specs_for
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alan Stebbens
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 4.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 4.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: json
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.19.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.19.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: psych
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 5.3.1
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 5.3.1
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: fuubar
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 2.5.0
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 2.5.0
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: irb
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rake
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 13.0.0
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 13.0.0
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rspec
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 3.0.0
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 3.0.0
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: 1.0.0
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: 1.0.0
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: simplecov
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 0.22.0
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 0.22.0
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: yard
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: 0.9.43
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: 0.9.43
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: yard-rspec
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0.1'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0.1'
|
|
166
|
+
description: A CLI tool that maps Ruby source files to their RSpec counterparts and
|
|
167
|
+
runs them. Supports git-changed files, stdin file lists, component monorepo layouts,
|
|
168
|
+
and RSpec tag filtering.
|
|
169
|
+
email:
|
|
170
|
+
- aks@stebbens.org
|
|
171
|
+
executables:
|
|
172
|
+
- specs-for
|
|
173
|
+
- specs_for
|
|
174
|
+
extensions: []
|
|
175
|
+
extra_rdoc_files: []
|
|
176
|
+
files:
|
|
177
|
+
- CHANGELOG.md
|
|
178
|
+
- CLAUDE.md
|
|
179
|
+
- LICENSE.txt
|
|
180
|
+
- README.md
|
|
181
|
+
- Rakefile
|
|
182
|
+
- bin/specs-for
|
|
183
|
+
- bin/specs_for
|
|
184
|
+
- lib/specs_for.rb
|
|
185
|
+
- lib/specs_for/finder.rb
|
|
186
|
+
- lib/specs_for/runner.rb
|
|
187
|
+
- lib/specs_for/version.rb
|
|
188
|
+
- sig/specs_for.rbs
|
|
189
|
+
homepage: https://github.com/aks/specs-for
|
|
190
|
+
licenses:
|
|
191
|
+
- MIT
|
|
192
|
+
metadata:
|
|
193
|
+
homepage_uri: https://github.com/aks/specs-for
|
|
194
|
+
source_code_uri: https://github.com/aks/specs-for
|
|
195
|
+
changelog_uri: https://github.com/aks/specs-for/blob/main/CHANGELOG.md
|
|
196
|
+
rdoc_options: []
|
|
197
|
+
require_paths:
|
|
198
|
+
- lib
|
|
199
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
200
|
+
requirements:
|
|
201
|
+
- - ">="
|
|
202
|
+
- !ruby/object:Gem::Version
|
|
203
|
+
version: 3.3.10
|
|
204
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - ">="
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
208
|
+
version: '0'
|
|
209
|
+
requirements: []
|
|
210
|
+
rubygems_version: 4.0.11
|
|
211
|
+
specification_version: 4
|
|
212
|
+
summary: Produce and run rspec on a set of given or discovered filenames
|
|
213
|
+
test_files: []
|