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::ExecutionEvidence::require_complete)
49    .map_err(|error| {
50        if let Some(execution) = error.execution_error()
51            && matches!(execution.failure, ExecutionFailure::ExitStatus)
52            && execution.cleanup.is_none()
53        {
54            return io::Error::new(
55                io::ErrorKind::InvalidInput,
56                format!(
57                    "Cargo workspace discovery failed ({}): {}",
58                    execution
59                        .evidence
60                        .status
61                        .expect("failed command has a status"),
62                    String::from_utf8_lossy(&execution.evidence.stderr).trim()
63                ),
64            );
65        }
66        let kind = match error.execution_error().map(|execution| &execution.failure) {
67            Some(ExecutionFailure::Io { source, .. }) => source.kind(),
68            Some(ExecutionFailure::ExitStatus) => io::ErrorKind::InvalidInput,
69            _ => io::ErrorKind::Other,
70        };
71        io::Error::new(kind, error)
72    })?;
73    let response: serde_json::Value = serde_json::from_slice(&output.stdout)
74        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
75    let manifest = response
76        .get("root")
77        .and_then(serde_json::Value::as_str)
78        .map(Path::new)
79        .filter(|path| {
80            path.is_absolute() && path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml"))
81        })
82        .ok_or_else(|| {
83            io::Error::new(
84                io::ErrorKind::InvalidData,
85                "Cargo returned no absolute workspace manifest",
86            )
87        })?;
88    manifest.parent().map(Path::to_owned).ok_or_else(|| {
89        io::Error::new(
90            io::ErrorKind::InvalidData,
91            "Cargo workspace manifest has no parent",
92        )
93    })
94}
95
96/// Return `<workspace>/target/<name>` for isolated host-side test artifacts.
97#[must_use]
98pub fn test_target_dir(workspace_root: &Path, name: &str) -> PathBuf {
99    workspace_root.join("target").join(name)
100}
101
102#[cfg(all(test, unix))]
103mod tests {
104    use super::*;
105    use ic_host_process::tool::ToolError;
106    use std::fs;
107
108    #[test]
109    fn discovery_refuses_oversized_complete_json_and_preserves_invocation_errors() {
110        let root = crate::artifacts::test_support::unique_temp_directory("discovery-capture");
111        let mut response = br#"{"root":"/tmp/Cargo.toml"}"#.to_vec();
112        response.resize(64 * 1024, b' ');
113        fs::write(root.join("response"), &response).unwrap();
114        let mut command = Command::new("/bin/sh");
115        command.current_dir(&root).args(["-c", "cat response"]);
116        assert_eq!(locate_workspace(&mut command).unwrap(), Path::new("/tmp"));
117        response.push(b' ');
118        fs::write(root.join("response"), &response).unwrap();
119        let error = locate_workspace(&mut command).unwrap_err();
120        assert!(
121            error
122                .get_ref()
123                .unwrap()
124                .downcast_ref::<ToolError>()
125                .is_some()
126        );
127        let mut failed = Command::new("/bin/sh");
128        failed.args(["-c", "printf refused >&2; exit 23"]);
129        let error = locate_workspace(&mut failed).unwrap_err();
130        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
131        assert!(error.to_string().contains("refused"));
132        let mut missing = Command::new(root.join("absent"));
133        let error = locate_workspace(&mut missing).unwrap_err();
134        assert_eq!(error.kind(), io::ErrorKind::NotFound);
135        assert!(
136            error
137                .get_ref()
138                .unwrap()
139                .downcast_ref::<ToolError>()
140                .is_some()
141        );
142        fs::remove_dir_all(root).unwrap();
143    }
144}