Skip to content

Commit 1658da7

Browse files
committed
wip
1 parent 2bfe03d commit 1658da7

File tree

4 files changed

+250
-5
lines changed

4 files changed

+250
-5
lines changed

TESTING.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,29 @@ This document describes the test suite for `cargox`, particularly focusing on sa
1010
- `split_spec_with_version` - Verifies parsing of crate names with `@version` syntax
1111
- `split_spec_rejects_empty` - Ensures invalid specs are rejected
1212

13-
### 2. Install Directory Tests
13+
### 2. Argument Parsing Tests
14+
15+
These tests verify that command-line arguments are correctly parsed and that arguments intended for the target binary are not intercepted by cargox.
16+
17+
#### Integration Tests (`tests/help_flag.rs`)
18+
19+
**Critical regression prevention tests** for ensuring `cargox bat --help` shows bat's help, not cargox's help:
20+
21+
- `test_help_flag_passed_to_binary` - **Most critical test**: Verifies that flags like `--help` after the crate spec are passed to the target binary and not intercepted by cargox's argument parser. Ensures we don't regress the fix for this common use case.
22+
23+
- `test_cargox_help_still_works` - Verifies that `cargox --help` (without a crate spec) still shows cargox's own help text.
24+
25+
- `test_cargox_flags_before_crate_spec` - Verifies that flags before the crate spec (like `cargox --force bat`) are correctly parsed by cargox.
26+
27+
- `test_bin_flag_parsing` - Verifies that the `--bin` flag works correctly and that subsequent arguments are passed to the binary.
28+
29+
#### Unit Tests (`src/cli.rs`)
30+
31+
- `parse_args_separates_binary_args_correctly` - Unit test demonstrating the difference between standard clap parsing and custom argument separation
32+
- `parse_args_handles_bin_flag` - Verifies `--bin` flag parsing
33+
- `parse_args_handles_force_flag` - Verifies `-f`/`--force` flag parsing
34+
35+
### 3. Install Directory Tests
1436

1537
These tests verify that `cargox` uses the correct, sandboxed installation directories:
1638

src/cli.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use anyhow::{anyhow, Result};
12
use clap::Parser;
3+
use std::env;
24
use std::ffi::OsString;
35

