Skip to content

Commit d5f4648

Browse files
committed
test(cli): add 113 flightctl integration tests across 7 focused files
Forward-ports the test additions from #71 onto current main. The original PR's branch had drifted ~2 months and would have deleted many recently-added workspace members and the existing cli_depth_tests.rs / depth_tests.rs files, so only the new files and dev-dependency additions are picked up here. New test files: - output_format.rs (13) — JSON/human formatting, overlay commands - version_cmd.rs ( 9) — --version flag, version subcommand - help_completeness.rs (26) — all subcommands listed, leaf flag coverage - profile_cmd.rs (12) — list/validate/apply/export/activate/show - device_cmd.rs (13) — list/info/dump/calibrate/test - diag_cmd.rs (17) — bundle/health/metrics/trace/record/replay/export - error_handling.rs (24) — invalid args, exit codes, no-panic checks Adds assert_cmd + predicates as workspace and flight-cli dev-deps, preserving the existing insta dev-dep that backs src/scripting and src/batch snapshot tests. Annotates the unused parse_json_from helper with #[allow(dead_code)] to silence the dead-code warning. Verified: all 113 new tests pass; build is warning-clean.
1 parent a0acf7b commit d5f4648

10 files changed

Lines changed: 1583 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ cc = "1.2.55"
208208

209209
# Testing and benchmarking utilities
210210
tokio-test = "0.4.5"
211+
assert_cmd = "2.0.16"
212+
predicates = "3.1.3"
211213
chrono = { version = "0.4.43", features = ["serde"] }
212214

213215
# Additional common dependencies

crates/flight-cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ dirs.workspace = true
4343

