ic_testkit/artifacts/
workspace.rs1use ic_host_process::tool::{ExecutionFailure, OutputLimit, OutputLimits, capture_command};
2use std::{
3 io,
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8pub 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#[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}