Skip to main content

ic_testkit/artifacts/
workspace.rs

1use ic_host_process::tool::{ExecutionFailure, OutputLimit, OutputLimits, capture_command};
2use std::{
3    io,
4    path::{Path, PathBuf},
5    process::Command,
6};
7
8/// Ask Cargo for the workspace owning a crate manifest directory.
9///
10/// Cargo owns membership, explicit `package.workspace`, exclusions and nested
11/// independent workspaces. This performs an offline `locate-project`, without
12/// resolving dependencies or installing a toolchain. It never guesses from the
13/// directory layout or falls back to the supplied directory.
14/// Both discovery streams must fit in 64 KiB; oversized output is refused
15/// before parsing. No elapsed-time deadline is imposed.
16///
17/// # Errors
18/// Returns filesystem, Cargo invocation or invalid Cargo response errors.
19pub fn workspace_root_for(crate_manifest_dir: impl AsRef<Path>) -> io::Result<PathBuf> {
20    let manifest = crate_manifest_dir
21        .as_ref()
22        .canonicalize()?
23        .join("Cargo.toml");
24    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
25    let mut command = Command::new(cargo);
26    command
27        .env("RUSTUP_AUTO_INSTALL", "0")
28        .args([
29            "--offline",
30            "locate-project",
31            "--workspace",
32            "--message-format=json",
33        ])
34        .arg("--manifest-path")
35        .arg(manifest);
36    locate_workspace(&mut command)
37}
38
39fn locate_workspace(command: &mut Command) -> io::Result<PathBuf> {
40    let output = capture_command(
41        command,
42        OutputLimits {
43            stdout: OutputLimit::Terminate(64 * 1024),
44            stderr: OutputLimit::Terminate(64 * 1024),
45            timeout: None,
46        },
47    )
48    .and_then(ic_host_process::tool::CompletedExecution::require_complete)
49    .map_err(|error| {
50        let kind = match error.execution_error().map(|execution| &execution.failure) {
51            Some(ExecutionFailure::Io { source, .. }) => source.kind(),
52            _ => io::ErrorKind::Other,
53        };
54        io::Error::new(kind, error)
55    })?;
56    if !output.status.success() {
57        return Err(io::Error::new(
58            io::ErrorKind::InvalidInput,
59            format!(
60                "Cargo workspace discovery failed ({}): {}",
61                output.status,
62                String::from_utf8_lossy(&output.stderr).trim()
63            ),
64        ));
65    }
66    let response: serde_json::Value = serde_json::from_slice(&output.stdout)
67        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
68    let manifest = response
69        .get("root")
70        .and_then(serde_json::Value::as_str)
71        .map(Path::new)
72        .filter(|path| {
73            path.is_absolute() && path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml"))
74        })
75        .ok_or_else(|| {
76            io::Error::new(
77                io::ErrorKind::InvalidData,
78                "Cargo returned no absolute workspace manifest",
79            )
80        })?;
81    manifest.parent().map(Path::to_owned).ok_or_else(|| {
82        io::Error::new(
83            io::ErrorKind::InvalidData,
84            "Cargo workspace manifest has no parent",
85        )
86    })
87}
88
89/// Return `<workspace>/target/<name>` for isolated host-side test artifacts.
90#[must_use]
91pub fn test_target_dir(workspace_root: &Path, name: &str) -> PathBuf {
92    workspace_root.join("target").join(name)
93}
94
95#[cfg(all(test, unix))]
96mod tests {
97    use super::*;
98    use ic_host_process::tool::ToolError;
99    use std::fs;
100
101    #[test]
102    fn discovery_refuses_oversized_complete_json_and_preserves_invocation_errors() {
103        let root = crate::artifacts::test_support::unique_temp_directory("discovery-capture");
104        let mut response = br#"{"root":"/tmp/Cargo.toml"}"#.to_vec();
105        response.resize(64 * 1024, b' ');
106        fs::write(root.join("response"), &response).unwrap();
107        let mut command = Command::new("/bin/sh");
108        command.current_dir(&root).args(["-c", "cat response"]);
109        assert_eq!(locate_workspace(&mut command).unwrap(), Path::new("/tmp"));
110        response.push(b' ');
111        fs::write(root.join("response"), &response).unwrap();
112        let error = locate_workspace(&mut command).unwrap_err();
113        assert!(
114            error
115                .get_ref()
116                .unwrap()
117                .downcast_ref::<ToolError>()
118                .is_some()
119        );
120        let mut failed = Command::new("/bin/sh");
121        failed.args(["-c", "printf refused >&2; exit 23"]);
122        let error = locate_workspace(&mut failed).unwrap_err();
123        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
124        assert!(error.to_string().contains("refused"));
125        let mut missing = Command::new(root.join("absent"));
126        let error = locate_workspace(&mut missing).unwrap_err();
127        assert_eq!(error.kind(), io::ErrorKind::NotFound);
128        assert!(
129            error
130                .get_ref()
131                .unwrap()
132                .downcast_ref::<ToolError>()
133                .is_some()
134        );
135        fs::remove_dir_all(root).unwrap();
136    }
137}