4444
[dev-dependencies]
4545
insta.workspace = true
46+
assert_cmd.workspace = true
47+
predicates.workspace = true
48+
serde_json.workspace = true
49+
tempfile.workspace = true
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2024 Flight Hub Team
3+
4+
//! Shared test helpers for flight-cli integration tests
5+
6+
use serde_json::Value;
7+
8+
/// Build an `assert_cmd::Command` pointing at the `flightctl` binary.
9+
pub fn cli() -> assert_cmd::Command {
10+
assert_cmd::Command::new(assert_cmd::cargo_bin!("flightctl"))
11+
}
12+
13+
/// Find the first JSON object line in `text` and parse it, or panic.
14+
#[allow(dead_code)]
15+
pub fn parse_json_from(text: &str) -> Value {
16+
text.lines()
17+
.find(|l| l.trim().starts_with('{'))
18+
.and_then(|l| serde_json::from_str(l).ok())
19+
.unwrap_or_else(|| panic!("No valid JSON line found in:\n{}", text))
20+
}
21+
22+
/// Try to find and parse the first JSON object line in `text`.
23+
#[allow(dead_code)]
24+
pub fn try_parse_json_from(text: &str) -> Option<Value> {
25+
text.lines()
26+
.find(|l| l.trim().starts_with('{'))
27+
.and_then(|l| serde_json::from_str(l).ok())
28+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2024 Flight Hub Team
3+
4+
//! Tests for `flightctl devices` subcommands
5+
6+
mod common;
7+
8+
use common::{cli, parse_json_from};
9+
use serde_json::Value;
10+
11+
// ── devices list ──────────────────────────────────────────────────────────
12+
13+
#[test]
14+
fn devices_list_does_not_panic() {
15+
let output = cli().args(["devices", "list"]).output().unwrap();
16+
// Must not panic (exit code 101); both success and failure are acceptable
17+
assert_ne!(output.status.code(), Some(101));
18+
}
19+
20+
#[test]
21+
fn devices_list_json_has_stable_fields() {
22+
let output = cli().args(["--json", "devices", "list"]).output().unwrap();
23+
// Must not panic
24+
assert_ne!(output.status.code(), Some(101));
25+
26+
if output.status.success() {
27+
let stdout = String::from_utf8(output.stdout).unwrap();
28+
let json: Value = parse_json_from(&stdout);
29+
assert_eq!(json["success"], true);
30+
} else {
31+
let stderr = String::from_utf8(output.stderr).unwrap();
32+
let json: Value = parse_json_from(&stderr);
33+
34+
assert_eq!(json["success"], false);
35+
assert!(json["error"].is_string());
36+
assert!(json["error_code"].is_string());
37+
38+
let error_code = json["error_code"].as_str().unwrap();
39+
let valid_codes = [
40+
"CONNECTION_FAILED",
41+
"VERSION_MISMATCH",
42+
"UNSUPPORTED_FEATURE",
43+
"TRANSPORT_ERROR",
44+
"SERIALIZATION_ERROR",
45+
"GRPC_ERROR",
46+
"UNKNOWN_ERROR",
47+
];
48+
assert!(
49+
valid_codes.contains(&error_code),
50+
"error_code '{}' should be a known code",
51+
error_code
52+
);
53+
}
54+
}
55+
56+
#[test]
57+
fn devices_list_with_include_disconnected_flag_accepted() {
58+
let output = cli()
59+
.args(["devices", "list", "--include-disconnected"])
60+
.output()
61+
.unwrap();
62+
// Flag should be accepted; must not panic
63+
assert_ne!(output.status.code(), Some(101));
64+
}
65+
66+
#[test]
67+
fn devices_list_with_filter_types_flag_accepted() {
68+
let output = cli()
69+
.args(["devices", "list", "--filter-types", "joystick,throttle"])
70+
.output()
71+
.unwrap();
72+
// Flag should be accepted; must not panic
73+
assert_ne!(output.status.code(), Some(101));
74+
}
75+
76+
// ── devices info ──────────────────────────────────────────────────────────
77+
78+
#[test]
79+
fn devices_info_requires_device_id() {
80+
cli()
81+
.args(["devices", "info"])
82+
.assert()
83+
.failure()
84+
.stderr(predicates::str::contains("required"));
85+
}
86+
87+
#[test]
88+
fn devices_info_fails_gracefully_without_daemon() {
89+
let output = cli()
90+
.args(["devices", "info", "test-device-123"])
91+
.output()
92+
.unwrap();
93+
assert!(!output.status.success());
94+
assert_ne!(output.status.code(), Some(101));
95+
}
96+
97+
// ── devices dump ──────────────────────────────────────────────────────────
98+
99+
#[test]
100+
fn devices_dump_requires_device_id() {
101+
cli()
102+
.args(["devices", "dump"])
103+
.assert()
104+
.failure()
105+
.stderr(predicates::str::contains("required"));
106+
}
107+
108+
// ── devices calibrate ─────────────────────────────────────────────────────
109+
110+
#[test]
111+
fn devices_calibrate_requires_device_id() {
112+
cli()
113+
.args(["devices", "calibrate"])
114+
.assert()
115+
.failure()
116+
.stderr(predicates::str::contains("required"));
117+
}
118+
119+
#[test]
120+
fn devices_calibrate_fails_gracefully_without_daemon() {
121+
let output = cli()
122+
.args(["devices", "calibrate", "test-device"])
123+
.output()
124+
.unwrap();
125+
assert!(!output.status.success());
126+
assert_ne!(output.status.code(), Some(101));
127+
}
128+
129+
// ── devices test ──────────────────────────────────────────────────────────
130+
131+
#[test]
132+
fn devices_test_requires_device_id() {
133+
cli()
134+
.args(["devices", "test"])
135+
.assert()
136+
.failure()
137+
.stderr(predicates::str::contains("required"));
138+
}
139+
140+
#[test]
141+
fn devices_test_fails_gracefully_without_daemon() {
142+
let output = cli()
143+
.args(["devices", "test", "test-device"])
144+
.output()
145+
.unwrap();
146+
assert!(!output.status.success());
147+
assert_ne!(output.status.code(), Some(101));
148+
}
149+
150+
#[test]
151+
fn devices_test_accepts_interval_and_count_flags() {
152+
let output = cli()
153+
.args([
154+
"devices",
155+
"test",
156+
"test-device",
157+
"--interval-ms",
158+
"50",
159+
"--count",
160+
"10",
161+
])
162+
.output()
163+
.unwrap();
164+
// Fails because no daemon, but flags should be accepted (no parse errors)
165+
assert!(!output.status.success());
166+
assert_ne!(output.status.code(), Some(101));
167+
}

0 commit comments

Comments
 (0)