Skip to content

Commit e4ff1ea

Browse files
navidemadclaude
andcommitted
feat(ruby): add Ruby on Rails support (rspec, rubocop, rake, bundle)
Unifies 5 competing PRs (rtk-ai#198, rtk-ai#292, rtk-ai#379, rtk-ai#534, rtk-ai#643) into a single coherent implementation. New commands: - rtk rspec: JSON parsing with text fallback (60%+ savings) - rtk rubocop: JSON parsing, group by cop/severity (60%+ savings) - rtk rake test: Minitest state machine parser (85-90% savings) - rtk bundle install: TOML filter, strip Using lines (90%+ savings) Shared infrastructure: ruby_exec(), fallback_tail(), exit_code_from_output(), count_tokens() in utils.rs. Discover/rewrite rules for rspec, rubocop, rake, rails, bundle including bundle exec and bin/ variants. 56 new unit tests + 4 inline TOML tests. All 1035 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e1fc20 commit e4ff1ea

File tree

9 files changed

+2465
-5
lines changed

9 files changed

+2465
-5
lines changed

PULL_REQUEST_DESCRIPTION.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
![Ruby](https://img.shields.io/badge/Ruby-CC342D?logo=ruby&logoColor=white) ![Rails](https://img.shields.io/badge/Rails-D30001?logo=rubyonrails&logoColor=white)
2+
3+
# Summary
4+
5+
Unifies **5 competing PRs** (#198, #292, #379, #534, #643) into a single coherent Ruby on Rails implementation for RTK.
6+
7+
Adds **RSpec**, **RuboCop**, **Minitest** (via rake/rails test), and **Bundler** support with 3 new Rust command modules, 1 TOML filter, shared Ruby infrastructure, and automatic discover/rewrite rules.
8+
9+
Includes **56 unit tests** across the 3 modules and 4 inline TOML tests — all 1035 tests passing.
10+
11+
# New Commands
12+
13+
| Command | Supported Formats | Token Savings | Notes |
14+
| :--- | :--- | :--- | :--- |
15+
| **`rtk rspec`** | JSON, Text fallback | ![60%+ JSON](https://img.shields.io/badge/-60%25%2B_JSON-85E89D) ![30%+ text](https://img.shields.io/badge/-30%25%2B_text-FFD33D) | Injects `--format json` automatically. Falls back to text parsing when user specifies a custom format. |
16+
| **`rtk rubocop`** | JSON, Autocorrect | ![60%+](https://img.shields.io/badge/-60%25%2B-85E89D) | Injects `--format json`, groups offenses by cop name and severity. Skips JSON in autocorrect mode (`-a`, `-A`). |
17+
| **`rtk rake test`** | Text (state machine) | ![85-90%](https://img.shields.io/badge/-85--90%25-85E89D) | Parses Minitest output. Handles both standard and minitest-reporters formats. |
18+
| **`rtk bundle install`** | TOML filter | ![90%+](https://img.shields.io/badge/-90%25%2B-85E89D) | Strips `Using` lines, short-circuits to `ok bundle: complete` on success. |
19+
20+
<details>
21+
<summary><b>Key Design Decisions</b> (click to expand)</summary>
22+
23+
1. **JSON injection for rspec/rubocop** — Injects `--format json` unless user specified `-f`/`--format`/`-fj`/`--format=...`. Detects autocorrect mode (`-a`, `-A`) in rubocop to skip JSON.
24+
2. **Noise stripping in rspec** — Strips Spring preloader, SimpleCov coverage reports, DEPRECATION warnings, `Finished in` timing, and Capybara screenshot details (keeps only path).
25+
3. **3-tier JSON fallback in rspec** — Strip noise, parse JSON, try original, text parser, `fallback_tail()`. Logs serde error on final fallback for debugging.
26+
4. **Safe JSON fallback in rubocop** — JSON parse failure uses `fallback_tail()` instead of feeding JSON through the text parser.
27+
5. **State machine parsers** — Both rspec (text fallback) and minitest use state-machine text parsers for structured extraction.
28+
6. **TOML for bundle**`bundle install/update` has simple output with a `match_output` short-circuit (90%+ savings on success), making it a natural fit for the TOML DSL rather than a full Rust module.
29+
7. **Defensive arithmetic**`saturating_sub` throughout, graceful degradation on parse failure.
30+
8. **Signal-aware exit codes**`exit_code_from_output` returns `128 + signal` on Unix per convention.
31+
32+
</details>
33+
34+
<details>
35+
<summary><b>Shared Infrastructure & Registry</b> (click to expand)</summary>
36+
37+
### Shared Infrastructure (`utils.rs`)
38+
- **`ruby_exec(tool)`** — Auto-detects `bundle exec` when `Gemfile` exists in working directory. Transitive deps like `rake` (pulled in via `rails`) still go through bundler for version isolation.
39+
- **`fallback_tail(output, label, n)`** — Last-resort filter fallback showing final N lines with diagnostic logging.
40+
- **`exit_code_from_output(output, label)`** — Signal-aware exit code extraction: returns `128 + signal` on Unix per convention.
41+
- **`count_tokens(text)`** — Shared test helper for token savings assertions.
42+
43+
### Discover Registry
44+
- Detection patterns for `rspec`, `rubocop`, `rake test`, `rails test`, `bundle install/update` (with `bundle exec` and `bin/` variants)
45+
- Rewrite prefixes cover all common invocation patterns including `bin/rspec`, `bin/rails test`, `bundle exec rake test`
46+
47+
</details>
48+
49+
# Hook Integration
50+
51+
The discover registry now correctly rewrites the following commands:
52+
53+
| Rewritten to | From input |
54+
| :--- | :--- |
55+
| `rtk rspec` | `rspec`, `bundle exec rspec`, `bin/rspec` |
56+
| `rtk rubocop` | `rubocop`, `bundle exec rubocop` |
57+
| `rtk rake test` | `rake test`, `rails test`, `bundle exec rake test`, `bundle exec rails test`, `bin/rails test` |
58+
| `rtk bundle ...` | `bundle install`, `bundle update` |
59+
60+
# How to Test
61+
62+
```bash
63+
# 1. Run unit tests (no Ruby required)
64+
cargo test --all
65+
66+
# 2. Run Ruby-specific tests only
67+
cargo test rspec_cmd # 28 tests
68+
cargo test rubocop_cmd # 18 tests
69+
cargo test rake_cmd # 10 tests
70+
71+
# 3. Build and install locally
72+
cargo install --path .
73+
74+
# 4. Manual testing (requires Ruby/Rails project)
75+
rtk rspec spec/models/
76+
rtk rubocop
77+
rtk rake test
78+
rtk bundle install
79+
```
80+
81+
# Attribution
82+
83+
> [!NOTE]
84+
> This PR unifies 5 competing implementations. Below is what was taken from each and why.
85+
86+
### PR #198 (by @deril) — RSpec only
87+
- **Incorporated**: `#[serde(default)]` on `backtrace` field — reviewer-requested fix for RSpec versions that omit backtrace from JSON
88+
- **Not taken**: Simpler rspec implementation — superseded by #292/#643's more robust version with noise stripping and state-machine text parser
89+
90+
### PR #292 (by @navidemad) — RSpec + RuboCop
91+
- **Incorporated**: **Primary source** for `rspec_cmd.rs`, `rubocop_cmd.rs`, and shared utils (`ruby_exec`, `fallback_tail`, `exit_code_from_output`, `count_tokens`) — the most mature implementations with noise-stripping regex, 3-tier JSON fallback, state-machine text parser, and signal-aware exit codes
92+
- **Not taken**: E2E smoke test script (`test-ruby.sh`) — requires Rails installed on CI
93+
94+
### PR #379 (by @navidemad) — Bundle + Rails (TOML DSL hybrid)
95+
- **Incorporated**: The **TOML filter concept** for `bundle install/update` — simpler than a full Rust module for low-savings commands
96+
- **Not taken**: The 7 Rails TOML filters (db:migrate, generate, etc.) — lower savings (10-40%) and tightly coupled to TOML DSL internals. Also `rails_cmd.rs` routes/other routing — too complex for initial merge
97+
98+
### PR #534 (by @cosgroveb) — RSpec with parser trait
99+
- **Incorporated**: **Improved format flag detection** — handles `-fj`, `-fjson`, `-fdocumentation`, `--format=...` patterns that the other PRs missed, plus 4 dedicated tests
100+
- **Not taken**: `parser` module trait-based architecture (adds indirection without benefit for standalone modules), tempfile `--out` approach (adds complexity and temp file cleanup), `Gemfile.lock` detection (the `Gemfile` check is simpler and covers the same cases)
101+
102+
### PR #643 (by @Maimer) — Most Complete
103+
- **Incorporated**: `rake_cmd.rs` (**unique** to this PR — only implementation of Minitest parsing), discover rules structure (most comprehensive), `bundle-install.toml`. **Preferred as base** when approaches conflicted.
104+
105+
### Summary
106+
107+
| Module | Source PRs | Tests |
108+
| :--- | :--- | :--- |
109+
| `rspec_cmd.rs` | #292/#643 + #534 format detection + #198 serde fix | 28 |
110+
| `rubocop_cmd.rs` | #292/#643 | 18 |
111+
| `rake_cmd.rs` | #643 (unique) | 10 |
112+
| `bundle-install.toml` | #643 + #379 (concept) | 4 |
113+
| `utils.rs` additions | #292/#643 ||
114+
| `discover/rules.rs` | #643 (most complete) ||
115+
116+
Closes #292, #379
117+
118+
Based on work by @deril (#198), @cosgroveb (#534), and @Maimer (#643) — thank you for your contributions.

src/discover/rules.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ pub const PATTERNS: &[&str] = &[
4444
// Go tooling
4545
r"^go\s+(test|build|vet)",
4646
r"^golangci-lint(\s|$)",
47+
// Ruby tooling
48+
r"^bundle\s+(install|update)\b",
49+
r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test",
50+
r"^(?:bundle\s+exec\s+)?rspec(?:\s|$)",
51+
r"^(?:bundle\s+exec\s+)?rubocop(?:\s|$)",
4752
// AWS CLI
4853
r"^aws\s+",
4954
// PostgreSQL
@@ -332,6 +337,45 @@ pub const RULES: &[RtkRule] = &[
332337
subcmd_savings: &[],
333338
subcmd_status: &[],
334339
},
340+
// Ruby tooling
341+
RtkRule {
342+
rtk_cmd: "rtk bundle",
343+
rewrite_prefixes: &["bundle"],
344+
category: "Ruby",
345+
savings_pct: 70.0,
346+
subcmd_savings: &[],
347+
subcmd_status: &[],
348+
},
349+
RtkRule {
350+
rtk_cmd: "rtk rake",
351+
rewrite_prefixes: &[
352+
"bundle exec rails",
353+
"bundle exec rake",
354+
"bin/rails",
355+
"rails",
356+
"rake",
357+
],
358+
category: "Ruby",
359+
savings_pct: 85.0,
360+
subcmd_savings: &[("test", 90.0)],
361+
subcmd_status: &[],
362+
},
363+
RtkRule {
364+
rtk_cmd: "rtk rspec",
365+
rewrite_prefixes: &["bundle exec rspec", "bin/rspec", "rspec"],
366+
category: "Tests",
367+
savings_pct: 65.0,
368+
subcmd_savings: &[],
369+
subcmd_status: &[],
370+
},
371+
RtkRule {
372+
rtk_cmd: "rtk rubocop",
373+
rewrite_prefixes: &["bundle exec rubocop", "rubocop"],
374+
category: "Build",
375+
savings_pct: 65.0,
376+
subcmd_savings: &[],
377+
subcmd_status: &[],
378+
},
335379
// AWS CLI
336380
RtkRule {
337381
rtk_cmd: "rtk aws",

src/filters/bundle-install.toml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
[filters.bundle-install]
2+
description = "Compact bundle install/update — strip 'Using' lines, keep installs and errors"
3+
match_command = "^bundle\\s+(install|update)\\b"
4+
strip_ansi = true
5+
strip_lines_matching = [
6+
"^Using ",
7+
"^\\s*$",
8+
"^Fetching gem metadata",
9+
"^Resolving dependencies",
10+
]
11+
match_output = [
12+
{ pattern = "Bundle complete!", message = "ok bundle: complete" },
13+
{ pattern = "Bundle updated!", message = "ok bundle: updated" },
14+
]
15+
max_lines = 30
16+
17+
[[tests.bundle-install]]
18+
name = "all cached short-circuits"
19+
input = """
20+
Using bundler 2.5.6
21+
Using rake 13.1.0
22+
Using ast 2.4.2
23+
Using base64 0.2.0
24+
Using minitest 5.22.2
25+
Bundle complete! 85 Gemfile dependencies, 200 gems now installed.
26+
Use `bundle info [gemname]` to see where a bundled gem is installed.
27+
"""
28+
expected = "ok bundle: complete"
29+
30+
[[tests.bundle-install]]
31+
name = "mixed install keeps Fetching and Installing lines"
32+
input = """
33+
Fetching gem metadata from https://rubygems.org/.........
34+
Resolving dependencies...
35+
Using rake 13.1.0
36+
Using ast 2.4.2
37+
Fetching rspec 3.13.0
38+
Installing rspec 3.13.0
39+
Using rubocop 1.62.0
40+
Fetching simplecov 0.22.0
41+
Installing simplecov 0.22.0
42+
Bundle complete! 85 Gemfile dependencies, 202 gems now installed.
43+
"""
44+
expected = "ok bundle: complete"
45+
46+
[[tests.bundle-install]]
47+
name = "update output"
48+
input = """
49+
Fetching gem metadata from https://rubygems.org/.........
50+
Resolving dependencies...
51+
Using rake 13.1.0
52+
Fetching rspec 3.14.0 (was 3.13.0)
53+
Installing rspec 3.14.0 (was 3.13.0)
54+
Bundle updated!
55+
"""
56+
expected = "ok bundle: updated"
57+
58+
[[tests.bundle-install]]
59+
name = "empty output"
60+
input = ""
61+
expected = ""

src/main.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ mod prettier_cmd;
4646
mod prisma_cmd;
4747
mod psql_cmd;
4848
mod pytest_cmd;
49+
mod rake_cmd;
4950
mod read;
5051
mod rewrite_cmd;
52+
mod rspec_cmd;
53+
mod rubocop_cmd;
5154
mod ruff_cmd;
5255
mod runner;
5356
mod session_cmd;
@@ -641,6 +644,27 @@ enum Commands {
641644
args: Vec<String>,
642645
},
643646

647+
/// Rake/Rails test with compact Minitest output (Ruby)
648+
Rake {
649+
/// Rake arguments (e.g., test, test TEST=path/to/test.rb)
650+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
651+
args: Vec<String>,
652+
},
653+
654+
/// RuboCop linter with compact output (Ruby)
655+
Rubocop {
656+
/// RuboCop arguments (e.g., --auto-correct, -A)
657+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
658+
args: Vec<String>,
659+
},
660+
661+
/// RSpec test runner with compact output (Rails/Ruby)
662+
Rspec {
663+
/// RSpec arguments (e.g., spec/models, --tag focus)
664+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
665+
args: Vec<String>,
666+
},
667+
644668
/// Pip package manager with compact output (auto-detects uv)
645669
Pip {
646670
/// Pip arguments (e.g., list, outdated, install)
@@ -1986,6 +2010,18 @@ fn main() -> Result<()> {
19862010
mypy_cmd::run(&args, cli.verbose)?;
19872011
}
19882012

2013+
Commands::Rake { args } => {
2014+
rake_cmd::run(&args, cli.verbose)?;
2015+
}
2016+
2017+
Commands::Rubocop { args } => {
2018+
rubocop_cmd::run(&args, cli.verbose)?;
2019+
}
2020+
2021+
Commands::Rspec { args } => {
2022+
rspec_cmd::run(&args, cli.verbose)?;
2023+
}
2024+
19892025
Commands::Pip { args } => {
19902026
pip_cmd::run(&args, cli.verbose)?;
19912027
}
@@ -2245,6 +2281,9 @@ fn is_operational_command(cmd: &Commands) -> bool {
22452281
| Commands::Curl { .. }
22462282
| Commands::Ruff { .. }
22472283
| Commands::Pytest { .. }
2284+
| Commands::Rake { .. }
2285+
| Commands::Rubocop { .. }
2286+
| Commands::Rspec { .. }
22482287
| Commands::Pip { .. }
22492288
| Commands::Go { .. }
22502289
| Commands::GolangciLint { .. }

0 commit comments

Comments
 (0)