1use super::{
22 format::{
23 BUILD_SCOPE_JSON_PATH, CARGO_METADATA_JSON_PATH, OutputDict, PORTABLE_MANIFEST_FILE_NAME,
24 PORTABLE_RECORDING_FORMAT_VERSION, PortableManifest, RECORD_OPTS_JSON_PATH,
25 RERUN_INFO_JSON_PATH, RUN_LOG_FILE_NAME, RecordedBuildScope, RerunInfo, STDERR_DICT_PATH,
26 STDOUT_DICT_PATH, STORE_FORMAT_VERSION, STORE_ZIP_FILE_NAME, TEST_LIST_JSON_PATH,
27 has_zip_extension, stored_file_options,
28 },
29 reader::{StoreReader, decompress_with_dict},
30 store::{RecordedRunInfo, RunFilesExist, StoreRunsDir},
31 summary::{RecordOpts, TestEventSummary},
32};
33use crate::{
34 errors::{PortableRecordingError, PortableRecordingReadError, RecordReadError},
35 output_spec::RecordingSpec,
36 user_config::elements::MAX_MAX_OUTPUT_SIZE,
37};
38use atomicwrites::{AtomicFile, OverwriteBehavior};
39use bytesize::ByteSize;
40use camino::{Utf8Path, Utf8PathBuf};
41use countio::Counter;
42use debug_ignore::DebugIgnore;
43use eazip::{Archive, ArchiveWriter, CompressionMethod};
44use itertools::Either;
45use nextest_metadata::TestListSummary;
46use std::{
47 borrow::Cow,
48 fs::File,
49 io::{self, BufRead, BufReader, Cursor, Read, Seek, SeekFrom, Write},
50};
51
52#[derive(Debug)]
54pub struct PortableRecordingResult {
55 pub path: Utf8PathBuf,
57 pub size: u64,
59}
60
61#[derive(Debug)]
63pub struct ExtractOuterFileResult {
64 pub bytes_written: u64,
66 pub exceeded_limit: Option<u64>,
71}
72
73#[derive(Debug)]
75pub struct PortableRecordingWriter<'a> {
76 run_info: &'a RecordedRunInfo,
77 run_dir: Utf8PathBuf,
78}
79
80impl<'a> PortableRecordingWriter<'a> {
81 pub fn new(
85 run_info: &'a RecordedRunInfo,
86 runs_dir: StoreRunsDir<'_>,
87 ) -> Result<Self, PortableRecordingError> {
88 let run_dir = runs_dir.run_dir(run_info.run_id);
89
90 if !run_dir.exists() {
91 return Err(PortableRecordingError::RunDirNotFound { path: run_dir });
92 }
93
94 let store_zip_path = run_dir.join(STORE_ZIP_FILE_NAME);
95 if !store_zip_path.exists() {
96 return Err(PortableRecordingError::RequiredFileMissing {
97 run_dir,
98 file_name: STORE_ZIP_FILE_NAME,
99 });
100 }
101
102 let run_log_path = run_dir.join(RUN_LOG_FILE_NAME);
103 if !run_log_path.exists() {
104 return Err(PortableRecordingError::RequiredFileMissing {
105 run_dir,
106 file_name: RUN_LOG_FILE_NAME,
107 });
108 }
109
110 Ok(Self { run_info, run_dir })
111 }
112
113 pub fn default_filename(&self) -> String {
117 format!("nextest-run-{}.zip", self.run_info.run_id)
118 }
119
120 pub fn write_to_dir(
125 &self,
126 output_dir: &Utf8Path,
127 ) -> Result<PortableRecordingResult, PortableRecordingError> {
128 let output_path = output_dir.join(self.default_filename());
129 self.write_to_path(&output_path)
130 }
131
132 pub fn write_to_path(
136 &self,
137 output_path: &Utf8Path,
138 ) -> Result<PortableRecordingResult, PortableRecordingError> {
139 let atomic_file = AtomicFile::new(output_path, OverwriteBehavior::AllowOverwrite);
140
141 let final_size = atomic_file
142 .write(|temp_file| {
143 let counter = Counter::new(temp_file);
144 let mut zip_writer = ArchiveWriter::new(counter);
145
146 self.write_manifest(&mut zip_writer)?;
147 self.copy_file(&mut zip_writer, RUN_LOG_FILE_NAME)?;
148 self.copy_file(&mut zip_writer, STORE_ZIP_FILE_NAME)?;
149
150 let counter = zip_writer
151 .finish()
152 .map_err(PortableRecordingError::ZipFinalize)?;
153
154 let counter_bytes = counter.writer_bytes() as u64;
159 let file = counter.into_inner();
160 let size = file.metadata().map(|m| m.len()).unwrap_or(counter_bytes);
161
162 Ok(size)
163 })
164 .map_err(|err| match err {
165 atomicwrites::Error::Internal(source) => PortableRecordingError::AtomicWrite {
166 path: output_path.to_owned(),
167 source,
168 },
169 atomicwrites::Error::User(e) => e,
170 })?;
171
172 Ok(PortableRecordingResult {
173 path: output_path.to_owned(),
174 size: final_size,
175 })
176 }
177
178 fn write_manifest<W: Write>(
180 &self,
181 zip_writer: &mut ArchiveWriter<W>,
182 ) -> Result<(), PortableRecordingError> {
183 let manifest = PortableManifest::new(self.run_info);
184 let manifest_json = serde_json::to_vec_pretty(&manifest)
185 .map_err(PortableRecordingError::SerializeManifest)?;
186
187 let options = stored_file_options();
188
189 zip_writer
190 .add_file(PORTABLE_MANIFEST_FILE_NAME, &manifest_json[..], &options)
191 .map_err(|source| PortableRecordingError::ZipWrite {
192 file_name: PORTABLE_MANIFEST_FILE_NAME,
193 source,
194 })?;
195
196 Ok(())
197 }
198
199 fn copy_file<W: Write>(
204 &self,
205 zip_writer: &mut ArchiveWriter<W>,
206 file_name: &'static str,
207 ) -> Result<(), PortableRecordingError> {
208 let source_path = self.run_dir.join(file_name);
209 let mut file = File::open(&source_path)
210 .map_err(|source| PortableRecordingError::ReadFile { file_name, source })?;
211
212 let options = stored_file_options();
213
214 let mut streamer = zip_writer
215 .stream_file(file_name, &options)
216 .map_err(|source| PortableRecordingError::ZipStartFile { file_name, source })?;
217
218 io::copy(&mut file, &mut streamer)
219 .map_err(|source| PortableRecordingError::ZipWrite { file_name, source })?;
220
221 streamer
222 .finish()
223 .map_err(|source| PortableRecordingError::ZipWrite { file_name, source })?;
224
225 Ok(())
226 }
227}
228
229const SPOOL_SIZE_LIMIT: ByteSize = ByteSize(4 * 1024 * 1024 * 1024);
239
240#[cfg(windows)]
247enum WindowsFileKind {
248 Disk,
250 Pipe,
252 Other(u32),
255}
256
257#[cfg(windows)]
259fn classify_windows_handle(file: &File) -> WindowsFileKind {
260 use std::os::windows::io::AsRawHandle;
261 use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, FILE_TYPE_PIPE, GetFileType};
262
263 let file_type = unsafe { GetFileType(file.as_raw_handle()) };
265 match file_type {
266 FILE_TYPE_DISK => WindowsFileKind::Disk,
267 FILE_TYPE_PIPE => WindowsFileKind::Pipe,
268 other => WindowsFileKind::Other(other),
269 }
270}
271
272#[cfg(unix)]
275fn is_not_seekable_error(e: &io::Error) -> bool {
276 e.raw_os_error() == Some(libc::ESPIPE)
278}
279
280fn ensure_seekable(file: File, path: &Utf8Path) -> Result<File, PortableRecordingReadError> {
289 ensure_seekable_impl(file, path, SPOOL_SIZE_LIMIT)
290}
291
292fn ensure_seekable_impl(
296 file: File,
297 path: &Utf8Path,
298 spool_limit: ByteSize,
299) -> Result<File, PortableRecordingReadError> {
300 #[cfg(unix)]
303 {
304 let mut file = file;
305 match file.stream_position() {
306 Ok(_) => Ok(file),
307 Err(e) if is_not_seekable_error(&e) => spool_to_temp(file, path, spool_limit),
308 Err(e) => {
309 Err(PortableRecordingReadError::SeekProbe {
312 path: path.to_owned(),
313 error: e,
314 })
315 }
316 }
317 }
318
319 #[cfg(windows)]
323 match classify_windows_handle(&file) {
324 WindowsFileKind::Disk => Ok(file),
325 WindowsFileKind::Pipe => spool_to_temp(file, path, spool_limit),
326 WindowsFileKind::Other(file_type) => Err(PortableRecordingReadError::SeekProbe {
327 path: path.to_owned(),
328 error: io::Error::other(format!(
329 "unexpected file handle type {file_type:#x} (expected disk or pipe)"
330 )),
331 }),
332 }
333}
334
335fn spool_to_temp(
339 file: File,
340 path: &Utf8Path,
341 spool_limit: ByteSize,
342) -> Result<File, PortableRecordingReadError> {
343 let mut temp =
344 camino_tempfile::tempfile().map_err(|error| PortableRecordingReadError::SpoolTempFile {
345 path: path.to_owned(),
346 error,
347 })?;
348
349 let bytes_copied = io::copy(
353 &mut (&file).take(spool_limit.0.saturating_add(1)),
354 &mut temp,
355 )
356 .map_err(|error| PortableRecordingReadError::SpoolTempFile {
357 path: path.to_owned(),
358 error,
359 })?;
360
361 if bytes_copied > spool_limit.0 {
362 return Err(PortableRecordingReadError::SpoolTooLarge {
363 path: path.to_owned(),
364 limit: spool_limit,
365 });
366 }
367
368 temp.seek(SeekFrom::Start(0))
370 .map_err(|error| PortableRecordingReadError::SpoolTempFile {
371 path: path.to_owned(),
372 error,
373 })?;
374
375 Ok(temp)
376}
377
378type ArchiveReadStorage = Either<File, Cursor<Vec<u8>>>;
383
384pub struct PortableRecording {
386 archive_path: Utf8PathBuf,
387 manifest: PortableManifest,
388 outer_archive: Archive<BufReader<ArchiveReadStorage>>,
389}
390
391impl std::fmt::Debug for PortableRecording {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 f.debug_struct("PortableRecording")
394 .field("archive_path", &self.archive_path)
395 .field("manifest", &self.manifest)
396 .finish_non_exhaustive()
397 }
398}
399
400impl RunFilesExist for PortableRecording {
401 fn store_zip_exists(&self) -> bool {
402 self.outer_archive.index_of(STORE_ZIP_FILE_NAME).is_some()
403 }
404
405 fn run_log_exists(&self) -> bool {
406 self.outer_archive.index_of(RUN_LOG_FILE_NAME).is_some()
407 }
408}
409
410impl PortableRecording {
411 pub fn open(path: &Utf8Path) -> Result<Self, PortableRecordingReadError> {
421 let file = File::open(path).map_err(|error| PortableRecordingReadError::OpenArchive {
422 path: path.to_owned(),
423 error,
424 })?;
425
426 let file = ensure_seekable(file, path)?;
429
430 let mut outer_archive =
431 Archive::new(BufReader::new(Either::Left(file))).map_err(|error| {
432 PortableRecordingReadError::ReadArchive {
433 path: path.to_owned(),
434 error,
435 }
436 })?;
437
438 if outer_archive
440 .index_of(PORTABLE_MANIFEST_FILE_NAME)
441 .is_some()
442 {
443 return Self::open_validated(path, outer_archive);
444 }
445
446 let mut file_count = 0;
450 let mut zip_count = 0;
451 let mut zip_file: Option<String> = None;
452 for metadata in outer_archive.entries() {
453 let name = metadata.name();
454 if name.ends_with('/') || name.ends_with('\\') {
455 continue;
457 }
458 file_count += 1;
459 if has_zip_extension(Utf8Path::new(name)) {
460 zip_count += 1;
461 if zip_count == 1 {
462 zip_file = Some(name.to_owned());
463 }
464 }
465 }
466
467 if let Some(inner_name) = zip_file.filter(|_| file_count == 1 && zip_count == 1) {
468 let inner_bytes = read_outer_file(&mut outer_archive, inner_name.into(), path)?;
473 let inner_archive = Archive::new(BufReader::new(Either::Right(Cursor::new(
474 inner_bytes,
475 ))))
476 .map_err(|error| PortableRecordingReadError::ReadArchive {
477 path: path.to_owned(),
478 error,
479 })?;
480 Self::open_validated(path, inner_archive)
481 } else {
482 Err(PortableRecordingReadError::NotAWrapperArchive {
483 path: path.to_owned(),
484 file_count,
485 zip_count,
486 })
487 }
488 }
489
490 fn open_validated(
492 path: &Utf8Path,
493 mut outer_archive: Archive<BufReader<ArchiveReadStorage>>,
494 ) -> Result<Self, PortableRecordingReadError> {
495 let manifest_bytes =
497 read_outer_file(&mut outer_archive, PORTABLE_MANIFEST_FILE_NAME.into(), path)?;
498 let manifest: PortableManifest =
499 serde_json::from_slice(&manifest_bytes).map_err(|error| {
500 PortableRecordingReadError::ParseManifest {
501 path: path.to_owned(),
502 error,
503 }
504 })?;
505
506 if let Err(incompatibility) = manifest
508 .format_version
509 .check_readable_by(PORTABLE_RECORDING_FORMAT_VERSION)
510 {
511 return Err(PortableRecordingReadError::UnsupportedFormatVersion {
512 path: path.to_owned(),
513 found: manifest.format_version,
514 supported: PORTABLE_RECORDING_FORMAT_VERSION,
515 incompatibility,
516 });
517 }
518
519 let store_version = manifest.store_format_version();
521 if let Err(incompatibility) = store_version.check_readable_by(STORE_FORMAT_VERSION) {
522 return Err(PortableRecordingReadError::UnsupportedStoreFormatVersion {
523 path: path.to_owned(),
524 found: store_version,
525 supported: STORE_FORMAT_VERSION,
526 incompatibility,
527 });
528 }
529
530 Ok(Self {
531 archive_path: path.to_owned(),
532 manifest,
533 outer_archive,
534 })
535 }
536
537 pub fn archive_path(&self) -> &Utf8Path {
539 &self.archive_path
540 }
541
542 pub fn run_info(&self) -> RecordedRunInfo {
544 self.manifest.run_info()
545 }
546
547 pub fn read_run_log(&mut self) -> Result<PortableRecordingRunLog, PortableRecordingReadError> {
553 let run_log_bytes = read_outer_file(
554 &mut self.outer_archive,
555 RUN_LOG_FILE_NAME.into(),
556 &self.archive_path,
557 )?;
558 Ok(PortableRecordingRunLog {
559 archive_path: self.archive_path.clone(),
560 run_log_bytes,
561 })
562 }
563
564 pub fn extract_outer_file_to_path(
573 &mut self,
574 file_name: &'static str,
575 output_path: &Utf8Path,
576 check_limit: bool,
577 ) -> Result<ExtractOuterFileResult, PortableRecordingReadError> {
578 extract_outer_file_to_path(
579 &mut self.outer_archive,
580 file_name,
581 &self.archive_path,
582 output_path,
583 check_limit,
584 )
585 }
586
587 pub fn open_store(&mut self) -> Result<PortableStoreReader<'_>, PortableRecordingReadError> {
594 let file = self
595 .outer_archive
596 .get_by_name(STORE_ZIP_FILE_NAME)
597 .ok_or_else(|| PortableRecordingReadError::MissingFile {
598 path: self.archive_path.clone(),
599 file_name: Cow::Borrowed(STORE_ZIP_FILE_NAME),
600 })?;
601
602 let compression = file.metadata().compression_method;
603 if compression != CompressionMethod::STORE {
604 return Err(PortableRecordingReadError::CompressedInnerArchive {
605 archive_path: self.archive_path.clone(),
606 compression,
607 });
608 }
609
610 let raw = file
612 .read_stored()
613 .map_err(|error| PortableRecordingReadError::ReadArchive {
614 path: self.archive_path.clone(),
615 error,
616 })?;
617
618 let store_archive =
619 Archive::new(raw).map_err(|error| PortableRecordingReadError::ReadArchive {
620 path: self.archive_path.clone(),
621 error,
622 })?;
623
624 Ok(PortableStoreReader {
625 archive_path: &self.archive_path,
626 store_archive,
627 stdout_dict: None,
628 stderr_dict: None,
629 })
630 }
631}
632
633fn read_outer_file(
635 archive: &mut Archive<BufReader<ArchiveReadStorage>>,
636 file_name: Cow<'static, str>,
637 archive_path: &Utf8Path,
638) -> Result<Vec<u8>, PortableRecordingReadError> {
639 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
640 let mut file =
641 archive
642 .get_by_name(&file_name)
643 .ok_or_else(|| PortableRecordingReadError::MissingFile {
644 path: archive_path.to_owned(),
645 file_name: file_name.clone(),
646 })?;
647
648 let claimed_size = file.metadata().uncompressed_size;
649 if claimed_size > limit {
650 return Err(PortableRecordingReadError::FileTooLarge {
651 path: archive_path.to_owned(),
652 file_name,
653 size: claimed_size,
654 limit,
655 });
656 }
657
658 let capacity = usize::try_from(claimed_size).unwrap_or(usize::MAX);
659 let mut contents = Vec::with_capacity(capacity);
660
661 file.read()
662 .and_then(|reader| reader.take(limit).read_to_end(&mut contents))
663 .map_err(|error| PortableRecordingReadError::ReadArchive {
664 path: archive_path.to_owned(),
665 error,
666 })?;
667
668 Ok(contents)
669}
670
671fn extract_outer_file_to_path(
673 archive: &mut Archive<BufReader<ArchiveReadStorage>>,
674 file_name: &'static str,
675 archive_path: &Utf8Path,
676 output_path: &Utf8Path,
677 check_limit: bool,
678) -> Result<ExtractOuterFileResult, PortableRecordingReadError> {
679 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
680 let mut file =
681 archive
682 .get_by_name(file_name)
683 .ok_or_else(|| PortableRecordingReadError::MissingFile {
684 path: archive_path.to_owned(),
685 file_name: Cow::Borrowed(file_name),
686 })?;
687
688 let claimed_size = file.metadata().uncompressed_size;
689 let exceeded_limit = if check_limit && claimed_size > limit {
690 Some(claimed_size)
691 } else {
692 None
693 };
694
695 let mut output_file =
696 File::create(output_path).map_err(|error| PortableRecordingReadError::ExtractFile {
697 archive_path: archive_path.to_owned(),
698 file_name,
699 output_path: output_path.to_owned(),
700 error,
701 })?;
702
703 let mut reader = file
704 .read()
705 .map_err(|error| PortableRecordingReadError::ReadArchive {
706 path: archive_path.to_owned(),
707 error,
708 })?;
709
710 let bytes_written = io::copy(&mut reader, &mut output_file).map_err(|error| {
711 PortableRecordingReadError::ExtractFile {
712 archive_path: archive_path.to_owned(),
713 file_name,
714 output_path: output_path.to_owned(),
715 error,
716 }
717 })?;
718
719 Ok(ExtractOuterFileResult {
720 bytes_written,
721 exceeded_limit,
722 })
723}
724
725#[derive(Debug)]
730pub struct PortableRecordingRunLog {
731 archive_path: Utf8PathBuf,
732 run_log_bytes: Vec<u8>,
733}
734
735impl PortableRecordingRunLog {
736 pub fn events(&self) -> Result<PortableRecordingEventIter<'_>, RecordReadError> {
738 let decoder =
741 zstd::stream::Decoder::with_buffer(&self.run_log_bytes[..]).map_err(|error| {
742 RecordReadError::OpenRunLog {
743 path: self.archive_path.join(RUN_LOG_FILE_NAME),
744 error,
745 }
746 })?;
747 Ok(PortableRecordingEventIter {
748 reader: DebugIgnore(BufReader::new(decoder)),
750 line_buf: String::new(),
751 line_number: 0,
752 })
753 }
754}
755
756#[derive(Debug)]
758pub struct PortableRecordingEventIter<'a> {
759 reader: DebugIgnore<BufReader<zstd::stream::Decoder<'static, &'a [u8]>>>,
760 line_buf: String,
761 line_number: usize,
762}
763
764impl Iterator for PortableRecordingEventIter<'_> {
765 type Item = Result<TestEventSummary<RecordingSpec>, RecordReadError>;
766
767 fn next(&mut self) -> Option<Self::Item> {
768 loop {
769 self.line_buf.clear();
770 self.line_number += 1;
771
772 match self.reader.read_line(&mut self.line_buf) {
773 Ok(0) => return None,
774 Ok(_) => {
775 let trimmed = self.line_buf.trim();
776 if trimmed.is_empty() {
777 continue;
778 }
779 return Some(serde_json::from_str(trimmed).map_err(|error| {
780 RecordReadError::ParseEvent {
781 line_number: self.line_number,
782 error,
783 }
784 }));
785 }
786 Err(error) => {
787 return Some(Err(RecordReadError::ReadRunLog {
788 line_number: self.line_number,
789 error,
790 }));
791 }
792 }
793 }
794 }
795}
796
797pub struct PortableStoreReader<'a> {
802 archive_path: &'a Utf8Path,
803 store_archive: Archive<io::Take<&'a mut BufReader<ArchiveReadStorage>>>,
804 stdout_dict: Option<Vec<u8>>,
806 stderr_dict: Option<Vec<u8>>,
808}
809
810impl std::fmt::Debug for PortableStoreReader<'_> {
811 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812 f.debug_struct("PortableStoreReader")
813 .field("archive_path", &self.archive_path)
814 .field("stdout_dict", &self.stdout_dict.as_ref().map(|d| d.len()))
815 .field("stderr_dict", &self.stderr_dict.as_ref().map(|d| d.len()))
816 .finish_non_exhaustive()
817 }
818}
819
820impl PortableStoreReader<'_> {
821 fn read_store_file(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError> {
823 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
824 let mut file = self.store_archive.get_by_name(file_name).ok_or_else(|| {
825 RecordReadError::FileNotFound {
826 file_name: file_name.to_string(),
827 }
828 })?;
829
830 let claimed_size = file.metadata().uncompressed_size;
831 if claimed_size > limit {
832 return Err(RecordReadError::FileTooLarge {
833 file_name: file_name.to_string(),
834 size: claimed_size,
835 limit,
836 });
837 }
838
839 let capacity = usize::try_from(claimed_size).unwrap_or(usize::MAX);
840 let mut contents = Vec::with_capacity(capacity);
841
842 file.read()
843 .and_then(|reader| reader.take(limit).read_to_end(&mut contents))
844 .map_err(|error| RecordReadError::Decompress {
845 file_name: file_name.to_string(),
846 error,
847 })?;
848
849 let actual_size = contents.len() as u64;
850 if actual_size != claimed_size {
851 return Err(RecordReadError::SizeMismatch {
852 file_name: file_name.to_string(),
853 claimed_size,
854 actual_size,
855 });
856 }
857
858 Ok(contents)
859 }
860
861 fn get_dict_for_output(&self, file_name: &str) -> Option<&[u8]> {
863 match OutputDict::for_output_file_name(file_name) {
864 OutputDict::Stdout => Some(
865 self.stdout_dict
866 .as_ref()
867 .expect("load_dictionaries must be called first"),
868 ),
869 OutputDict::Stderr => Some(
870 self.stderr_dict
871 .as_ref()
872 .expect("load_dictionaries must be called first"),
873 ),
874 OutputDict::None => None,
875 }
876 }
877}
878
879impl StoreReader for PortableStoreReader<'_> {
880 fn read_cargo_metadata(&mut self) -> Result<String, RecordReadError> {
881 let bytes = self.read_store_file(CARGO_METADATA_JSON_PATH)?;
882 String::from_utf8(bytes).map_err(|e| RecordReadError::Decompress {
883 file_name: CARGO_METADATA_JSON_PATH.to_string(),
884 error: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
885 })
886 }
887
888 fn read_test_list(&mut self) -> Result<TestListSummary, RecordReadError> {
889 let bytes = self.read_store_file(TEST_LIST_JSON_PATH)?;
890 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
891 file_name: TEST_LIST_JSON_PATH.to_string(),
892 error,
893 })
894 }
895
896 fn read_record_opts(&mut self) -> Result<RecordOpts, RecordReadError> {
897 let bytes = self.read_store_file(RECORD_OPTS_JSON_PATH)?;
898 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
899 file_name: RECORD_OPTS_JSON_PATH.to_string(),
900 error,
901 })
902 }
903
904 fn read_build_scope(&mut self) -> Result<RecordedBuildScope, RecordReadError> {
905 let bytes = self.read_store_file(BUILD_SCOPE_JSON_PATH)?;
906 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
907 file_name: BUILD_SCOPE_JSON_PATH.to_string(),
908 error,
909 })
910 }
911
912 fn read_rerun_info(&mut self) -> Result<Option<RerunInfo>, RecordReadError> {
913 match self.read_store_file(RERUN_INFO_JSON_PATH) {
914 Ok(bytes) => {
915 let info = serde_json::from_slice(&bytes).map_err(|error| {
916 RecordReadError::DeserializeMetadata {
917 file_name: RERUN_INFO_JSON_PATH.to_string(),
918 error,
919 }
920 })?;
921 Ok(Some(info))
922 }
923 Err(RecordReadError::FileNotFound { .. }) => {
924 Ok(None)
926 }
927 Err(e) => Err(e),
928 }
929 }
930
931 fn load_dictionaries(&mut self) -> Result<(), RecordReadError> {
932 self.stdout_dict = Some(self.read_store_file(STDOUT_DICT_PATH)?);
933 self.stderr_dict = Some(self.read_store_file(STDERR_DICT_PATH)?);
934 Ok(())
935 }
936
937 fn read_output(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError> {
938 let path = format!("out/{file_name}");
939 let compressed = self.read_store_file(&path)?;
940 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
941
942 let dict_bytes = self.get_dict_for_output(file_name).ok_or_else(|| {
943 RecordReadError::UnknownOutputType {
944 file_name: file_name.to_owned(),
945 }
946 })?;
947
948 decompress_with_dict(&compressed, dict_bytes, limit).map_err(|error| {
949 RecordReadError::Decompress {
950 file_name: path,
951 error,
952 }
953 })
954 }
955
956 fn extract_file_to_path(
957 &mut self,
958 store_path: &str,
959 output_path: &Utf8Path,
960 ) -> Result<u64, RecordReadError> {
961 let mut file = self.store_archive.get_by_name(store_path).ok_or_else(|| {
962 RecordReadError::FileNotFound {
963 file_name: store_path.to_owned(),
964 }
965 })?;
966
967 let mut output_file =
968 File::create(output_path).map_err(|error| RecordReadError::ExtractFile {
969 store_path: store_path.to_owned(),
970 output_path: output_path.to_owned(),
971 error,
972 })?;
973
974 let mut reader = file
975 .read()
976 .map_err(|error| RecordReadError::ReadArchiveFile {
977 file_name: store_path.to_owned(),
978 error,
979 })?;
980
981 io::copy(&mut reader, &mut output_file).map_err(|error| RecordReadError::ExtractFile {
982 store_path: store_path.to_owned(),
983 output_path: output_path.to_owned(),
984 error,
985 })
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992 use crate::record::{
993 format::{PORTABLE_RECORDING_FORMAT_VERSION, STORE_FORMAT_VERSION},
994 store::{CompletedRunStats, RecordedRunStatus, RecordedSizes},
995 };
996 use camino_tempfile::{NamedUtf8TempFile, Utf8TempDir};
997 use chrono::Local;
998 use eazip::write::FileOptions;
999 use quick_junit::ReportUuid;
1000 use semver::Version;
1001 use std::{collections::BTreeMap, io::Read};
1002
1003 fn create_test_run_dir(run_id: ReportUuid) -> (Utf8TempDir, Utf8PathBuf) {
1004 let temp_dir = camino_tempfile::tempdir().expect("create temp dir");
1005 let runs_dir = temp_dir.path().to_owned();
1006 let run_dir = runs_dir.join(run_id.to_string());
1007 std::fs::create_dir_all(&run_dir).expect("create run dir");
1008
1009 let store_path = run_dir.join(STORE_ZIP_FILE_NAME);
1010 let store_file = File::create(&store_path).expect("create store.zip");
1011 let mut zip_writer = ArchiveWriter::new(store_file);
1012 let options = FileOptions::default();
1013 zip_writer
1014 .add_file("test.txt", &b"test content"[..], &options)
1015 .expect("add file");
1016 zip_writer.finish().expect("finish zip");
1017
1018 let log_path = run_dir.join(RUN_LOG_FILE_NAME);
1019 let log_file = File::create(&log_path).expect("create run.log.zst");
1020 let mut encoder = zstd::stream::Encoder::new(log_file, 3).expect("create encoder");
1021 encoder.write_all(b"test log content").expect("write log");
1022 encoder.finish().expect("finish encoder");
1023
1024 (temp_dir, runs_dir)
1025 }
1026
1027 fn create_test_run_info(run_id: ReportUuid) -> RecordedRunInfo {
1028 let now = Local::now().fixed_offset();
1029 RecordedRunInfo {
1030 run_id,
1031 store_format_version: STORE_FORMAT_VERSION,
1032 nextest_version: Version::new(0, 9, 111),
1033 started_at: now,
1034 last_written_at: now,
1035 duration_secs: Some(12.345),
1036 cli_args: vec!["cargo".to_owned(), "nextest".to_owned(), "run".to_owned()],
1037 env_vars: BTreeMap::from([("CARGO_TERM_COLOR".to_owned(), "always".to_owned())]),
1038 parent_run_id: None,
1039 sizes: RecordedSizes::default(),
1040 status: RecordedRunStatus::Completed(CompletedRunStats {
1041 initial_run_count: 10,
1042 passed: 9,
1043 failed: 1,
1044 exit_code: 100,
1045 }),
1046 }
1047 }
1048
1049 #[test]
1050 fn test_default_filename() {
1051 let run_id = ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000);
1052 let (_temp_dir, runs_dir) = create_test_run_dir(run_id);
1053 let run_info = create_test_run_info(run_id);
1054
1055 let writer = PortableRecordingWriter::new(&run_info, StoreRunsDir::new(&runs_dir))
1056 .expect("create writer");
1057
1058 assert_eq!(
1059 writer.default_filename(),
1060 "nextest-run-550e8400-e29b-41d4-a716-446655440000.zip"
1061 );
1062 }
1063
1064 #[test]
1065 fn test_write_portable_recording() {
1066 let run_id = ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000);
1067 let (_temp_dir, runs_dir) = create_test_run_dir(run_id);
1068 let run_info = create_test_run_info(run_id);
1069
1070 let writer = PortableRecordingWriter::new(&run_info, StoreRunsDir::new(&runs_dir))
1071 .expect("create writer");
1072
1073 let output_dir = camino_tempfile::tempdir().expect("create output dir");
1074
1075 let result = writer
1076 .write_to_dir(output_dir.path())
1077 .expect("write archive");
1078
1079 assert!(result.path.exists());
1080 assert!(result.size > 0);
1081
1082 let actual_size = std::fs::metadata(&result.path)
1084 .expect("get file metadata")
1085 .len();
1086 assert_eq!(
1087 result.size, actual_size,
1088 "reported size should match actual file size"
1089 );
1090
1091 assert_eq!(
1092 result.path.file_name(),
1093 Some("nextest-run-550e8400-e29b-41d4-a716-446655440000.zip")
1094 );
1095
1096 let archive_file = File::open(&result.path).expect("open archive");
1097 let mut archive = Archive::new(BufReader::new(archive_file)).expect("read archive");
1098
1099 assert_eq!(archive.entries().len(), 3);
1100
1101 {
1102 let mut manifest_file = archive
1103 .get_by_name(PORTABLE_MANIFEST_FILE_NAME)
1104 .expect("manifest");
1105 let mut manifest_content = String::new();
1106 manifest_file
1107 .read()
1108 .expect("get reader")
1109 .read_to_string(&mut manifest_content)
1110 .expect("read manifest");
1111 let manifest: PortableManifest =
1112 serde_json::from_str(&manifest_content).expect("parse manifest");
1113 assert_eq!(manifest.format_version, PORTABLE_RECORDING_FORMAT_VERSION);
1114 assert_eq!(manifest.run.run_id, run_id);
1115 }
1116
1117 {
1118 let store_file = archive.get_by_name(STORE_ZIP_FILE_NAME).expect("store.zip");
1119 assert!(store_file.metadata().uncompressed_size > 0);
1120 }
1121
1122 {
1123 let log_file = archive.get_by_name(RUN_LOG_FILE_NAME).expect("run.log.zst");
1124 assert!(log_file.metadata().uncompressed_size > 0);
1125 }
1126 }
1127
1128 #[test]
1129 fn test_missing_run_dir() {
1130 let run_id = ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000);
1131 let temp_dir = camino_tempfile::tempdir().expect("create temp dir");
1132 let runs_dir = temp_dir.path().to_owned();
1133 let run_info = create_test_run_info(run_id);
1134
1135 let result = PortableRecordingWriter::new(&run_info, StoreRunsDir::new(&runs_dir));
1136
1137 assert!(matches!(
1138 result,
1139 Err(PortableRecordingError::RunDirNotFound { .. })
1140 ));
1141 }
1142
1143 #[test]
1144 fn test_missing_store_zip() {
1145 let run_id = ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000);
1146 let temp_dir = camino_tempfile::tempdir().expect("create temp dir");
1147 let runs_dir = temp_dir.path().to_owned();
1148 let run_dir = runs_dir.join(run_id.to_string());
1149 std::fs::create_dir_all(&run_dir).expect("create run dir");
1150
1151 let log_path = run_dir.join(RUN_LOG_FILE_NAME);
1152 let log_file = File::create(&log_path).expect("create run.log.zst");
1153 let mut encoder = zstd::stream::Encoder::new(log_file, 3).expect("create encoder");
1154 encoder.write_all(b"test").expect("write");
1155 encoder.finish().expect("finish");
1156
1157 let run_info = create_test_run_info(run_id);
1158 let result = PortableRecordingWriter::new(&run_info, StoreRunsDir::new(&runs_dir));
1159
1160 assert!(
1161 matches!(
1162 &result,
1163 Err(PortableRecordingError::RequiredFileMissing { file_name, .. })
1164 if *file_name == STORE_ZIP_FILE_NAME
1165 ),
1166 "expected RequiredFileMissing for store.zip, got {result:?}"
1167 );
1168 }
1169
1170 #[test]
1171 fn test_missing_run_log() {
1172 let run_id = ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000);
1173 let temp_dir = camino_tempfile::tempdir().expect("create temp dir");
1174 let runs_dir = temp_dir.path().to_owned();
1175 let run_dir = runs_dir.join(run_id.to_string());
1176 std::fs::create_dir_all(&run_dir).expect("create run dir");
1177
1178 let store_path = run_dir.join(STORE_ZIP_FILE_NAME);
1179 let store_file = File::create(&store_path).expect("create store.zip");
1180 let mut zip_writer = ArchiveWriter::new(store_file);
1181 let options = FileOptions::default();
1182 zip_writer
1183 .add_file("test.txt", &b"test"[..], &options)
1184 .expect("add file");
1185 zip_writer.finish().expect("finish");
1186
1187 let run_info = create_test_run_info(run_id);
1188 let result = PortableRecordingWriter::new(&run_info, StoreRunsDir::new(&runs_dir));
1189
1190 assert!(
1191 matches!(
1192 &result,
1193 Err(PortableRecordingError::RequiredFileMissing { file_name, .. })
1194 if *file_name == RUN_LOG_FILE_NAME
1195 ),
1196 "expected RequiredFileMissing for run.log.zst, got {result:?}"
1197 );
1198 }
1199
1200 #[test]
1201 fn test_ensure_seekable_regular_file() {
1202 let temp = NamedUtf8TempFile::new().expect("created temp file");
1204 let path = temp.path().to_owned();
1205
1206 std::fs::write(&path, b"hello world").expect("wrote to temp file");
1207 let file = File::open(&path).expect("opened temp file");
1208
1209 #[cfg(unix)]
1211 let original_fd = {
1212 use std::os::unix::io::AsRawFd;
1213 file.as_raw_fd()
1214 };
1215
1216 let result = ensure_seekable(file, &path).expect("ensure_seekable succeeded");
1217
1218 #[cfg(unix)]
1220 {
1221 use std::os::unix::io::AsRawFd;
1222 assert_eq!(
1223 result.as_raw_fd(),
1224 original_fd,
1225 "seekable file should be returned as-is"
1226 );
1227 }
1228
1229 let mut contents = String::new();
1231 let mut reader = io::BufReader::new(result);
1232 reader
1233 .read_to_string(&mut contents)
1234 .expect("read file contents");
1235 assert_eq!(contents, "hello world");
1236 }
1237
1238 #[cfg(unix)]
1241 fn pipe_reader_to_file(reader: std::io::PipeReader) -> File {
1242 use std::os::fd::OwnedFd;
1243 File::from(OwnedFd::from(reader))
1244 }
1245
1246 #[cfg(windows)]
1249 fn pipe_reader_to_file(reader: std::io::PipeReader) -> File {
1250 use std::os::windows::io::OwnedHandle;
1251 File::from(OwnedHandle::from(reader))
1252 }
1253
1254 #[test]
1259 fn test_ensure_seekable_pipe() {
1260 let (pipe_reader, mut pipe_writer) = std::io::pipe().expect("created pipe");
1261 let test_data = b"zip-like test content for pipe spooling";
1262
1263 pipe_writer.write_all(test_data).expect("wrote to pipe");
1265 drop(pipe_writer);
1266
1267 let pipe_file = pipe_reader_to_file(pipe_reader);
1268
1269 let path = Utf8Path::new("/dev/fd/99");
1270 let result = ensure_seekable(pipe_file, path).expect("ensure_seekable succeeded");
1271
1272 let mut contents = Vec::new();
1274 let mut reader = io::BufReader::new(result);
1275 reader
1276 .read_to_end(&mut contents)
1277 .expect("read spooled contents");
1278 assert_eq!(contents, test_data);
1279 }
1280
1281 #[test]
1287 fn test_ensure_seekable_empty_pipe() {
1288 let (pipe_reader, pipe_writer) = std::io::pipe().expect("created pipe");
1289 drop(pipe_writer);
1291
1292 let pipe_file = pipe_reader_to_file(pipe_reader);
1293 let path = Utf8Path::new("/dev/fd/42");
1294 let mut result = ensure_seekable(pipe_file, path).expect("empty pipe should succeed");
1295
1296 let mut contents = Vec::new();
1297 result.read_to_end(&mut contents).expect("read contents");
1298 assert!(contents.is_empty());
1299 }
1300
1301 #[test]
1306 fn test_ensure_seekable_spool_too_large() {
1307 let (pipe_reader, mut pipe_writer) = std::io::pipe().expect("created pipe");
1308
1309 pipe_writer
1311 .write_all(b"01234567890123456789")
1312 .expect("wrote to pipe");
1313 drop(pipe_writer);
1314
1315 let pipe_file = pipe_reader_to_file(pipe_reader);
1316
1317 let path = Utf8Path::new("/dev/fd/42");
1318 let result = ensure_seekable_impl(pipe_file, path, ByteSize(10));
1319 assert!(
1320 matches!(
1321 &result,
1322 Err(PortableRecordingReadError::SpoolTooLarge {
1323 limit: ByteSize(10),
1324 ..
1325 })
1326 ),
1327 "expected SpoolTooLarge, got {result:?}"
1328 );
1329 }
1330
1331 #[test]
1336 fn test_ensure_seekable_spool_one_over_limit() {
1337 let (pipe_reader, mut pipe_writer) = std::io::pipe().expect("created pipe");
1338
1339 pipe_writer
1341 .write_all(b"01234567890")
1342 .expect("wrote to pipe");
1343 drop(pipe_writer);
1344
1345 let pipe_file = pipe_reader_to_file(pipe_reader);
1346
1347 let path = Utf8Path::new("/dev/fd/42");
1348 let result = ensure_seekable_impl(pipe_file, path, ByteSize(10));
1349 assert!(
1350 matches!(
1351 &result,
1352 Err(PortableRecordingReadError::SpoolTooLarge {
1353 limit: ByteSize(10),
1354 ..
1355 })
1356 ),
1357 "expected SpoolTooLarge at limit+1 bytes, got {result:?}"
1358 );
1359 }
1360
1361 #[test]
1363 fn test_ensure_seekable_spool_exact_limit() {
1364 let (pipe_reader, mut pipe_writer) = std::io::pipe().expect("created pipe");
1365
1366 pipe_writer.write_all(b"0123456789").expect("wrote to pipe");
1368 drop(pipe_writer);
1369
1370 let pipe_file = pipe_reader_to_file(pipe_reader);
1371
1372 let path = Utf8Path::new("/dev/fd/42");
1373 let mut result = ensure_seekable_impl(pipe_file, path, ByteSize(10))
1374 .expect("exact limit should succeed");
1375
1376 let mut contents = Vec::new();
1378 result.read_to_end(&mut contents).expect("read contents");
1379 assert_eq!(contents, b"0123456789");
1380 }
1381}