1use crate::{
15 config::scripts::ScriptId,
16 list::OwnedTestInstanceId,
17 reporter::{
18 TestOutputDisplay,
19 events::{
20 CancelReason, ExecuteStatus, ExecutionStatuses, RetryData, RunFinishedStats, RunStats,
21 SetupScriptExecuteStatus, StressIndex, StressProgress, TestEvent, TestEventKind,
22 },
23 },
24 run_mode::NextestRunMode,
25 runner::StressCondition,
26 test_output::ChildSingleOutput,
27};
28use chrono::{DateTime, FixedOffset};
29use nextest_metadata::MismatchReason;
30use quick_junit::ReportUuid;
31use serde::{Deserialize, Serialize};
32use std::{fmt, num::NonZero, time::Duration};
33
34#[derive(Clone, Debug, Default, Deserialize, Serialize)]
43#[serde(rename_all = "kebab-case")]
44#[non_exhaustive]
45pub struct RecordOpts {
46 #[serde(default)]
48 pub run_mode: NextestRunMode,
49}
50
51impl RecordOpts {
52 pub fn new(run_mode: NextestRunMode) -> Self {
54 Self { run_mode }
55 }
56}
57
58#[derive(Deserialize, Serialize, Debug, PartialEq)]
71#[serde(
72 rename_all = "kebab-case",
73 bound(
74 serialize = "O: Serialize",
75 deserialize = "O: serde::de::DeserializeOwned"
76 )
77)]
78#[cfg_attr(
79 test,
80 derive(test_strategy::Arbitrary),
81 arbitrary(bound(O: proptest::arbitrary::Arbitrary + PartialEq + 'static))
82)]
83pub struct TestEventSummary<O> {
84 #[cfg_attr(
86 test,
87 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
88 )]
89 pub timestamp: DateTime<FixedOffset>,
90
91 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
93 pub elapsed: Duration,
94
95 pub kind: TestEventKindSummary<O>,
97}
98
99impl TestEventSummary<ChildSingleOutput> {
100 pub(crate) fn from_test_event(event: TestEvent<'_>) -> Option<Self> {
105 let kind = TestEventKindSummary::from_test_event_kind(event.kind)?;
106 Some(Self {
107 timestamp: event.timestamp,
108 elapsed: event.elapsed,
109 kind,
110 })
111 }
112}
113
114#[derive(Deserialize, Serialize, Debug, PartialEq)]
121#[serde(tag = "type", rename_all = "kebab-case")]
122#[cfg_attr(
123 test,
124 derive(test_strategy::Arbitrary),
125 arbitrary(bound(O: proptest::arbitrary::Arbitrary + PartialEq + 'static))
126)]
127pub enum TestEventKindSummary<O> {
128 Core(CoreEventKind),
130 Output(OutputEventKind<O>),
132}
133
134#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
139#[serde(tag = "kind", rename_all = "kebab-case")]
140#[cfg_attr(test, derive(test_strategy::Arbitrary))]
141pub enum CoreEventKind {
142 #[serde(rename_all = "kebab-case")]
144 RunStarted {
145 run_id: ReportUuid,
147 profile_name: String,
149 cli_args: Vec<String>,
151 stress_condition: Option<StressConditionSummary>,
153 },
154
155 #[serde(rename_all = "kebab-case")]
157 StressSubRunStarted {
158 progress: StressProgress,
160 },
161
162 #[serde(rename_all = "kebab-case")]
164 SetupScriptStarted {
165 stress_index: Option<StressIndexSummary>,
167 index: usize,
169 total: usize,
171 script_id: ScriptId,
173 program: String,
175 args: Vec<String>,
177 no_capture: bool,
179 },
180
181 #[serde(rename_all = "kebab-case")]
183 SetupScriptSlow {
184 stress_index: Option<StressIndexSummary>,
186 script_id: ScriptId,
188 program: String,
190 args: Vec<String>,
192 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
194 elapsed: Duration,
195 will_terminate: bool,
197 },
198
199 #[serde(rename_all = "kebab-case")]
201 TestStarted {
202 stress_index: Option<StressIndexSummary>,
204 test_instance: OwnedTestInstanceId,
206 current_stats: RunStats,
208 running: usize,
210 command_line: Vec<String>,
212 },
213
214 #[serde(rename_all = "kebab-case")]
216 TestSlow {
217 stress_index: Option<StressIndexSummary>,
219 test_instance: OwnedTestInstanceId,
221 retry_data: RetryData,
223 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
225 elapsed: Duration,
226 will_terminate: bool,
228 },
229
230 #[serde(rename_all = "kebab-case")]
232 TestRetryStarted {
233 stress_index: Option<StressIndexSummary>,
235 test_instance: OwnedTestInstanceId,
237 retry_data: RetryData,
239 running: usize,
241 command_line: Vec<String>,
243 },
244
245 #[serde(rename_all = "kebab-case")]
247 TestSkipped {
248 stress_index: Option<StressIndexSummary>,
250 test_instance: OwnedTestInstanceId,
252 reason: MismatchReason,
254 },
255
256 #[serde(rename_all = "kebab-case")]
258 RunBeginCancel {
259 setup_scripts_running: usize,
261 running: usize,
263 reason: CancelReason,
265 },
266
267 #[serde(rename_all = "kebab-case")]
269 RunPaused {
270 setup_scripts_running: usize,
272 running: usize,
274 },
275
276 #[serde(rename_all = "kebab-case")]
278 RunContinued {
279 setup_scripts_running: usize,
281 running: usize,
283 },
284
285 #[serde(rename_all = "kebab-case")]
287 StressSubRunFinished {
288 progress: StressProgress,
290 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
292 sub_elapsed: Duration,
293 sub_stats: RunStats,
295 },
296
297 #[serde(rename_all = "kebab-case")]
299 RunFinished {
300 run_id: ReportUuid,
302 #[cfg_attr(
304 test,
305 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
306 )]
307 start_time: DateTime<FixedOffset>,
308 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
310 elapsed: Duration,
311 run_stats: RunFinishedStats,
313 outstanding_not_seen: Option<TestsNotSeenSummary>,
315 },
316}
317
318#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
320#[serde(rename_all = "kebab-case")]
321#[cfg_attr(test, derive(test_strategy::Arbitrary))]
322pub struct TestsNotSeenSummary {
323 pub not_seen: Vec<OwnedTestInstanceId>,
325 pub total_not_seen: usize,
327}
328
329#[derive(Deserialize, Serialize, Debug, PartialEq)]
334#[serde(tag = "kind", rename_all = "kebab-case")]
335#[cfg_attr(
336 test,
337 derive(test_strategy::Arbitrary),
338 arbitrary(bound(O: proptest::arbitrary::Arbitrary + PartialEq + 'static))
339)]
340pub enum OutputEventKind<O> {
341 #[serde(rename_all = "kebab-case")]
343 SetupScriptFinished {
344 stress_index: Option<StressIndexSummary>,
346 index: usize,
348 total: usize,
350 script_id: ScriptId,
352 program: String,
354 args: Vec<String>,
356 no_capture: bool,
358 run_status: SetupScriptExecuteStatus<O>,
360 },
361
362 #[serde(rename_all = "kebab-case")]
364 TestAttemptFailedWillRetry {
365 stress_index: Option<StressIndexSummary>,
367 test_instance: OwnedTestInstanceId,
369 run_status: ExecuteStatus<O>,
371 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
373 delay_before_next_attempt: Duration,
374 failure_output: TestOutputDisplay,
376 running: usize,
378 },
379
380 #[serde(rename_all = "kebab-case")]
382 TestFinished {
383 stress_index: Option<StressIndexSummary>,
385 test_instance: OwnedTestInstanceId,
387 success_output: TestOutputDisplay,
389 failure_output: TestOutputDisplay,
391 junit_store_success_output: bool,
393 junit_store_failure_output: bool,
395 run_statuses: ExecutionStatuses<O>,
397 current_stats: RunStats,
399 running: usize,
401 },
402}
403
404impl TestEventKindSummary<ChildSingleOutput> {
405 fn from_test_event_kind(kind: TestEventKind<'_>) -> Option<Self> {
406 Some(match kind {
407 TestEventKind::RunStarted {
408 run_id,
409 test_list: _,
410 profile_name,
411 cli_args,
412 stress_condition,
413 } => Self::Core(CoreEventKind::RunStarted {
414 run_id,
415 profile_name,
416 cli_args,
417 stress_condition: stress_condition.map(StressConditionSummary::from),
418 }),
419 TestEventKind::StressSubRunStarted { progress } => {
420 Self::Core(CoreEventKind::StressSubRunStarted { progress })
421 }
422 TestEventKind::SetupScriptStarted {
423 stress_index,
424 index,
425 total,
426 script_id,
427 program,
428 args,
429 no_capture,
430 } => Self::Core(CoreEventKind::SetupScriptStarted {
431 stress_index: stress_index.map(StressIndexSummary::from),
432 index,
433 total,
434 script_id,
435 program,
436 args: args.to_vec(),
437 no_capture,
438 }),
439 TestEventKind::SetupScriptSlow {
440 stress_index,
441 script_id,
442 program,
443 args,
444 elapsed,
445 will_terminate,
446 } => Self::Core(CoreEventKind::SetupScriptSlow {
447 stress_index: stress_index.map(StressIndexSummary::from),
448 script_id,
449 program,
450 args: args.to_vec(),
451 elapsed,
452 will_terminate,
453 }),
454 TestEventKind::TestStarted {
455 stress_index,
456 test_instance,
457 current_stats,
458 running,
459 command_line,
460 } => Self::Core(CoreEventKind::TestStarted {
461 stress_index: stress_index.map(StressIndexSummary::from),
462 test_instance: test_instance.to_owned(),
463 current_stats,
464 running,
465 command_line,
466 }),
467 TestEventKind::TestSlow {
468 stress_index,
469 test_instance,
470 retry_data,
471 elapsed,
472 will_terminate,
473 } => Self::Core(CoreEventKind::TestSlow {
474 stress_index: stress_index.map(StressIndexSummary::from),
475 test_instance: test_instance.to_owned(),
476 retry_data,
477 elapsed,
478 will_terminate,
479 }),
480 TestEventKind::TestRetryStarted {
481 stress_index,
482 test_instance,
483 retry_data,
484 running,
485 command_line,
486 } => Self::Core(CoreEventKind::TestRetryStarted {
487 stress_index: stress_index.map(StressIndexSummary::from),
488 test_instance: test_instance.to_owned(),
489 retry_data,
490 running,
491 command_line,
492 }),
493 TestEventKind::TestSkipped {
494 stress_index,
495 test_instance,
496 reason,
497 } => Self::Core(CoreEventKind::TestSkipped {
498 stress_index: stress_index.map(StressIndexSummary::from),
499 test_instance: test_instance.to_owned(),
500 reason,
501 }),
502 TestEventKind::RunBeginCancel {
503 setup_scripts_running,
504 current_stats,
505 running,
506 } => Self::Core(CoreEventKind::RunBeginCancel {
507 setup_scripts_running,
508 running,
509 reason: current_stats
510 .cancel_reason
511 .expect("RunBeginCancel event has cancel reason"),
512 }),
513 TestEventKind::RunPaused {
514 setup_scripts_running,
515 running,
516 } => Self::Core(CoreEventKind::RunPaused {
517 setup_scripts_running,
518 running,
519 }),
520 TestEventKind::RunContinued {
521 setup_scripts_running,
522 running,
523 } => Self::Core(CoreEventKind::RunContinued {
524 setup_scripts_running,
525 running,
526 }),
527 TestEventKind::StressSubRunFinished {
528 progress,
529 sub_elapsed,
530 sub_stats,
531 } => Self::Core(CoreEventKind::StressSubRunFinished {
532 progress,
533 sub_elapsed,
534 sub_stats,
535 }),
536 TestEventKind::RunFinished {
537 run_id,
538 start_time,
539 elapsed,
540 run_stats,
541 outstanding_not_seen,
542 } => Self::Core(CoreEventKind::RunFinished {
543 run_id,
544 start_time,
545 elapsed,
546 run_stats,
547 outstanding_not_seen: outstanding_not_seen.map(|t| TestsNotSeenSummary {
548 not_seen: t.not_seen,
549 total_not_seen: t.total_not_seen,
550 }),
551 }),
552
553 TestEventKind::SetupScriptFinished {
554 stress_index,
555 index,
556 total,
557 script_id,
558 program,
559 args,
560 junit_store_success_output: _,
561 junit_store_failure_output: _,
562 no_capture,
563 run_status,
564 } => Self::Output(OutputEventKind::SetupScriptFinished {
565 stress_index: stress_index.map(StressIndexSummary::from),
566 index,
567 total,
568 script_id,
569 program,
570 args: args.to_vec(),
571 no_capture,
572 run_status,
573 }),
574 TestEventKind::TestAttemptFailedWillRetry {
575 stress_index,
576 test_instance,
577 run_status,
578 delay_before_next_attempt,
579 failure_output,
580 running,
581 } => Self::Output(OutputEventKind::TestAttemptFailedWillRetry {
582 stress_index: stress_index.map(StressIndexSummary::from),
583 test_instance: test_instance.to_owned(),
584 run_status,
585 delay_before_next_attempt,
586 failure_output,
587 running,
588 }),
589 TestEventKind::TestFinished {
590 stress_index,
591 test_instance,
592 success_output,
593 failure_output,
594 junit_store_success_output,
595 junit_store_failure_output,
596 run_statuses,
597 current_stats,
598 running,
599 } => Self::Output(OutputEventKind::TestFinished {
600 stress_index: stress_index.map(StressIndexSummary::from),
601 test_instance: test_instance.to_owned(),
602 success_output,
603 failure_output,
604 junit_store_success_output,
605 junit_store_failure_output,
606 run_statuses,
607 current_stats,
608 running,
609 }),
610
611 TestEventKind::InfoStarted { .. }
612 | TestEventKind::InfoResponse { .. }
613 | TestEventKind::InfoFinished { .. }
614 | TestEventKind::InputEnter { .. }
615 | TestEventKind::RunBeginKill { .. } => return None,
616 })
617 }
618}
619
620#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
622#[serde(rename_all = "kebab-case")]
623#[cfg_attr(test, derive(test_strategy::Arbitrary))]
624pub struct StressIndexSummary {
625 pub current: u32,
627 pub total: Option<NonZero<u32>>,
629}
630
631impl From<StressIndex> for StressIndexSummary {
632 fn from(index: StressIndex) -> Self {
633 Self {
634 current: index.current,
635 total: index.total,
636 }
637 }
638}
639
640#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
642#[serde(tag = "type", rename_all = "kebab-case")]
643#[cfg_attr(test, derive(test_strategy::Arbitrary))]
644pub enum StressConditionSummary {
645 Count {
647 count: Option<u32>,
649 },
650 Duration {
652 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
654 duration: Duration,
655 },
656}
657
658impl From<StressCondition> for StressConditionSummary {
659 fn from(condition: StressCondition) -> Self {
660 use crate::runner::StressCount;
661 match condition {
662 StressCondition::Count(count) => Self::Count {
663 count: match count {
664 StressCount::Count { count: n } => Some(n.get()),
665 StressCount::Infinite => None,
666 },
667 },
668 StressCondition::Duration(duration) => Self::Duration { duration },
669 }
670 }
671}
672
673#[derive(Clone, Copy, Debug, PartialEq, Eq)]
678pub(crate) enum OutputKind {
679 Stdout,
681 Stderr,
683 Combined,
685}
686
687impl OutputKind {
688 pub(crate) fn as_str(self) -> &'static str {
690 match self {
691 Self::Stdout => "stdout",
692 Self::Stderr => "stderr",
693 Self::Combined => "combined",
694 }
695 }
696}
697
698#[derive(Clone, Debug, PartialEq, Eq)]
709pub struct OutputFileName(String);
710
711impl OutputFileName {
712 pub(crate) fn from_content(content: &[u8], kind: OutputKind) -> Self {
717 let hash = xxhash_rust::xxh3::xxh3_64(content);
718 Self(format!("{hash:016x}-{}", kind.as_str()))
719 }
720
721 pub fn as_str(&self) -> &str {
723 &self.0
724 }
725
726 fn validate(s: &str) -> bool {
730 if s.contains('/') || s.contains('\\') || s.contains("..") {
731 return false;
732 }
733
734 let valid_suffixes = ["-stdout", "-stderr", "-combined"];
735 for suffix in valid_suffixes {
736 if let Some(hash_part) = s.strip_suffix(suffix)
737 && hash_part.len() == 16
738 && hash_part
739 .chars()
740 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
741 {
742 return true;
743 }
744 }
745
746 false
747 }
748}
749
750impl fmt::Display for OutputFileName {
751 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752 f.write_str(&self.0)
753 }
754}
755
756impl AsRef<str> for OutputFileName {
757 fn as_ref(&self) -> &str {
758 &self.0
759 }
760}
761
762impl Serialize for OutputFileName {
763 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
764 where
765 S: serde::Serializer,
766 {
767 self.0.serialize(serializer)
768 }
769}
770
771impl<'de> Deserialize<'de> for OutputFileName {
772 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
773 where
774 D: serde::Deserializer<'de>,
775 {
776 let s = String::deserialize(deserializer)?;
777 if Self::validate(&s) {
778 Ok(Self(s))
779 } else {
780 Err(serde::de::Error::custom(format!(
781 "invalid output file name: {s}"
782 )))
783 }
784 }
785}
786
787#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
789#[serde(tag = "status", rename_all = "kebab-case")]
790pub enum ZipStoreOutput {
791 Empty,
793
794 #[serde(rename_all = "kebab-case")]
796 Full {
797 file_name: OutputFileName,
799 },
800
801 #[serde(rename_all = "kebab-case")]
803 Truncated {
804 file_name: OutputFileName,
806 original_size: u64,
808 },
809}
810
811impl ZipStoreOutput {
812 pub fn file_name(&self) -> Option<&OutputFileName> {
814 match self {
815 ZipStoreOutput::Empty => None,
816 ZipStoreOutput::Full { file_name } | ZipStoreOutput::Truncated { file_name, .. } => {
817 Some(file_name)
818 }
819 }
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use test_strategy::proptest;
827
828 #[proptest]
829 fn test_event_summary_roundtrips(value: TestEventSummary<ZipStoreOutput>) {
830 let json = serde_json::to_string(&value).expect("serialization succeeds");
831 let roundtrip: TestEventSummary<ZipStoreOutput> =
832 serde_json::from_str(&json).expect("deserialization succeeds");
833 proptest::prop_assert_eq!(value, roundtrip);
834 }
835
836 #[test]
837 fn test_output_file_name_from_content_stdout() {
838 let content = b"hello world";
839 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
840
841 let s = file_name.as_str();
842 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
843 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stdout'");
844
845 let hash_part = &s[..16];
846 assert!(
847 hash_part.chars().all(|c| c.is_ascii_hexdigit()),
848 "hash portion should be hex: {hash_part}"
849 );
850 }
851
852 #[test]
853 fn test_output_file_name_from_content_stderr() {
854 let content = b"error message";
855 let file_name = OutputFileName::from_content(content, OutputKind::Stderr);
856
857 let s = file_name.as_str();
858 assert!(s.ends_with("-stderr"), "should end with -stderr: {s}");
859 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stderr'");
860 }
861
862 #[test]
863 fn test_output_file_name_from_content_combined() {
864 let content = b"combined output";
865 let file_name = OutputFileName::from_content(content, OutputKind::Combined);
866
867 let s = file_name.as_str();
868 assert!(s.ends_with("-combined"), "should end with -combined: {s}");
869 assert_eq!(
870 s.len(),
871 16 + 1 + 8,
872 "should be 16 hex + hyphen + 'combined'"
873 );
874 }
875
876 #[test]
877 fn test_output_file_name_deterministic() {
878 let content = b"deterministic content";
879 let name1 = OutputFileName::from_content(content, OutputKind::Stdout);
880 let name2 = OutputFileName::from_content(content, OutputKind::Stdout);
881 assert_eq!(name1.as_str(), name2.as_str());
882 }
883
884 #[test]
885 fn test_output_file_name_different_content_different_hash() {
886 let content1 = b"content one";
887 let content2 = b"content two";
888 let name1 = OutputFileName::from_content(content1, OutputKind::Stdout);
889 let name2 = OutputFileName::from_content(content2, OutputKind::Stdout);
890 assert_ne!(name1.as_str(), name2.as_str());
891 }
892
893 #[test]
894 fn test_output_file_name_same_content_different_kind() {
895 let content = b"same content";
896 let stdout = OutputFileName::from_content(content, OutputKind::Stdout);
897 let stderr = OutputFileName::from_content(content, OutputKind::Stderr);
898 assert_ne!(stdout.as_str(), stderr.as_str());
899
900 let stdout_hash = &stdout.as_str()[..16];
901 let stderr_hash = &stderr.as_str()[..16];
902 assert_eq!(stdout_hash, stderr_hash);
903 }
904
905 #[test]
906 fn test_output_file_name_empty_content() {
907 let file_name = OutputFileName::from_content(b"", OutputKind::Stdout);
908 let s = file_name.as_str();
909 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
910 assert!(OutputFileName::validate(s), "should be valid: {s}");
911 }
912
913 #[test]
914 fn test_output_file_name_validate_valid_content_addressed() {
915 assert!(OutputFileName::validate("0123456789abcdef-stdout"));
917 assert!(OutputFileName::validate("fedcba9876543210-stderr"));
918 assert!(OutputFileName::validate("aaaaaaaaaaaaaaaa-combined"));
919 assert!(OutputFileName::validate("0000000000000000-stdout"));
920 assert!(OutputFileName::validate("ffffffffffffffff-stderr"));
921 }
922
923 #[test]
924 fn test_output_file_name_validate_invalid_patterns() {
925 assert!(!OutputFileName::validate("0123456789abcde-stdout"));
927 assert!(!OutputFileName::validate("abc-stdout"));
928
929 assert!(!OutputFileName::validate("0123456789abcdef0-stdout"));
931
932 assert!(!OutputFileName::validate("0123456789abcdef-unknown"));
934 assert!(!OutputFileName::validate("0123456789abcdef-out"));
935 assert!(!OutputFileName::validate("0123456789abcdef"));
936
937 assert!(!OutputFileName::validate("0123456789abcdeg-stdout"));
939 assert!(!OutputFileName::validate("0123456789ABCDEF-stdout")); assert!(!OutputFileName::validate("../0123456789abcdef-stdout"));
943 assert!(!OutputFileName::validate("0123456789abcdef-stdout/"));
944 assert!(!OutputFileName::validate("foo/0123456789abcdef-stdout"));
945 assert!(!OutputFileName::validate("..\\0123456789abcdef-stdout"));
946 }
947
948 #[test]
949 fn test_output_file_name_validate_rejects_old_format() {
950 assert!(!OutputFileName::validate("test-abc123-1-stdout"));
952 assert!(!OutputFileName::validate("test-abc123-s5-1-stderr"));
953 assert!(!OutputFileName::validate("script-def456-stdout"));
954 assert!(!OutputFileName::validate("script-def456-s3-stderr"));
955 }
956
957 #[test]
958 fn test_output_file_name_serde_round_trip() {
959 let content = b"test content for serde";
960 let original = OutputFileName::from_content(content, OutputKind::Stdout);
961
962 let json = serde_json::to_string(&original).expect("serialization failed");
963 let deserialized: OutputFileName =
964 serde_json::from_str(&json).expect("deserialization failed");
965
966 assert_eq!(original.as_str(), deserialized.as_str());
967 }
968
969 #[test]
970 fn test_output_file_name_deserialize_invalid() {
971 let json = r#""invalid-file-name""#;
973 let result: Result<OutputFileName, _> = serde_json::from_str(json);
974 assert!(
975 result.is_err(),
976 "should fail to deserialize invalid pattern"
977 );
978
979 let json = r#""test-abc123-1-stdout""#; let result: Result<OutputFileName, _> = serde_json::from_str(json);
981 assert!(result.is_err(), "should reject old format");
982 }
983
984 #[test]
985 fn test_zip_store_output_file_name() {
986 let content = b"some output";
987 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
988
989 let empty = ZipStoreOutput::Empty;
990 assert!(empty.file_name().is_none());
991
992 let full = ZipStoreOutput::Full {
993 file_name: file_name.clone(),
994 };
995 assert_eq!(
996 full.file_name().map(|f| f.as_str()),
997 Some(file_name.as_str())
998 );
999
1000 let truncated = ZipStoreOutput::Truncated {
1001 file_name: file_name.clone(),
1002 original_size: 1000,
1003 };
1004 assert_eq!(
1005 truncated.file_name().map(|f| f.as_str()),
1006 Some(file_name.as_str())
1007 );
1008 }
1009}