46
/// Run Cargo binaries on demand, installing them via `cargo-binstall` when missing.
@@ -29,3 +31,110 @@ pub struct Cli {
2931
#[arg(trailing_var_arg = true, value_name = "binary-args")]
3032
pub args: Vec<OsString>,
3133
}
34+
35+
impl Cli {
36+
/// Parse arguments, ensuring that arguments after the crate spec are passed to the binary
37+
/// rather than being intercepted by clap. This allows `cargox bat --help` to show bat's
38+
/// help rather than cargox's help.
39+
pub fn parse_args() -> Result<Self> {
40+
let mut args: Vec<OsString> = env::args_os().collect();
41+
42+
// Skip the program name
43+
if args.is_empty() {
44+
return Err(anyhow!("no program name in arguments"));
45+
}
46+
args.remove(0);
47+
48+
// Find the first positional argument (crate spec) by iterating through args
49+
// and stopping at the first argument that doesn't start with `-` and isn't a value for a flag
50+
let mut crate_spec_idx = None;
51+
let mut i = 0;
52+
let mut skip_next = false;
53+
54+
while i < args.len() {
55+
if skip_next {
56+
skip_next = false;
57+
i += 1;
58+
continue;
59+
}
60+
61+
let arg = args[i].to_string_lossy();
62+
63+
// Check if this is a flag that takes a value
64+
if arg == "--bin" {
65+
skip_next = true;
66+
i += 1;
67+
continue;
68+
}
69+
70+
// If it doesn't start with `-`, it's the crate spec
71+
if !arg.starts_with('-') {
72+
crate_spec_idx = Some(i);
73+
break;
74+
}
75+
76+
i += 1;
77+
}
78+
79+
// If we found a crate spec, split args at that point
80+
let (cargox_args, binary_args) = if let Some(idx) = crate_spec_idx {
81+
let mut cargox_args = args[..idx].to_vec();
82+
// Add the crate spec to cargox args
83+
cargox_args.push(args[idx].clone());
84+
// Everything after crate spec goes to the binary
85+
let binary_args = args[idx + 1..].to_vec();
86+
(cargox_args, binary_args)
87+
} else {
88+
// No crate spec found, let clap handle it (will show help or error)
89+
(args, vec![])
90+
};
91+
92+
// Parse cargox arguments with clap
93+
let mut cli = match Cli::try_parse_from(
94+
std::iter::once(OsString::from("cargox")).chain(cargox_args),
95+
) {
96+
Ok(cli) => cli,
97+
Err(e) => {
98+
// Let clap print the error/help message and exit
99+
e.exit();
100+
}
101+
};
102+
103+
// Set the binary arguments
104+
cli.args = binary_args;
105+
106+
Ok(cli)
107+
}
108+
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
114+
#[test]
115+
fn parse_args_separates_binary_args_correctly() {
116+
// Simulate: cargox bat --help
117+
let result = Cli::try_parse_from(["cargox", "bat", "--help"]);
118+
// This would fail because clap would see --help and try to show cargox's help
119+
// Our custom parse_args should prevent this by separating the args
120+
assert!(result.is_err()); // Without custom parsing, this intercepts --help
121+
122+
// The custom parse needs to be tested differently since it reads from env::args_os()
123+
// We'll add an integration test for this behavior
124+
}
125+
126+
#[test]
127+
fn parse_args_handles_bin_flag() {
128+
let cli = Cli::try_parse_from(["cargox", "--bin", "foo", "mycrate"]).unwrap();
129+
assert_eq!(cli.crate_spec, "mycrate");
130+
assert_eq!(cli.bin, Some("foo".to_string()));
131+
assert_eq!(cli.args.len(), 0);
132+
}
133+
134+
#[test]
135+
fn parse_args_handles_force_flag() {
136+
let cli = Cli::try_parse_from(["cargox", "-f", "mycrate"]).unwrap();
137+
assert_eq!(cli.crate_spec, "mycrate");
138+
assert!(cli.force);
139+
}
140+
}

src/main.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use std::path::PathBuf;
88
use std::process::{ExitStatus, exit};
99

1010
use anyhow::{Context, Result};
11-
use clap::Parser;
1211

1312
use cli::Cli;
1413
use executor::execute_binary;
@@ -24,7 +23,7 @@ fn main() {
2423
}
2524

