返回 CodeWhale
binary.rs
根目录 / crates / tui / tests / support / binary.rs
1 //! Canonical executable lookup shared by both mounted acceptance harnesses.
2 //!
3 //! The Engine crate is a library. Build `codewhale-cli --bin codewhale` before
4 //! acceptance tests, or supply QA_TUI_BIN for a deliberately selected QA build.
5
6 use std::path::{Path, PathBuf};
7
8 pub fn codewhale() -> PathBuf {
9 let qa = std::env::var_os("QA_TUI_BIN")
10 .filter(|value| !value.is_empty())
11 .map(PathBuf::from);
12 let cargo = std::env::var_os("CARGO_BIN_EXE_codewhale")
13 .filter(|value| !value.is_empty())
14 .map(PathBuf::from)
15 .or_else(|| option_env!("CARGO_BIN_EXE_codewhale").map(PathBuf::from));
16 let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
17 .parent()
18 .and_then(Path::parent)
19 .expect("Engine workspace root");
20 let target = std::env::var_os("CARGO_TARGET_DIR")
21 .filter(|value| !value.is_empty())
22 .map(PathBuf::from)
23 .unwrap_or_else(|| workspace.join("target"));
24 let target = if target.is_absolute() {
25 target
26 } else {
27 workspace.join(target)
28 };
29 resolve(
30 qa,
31 cargo,
32 &std::env::current_exe().expect("current test executable"),
33 &target,
34 )
35 .unwrap_or_else(|message| panic!("{message}"))
36 }
37
38 fn resolve(
39 qa: Option<PathBuf>,
40 cargo: Option<PathBuf>,
41 test_executable: &Path,
42 target_directory: &Path,
43 ) -> Result<PathBuf, String> {
44 if let Some(path) = qa.or(cargo) {
45 return require_file(path);
46 }
47 let directory = test_executable
48 .parent()
49 .ok_or("test executable has no parent")?;
50 let directory = if directory.ends_with("deps") {
51 directory
52 .parent()
53 .ok_or("test deps directory has no parent")?
54 } else {
55 directory
56 };
57 let name = format!("codewhale{}", std::env::consts::EXE_SUFFIX);
58 let adjacent = directory.join(&name);
59 if adjacent.is_file() {
60 return Ok(adjacent);
61 }
62 // Cargo's split build-dir stores test deps separately from final binaries.
63 // Only inspect the known workspace/explicit target directory; never scan
64 // PATH, another checkout, or a legacy executable name.
65 let profile = directory
66 .file_name()
67 .ok_or("test profile directory has no name")?;
68 // Explicit --target builds add one triple directory to both layouts.
69 // Prefer that exact projection over a coexisting host executable.
70 if let Some(triple) = directory.parent().and_then(Path::file_name) {
71 let cross_target = target_directory.join(triple).join(profile).join(&name);
72 if cross_target.is_file() {
73 return Ok(cross_target);
74 }
75 }
76 require_file(target_directory.join(profile).join(&name))
77 }
78
79 // This synchronous resolver belongs only to Cargo acceptance test binaries.
80 #[cfg(test)]
81 fn require_file(path: PathBuf) -> Result<PathBuf, String> {
82 if path.is_file() {
83 if path.is_absolute() {
84 Ok(path)
85 } else {
86 path.canonicalize().map_err(|error| {
87 format!(
88 "cannot resolve selected Codewhale executable {} before changing workspace: {error}",
89 path.display()
90 )
91 })
92 }
93 } else {
94 Err(format!(
95 "canonical Codewhale executable is missing at {}: build `cargo build -p codewhale-cli --bin codewhale --locked` before Engine acceptance tests, or set QA_TUI_BIN to the intended QA binary",
96 path.display()
97 ))
98 }
99 }
100
101 #[cfg(test)]
102 mod tests {
103 use super::*;
104
105 #[test]
106 fn relative_explicit_selection_survives_an_acceptance_workspace_change() {
107 let current = std::env::current_dir().unwrap();
108 let root = tempfile::TempDir::new_in(&current).unwrap();
109 let binary = root.path().join("selected QA build");
110 std::fs::write(&binary, b"canonical").unwrap();
111 let relative = binary.strip_prefix(&current).unwrap().to_path_buf();
112 for (qa, cargo) in [
113 (Some(relative.clone()), None),
114 (None, Some(relative.clone())),
115 ] {
116 let selected = resolve(qa, cargo, &binary, root.path()).unwrap();
117 assert!(selected.is_absolute());
118 assert_eq!(selected, binary.canonicalize().unwrap());
119 assert!(
120 root.path()
121 .join("another workspace")
122 .join(selected)
123 .is_file()
124 );
125 }
126 }
127
128 #[test]
129 fn canonical_lookup_uses_the_test_target_directory_and_never_legacy_binary() {
130 let root = tempfile::TempDir::new().unwrap();
131 let debug = root.path().join("target with spaces").join("debug");
132 std::fs::create_dir_all(debug.join("deps")).unwrap();
133 let canonical = debug.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
134 let legacy = debug.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
135 std::fs::write(&legacy, b"legacy").unwrap();
136 let executable = debug.join("deps").join("integration-hash");
137 assert!(
138 resolve(
139 None,
140 None,
141 &executable,
142 &root.path().join("target with spaces")
143 )
144 .is_err()
145 );
146 std::fs::write(&canonical, b"canonical").unwrap();
147 assert_eq!(
148 resolve(
149 None,
150 None,
151 &executable,
152 &root.path().join("target with spaces")
153 )
154 .unwrap(),
155 canonical
156 );
157 }
158
159 #[test]
160 fn explicit_qa_selection_wins_and_a_missing_selection_never_falls_back() {
161 let root = tempfile::TempDir::new().unwrap();
162 let qa = root.path().join("selected release");
163 let cargo = root.path().join("codewhale");
164 std::fs::write(&qa, b"qa").unwrap();
165 std::fs::write(&cargo, b"cargo").unwrap();
166 assert_eq!(
167 resolve(Some(qa.clone()), Some(cargo.clone()), &cargo, root.path()).unwrap(),
168 qa
169 );
170 assert!(
171 resolve(
172 Some(root.path().join("missing")),
173 Some(cargo),
174 &qa,
175 root.path()
176 )
177 .is_err()
178 );
179 }
180
181 #[test]
182 fn split_build_cache_resolves_the_known_final_target_and_cross_profile() {
183 let root = tempfile::TempDir::new().unwrap();
184 let target = root.path().join("final target");
185 let cache = root.path().join("build cache");
186 for triple in [None, Some("aarch64-unknown-linux-gnu")] {
187 let final_profile = match triple {
188 Some(triple) => target.join(triple).join("debug"),
189 None => target.join("debug"),
190 };
191 std::fs::create_dir_all(&final_profile).unwrap();
192 let canonical =
193 final_profile.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
194 std::fs::write(&canonical, b"canonical").unwrap();
195 let test = match triple {
196 Some(triple) => cache.join(triple).join("debug/deps/integration-hash"),
197 None => cache.join("debug/deps/integration-hash"),
198 };
199 // The requested cross target must not select coexisting host output.
200 assert_eq!(resolve(None, None, &test, &target).unwrap(), canonical);
201 }
202 }
203 }
204
204 lines RUST