1use super::POCKET_IC_SERVER_VERSION;
8use ic_host_artifacts::artifact::{Sha256Digest, copy_gzip, hash_gzip, hash_reader};
9use ic_host_fs::path::validate_physical_directory_allow_missing;
10use ic_host_fs::{durable, read::read_file_no_follow};
11use ic_host_process::{
12 child::CleanupPolicy,
13 tool::{
14 AdmittedTool, ExecutionContext, OutputLimit, OutputLimits, VersionSpec,
15 capture_group_command_with_cancellation,
16 },
17};
18use std::{
19 error::Error,
20 ffi::OsString,
21 fs::{self, File},
22 io,
23 path::{Path, PathBuf},
24 process::{Command, Stdio},
25 sync::atomic::{AtomicU64, Ordering},
26 time::Duration,
27};
28
29type Result<T> = std::result::Result<T, Box<dyn Error>>;
30const ARCHIVE_LIMIT: usize = 80 * 1024 * 1024;
31const EXECUTABLE_LIMIT: usize = 512 * 1024 * 1024;
32static SEQUENCE: AtomicU64 = AtomicU64::new(0);
33pub const DEFAULT_DIRECTORY: &str = ".tools/ic-testkit-server";
35
36struct Asset {
37 host: &'static str,
38 digest: Sha256Digest,
39}
40
41fn write_evidence(path: &Path, bytes: &[u8]) -> Result<()> {
42 durable::write_bytes(
43 path,
44 durable::WriteOptions {
45 mode: durable::PublicationMode::Replace,
46 permissions: 0o666,
47 },
48 bytes,
49 )?;
50 Ok(())
51}
52
53fn selected_asset() -> Result<Asset> {
54 let (host, digest) = match (std::env::consts::OS, std::env::consts::ARCH) {
56 ("linux", "x86_64") => (
57 "x86_64-linux",
58 "131219d90dcf9bf6f3ed8ee02d8f55504ea24a6db382f5680675b7bd6b7ed3bf",
59 ),
60 ("macos", "x86_64") => (
61 "x86_64-darwin",
62 "af9ad2d781530a43556ef2d1c8f93db99a2425c6cabdc78520f61922399ed530",
63 ),
64 ("macos", "aarch64") => (
65 "arm64-darwin",
66 "9ae843fbb7ae6c6eb30137671c8629a80ef53b3a3652eb85b344847ac39f9fcd",
67 ),
68 _ => return Err("no reviewed PocketIC server asset for this host".into()),
69 };
70 Ok(Asset {
71 host,
72 digest: digest.parse()?,
73 })
74}
75
76fn root_path(directory: &Path) -> Result<PathBuf> {
77 if directory.as_os_str().is_empty() {
78 return Err("server directory must not be empty".into());
79 }
80 let absolute = if directory.is_absolute() {
81 directory.to_owned()
82 } else {
83 std::env::current_dir()?.join(directory)
84 };
85 if absolute
86 .as_os_str()
87 .as_encoded_bytes()
88 .iter()
89 .any(|byte| matches!(byte, b'\n' | b'\r'))
90 {
91 return Err("server directory and its ancestors must not contain line breaks".into());
92 }
93 validate_physical_directory_allow_missing(&absolute)?;
94 Ok(absolute.components().collect())
95}
96
97fn bundle_path(root: &Path, asset: &Asset) -> PathBuf {
98 root.join(format!("{}-{}", POCKET_IC_SERVER_VERSION, asset.host))
99}
100
101fn authenticated_archive(archive: &Path, digest: Sha256Digest) -> Result<Vec<u8>> {
102 let bytes = read_file_no_follow(archive, ARCHIVE_LIMIT)?;
103 hash_reader(bytes.as_slice(), ARCHIVE_LIMIT as u64)?.verify(digest)?;
104 Ok(bytes)
105}
106
107fn admit(executable: &Path, digest: Sha256Digest) -> Result<()> {
108 let directory = executable.parent().ok_or("missing executable parent")?;
109 AdmittedTool::admit_no_follow(
110 &VersionSpec {
111 executable,
112 executable_bytes: EXECUTABLE_LIMIT as u64,
113 version_arguments: &[OsString::from("--version")],
114 },
115 digest,
116 &format!("pocket-ic-server {POCKET_IC_SERVER_VERSION}"),
117 &ExecutionContext {
118 current_dir: directory,
119 environment: &[],
120 },
121 OutputLimits {
122 stdout: OutputLimit::Terminate(4096),
123 stderr: OutputLimit::Terminate(4096),
124 timeout: Some(Duration::from_secs(10)),
125 },
126 )?;
127 Ok(())
128}
129
130fn check_bundle(bundle: &Path, asset: &Asset) -> Result<PathBuf> {
131 let metadata = fs::symlink_metadata(bundle)?;
132 if !metadata.is_dir() || metadata.is_symlink() {
133 return Err("PocketIC bundle must be a physical directory".into());
134 }
135 let bytes = authenticated_archive(&bundle.join("archive.gz"), asset.digest)?;
136 let digest = hash_gzip(&bytes, ARCHIVE_LIMIT, EXECUTABLE_LIMIT as u64)?.sha256;
137 drop(bytes);
138 let executable = bundle.join("pocket-ic");
139 admit(&executable, digest)?;
140 Ok(executable)
141}
142
143pub fn check(directory: &Path) -> Result<PathBuf> {
154 let asset = selected_asset()?;
155 check_bundle(&bundle_path(&root_path(directory)?, &asset), &asset)
156}
157
158pub fn setup(directory: &Path, mut interrupted: impl FnMut() -> bool) -> Result<PathBuf> {
169 let asset = selected_asset()?;
170 setup_with(directory, &asset, |archive| {
171 let url = format!(
172 "https://github.com/dfinity/pocketic/releases/download/{POCKET_IC_SERVER_VERSION}/pocket-ic-{}.gz",
173 asset.host
174 );
175 let mut command = Command::new("curl");
176 command
177 .args([
178 "--proto",
179 "=https",
180 "--proto-redir",
181 "=https",
182 "--tlsv1.2",
183 "--fail",
184 "--silent",
185 "--show-error",
186 "--location",
187 "--connect-timeout",
188 "15",
189 "--max-time",
190 "300",
191 "--max-filesize",
192 "83886080",
193 "--output",
194 ])
195 .arg(archive)
196 .arg(url)
197 .stdin(Stdio::null())
198 .stdout(Stdio::piped())
199 .stderr(Stdio::piped());
200 let outcome = capture_group_command_with_cancellation(
201 &mut command,
202 OutputLimits {
203 stdout: OutputLimit::Terminate(4096),
204 stderr: OutputLimit::Terminate(64 * 1024),
205 timeout: Some(Duration::from_secs(310)),
206 },
207 CleanupPolicy::KillAndWait,
208 None,
209 &mut interrupted,
210 )
211 .and_then(ic_host_process::tool::CompletedExecution::require_success);
212 let stderr = match &outcome {
213 Ok(completed) => Some(completed.stderr.as_slice()),
214 Err(error) => error.evidence().map(|evidence| evidence.stderr.as_slice()),
215 };
216 if let Some(stderr) = stderr
217 && let Err(record_error) = write_evidence(
218 &archive
219 .parent()
220 .ok_or("missing archive parent")?
221 .join("download.stderr"),
222 stderr,
223 )
224 {
225 if outcome.is_ok() {
226 return Err(record_error);
227 }
228 eprintln!("could not record download stderr: {record_error}");
230 }
231 outcome?;
232 Ok(())
233 })
234}
235
236fn setup_with(
237 directory: &Path,
238 asset: &Asset,
239 download: impl FnOnce(&Path) -> Result<()>,
240) -> Result<PathBuf> {
241 let root = root_path(directory)?;
242 let lock = durable::open_regular_lock_file_with_parents(&root.join(".setup-v1.lock"))?;
245 durable::lock_exclusive(&lock)?;
246 let bundle = bundle_path(&root, asset);
247 let attempt = root.join(format!(
248 ".setup-v1-{}-{}",
249 std::process::id(),
250 SEQUENCE.fetch_add(1, Ordering::Relaxed)
251 ));
252 let staging_directory = match fs::symlink_metadata(&bundle) {
253 Ok(_) => {
254 check_bundle(&bundle, asset)?;
255 None
256 }
257 Err(error) if error.kind() == io::ErrorKind::NotFound => {
258 let held = durable::create_private_directory(&attempt)?;
259 eprintln!("PocketIC setup evidence: {}", attempt.display());
260 if let Err(error) = install(&attempt, asset, download) {
261 if let Err(record_error) =
262 write_evidence(&attempt.join("failure.txt"), error.to_string().as_bytes())
263 {
264 eprintln!("could not record setup failure: {record_error}");
265 }
266 return Err(error);
267 }
268 Some(held)
269 }
270 Err(error) => return Err(error.into()),
271 };
272 durable::commit_existing_directory_with(
276 &attempt,
277 &bundle,
278 |selected, path| -> io::Result<()> {
279 if let Some(held) = &staging_directory {
280 durable::admit_directory(path, held)?;
281 }
282 selected.sync_all()?;
283 Ok(())
284 },
285 |_, _| Ok(()),
286 )?;
287 eprintln!("PocketIC setup evidence retained: {}", bundle.display());
288 Ok(bundle.join("pocket-ic"))
289}
290
291fn install(
292 attempt: &Path,
293 asset: &Asset,
294 download: impl FnOnce(&Path) -> Result<()>,
295) -> Result<()> {
296 let archive = attempt.join("archive.gz");
297 download(&archive)?;
298 let bytes = authenticated_archive(&archive, asset.digest)?;
299 File::open(&archive)?.sync_all()?;
300 let executable = attempt.join("pocket-ic");
301 let identity = durable::write_with(
302 &executable,
303 durable::WriteOptions {
304 mode: durable::PublicationMode::CreateNew,
305 permissions: 0o755,
306 },
307 |file| copy_gzip(&bytes, file, ARCHIVE_LIMIT, EXECUTABLE_LIMIT as u64),
308 )?;
309 drop(bytes);
310 if let Err(error) = admit(&executable, identity.sha256) {
313 if let Some(evidence) = error
314 .downcast_ref::<ic_host_process::tool::ToolError>()
315 .and_then(|error| error.evidence())
316 {
317 for (name, bytes) in [
318 ("version.stdout", &evidence.stdout),
319 ("version.stderr", &evidence.stderr),
320 ] {
321 if let Err(record_error) = write_evidence(&attempt.join(name), bytes) {
322 eprintln!("could not record version evidence: {record_error}");
323 }
324 }
325 }
326 return Err(error);
327 }
328 Ok(())
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use ic_host_artifacts::artifact::encode_gzip;
335 use std::os::unix::fs::symlink;
336
337 struct Fixture {
338 root: ic_host_fs::testing::FixtureDirectory,
339 }
340 impl Fixture {
341 fn new() -> Self {
342 let root = ic_host_fs::testing::FixtureDirectory::new(
343 &std::env::temp_dir(),
344 "testkit-provision",
345 )
346 .unwrap();
347 Self { root }
348 }
349 }
350 fn archive(version: &str) -> (Asset, Vec<u8>) {
351 let script = format!("#!/bin/sh\nprintf 'pocket-ic-server {version}\\n'\n");
352 let mut bytes = Vec::new();
353 encode_gzip(script.as_bytes(), &mut bytes, 1, 65536).unwrap();
354 (
355 Asset {
356 host: "fixture",
357 digest: Sha256Digest::compute(&bytes),
358 },
359 bytes,
360 )
361 }
362
363 #[test]
364 fn authenticated_setup_reuses_offline_and_refuses_changed_bytes_before_execution() {
365 if ic_host_process::testing::run_isolated(
366 "IC_TESTKIT_ASSET_CASE",
367 "server asset test",
368 ic_host_process::tool::OutputLimits {
369 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
370 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
371 timeout: Some(std::time::Duration::from_secs(120)),
372 },
373 ) {
374 return;
375 }
376 let root = Fixture::new();
377 let (asset, bytes) = archive(POCKET_IC_SERVER_VERSION);
378 let executable = setup_with(root.root.path(), &asset, |path| {
379 fs::write(path, &bytes)?;
380 Ok(())
381 })
382 .unwrap();
383 let original = fs::read(&executable).unwrap();
384 assert_eq!(
385 setup_with(root.root.path(), &asset, |_| panic!(
386 "verified reuse must not download"
387 ))
388 .unwrap(),
389 executable
390 );
391 let entries = fs::read_dir(root.root.path()).unwrap().count();
392 assert_eq!(
393 check_bundle(&bundle_path(root.root.path(), &asset), &asset).unwrap(),
394 executable
395 );
396 assert_eq!(fs::read_dir(root.root.path()).unwrap().count(), entries);
397 let marker = root.root.path().join("executed");
398 fs::write(
399 &executable,
400 format!("#!/bin/sh\ntouch '{}'\n", marker.display()),
401 )
402 .unwrap();
403 assert!(check_bundle(&bundle_path(root.root.path(), &asset), &asset).is_err());
404 assert!(!marker.exists());
405 fs::write(&executable, original).unwrap();
406 assert!(check_bundle(&bundle_path(root.root.path(), &asset), &asset).is_ok());
407 let saved = root.root.path().join("saved");
408 fs::rename(&executable, &saved).unwrap();
409 symlink(&saved, &executable).unwrap();
410 assert!(check_bundle(&bundle_path(root.root.path(), &asset), &asset).is_err());
411 }
412
413 #[test]
414 fn offline_check_authenticates_before_decoding_and_rejects_invalid_gzip() {
415 use ic_host_artifacts::artifact::{ArtifactError, GzipError};
416
417 if ic_host_process::testing::run_isolated(
418 "IC_TESTKIT_ASSET_CASE",
419 "server asset test",
420 ic_host_process::tool::OutputLimits {
421 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
422 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
423 timeout: Some(std::time::Duration::from_secs(120)),
424 },
425 ) {
426 return;
427 }
428
429 let root = Fixture::new();
430 let (mut asset, original) = archive(POCKET_IC_SERVER_VERSION);
431 let executable = setup_with(root.root.path(), &asset, |path| {
432 fs::write(path, &original)?;
433 Ok(())
434 })
435 .unwrap();
436 let installed = fs::read(&executable).unwrap();
437 let bundle = bundle_path(root.root.path(), &asset);
438 let path = bundle.join("archive.gz");
439
440 fs::write(&path, b"not gzip").unwrap();
442 let error = check_bundle(&bundle, &asset).unwrap_err();
443 assert!(matches!(
444 error.downcast_ref::<ArtifactError>(),
445 Some(ArtifactError::DigestMismatch { .. })
446 ));
447
448 let mut bad_crc = original.clone();
449 let crc = bad_crc.len() - 8;
450 bad_crc[crc] ^= 1;
451 let mut bad_length = original.clone();
452 let length = bad_length.len() - 4;
453 bad_length[length] ^= 1;
454 for bytes in [
455 b"not gzip".to_vec(),
456 original[..original.len() - 1].to_vec(),
457 bad_crc,
458 bad_length,
459 [original.as_slice(), b"trailing"].concat(),
460 [original.as_slice(), original.as_slice()].concat(),
461 ] {
462 fs::write(&path, &bytes).unwrap();
463 asset.digest = Sha256Digest::compute(&bytes);
464 let error = check_bundle(&bundle, &asset).unwrap_err();
465 assert!(error.downcast_ref::<GzipError>().is_some());
467 assert_eq!(fs::read(&executable).unwrap(), installed);
468 assert_eq!(fs::read(&path).unwrap(), bytes);
469 }
470 fs::write(&path, &original).unwrap();
471 asset.digest = Sha256Digest::compute(&original);
472 assert_eq!(check_bundle(&bundle, &asset).unwrap(), executable);
473 }
474
475 #[test]
476 fn setup_rejects_corrupt_payloads_without_execution_or_publication() {
477 use ic_host_artifacts::artifact::GzipError;
478
479 if ic_host_process::testing::run_isolated(
480 "IC_TESTKIT_ASSET_CASE",
481 "server asset test",
482 ic_host_process::tool::OutputLimits {
483 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
484 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
485 timeout: Some(std::time::Duration::from_secs(120)),
486 },
487 ) {
488 return;
489 }
490 let root = Fixture::new();
491 let marker = root.root.path().join("executed");
492 let script = format!("#!/bin/sh\nprintf executed > '{}'\n", marker.display());
493 let mut original = Vec::new();
494 encode_gzip(script.as_bytes(), &mut original, 1, 65536).unwrap();
495 let mut bad_crc = original.clone();
496 let crc = bad_crc.len() - 8;
497 bad_crc[crc] ^= 1;
498 for bytes in [
499 bad_crc,
500 original[..original.len() - 1].to_vec(),
501 [original.as_slice(), b"trailing"].concat(),
502 ] {
503 let asset = Asset {
504 host: "fixture",
505 digest: Sha256Digest::compute(&bytes),
506 };
507 let error = setup_with(root.root.path(), &asset, |path| {
508 fs::write(path, &bytes)?;
509 Ok(())
510 })
511 .unwrap_err();
512 assert!(matches!(
513 error.downcast_ref::<durable::NamedWriteError<GzipError>>(),
514 Some(durable::NamedWriteError::Producer { .. })
515 ));
516 assert!(!marker.exists());
517 assert!(!bundle_path(root.root.path(), &asset).exists());
518 assert!(fs::read_dir(root.root.path()).unwrap().any(|entry| {
519 let path = entry.unwrap().path();
520 path.join("failure.txt").is_file()
521 && fs::read(path.join("archive.gz")).ok().as_deref() == Some(bytes.as_slice())
522 }));
523 }
524 }
525
526 #[test]
527 fn failed_setup_retains_evidence_and_previous_bundles_then_can_retry() {
528 if ic_host_process::testing::run_isolated(
529 "IC_TESTKIT_ASSET_CASE",
530 "server asset test",
531 ic_host_process::tool::OutputLimits {
532 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
533 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
534 timeout: Some(std::time::Duration::from_secs(120)),
535 },
536 ) {
537 return;
538 }
539 let root = Fixture::new();
540 let previous = root.root.path().join("previous-version");
541 fs::create_dir(&previous).unwrap();
542 fs::write(previous.join("pocket-ic"), b"previous bytes").unwrap();
543 let (asset, bytes) = archive(POCKET_IC_SERVER_VERSION);
544 assert!(
545 setup_with(root.root.path(), &asset, |path| {
546 fs::write(path, b"partial download")?;
547 Err("interrupted download".into())
548 })
549 .is_err()
550 );
551 assert!(!bundle_path(root.root.path(), &asset).exists());
552 let failed = fs::read_dir(root.root.path())
553 .unwrap()
554 .find_map(|entry| {
555 let path = entry.unwrap().path();
556 path.join("failure.txt").is_file().then_some(path)
557 })
558 .unwrap();
559 assert_eq!(
560 fs::read(failed.join("archive.gz")).unwrap(),
561 b"partial download"
562 );
563 assert!(
564 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
565 let _ = setup_with(root.root.path(), &asset, |path| {
566 fs::write(path, b"interrupted candidate")?;
567 panic!("injected interruption before admission");
568 });
569 }))
570 .is_err()
571 );
572 assert!(!bundle_path(root.root.path(), &asset).exists());
573 assert!(fs::read_dir(root.root.path()).unwrap().any(|entry| {
574 fs::read(entry.unwrap().path().join("archive.gz"))
575 .ok()
576 .as_deref()
577 == Some(b"interrupted candidate")
578 }));
579 assert!(
580 setup_with(root.root.path(), &asset, |path| {
581 fs::write(path, &bytes)?;
582 Ok(())
583 })
584 .is_ok()
585 );
586 assert!(failed.join("failure.txt").is_file());
587 assert_eq!(
588 fs::read(previous.join("pocket-ic")).unwrap(),
589 b"previous bytes"
590 );
591 }
592
593 #[test]
594 fn digest_decode_and_version_failures_never_publish() {
595 if ic_host_process::testing::run_isolated(
596 "IC_TESTKIT_ASSET_CASE",
597 "server asset test",
598 ic_host_process::tool::OutputLimits {
599 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
600 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
601 timeout: Some(std::time::Duration::from_secs(120)),
602 },
603 ) {
604 return;
605 }
606 for mode in [0, 1, 2] {
607 let root = Fixture::new();
608 let (mut asset, mut bytes) = archive("15.0.0");
609 if mode == 0 {
610 asset.digest = Sha256Digest::compute(b"wrong");
611 }
612 if mode == 1 {
613 bytes = b"not gzip".to_vec();
614 asset.digest = Sha256Digest::compute(&bytes);
615 }
616 assert!(
617 setup_with(root.root.path(), &asset, |path| {
618 fs::write(path, &bytes)?;
619 Ok(())
620 })
621 .is_err()
622 );
623 assert!(!bundle_path(root.root.path(), &asset).exists());
624 }
625 }
626
627 #[test]
628 fn redirected_paths_are_refused_without_touching_the_target() {
629 if ic_host_process::testing::run_isolated(
630 "IC_TESTKIT_ASSET_CASE",
631 "server asset test",
632 ic_host_process::tool::OutputLimits {
633 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
634 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
635 timeout: Some(std::time::Duration::from_secs(120)),
636 },
637 ) {
638 return;
639 }
640 let root = Fixture::new();
641 let (asset, _) = archive(POCKET_IC_SERVER_VERSION);
642 let target = root.root.path().join("target");
643 fs::create_dir(&target).unwrap();
644 let redirected = root.root.path().join("redirected");
645 symlink(&target, &redirected).unwrap();
646 assert!(
647 setup_with(&redirected.join("child"), &asset, |_| panic!(
648 "redirected ancestor reached downloader"
649 ))
650 .is_err()
651 );
652 assert_eq!(fs::read_dir(&target).unwrap().count(), 0);
653 symlink(&target, root.root.path().join(".setup-v1.lock")).unwrap();
654 assert!(
655 setup_with(root.root.path(), &asset, |_| panic!(
656 "redirected lock reached downloader"
657 ))
658 .is_err()
659 );
660 }
661
662 #[test]
663 fn replaced_staging_directory_is_not_published() {
664 if ic_host_process::testing::run_isolated(
665 "IC_TESTKIT_ASSET_CASE",
666 "server asset test",
667 ic_host_process::tool::OutputLimits {
668 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
669 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
670 timeout: Some(std::time::Duration::from_secs(120)),
671 },
672 ) {
673 return;
674 }
675 let root = Fixture::new();
676 let (asset, bytes) = archive(POCKET_IC_SERVER_VERSION);
677 let retained = root.root.path().join("retained-original-stage");
678 let error = setup_with(root.root.path(), &asset, |archive| {
679 fs::write(archive, &bytes)?;
680 let stage = archive.parent().unwrap();
681 fs::rename(stage, &retained)?;
682 fs::create_dir(stage)?;
683 fs::write(archive, &bytes)?;
684 Ok(())
685 })
686 .expect_err("replacement must not inherit the held directory claim");
687 assert!(matches!(
688 error.downcast_ref::<durable::DirectoryCommitError<io::Error>>(),
689 Some(durable::DirectoryCommitError::Prepare(source))
690 if source.kind() == io::ErrorKind::InvalidInput
691 ));
692 assert!(!bundle_path(root.root.path(), &asset).exists());
693 assert_eq!(fs::read(retained.join("archive.gz")).unwrap(), bytes);
694 }
695
696 #[test]
697 fn occupied_destination_preserves_both_bundle_and_verified_staging() {
698 if ic_host_process::testing::run_isolated(
699 "IC_TESTKIT_ASSET_CASE",
700 "server asset test",
701 ic_host_process::tool::OutputLimits {
702 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
703 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
704 timeout: Some(std::time::Duration::from_secs(120)),
705 },
706 ) {
707 return;
708 }
709 let root = Fixture::new();
710 let (asset, bytes) = archive(POCKET_IC_SERVER_VERSION);
711 let bundle = bundle_path(root.root.path(), &asset);
712 let mut attempt = None;
713 let error = setup_with(root.root.path(), &asset, |archive| {
714 fs::write(archive, &bytes)?;
715 attempt = archive.parent().map(Path::to_owned);
716 fs::create_dir(&bundle)?;
717 fs::write(bundle.join("unrelated"), b"retained destination")?;
718 Ok(())
719 })
720 .unwrap_err();
721 assert!(matches!(
722 error.downcast_ref::<durable::DirectoryCommitError<io::Error>>(),
723 Some(durable::DirectoryCommitError::Conflict)
724 ));
725 assert_eq!(
726 fs::read(bundle.join("unrelated")).unwrap(),
727 b"retained destination"
728 );
729 let attempt = attempt.unwrap();
730 assert_eq!(fs::read(attempt.join("archive.gz")).unwrap(), bytes);
731 assert!(attempt.join("pocket-ic").is_file());
732 }
733
734 #[test]
735 fn concurrent_setups_download_once_and_recheck_under_the_lock() {
736 if ic_host_process::testing::run_isolated(
737 "IC_TESTKIT_ASSET_CASE",
738 "server asset test",
739 ic_host_process::tool::OutputLimits {
740 stdout: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
741 stderr: ic_host_process::tool::OutputLimit::Terminate(64 * 1024),
742 timeout: Some(std::time::Duration::from_secs(120)),
743 },
744 ) {
745 return;
746 }
747 let root = Fixture::new();
748 let (asset, bytes) = archive(POCKET_IC_SERVER_VERSION);
749 let calls = AtomicU64::new(0);
750 std::thread::scope(|scope| {
751 let handles = (0..2)
752 .map(|_| {
753 scope.spawn(|| {
754 setup_with(root.root.path(), &asset, |path| {
755 calls.fetch_add(1, Ordering::Relaxed);
756 fs::write(path, &bytes)?;
757 Ok(())
758 })
759 .unwrap()
760 })
761 })
762 .collect::<Vec<_>>();
763 for handle in handles {
764 assert_eq!(
765 handle.join().unwrap(),
766 bundle_path(root.root.path(), &asset).join("pocket-ic")
767 );
768 }
769 });
770 assert_eq!(calls.load(Ordering::Relaxed), 1);
771 }
772}