Skip to content

Commit fa82ba0

Browse files
committed
test: add CLI integration tests for find and resolve commands (Fixes #355)
1 parent e364131 commit fa82ba0

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed

crates/pet/tests/cli_test.rs

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! CLI integration tests for the `pet find` and `pet resolve` commands.
5+
//!
6+
//! These tests spawn the pet binary via `std::process::Command` and validate
7+
//! its JSON output. All tests are gated behind the `ci` feature flag since
8+
//! they require a real Python installation on PATH.
9+
10+
use serde_json::Value;
11+
use std::process::Command;
12+
13+
/// Helper to run `pet find --json` with optional extra args and return parsed JSON.
14+
fn run_find_json(extra_args: &[&str]) -> (Value, std::process::Output) {
15+
let mut cmd = Command::new(env!("CARGO_BIN_EXE_pet"));
16+
cmd.arg("find").arg("--json");
17+
for arg in extra_args {
18+
cmd.arg(arg);
19+
}
20+
let output = cmd.output().expect("failed to run pet find");
21+
assert!(
22+
output.status.success(),
23+
"pet find failed with stderr: {}",
24+
String::from_utf8_lossy(&output.stderr)
25+
);
26+
let json: Value =
27+
serde_json::from_slice(&output.stdout).expect("pet find stdout is not valid JSON");
28+
(json, output)
29+
}
30+
31+
/// Test 1: `find --json` produces valid output with `managers` and `environments` arrays.
32+
#[cfg_attr(feature = "ci", test)]
33+
#[allow(dead_code)]
34+
fn find_json_output_is_valid() {
35+
let (json, _) = run_find_json(&[]);
36+
37+
assert!(
38+
json["managers"].is_array(),
39+
"expected 'managers' array in output"
40+
);
41+
assert!(
42+
json["environments"].is_array(),
43+
"expected 'environments' array in output"
44+
);
45+
46+
// Each environment should have at minimum an executable and kind
47+
let environments = json["environments"].as_array().unwrap();
48+
assert!(
49+
!environments.is_empty(),
50+
"expected at least one environment to be discovered"
51+
);
52+
for env in environments {
53+
assert!(
54+
env["executable"].is_string(),
55+
"environment missing 'executable': {env}"
56+
);
57+
assert!(env["kind"].is_string(), "environment missing 'kind': {env}");
58+
}
59+
}
60+
61+
/// Test 2: `resolve --json` returns a resolved environment with a version.
62+
#[cfg_attr(feature = "ci", test)]
63+
#[allow(dead_code)]
64+
fn resolve_json_output_has_version() {
65+
// First, find an environment to resolve
66+
let (found, _) = run_find_json(&[]);
67+
let environments = found["environments"]
68+
.as_array()
69+
.expect("expected environments array");
70+
assert!(
71+
!environments.is_empty(),
72+
"need at least one environment to test resolve"
73+
);
74+
75+
// Pick an environment that has an executable path (skip broken entries)
76+
let exe = environments
77+
.iter()
78+
.find_map(|e| e["executable"].as_str())
79+
.expect("no environment with an executable found");
80+
81+
// Now resolve it
82+
let output = Command::new(env!("CARGO_BIN_EXE_pet"))
83+
.args(["resolve", exe, "--json"])
84+
.output()
85+
.expect("failed to run pet resolve");
86+
87+
assert!(
88+
output.status.success(),
89+
"pet resolve failed with stderr: {}",
90+
String::from_utf8_lossy(&output.stderr)
91+
);
92+
93+
let resolved: Value =
94+
serde_json::from_slice(&output.stdout).expect("pet resolve stdout is not valid JSON");
95+
96+
// resolved should not be null
97+
assert!(!resolved.is_null(), "resolve returned null for {exe}");
98+
99+
// Should have a version
100+
assert!(
101+
resolved["version"].is_string(),
102+
"resolved environment missing 'version' for {exe}: {resolved}"
103+
);
104+
105+
// Executable should match what we passed in (or be a symlink equivalent)
106+
assert!(
107+
resolved["executable"].is_string(),
108+
"resolved environment missing 'executable'"
109+
);
110+
}
111+
112+
/// Convert a PascalCase kind from JSON to the kebab-case format expected by clap's `--kind` flag.
113+
fn to_cli_kind(json_kind: &str) -> String {
114+
let mut result = String::new();
115+
for (i, ch) in json_kind.chars().enumerate() {
116+
if ch.is_uppercase() && i > 0 {
117+
result.push('-');
118+
}
119+
result.push(ch.to_ascii_lowercase());
120+
}
121+
result
122+
}
123+
124+
/// Test 3: `find --kind <kind> --json` filters environments by kind.
125+
#[cfg_attr(feature = "ci", test)]
126+
#[allow(dead_code)]
127+
fn find_kind_filter_works() {
128+
// First find all environments to pick a kind that exists
129+
let (all, _) = run_find_json(&[]);
130+
let environments = all["environments"]
131+
.as_array()
132+
.expect("expected environments array");
133+
assert!(
134+
!environments.is_empty(),
135+
"need at least one environment to test kind filtering"
136+
);
137+
138+
// Pick the kind of the first environment and convert to CLI format
139+
let json_kind = environments[0]["kind"]
140+
.as_str()
141+
.expect("expected kind string");
142+
let cli_kind = to_cli_kind(json_kind);
143+
144+
// Now filter by that kind
145+
let (filtered, _) = run_find_json(&["--kind", &cli_kind]);
146+
let filtered_envs = filtered["environments"]
147+
.as_array()
148+
.expect("expected environments array");
149+
150+
assert!(
151+
!filtered_envs.is_empty(),
152+
"expected at least one environment of kind '{json_kind}'"
153+
);
154+
155+
// All returned environments must match the requested kind
156+
for env in filtered_envs {
157+
assert_eq!(
158+
env["kind"].as_str().unwrap(),
159+
json_kind,
160+
"environment kind mismatch: expected '{json_kind}', got {:?}",
161+
env["kind"]
162+
);
163+
}
164+
165+
// Filtered count should be <= total count
166+
assert!(
167+
filtered_envs.len() <= environments.len(),
168+
"filtered count ({}) should not exceed total count ({})",
169+
filtered_envs.len(),
170+
environments.len()
171+
);
172+
}
173+
174+
/// Test 4: `find --workspace --json` scopes to workspace environments only.
175+
#[cfg_attr(feature = "ci", test)]
176+
#[allow(dead_code)]
177+
fn find_workspace_scoping() {
178+
// Use an empty temp dir as the workspace — should find zero or very few environments
179+
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
180+
let temp_path = temp_dir.path().to_str().expect("temp path not valid UTF-8");
181+
182+
let (json, _) = run_find_json(&["--workspace", temp_path]);
183+
184+
assert!(
185+
json["managers"].is_array(),
186+
"expected 'managers' array in workspace output"
187+
);
188+
assert!(
189+
json["environments"].is_array(),
190+
"expected 'environments' array in workspace output"
191+
);
192+
193+
let scoped_envs = json["environments"].as_array().unwrap();
194+
for env in scoped_envs {
195+
assert!(
196+
env["executable"].is_string(),
197+
"workspace environment missing 'executable': {env}"
198+
);
199+
}
200+
201+
// An empty temp dir should yield fewer environments than an unrestricted find
202+
let (all, _) = run_find_json(&[]);
203+
let all_envs = all["environments"].as_array().unwrap();
204+
assert!(
205+
scoped_envs.len() <= all_envs.len(),
206+
"workspace-scoped count ({}) should not exceed global count ({})",
207+
scoped_envs.len(),
208+
all_envs.len()
209+
);
210+
}
211+
212+
/// Test 5: CLI flag and env var produce equivalent results for conda executable.
213+
#[cfg_attr(feature = "ci", test)]
214+
#[allow(dead_code)]
215+
fn cli_flag_and_env_var_equivalence() {
216+
// Use a non-existent path — the point is to verify both delivery mechanisms
217+
// produce the same Configuration, not that conda actually works.
218+
let fake_conda = if cfg!(windows) {
219+
"C:\\nonexistent\\conda.exe"
220+
} else {
221+
"/nonexistent/conda"
222+
};
223+
224+
// Via CLI flag
225+
let output_flag = Command::new(env!("CARGO_BIN_EXE_pet"))
226+
.args(["find", "--json", "--conda-executable", fake_conda])
227+
.output()
228+
.expect("failed to run pet find with --conda-executable");
229+
230+
// Via env var
231+
let output_env = Command::new(env!("CARGO_BIN_EXE_pet"))
232+
.args(["find", "--json"])
233+
.env("PET_CONDA_EXECUTABLE", fake_conda)
234+
.output()
235+
.expect("failed to run pet find with PET_CONDA_EXECUTABLE");
236+
237+
assert!(output_flag.status.success());
238+
assert!(output_env.status.success());
239+
240+
let json_flag: Value =
241+
serde_json::from_slice(&output_flag.stdout).expect("flag output is not valid JSON");
242+
let json_env: Value =
243+
serde_json::from_slice(&output_env.stdout).expect("env var output is not valid JSON");
244+
245+
// Both should produce valid output with the same structure
246+
assert!(json_flag["environments"].is_array());
247+
assert!(json_env["environments"].is_array());
248+
249+
// Environment counts should match (same discovery, just different config delivery)
250+
assert_eq!(
251+
json_flag["environments"].as_array().unwrap().len(),
252+
json_env["environments"].as_array().unwrap().len(),
253+
"CLI flag and env var should produce the same number of environments"
254+
);
255+
}
256+
257+
/// Test 6: CLI flag takes precedence over env var when both are set.
258+
/// Note: This is a crash-safety test — clap handles flag/env precedence internally,
259+
/// and the effective config isn't exposed in JSON output, so we verify the binary
260+
/// runs successfully when both are provided without conflicting.
261+
#[cfg_attr(feature = "ci", test)]
262+
#[allow(dead_code)]
263+
fn cli_flag_takes_precedence_over_env_var() {
264+
// Set env var to one value, CLI flag to another.
265+
// Both are non-existent paths — validates the binary handles both without error.
266+
let flag_value = if cfg!(windows) {
267+
"C:\\flag\\conda.exe"
268+
} else {
269+
"/flag/conda"
270+
};
271+
let env_value = if cfg!(windows) {
272+
"C:\\envvar\\conda.exe"
273+
} else {
274+
"/envvar/conda"
275+
};
276+
277+
let output = Command::new(env!("CARGO_BIN_EXE_pet"))
278+
.args(["find", "--json", "--conda-executable", flag_value])
279+
.env("PET_CONDA_EXECUTABLE", env_value)
280+
.output()
281+
.expect("failed to run pet find");
282+
283+
assert!(
284+
output.status.success(),
285+
"pet find failed: {}",
286+
String::from_utf8_lossy(&output.stderr)
287+
);
288+
289+
let json: Value = serde_json::from_slice(&output.stdout).expect("output is not valid JSON");
290+
assert!(json["environments"].is_array());
291+
}
292+
293+
/// Test 7: Glob expansion in search paths works for quoted globs.
294+
/// Requires glob expansion support from issue #354.
295+
#[cfg_attr(feature = "ci", test)]
296+
#[allow(dead_code)]
297+
fn find_glob_expansion_in_search_paths() {
298+
// Create a temp directory structure that matches a glob pattern
299+
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
300+
let sub_a = temp_dir.path().join("project_a");
301+
let sub_b = temp_dir.path().join("project_b");
302+
std::fs::create_dir_all(&sub_a).unwrap();
303+
std::fs::create_dir_all(&sub_b).unwrap();
304+
305+
// Build a glob pattern: <tempdir>/project_*
306+
let glob_pattern = format!(
307+
"{}{}project_*",
308+
temp_dir.path().display(),
309+
std::path::MAIN_SEPARATOR
310+
);
311+
312+
// Run pet find with the glob pattern as a search path — this should not error
313+
let output = Command::new(env!("CARGO_BIN_EXE_pet"))
314+
.args(["find", "--json", &glob_pattern])
315+
.output()
316+
.expect("failed to run pet find with glob pattern");
317+
318+
assert!(
319+
output.status.success(),
320+
"pet find with glob pattern failed: {}",
321+
String::from_utf8_lossy(&output.stderr)
322+
);
323+
324+
let json: Value = serde_json::from_slice(&output.stdout).expect("output is not valid JSON");
325+
assert!(
326+
json["environments"].is_array(),
327+
"expected valid JSON output with glob search path"
328+
);
329+
}
330+
331+
/// Test 8: `find --json` with `--environment-directories` via env var.
332+
#[cfg_attr(feature = "ci", test)]
333+
#[allow(dead_code)]
334+
fn find_environment_directories_via_env_var() {
335+
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
336+
337+
let output = Command::new(env!("CARGO_BIN_EXE_pet"))
338+
.args(["find", "--json"])
339+
.env(
340+
"PET_ENVIRONMENT_DIRECTORIES",
341+
temp_dir.path().to_string_lossy().as_ref(),
342+
)
343+
.output()
344+
.expect("failed to run pet find with PET_ENVIRONMENT_DIRECTORIES");
345+
346+
assert!(
347+
output.status.success(),
348+
"pet find with PET_ENVIRONMENT_DIRECTORIES failed: {}",
349+
String::from_utf8_lossy(&output.stderr)
350+
);
351+
352+
let json: Value = serde_json::from_slice(&output.stdout).expect("output is not valid JSON");
353+
assert!(json["environments"].is_array());
354+
}

0 commit comments

Comments
 (0)