1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
84
85
86
87
88
//! Process-level test: invalid args that survive clap's own parsing but fail
//! `Config::try_from` must be re-raised through clap's error machinery so the
//! user sees the validation message on stderr and the process exits non-zero.
//!
//! Covers `src/bin/s3util/main.rs` lines 25-30 — the `Err(error_message)` arm
//! of `match Config::try_from(cp_args)`.
//!
//! Doesn't require AWS: the error fires before any S3 call.
use std::process::{Command, Stdio};
#[test]
fn both_local_paths_exit_non_zero_with_validation_message_on_stderr() {
let bin = env!("CARGO_BIN_EXE_s3util");
// Two local paths are valid per clap's per-arg value_parser (check_storage_path)
// but rejected by Config::try_from's check_both_local guard. That error is
// re-wrapped as clap::ErrorKind::ValueValidation and printed by .exit().
let output = Command::new(bin)
.args(["cp", "/tmp/s3util_e2e_src", "/tmp/s3util_e2e_dst"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn s3util binary");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!output.status.success(),
"both-local-paths invocation must exit non-zero.\n\
status: {:?}\n--- stderr ---\n{stderr}\n--- stdout ---\n{stdout}",
output.status.code()
);
assert!(
stderr.contains("source and target cannot both be local paths"),
"expected the check_both_local validation message on stderr.\n\
--- stderr ---\n{stderr}"
);
}
#[test]
fn source_no_sign_request_env_var_triggers_conflict_at_parse_time() {
// Regression guard for the `env` attribute on `--source-no-sign-request`.
//
// Rather than mutate the test process's env (which races with parallel
// tests that parse CpArgs), we isolate the env var to a child `s3util`
// invocation. If clap reads SOURCE_NO_SIGN_REQUEST, combining it with
// --source-profile will trip the `conflicts_with_all` at parse time and
// the command exits non-zero with "cannot be used with" on stderr.
// If clap ever silently drops the env binding, --source-profile alone
// would be accepted and the command would proceed — a regression we
// want to catch.
let bin = env!("CARGO_BIN_EXE_s3util");
let output = Command::new(bin)
.args([
"cp",
"s3://b/k",
"/tmp/out",
"--source-profile",
"myprofile",
])
.env("SOURCE_NO_SIGN_REQUEST", "true")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn s3util binary");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!output.status.success(),
"SOURCE_NO_SIGN_REQUEST + --source-profile must exit non-zero.\n\
status: {:?}\n--- stderr ---\n{stderr}\n--- stdout ---\n{stdout}",
output.status.code()
);
assert!(
stderr.contains("cannot be used with"),
"expected clap conflict message on stderr.\n\
--- stderr ---\n{stderr}"
);
}