| 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(¤t).unwrap(); |
| 109 | let binary = root.path().join("selected QA build"); |
| 110 | std::fs::write(&binary, b"canonical").unwrap(); |
| 111 | let relative = binary.strip_prefix(¤t).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 |