2625
fn run_application() -> Result<ExitStatus> {
27-
let cli = parse_arguments();
26+
let cli = parse_arguments()?;
2827
let target = parse_target_from_cli(&cli)?;
2928

3029
if should_use_existing_binary(&cli, &target) {
@@ -34,8 +33,8 @@ fn run_application() -> Result<ExitStatus> {
3433
install_and_run_binary(&target, &cli)
3534
}
3635

37-
fn parse_arguments() -> Cli {
38-
Cli::parse()
36+
fn parse_arguments() -> Result<Cli> {
37+
Cli::parse_args()
3938
}
4039

4140
fn parse_target_from_cli(cli: &Cli) -> Result<Target> {

tests/help_flag.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use std::process::Command;
2+
3+
/// This test ensures that flags like --help after the crate spec are passed to the target binary
4+
/// and not intercepted by cargox's own argument parser.
5+
///
6+
/// Regression test for: cargox bat --help should show bat's help, not cargox's help
7+
#[test]
8+
fn test_help_flag_passed_to_binary() {
9+
// Build the binary first
10+
let binary_path = env!("CARGO_BIN_EXE_cargox");
11+
12+
// Run: cargox bat --help
13+
// We can't actually install bat in the test, but we can verify that:
14+
// 1. cargox doesn't show its own help (which would exit with code 0)
15+
// 2. The --help flag gets passed through to the execution logic
16+
//
17+
// Since bat isn't installed, this will fail trying to find/install bat,
18+
// but importantly it won't show cargox's help text
19+
let output = Command::new(binary_path)
20+
.args(["bat", "--help"])
21+
.output()
22+
.expect("Failed to execute cargox");
23+
24+
let stderr = String::from_utf8_lossy(&output.stderr);
25+
let stdout = String::from_utf8_lossy(&output.stdout);
26+
27+
// The key assertion: cargox's help should NOT appear
28+
// If the help was intercepted, we'd see "Run Cargo binaries on demand" in the output
29+
assert!(
30+
!stdout.contains("Run Cargo binaries on demand"),
31+
"cargox help text appeared in stdout, indicating --help was intercepted.\nStdout:\n{}",
32+
stdout
33+
);
34+
assert!(
35+
!stderr.contains("Run Cargo binaries on demand"),
36+
"cargox help text appeared in stderr, indicating --help was intercepted.\nStderr:\n{}",
37+
stderr
38+
);
39+
40+
// We should see some error about bat not being found/installed
41+
// or if it's already on PATH, it would run bat's help
42+
// Either way, we shouldn't see cargox's help
43+
}
44+
45+
/// Test that cargox's own help still works when invoked without a crate spec
46+
#[test]
47+
fn test_cargox_help_still_works() {
48+
let binary_path = env!("CARGO_BIN_EXE_cargox");
49+
50+
let output = Command::new(binary_path)
51+
.args(["--help"])
52+
.output()
53+
.expect("Failed to execute cargox");
54+
55+
let stdout = String::from_utf8_lossy(&output.stdout);
56+
57+
// When --help comes before the crate spec, it should show cargox's help
58+
assert!(
59+
stdout.contains("Run Cargo binaries on demand"),
60+
"cargox help should appear when --help is used without a crate spec.\nStdout:\n{}",
61+
stdout
62+
);
63+
assert!(output.status.success(), "cargox --help should exit successfully");
64+
}
65+
66+
/// Test that flags before the crate spec are still parsed by cargox
67+
#[test]
68+
fn test_cargox_flags_before_crate_spec() {
69+
let binary_path = env!("CARGO_BIN_EXE_cargox");
70+
71+
// Run: cargox --force bat
72+
// The --force flag should be recognized by cargox
73+
let output = Command::new(binary_path)
74+
.args(["--force", "bat"])
75+
.output()
76+
.expect("Failed to execute cargox");
77+
78+
let stderr = String::from_utf8_lossy(&output.stderr);
79+
80+
// If --force was parsed correctly, cargox would proceed with installation
81+
// We should not see cargox's help text
82+
assert!(
83+
!stderr.contains("Run Cargo binaries on demand"),
84+
"cargox help text appeared, indicating argument parsing failed.\nStderr:\n{}",
85+
stderr
86+
);
87+
}
88+
89+
/// Test that --bin flag works correctly
90+
#[test]
91+
fn test_bin_flag_parsing() {
92+
let binary_path = env!("CARGO_BIN_EXE_cargox");
93+
94+
// Run: cargox --bin custom mycrate --help
95+
// The --help should be passed to the binary, not cargox
96+
let output = Command::new(binary_path)
97+
.args(["--bin", "custom", "mycrate", "--help"])
98+
.output()
99+
.expect("Failed to execute cargox");
100+
101+
let stderr = String::from_utf8_lossy(&output.stderr);
102+
let stdout = String::from_utf8_lossy(&output.stdout);
103+
104+
// Should not show cargox help
105+
assert!(
106+
!stdout.contains("Run Cargo binaries on demand"),
107+
"cargox help appeared when it shouldn't.\nStdout:\n{}",
108+
stdout
109+
);
110+
assert!(
111+
!stderr.contains("Run Cargo binaries on demand"),
112+
"cargox help appeared when it shouldn't.\nStderr:\n{}",
113+
stderr
114+
);
115+
}

0 commit comments

Comments
 (0)