1use crate::util::{err, Error, Result};
46use std::path::{Path, PathBuf};
47
48pub const APP_BINARY_ENV: &str = "RIGHTKIT_QA_APP_BINARY";
50pub const UI_BINARY_ENV: &str = "RIGHTKIT_QA_UI_BINARY";
52pub const QA_CONTROL_MARKER: &str = "RIGHTKIT-QA-CONTROL-SERVER-PRESENT";
55pub const STASH_DIR: &str = ".rightkit-qa-bin";
57pub const POINTER_FILE: &str = ".rightkit-qa-app-binary";
59pub const DEV_PROFILES: [&str; 3] = ["release-iterate", "debug", "dev"];
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum BinarySource {
65 Env(String),
67 PointerFile(PathBuf),
69 BrokerTarget(PathBuf),
71 AppRoot,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ResolvedBinary {
77 pub path: PathBuf,
78 pub source: BinarySource,
79}
80
81pub struct BinaryQuery<'a> {
83 pub bin: &'a str,
85 pub app_root: &'a Path,
86 pub workspace_roots: Vec<PathBuf>,
88 pub broker_root: Option<PathBuf>,
90 pub extra_env: Option<&'a str>,
92 pub pointer_file: Option<&'a str>,
94 pub app_relative: Option<&'a str>,
96 pub env: &'a dyn Fn(&str) -> Option<String>,
97}
98
99pub fn broker_root_of(managed_root: &Path) -> PathBuf {
101 managed_root.join("rightkit-build-control")
102}
103
104pub fn resolve(q: &BinaryQuery) -> Result<ResolvedBinary> {
105 if q.bin.trim().is_empty() {
106 return err("binary name is empty");
107 }
108 let mut names = vec![APP_BINARY_ENV.to_string(), UI_BINARY_ENV.to_string()];
110 if let Some(n) = q.extra_env.filter(|n| !n.trim().is_empty()) {
111 names.push(n.to_string());
112 }
113 for name in names {
114 if let Some(v) = (q.env)(&name)
115 .map(|v| v.trim().to_string())
116 .filter(|v| !v.is_empty())
117 {
118 let path = absolute(q.app_root, &v);
119 validate(&path, q.bin).map_err(|e| Error(format!("{name}: {}", e.0)))?;
120 require_qa(&path).map_err(|e| Error(format!("{name}: {}", e.0)))?;
121 return Ok(ResolvedBinary {
122 path,
123 source: BinarySource::Env(name),
124 });
125 }
126 }
127 let pointer = absolute(q.app_root, q.pointer_file.unwrap_or(POINTER_FILE));
129 if let Ok(text) = std::fs::read_to_string(&pointer) {
130 if let Some(line) = text.lines().map(str::trim).find(|l| !l.is_empty()) {
131 let path = absolute(q.app_root, line);
132 validate(&path, q.bin)
133 .map_err(|e| Error(format!("pointer file {}: {}", pointer.display(), e.0)))?;
134 require_qa(&path)
135 .map_err(|e| Error(format!("pointer file {}: {}", pointer.display(), e.0)))?;
136 return Ok(ResolvedBinary {
137 path,
138 source: BinarySource::PointerFile(pointer),
139 });
140 }
141 }
142 let mut notes = Vec::new();
144 let mut not_qa: Option<PathBuf> = None;
145 if let Some(broker) = &q.broker_root {
146 let (found, skipped) = from_broker(broker, &q.workspace_roots, q.bin);
147 if let Some((path, ws)) = found {
148 return Ok(ResolvedBinary {
149 path,
150 source: BinarySource::BrokerTarget(ws),
151 });
152 }
153 if let Some(first) = skipped.first() {
154 not_qa = Some(first.clone());
155 }
156 notes.push(format!(
157 "no QA dev build of {} under {} (skipped without the control marker: {})",
158 q.bin,
159 broker.display(),
160 skipped.len()
161 ));
162 }
163 if let Some(rel) = q.app_relative {
165 let path = absolute(q.app_root, rel);
166 match validate(&path, q.bin) {
167 Ok(()) => match has_qa_marker(&path) {
168 Ok(true) => {
169 return Ok(ResolvedBinary {
170 path,
171 source: BinarySource::AppRoot,
172 })
173 }
174 _ => {
175 notes.push(format!("{} lacks the control marker", path.display()));
176 not_qa.get_or_insert(path);
177 }
178 },
179 Err(e) => notes.push(e.0),
180 }
181 }
182 if let Some(found) = not_qa {
183 return err(format!("{}; {}", not_qa_message(&found), notes.join("; ")));
184 }
185 err(format!(
186 "no dev binary for {}: set {APP_BINARY_ENV}, write a path into {}, or build it ({})",
187 q.bin,
188 pointer.display(),
189 notes.join("; ")
190 ))
191}
192
193fn not_qa_message(path: &Path) -> String {
194 format!(
195 "found {} but it isn't a test-mode/control build; run the suite with --build-ui or build with the app's QA feature",
196 path.display()
197 )
198}
199
200fn require_qa(path: &Path) -> Result<()> {
201 match has_qa_marker(path) {
202 Ok(true) => Ok(()),
203 Ok(false) => err(not_qa_message(path)),
204 Err(e) => err(format!("cannot read {}: {e}", path.display())),
205 }
206}
207
208pub fn has_qa_marker(path: &Path) -> std::io::Result<bool> {
211 use std::io::Read;
212 let narrow = QA_CONTROL_MARKER.as_bytes().to_vec();
213 let wide: Vec<u8> = QA_CONTROL_MARKER.bytes().flat_map(|b| [b, 0]).collect();
214 let keep = wide.len() - 1;
215 let find = |hay: &[u8], pat: &[u8]| hay.windows(pat.len()).any(|w| w == pat);
216 let mut f = std::fs::File::open(path)?;
217 let mut buf = vec![0u8; 1 << 20];
218 let mut carry: Vec<u8> = Vec::new();
219 loop {
220 let n = f.read(&mut buf)?;
221 if n == 0 {
222 return Ok(false);
223 }
224 carry.extend_from_slice(&buf[..n]);
225 if find(&carry, &narrow) || find(&carry, &wide) {
226 return Ok(true);
227 }
228 if carry.len() > keep {
229 carry.drain(..carry.len() - keep);
230 }
231 }
232}
233
234pub fn validate(path: &Path, bin: &str) -> Result<()> {
236 crate::installed::refuse_installed(path, "app binary")?;
237 let meta =
238 std::fs::metadata(path).map_err(|_| Error(format!("{} does not exist", path.display())))?;
239 if !meta.is_file() {
240 return err(format!("{} is not a file", path.display()));
241 }
242 if !name_matches(path, bin) {
243 return err(format!("{} is not named {bin}", path.display()));
244 }
245 if path
246 .parent()
247 .and_then(|p| p.file_name())
248 .is_some_and(|n| n == "release")
249 {
250 return err(format!("{} is a release-tier artifact", path.display()));
251 }
252 Ok(())
253}
254
255fn name_matches(path: &Path, bin: &str) -> bool {
256 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
257 return false;
258 };
259 let exe = format!("{bin}.exe");
260 name == bin || name == exe || (cfg!(windows) && name.eq_ignore_ascii_case(&exe))
261}
262
263fn absolute(root: &Path, text: &str) -> PathBuf {
264 let p = Path::new(text);
265 if p.is_absolute() {
266 p.to_path_buf()
267 } else {
268 root.join(p)
269 }
270}
271
272fn norm(text: &str) -> String {
273 let t = text.strip_prefix(r"\\?\").unwrap_or(text);
274 let t = t.replace('\\', "/");
275 let t = t.trim_end_matches('/').to_string();
276 if cfg!(windows) {
277 t.to_lowercase()
278 } else {
279 t
280 }
281}
282
283fn norm_path(p: &Path) -> String {
284 let real = p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
285 norm(&real.to_string_lossy())
286}
287
288fn from_broker(
292 broker: &Path,
293 roots: &[PathBuf],
294 bin: &str,
295) -> (Option<(PathBuf, PathBuf)>, Vec<PathBuf>) {
296 let wanted: Vec<String> = roots.iter().map(|r| norm_path(r)).collect();
297 let mut all: Vec<(std::time::SystemTime, PathBuf, PathBuf)> = Vec::new();
298 let Ok(rd) = std::fs::read_dir(broker.join("ws")) else {
299 return (None, vec![]);
300 };
301 for entry in rd.flatten() {
302 let ws = entry.path();
303 let Ok(text) = std::fs::read_to_string(ws.join("identity.json")) else {
304 continue;
305 };
306 let Ok(id) = serde_json::from_str::<serde_json::Value>(&text) else {
307 continue;
308 };
309 let Some(root) = id.get("canonicalRoot").and_then(|v| v.as_str()) else {
310 continue;
311 };
312 if !wanted.contains(&norm_path(Path::new(root))) {
313 continue;
314 }
315 for file in broker_candidates(&ws, bin) {
316 if validate(&file, bin).is_err() {
317 continue;
318 }
319 let Ok(t) = std::fs::metadata(&file).and_then(|m| m.modified()) else {
320 continue;
321 };
322 all.push((t, file, ws.clone()));
323 }
324 }
325 all.sort_by_key(|a| std::cmp::Reverse(a.0));
326 let mut skipped = Vec::new();
327 for (_, file, ws) in all {
328 if has_qa_marker(&file).unwrap_or(false) {
329 return (Some((file, ws)), skipped);
330 }
331 skipped.push(file);
332 }
333 (None, skipped)
334}
335
336fn subdirs(dir: &Path) -> Vec<PathBuf> {
337 std::fs::read_dir(dir)
338 .map(|rd| {
339 rd.flatten()
340 .map(|e| e.path())
341 .filter(|p| p.is_dir())
342 .collect()
343 })
344 .unwrap_or_default()
345}
346
347fn is_dev_profile(p: &Path) -> bool {
348 p.file_name()
349 .and_then(|n| n.to_str())
350 .is_some_and(|n| DEV_PROFILES.contains(&n))
351}
352
353fn broker_candidates(ws: &Path, bin: &str) -> Vec<PathBuf> {
355 let mut out = Vec::new();
356 for generation in subdirs(&ws.join("g")) {
357 for leaf in subdirs(&generation) {
358 if !matches!(
359 leaf.file_name().and_then(|n| n.to_str()),
360 Some("t" | "target")
361 ) {
362 continue;
363 }
364 for child in subdirs(&leaf) {
365 let profiles = if is_dev_profile(&child) {
366 vec![child]
367 } else {
368 subdirs(&child)
369 };
370 for profile in profiles.into_iter().filter(|p| is_dev_profile(p)) {
371 for name in [bin.to_string(), format!("{bin}.exe")] {
372 let f = profile.join(name);
373 if f.is_file() {
374 out.push(f);
375 }
376 }
377 }
378 }
379 }
380 }
381 out
382}
383
384pub fn is_stashed(path: &Path, app_root: &Path) -> bool {
386 let stash = app_root.join(STASH_DIR);
387 path.starts_with(&stash)
388 || match (path.canonicalize(), stash.canonicalize()) {
389 (Ok(p), Ok(s)) => p.starts_with(s),
390 _ => false,
391 }
392}
393
394pub fn resolve_stashed(q: &BinaryQuery) -> Result<ResolvedBinary> {
402 let found = resolve(q)?;
403 if is_stashed(&found.path, q.app_root) {
404 return Ok(found);
405 }
406 let path = stash_binary(&found.path, q.app_root, None, true)?;
407 Ok(ResolvedBinary {
408 path,
409 source: found.source,
410 })
411}
412
413pub fn stash_qa_build(exe: &Path, app_root: &Path, pointer_file: Option<&str>) -> Result<PathBuf> {
419 let dest = stash_binary(exe, app_root, Some(pointer_file), true)?;
420 Ok(dest)
421}
422
423pub fn stash_binary(
427 exe: &Path,
428 app_root: &Path,
429 pointer: Option<Option<&str>>,
430 require_marker: bool,
431) -> Result<PathBuf> {
432 if require_marker {
433 require_qa(exe)?;
434 }
435 let name = exe
436 .file_name()
437 .ok_or_else(|| Error(format!("{} has no file name", exe.display())))?;
438 let profile = exe
439 .parent()
440 .ok_or_else(|| Error("artifact has no profile dir".into()))?;
441 let up_name = profile
443 .parent()
444 .and_then(|p| p.file_name())
445 .and_then(|n| n.to_str())
446 .unwrap_or("");
447 let triple = if up_name.contains('-') {
448 up_name
449 } else {
450 "host"
451 };
452 let dir = app_root.join(STASH_DIR).join(triple);
455 std::fs::create_dir_all(&dir)
456 .map_err(|e| Error(format!("cannot create {}: {e}", dir.display())))?;
457 let dest = dir.join(name);
458 crate::inplace::replace_file_in_place(exe, &dest)?;
459 let stem = exe.file_stem().and_then(|n| n.to_str()).unwrap_or("");
460 for pdb in [stem.to_string(), stem.replace('-', "_")] {
461 let from = profile.join(format!("{pdb}.pdb"));
462 if from.is_file() {
463 crate::inplace::replace_file_in_place(&from, &dir.join(format!("{pdb}.pdb")))?;
464 }
465 }
466 if let Some(pointer_file) = pointer {
467 let pointer = absolute(app_root, pointer_file.unwrap_or(POINTER_FILE));
468 let tmp = pointer.with_extension(format!("tmp-{}", crate::util::new_id()));
469 std::fs::write(&tmp, format!("{}\n", dest.display()))
470 .and_then(|_| std::fs::rename(&tmp, &pointer))
471 .map_err(|e| Error(format!("cannot write {}: {e}", pointer.display())))?;
472 }
473 Ok(dest)
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use std::time::{Duration, SystemTime};
480
481 fn tmp(tag: &str) -> PathBuf {
482 let d = std::env::temp_dir().join(format!("rkqa-binary-{tag}-{}", crate::util::new_id()));
483 std::fs::create_dir_all(&d).unwrap();
484 d
485 }
486
487 fn touch(p: &Path, age_s: u64) {
489 write_exe(p, age_s, true);
490 }
491
492 fn write_exe(p: &Path, age_s: u64, marker: bool) {
493 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
494 let mut bytes = b"MZ....padding".to_vec();
495 if marker {
496 bytes.extend_from_slice(QA_CONTROL_MARKER.as_bytes());
497 }
498 std::fs::write(p, bytes).unwrap();
499 let t = SystemTime::now() - Duration::from_secs(age_s);
500 std::fs::File::options()
501 .write(true)
502 .open(p)
503 .unwrap()
504 .set_modified(t)
505 .unwrap();
506 }
507
508 fn exe(name: &str) -> String {
509 if cfg!(windows) {
510 format!("{name}.exe")
511 } else {
512 name.to_string()
513 }
514 }
515
516 fn broker(base: &Path, root: &Path) -> PathBuf {
518 let ws = base.join("rightkit-build-control/ws/abc123");
519 std::fs::create_dir_all(&ws).unwrap();
520 let id = serde_json::json!({"schemaVersion":1,"key":"abc123","canonicalRoot":root.to_string_lossy()});
521 std::fs::write(ws.join("identity.json"), id.to_string()).unwrap();
522 ws
523 }
524
525 fn query<'a>(
526 bin: &'a str,
527 app: &'a Path,
528 broker: Option<PathBuf>,
529 env: &'a dyn Fn(&str) -> Option<String>,
530 ) -> BinaryQuery<'a> {
531 BinaryQuery {
532 bin,
533 app_root: app,
534 workspace_roots: vec![app.to_path_buf()],
535 broker_root: broker,
536 extra_env: Some("MYAPP_APP_BINARY"),
537 pointer_file: None,
538 app_relative: Some("target/debug/myapp"),
539 env,
540 }
541 }
542
543 #[test]
544 fn broker_picks_newest_dev_profile_and_never_release() {
545 let base = tmp("broker");
546 let app = base.join("app");
547 std::fs::create_dir_all(&app).unwrap();
548 let ws = broker(&base, &app);
549 let old = ws
550 .join("g/001/t/x86_64-pc-windows-msvc/release-iterate")
551 .join(exe("myapp"));
552 let new = ws
553 .join("g/002/t/x86_64-pc-windows-msvc/debug")
554 .join(exe("myapp"));
555 let rel = ws
556 .join("g/002/t/x86_64-pc-windows-msvc/release")
557 .join(exe("myapp"));
558 touch(&old, 500);
559 touch(&new, 100);
560 touch(&rel, 1); let env = |_: &str| None;
562 let got = resolve(&query(
563 "myapp",
564 &app,
565 Some(base.join("rightkit-build-control")),
566 &env,
567 ))
568 .unwrap();
569 assert_eq!(got.path, new);
570 assert_eq!(got.source, BinarySource::BrokerTarget(ws));
571 }
572
573 #[test]
574 fn broker_ignores_other_workspaces_and_other_bins() {
575 let base = tmp("other");
576 let app = base.join("app");
577 let other = base.join("other");
578 std::fs::create_dir_all(&app).unwrap();
579 std::fs::create_dir_all(&other).unwrap();
580 let ws = broker(&base, &other);
581 touch(&ws.join("g/001/target/debug").join(exe("myapp")), 10);
582 let ws2 = base.join("rightkit-build-control/ws/def456");
583 std::fs::create_dir_all(&ws2).unwrap();
584 let id = serde_json::json!({"canonicalRoot": app.to_string_lossy()});
585 std::fs::write(ws2.join("identity.json"), id.to_string()).unwrap();
586 touch(&ws2.join("g/001/target/debug").join(exe("helper")), 10);
587 let env = |_: &str| None;
588 let e = resolve(&query(
589 "myapp",
590 &app,
591 Some(base.join("rightkit-build-control")),
592 &env,
593 ))
594 .unwrap_err();
595 assert!(e.0.contains("no dev binary for myapp"), "{}", e.0);
596 }
597
598 #[test]
599 fn env_beats_pointer_beats_broker_beats_app_root() {
600 let base = tmp("order");
601 let app = base.join("app");
602 let ws = broker(&base, &app);
603 let from_broker = ws.join("g/001/t/debug").join(exe("myapp"));
604 touch(&from_broker, 10);
605 let rel = app.join("target/debug/myapp");
606 touch(&rel, 10);
607 touch(&app.join("target/debug/myapp.exe"), 10);
608 let b = Some(base.join("rightkit-build-control"));
609
610 let none = |_: &str| None;
611 let got = resolve(&query("myapp", &app, b.clone(), &none)).unwrap();
612 assert!(matches!(got.source, BinarySource::BrokerTarget(_)));
613
614 let pointed = base.join("pointed").join(exe("myapp"));
615 touch(&pointed, 10);
616 std::fs::write(app.join(POINTER_FILE), format!("\n{}\n", pointed.display())).unwrap();
617 let got = resolve(&query("myapp", &app, b.clone(), &none)).unwrap();
618 assert_eq!(got.path, pointed);
619 assert!(matches!(got.source, BinarySource::PointerFile(_)));
620
621 let envd = base.join("envd").join(exe("myapp"));
622 touch(&envd, 10);
623 let p = envd.to_string_lossy().into_owned();
624 let env = move |k: &str| (k == "MYAPP_APP_BINARY").then(|| p.clone());
625 let got = resolve(&query("myapp", &app, b.clone(), &env)).unwrap();
626 assert_eq!(got.path, envd);
627 assert_eq!(got.source, BinarySource::Env("MYAPP_APP_BINARY".into()));
628
629 std::fs::remove_file(app.join(POINTER_FILE)).unwrap();
631 let got = resolve(&query("myapp", &app, None, &none)).unwrap();
632 assert_eq!(got.source, BinarySource::AppRoot);
633 }
634
635 #[test]
636 fn invalid_override_is_an_error_not_a_fall_through() {
637 let base = tmp("bad");
638 let app = base.join("app");
639 std::fs::create_dir_all(&app).unwrap();
640 let wrong = base.join("wrong").join(exe("helper"));
641 touch(&wrong, 1);
642 let p = wrong.to_string_lossy().into_owned();
643 let env = move |k: &str| (k == APP_BINARY_ENV).then(|| p.clone());
644 let e = resolve(&query("myapp", &app, None, &env)).unwrap_err();
645 assert!(e.0.contains("is not named myapp"), "{}", e.0);
646 let missing = |k: &str| (k == APP_BINARY_ENV).then(|| "nope/myapp".to_string());
647 assert!(resolve(&query("myapp", &app, None, &missing))
648 .unwrap_err()
649 .0
650 .contains("does not exist"));
651 }
652
653 #[test]
654 fn validate_rejects_directories_and_release_dirs() {
655 let base = tmp("val");
656 let dir = base.join("myapp");
657 std::fs::create_dir_all(&dir).unwrap();
658 assert!(validate(&dir, "myapp")
659 .unwrap_err()
660 .0
661 .contains("not a file"));
662 let rel = base.join("target/release").join(exe("myapp"));
663 touch(&rel, 1);
664 assert!(validate(&rel, "myapp")
665 .unwrap_err()
666 .0
667 .contains("release-tier"));
668 let ok = base.join("target/release-iterate").join(exe("myapp"));
669 touch(&ok, 1);
670 assert!(validate(&ok, "myapp").is_ok());
671 }
672
673 #[test]
674 fn marker_scan_finds_narrow_wide_and_chunk_straddling_bytes() {
675 let base = tmp("marker");
676 let narrow = base.join("n.exe");
677 write_exe(&narrow, 1, true);
678 assert!(has_qa_marker(&narrow).unwrap());
679 let none = base.join("none.exe");
680 write_exe(&none, 1, false);
681 assert!(!has_qa_marker(&none).unwrap());
682 let wide = base.join("w.exe");
683 let mut b = vec![7u8; 100];
684 b.extend(QA_CONTROL_MARKER.bytes().flat_map(|c| [c, 0]));
685 std::fs::write(&wide, b).unwrap();
686 assert!(has_qa_marker(&wide).unwrap());
687 let big = base.join("big.exe");
689 let mut b = vec![1u8; (1 << 20) - 10];
690 b.extend_from_slice(QA_CONTROL_MARKER.as_bytes());
691 std::fs::write(&big, b).unwrap();
692 assert!(has_qa_marker(&big).unwrap());
693 let part = base.join("part.exe");
695 std::fs::write(&part, &QA_CONTROL_MARKER.as_bytes()[..20]).unwrap();
696 assert!(!has_qa_marker(&part).unwrap());
697 }
698
699 #[test]
700 fn broker_skips_non_qa_newest_and_takes_the_qa_one() {
701 let base = tmp("skip");
702 let app = base.join("app");
703 std::fs::create_dir_all(&app).unwrap();
704 let ws = broker(&base, &app);
705 let qa = ws.join("g/001/t/debug").join(exe("myapp"));
706 let plain = ws.join("g/002/t/debug").join(exe("myapp"));
707 write_exe(&qa, 500, true);
708 write_exe(&plain, 5, false); let env = |_: &str| None;
710 let b = Some(base.join("rightkit-build-control"));
711 let got = resolve(&query("myapp", &app, b.clone(), &env)).unwrap();
712 assert_eq!(got.path, qa);
713 std::fs::remove_file(&qa).unwrap();
715 let e = resolve(&query("myapp", &app, b, &env)).unwrap_err();
716 assert!(
717 e.0.contains("but it isn't a test-mode/control build"),
718 "{}",
719 e.0
720 );
721 assert!(e.0.contains("--build-ui"), "{}", e.0);
722 }
723
724 #[test]
725 fn env_and_pointer_without_marker_are_errors() {
726 let base = tmp("nomarker");
727 let app = base.join("app");
728 std::fs::create_dir_all(&app).unwrap();
729 let plain = base.join("p").join(exe("myapp"));
730 write_exe(&plain, 1, false);
731 let p = plain.to_string_lossy().into_owned();
732 let env = move |k: &str| (k == APP_BINARY_ENV).then(|| p.clone());
733 assert!(resolve(&query("myapp", &app, None, &env))
734 .unwrap_err()
735 .0
736 .contains("isn't a test-mode/control build"));
737 std::fs::write(app.join(POINTER_FILE), plain.to_string_lossy().as_bytes()).unwrap();
738 let none = |_: &str| None;
739 let e = resolve(&query("myapp", &app, None, &none)).unwrap_err();
740 assert!(
741 e.0.contains("pointer file") && e.0.contains("isn't a test-mode/control build"),
742 "{}",
743 e.0
744 );
745 }
746
747 #[test]
748 fn app_root_candidate_without_marker_is_refused() {
749 let base = tmp("approot");
750 let app = base.join("app");
751 write_exe(&app.join("target/debug").join(exe("myapp")), 1, false);
752 write_exe(&app.join("target/debug/myapp"), 1, false);
753 let none = |_: &str| None;
754 let e = resolve(&query("myapp", &app, None, &none)).unwrap_err();
755 assert!(e.0.contains("isn't a test-mode/control build"), "{}", e.0);
756 }
757
758 #[test]
759 fn resolve_stashed_never_returns_a_broker_generation_path() {
760 let base = tmp("stashed");
761 let app = base.join("app");
762 std::fs::create_dir_all(&app).unwrap();
763 let ws = broker(&base, &app);
764 let gen1 = ws.join("g/001/t/debug").join(exe("myapp"));
765 touch(&gen1, 10);
766 let env = |_: &str| None;
767 let b = Some(base.join("rightkit-build-control"));
768 let q = query("myapp", &app, b.clone(), &env);
769 assert!(matches!(
770 resolve(&q).unwrap().source,
771 BinarySource::BrokerTarget(_)
772 ));
773 let got = resolve_stashed(&q).unwrap();
774 assert!(matches!(got.source, BinarySource::BrokerTarget(_)));
775 assert!(is_stashed(&got.path, &app));
776 assert!(got.path.starts_with(app.join(STASH_DIR)));
777 assert!(!got.path.starts_with(&ws));
778 let gen2 = ws.join("g/002/t/debug").join(exe("myapp"));
780 std::fs::create_dir_all(gen2.parent().unwrap()).unwrap();
781 std::fs::write(
782 &gen2,
783 [b"MZ-gen2".as_slice(), QA_CONTROL_MARKER.as_bytes()].concat(),
784 )
785 .unwrap();
786 let later = resolve_stashed(&query("myapp", &app, b, &env)).unwrap();
787 assert_eq!(later.path, got.path);
788 assert!(crate::inplace::previous_path(&got.path).is_file());
789 let rel = app.join("target/debug/myapp");
791 touch(&rel, 5);
792 let envd = base.join("envd").join(exe("myapp"));
793 touch(&envd, 5);
794 let p = envd.to_string_lossy().into_owned();
795 let env2 = move |k: &str| (k == "MYAPP_APP_BINARY").then(|| p.clone());
796 let from_env = resolve_stashed(&query("myapp", &app, None, &env2)).unwrap();
797 assert!(from_env.path.starts_with(app.join(STASH_DIR)));
798 assert_eq!(
799 from_env.source,
800 BinarySource::Env("MYAPP_APP_BINARY".into())
801 );
802 assert!(is_stashed(&from_env.path, &app) && !is_stashed(&envd, &app));
804 }
805
806 #[test]
807 fn stash_copies_atomically_and_writes_the_pointer() {
808 let base = tmp("stash");
809 let app = base.join("app");
810 std::fs::create_dir_all(&app).unwrap();
811 let built = base
812 .join("target/x86_64-pc-windows-msvc/debug")
813 .join(exe("myapp"));
814 write_exe(&built, 5, true);
815 std::fs::write(built.with_extension("pdb"), b"pdb").unwrap();
816 let got = stash_qa_build(&built, &app, None).unwrap();
817 assert_eq!(
818 got,
819 app.join(".rightkit-qa-bin/x86_64-pc-windows-msvc")
820 .join(exe("myapp"))
821 );
822 assert!(has_qa_marker(&got).unwrap());
823 assert!(got.with_extension("pdb").is_file());
824 write_exe(&built, 3, true);
826 std::fs::write(
827 &built,
828 [b"MZ-second".as_slice(), QA_CONTROL_MARKER.as_bytes()].concat(),
829 )
830 .unwrap();
831 let again = stash_qa_build(&built, &app, None).unwrap();
832 assert_eq!(again, got);
833 let prev = crate::inplace::previous_path(&got);
834 assert!(prev.is_file(), "the earlier stash is kept as .previous");
835 assert!(!has_qa_marker(&prev).is_err());
836 write_exe(&built, 1, false);
838 let none = |_: &str| None;
839 let r = resolve(&query("myapp", &app, None, &none)).unwrap();
840 assert_eq!(r.path, got);
841 assert!(matches!(r.source, BinarySource::PointerFile(_)));
842 let plain_layout = base.join("t2/debug").join(exe("myapp"));
844 write_exe(&plain_layout, 1, true);
845 assert!(stash_qa_build(&plain_layout, &app, None)
846 .unwrap()
847 .starts_with(app.join(".rightkit-qa-bin/host")));
848 let bad = base.join("t3/debug").join(exe("myapp"));
850 write_exe(&bad, 1, false);
851 assert!(stash_qa_build(&bad, &app, None).is_err());
852 }
853}