1#[cfg(test)]
18use crate::output_spec::ArbitraryOutputSpec;
19use crate::{
20 config::scripts::ScriptId,
21 list::OwnedTestInstanceId,
22 output_spec::{LiveSpec, OutputSpec, SerializableOutputSpec},
23 reporter::{
24 TestOutputDisplay,
25 events::{
26 CancelReason, ExecuteStatus, ExecutionStatuses, RetryData, RunFinishedStats, RunStats,
27 SetupScriptExecuteStatus, StressIndex, StressProgress, TestEvent, TestEventKind,
28 },
29 },
30 run_mode::NextestRunMode,
31 runner::StressCondition,
32};
33use chrono::{DateTime, FixedOffset};
34use nextest_metadata::MismatchReason;
35use quick_junit::ReportUuid;
36use serde::{Deserialize, Serialize};
37use std::{fmt, num::NonZero, time::Duration};
38
39#[derive(Clone, Debug, Default, Deserialize, Serialize)]
48#[serde(rename_all = "kebab-case")]
49#[non_exhaustive]
50pub struct RecordOpts {
51 #[serde(default)]
53 pub run_mode: NextestRunMode,
54}
55
56impl RecordOpts {
57 pub fn new(run_mode: NextestRunMode) -> Self {
59 Self { run_mode }
60 }
61}
62
63#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
72#[derive(Deserialize, Serialize)]
73#[serde(
74 rename_all = "kebab-case",
75 bound(
76 serialize = "S: SerializableOutputSpec",
77 deserialize = "S: SerializableOutputSpec"
78 )
79)]
80#[cfg_attr(
81 test,
82 derive(test_strategy::Arbitrary),
83 arbitrary(bound(S: ArbitraryOutputSpec))
84)]
85pub struct TestEventSummary<S: OutputSpec> {
86 #[cfg_attr(
88 test,
89 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
90 )]
91 pub timestamp: DateTime<FixedOffset>,
92
93 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
95 pub elapsed: Duration,
96
97 pub kind: TestEventKindSummary<S>,
99}
100
101impl TestEventSummary<LiveSpec> {
102 pub(crate) fn from_test_event(event: TestEvent<'_>) -> Option<Self> {
107 let kind = TestEventKindSummary::from_test_event_kind(event.kind)?;
108 Some(Self {
109 timestamp: event.timestamp,
110 elapsed: event.elapsed,
111 kind,
112 })
113 }
114}
115
116#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
126#[derive(Deserialize, Serialize)]
127#[serde(
128 tag = "type",
129 rename_all = "kebab-case",
130 bound(
131 serialize = "S: SerializableOutputSpec",
132 deserialize = "S: SerializableOutputSpec"
133 )
134)]
135#[cfg_attr(
136 test,
137 derive(test_strategy::Arbitrary),
138 arbitrary(bound(S: ArbitraryOutputSpec))
139)]
140pub enum TestEventKindSummary<S: OutputSpec> {
141 Core(CoreEventKind),
143 Output(OutputEventKind<S>),
145}
146
147#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
153#[serde(tag = "kind", rename_all = "kebab-case")]
154#[cfg_attr(test, derive(test_strategy::Arbitrary))]
155pub enum CoreEventKind {
156 #[serde(rename_all = "kebab-case")]
158 RunStarted {
159 run_id: ReportUuid,
161 profile_name: String,
163 cli_args: Vec<String>,
165 stress_condition: Option<StressConditionSummary>,
167 },
168
169 #[serde(rename_all = "kebab-case")]
171 StressSubRunStarted {
172 progress: StressProgress,
174 },
175
176 #[serde(rename_all = "kebab-case")]
178 SetupScriptStarted {
179 stress_index: Option<StressIndexSummary>,
181 index: usize,
183 total: usize,
185 script_id: ScriptId,
187 program: String,
189 args: Vec<String>,
191 no_capture: bool,
193 },
194
195 #[serde(rename_all = "kebab-case")]
197 SetupScriptSlow {
198 stress_index: Option<StressIndexSummary>,
200 script_id: ScriptId,
202 program: String,
204 args: Vec<String>,
206 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
208 elapsed: Duration,
209 will_terminate: bool,
211 },
212
213 #[serde(rename_all = "kebab-case")]
215 TestStarted {
216 stress_index: Option<StressIndexSummary>,
218 test_instance: OwnedTestInstanceId,
220 current_stats: RunStats,
222 running: usize,
224 command_line: Vec<String>,
226 },
227
228 #[serde(rename_all = "kebab-case")]
230 TestSlow {
231 stress_index: Option<StressIndexSummary>,
233 test_instance: OwnedTestInstanceId,
235 retry_data: RetryData,
237 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
239 elapsed: Duration,
240 will_terminate: bool,
242 },
243
244 #[serde(rename_all = "kebab-case")]
246 TestRetryStarted {
247 stress_index: Option<StressIndexSummary>,
249 test_instance: OwnedTestInstanceId,
251 retry_data: RetryData,
253 running: usize,
255 command_line: Vec<String>,
257 },
258
259 #[serde(rename_all = "kebab-case")]
261 TestSkipped {
262 stress_index: Option<StressIndexSummary>,
264 test_instance: OwnedTestInstanceId,
266 reason: MismatchReason,
268 },
269
270 #[serde(rename_all = "kebab-case")]
272 RunBeginCancel {
273 setup_scripts_running: usize,
275 running: usize,
277 reason: CancelReason,
279 },
280
281 #[serde(rename_all = "kebab-case")]
283 RunPaused {
284 setup_scripts_running: usize,
286 running: usize,
288 },
289
290 #[serde(rename_all = "kebab-case")]
292 RunContinued {
293 setup_scripts_running: usize,
295 running: usize,
297 },
298
299 #[serde(rename_all = "kebab-case")]
301 StressSubRunFinished {
302 progress: StressProgress,
304 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
306 sub_elapsed: Duration,
307 sub_stats: RunStats,
309 },
310
311 #[serde(rename_all = "kebab-case")]
313 RunFinished {
314 run_id: ReportUuid,
316 #[cfg_attr(
318 test,
319 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
320 )]
321 start_time: DateTime<FixedOffset>,
322 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
324 elapsed: Duration,
325 run_stats: RunFinishedStats,
327 outstanding_not_seen: Option<TestsNotSeenSummary>,
329 },
330}
331
332#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
334#[serde(rename_all = "kebab-case")]
335#[cfg_attr(test, derive(test_strategy::Arbitrary))]
336pub struct TestsNotSeenSummary {
337 pub not_seen: Vec<OwnedTestInstanceId>,
339 pub total_not_seen: usize,
341}
342
343#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
352#[derive(Deserialize, Serialize)]
353#[serde(
354 tag = "kind",
355 rename_all = "kebab-case",
356 bound(
357 serialize = "S: SerializableOutputSpec",
358 deserialize = "S: SerializableOutputSpec"
359 )
360)]
361#[cfg_attr(
362 test,
363 derive(test_strategy::Arbitrary),
364 arbitrary(bound(S: ArbitraryOutputSpec))
365)]
366pub enum OutputEventKind<S: OutputSpec> {
367 #[serde(rename_all = "kebab-case")]
369 SetupScriptFinished {
370 stress_index: Option<StressIndexSummary>,
372 index: usize,
374 total: usize,
376 script_id: ScriptId,
378 program: String,
380 args: Vec<String>,
382 no_capture: bool,
384 run_status: SetupScriptExecuteStatus<S>,
386 },
387
388 #[serde(rename_all = "kebab-case")]
390 TestAttemptFailedWillRetry {
391 stress_index: Option<StressIndexSummary>,
393 test_instance: OwnedTestInstanceId,
395 run_status: ExecuteStatus<S>,
397 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
399 delay_before_next_attempt: Duration,
400 failure_output: TestOutputDisplay,
402 running: usize,
404 },
405
406 #[serde(rename_all = "kebab-case")]
408 TestFinished {
409 stress_index: Option<StressIndexSummary>,
411 test_instance: OwnedTestInstanceId,
413 success_output: TestOutputDisplay,
415 failure_output: TestOutputDisplay,
417 junit_store_success_output: bool,
419 junit_store_failure_output: bool,
421 run_statuses: ExecutionStatuses<S>,
423 current_stats: RunStats,
425 running: usize,
427 },
428}
429
430impl TestEventKindSummary<LiveSpec> {
431 fn from_test_event_kind(kind: TestEventKind<'_>) -> Option<Self> {
432 Some(match kind {
433 TestEventKind::RunStarted {
434 run_id,
435 test_list: _,
436 profile_name,
437 cli_args,
438 stress_condition,
439 } => Self::Core(CoreEventKind::RunStarted {
440 run_id,
441 profile_name,
442 cli_args,
443 stress_condition: stress_condition.map(StressConditionSummary::from),
444 }),
445 TestEventKind::StressSubRunStarted { progress } => {
446 Self::Core(CoreEventKind::StressSubRunStarted { progress })
447 }
448 TestEventKind::SetupScriptStarted {
449 stress_index,
450 index,
451 total,
452 script_id,
453 program,
454 args,
455 no_capture,
456 } => Self::Core(CoreEventKind::SetupScriptStarted {
457 stress_index: stress_index.map(StressIndexSummary::from),
458 index,
459 total,
460 script_id,
461 program,
462 args: args.to_vec(),
463 no_capture,
464 }),
465 TestEventKind::SetupScriptSlow {
466 stress_index,
467 script_id,
468 program,
469 args,
470 elapsed,
471 will_terminate,
472 } => Self::Core(CoreEventKind::SetupScriptSlow {
473 stress_index: stress_index.map(StressIndexSummary::from),
474 script_id,
475 program,
476 args: args.to_vec(),
477 elapsed,
478 will_terminate,
479 }),
480 TestEventKind::TestStarted {
481 stress_index,
482 test_instance,
483 current_stats,
484 running,
485 command_line,
486 } => Self::Core(CoreEventKind::TestStarted {
487 stress_index: stress_index.map(StressIndexSummary::from),
488 test_instance: test_instance.to_owned(),
489 current_stats,
490 running,
491 command_line,
492 }),
493 TestEventKind::TestSlow {
494 stress_index,
495 test_instance,
496 retry_data,
497 elapsed,
498 will_terminate,
499 } => Self::Core(CoreEventKind::TestSlow {
500 stress_index: stress_index.map(StressIndexSummary::from),
501 test_instance: test_instance.to_owned(),
502 retry_data,
503 elapsed,
504 will_terminate,
505 }),
506 TestEventKind::TestRetryStarted {
507 stress_index,
508 test_instance,
509 retry_data,
510 running,
511 command_line,
512 } => Self::Core(CoreEventKind::TestRetryStarted {
513 stress_index: stress_index.map(StressIndexSummary::from),
514 test_instance: test_instance.to_owned(),
515 retry_data,
516 running,
517 command_line,
518 }),
519 TestEventKind::TestSkipped {
520 stress_index,
521 test_instance,
522 reason,
523 } => Self::Core(CoreEventKind::TestSkipped {
524 stress_index: stress_index.map(StressIndexSummary::from),
525 test_instance: test_instance.to_owned(),
526 reason,
527 }),
528 TestEventKind::RunBeginCancel {
529 setup_scripts_running,
530 current_stats,
531 running,
532 } => Self::Core(CoreEventKind::RunBeginCancel {
533 setup_scripts_running,
534 running,
535 reason: current_stats
536 .cancel_reason
537 .expect("RunBeginCancel event has cancel reason"),
538 }),
539 TestEventKind::RunPaused {
540 setup_scripts_running,
541 running,
542 } => Self::Core(CoreEventKind::RunPaused {
543 setup_scripts_running,
544 running,
545 }),
546 TestEventKind::RunContinued {
547 setup_scripts_running,
548 running,
549 } => Self::Core(CoreEventKind::RunContinued {
550 setup_scripts_running,
551 running,
552 }),
553 TestEventKind::StressSubRunFinished {
554 progress,
555 sub_elapsed,
556 sub_stats,
557 } => Self::Core(CoreEventKind::StressSubRunFinished {
558 progress,
559 sub_elapsed,
560 sub_stats,
561 }),
562 TestEventKind::RunFinished {
563 run_id,
564 start_time,
565 elapsed,
566 run_stats,
567 outstanding_not_seen,
568 } => Self::Core(CoreEventKind::RunFinished {
569 run_id,
570 start_time,
571 elapsed,
572 run_stats,
573 outstanding_not_seen: outstanding_not_seen.map(|t| TestsNotSeenSummary {
574 not_seen: t.not_seen,
575 total_not_seen: t.total_not_seen,
576 }),
577 }),
578
579 TestEventKind::SetupScriptFinished {
580 stress_index,
581 index,
582 total,
583 script_id,
584 program,
585 args,
586 junit_store_success_output: _,
587 junit_store_failure_output: _,
588 no_capture,
589 run_status,
590 } => Self::Output(OutputEventKind::SetupScriptFinished {
591 stress_index: stress_index.map(StressIndexSummary::from),
592 index,
593 total,
594 script_id,
595 program,
596 args: args.to_vec(),
597 no_capture,
598 run_status,
599 }),
600 TestEventKind::TestAttemptFailedWillRetry {
601 stress_index,
602 test_instance,
603 run_status,
604 delay_before_next_attempt,
605 failure_output,
606 running,
607 } => Self::Output(OutputEventKind::TestAttemptFailedWillRetry {
608 stress_index: stress_index.map(StressIndexSummary::from),
609 test_instance: test_instance.to_owned(),
610 run_status,
611 delay_before_next_attempt,
612 failure_output,
613 running,
614 }),
615 TestEventKind::TestFinished {
616 stress_index,
617 test_instance,
618 success_output,
619 failure_output,
620 junit_store_success_output,
621 junit_store_failure_output,
622 run_statuses,
623 current_stats,
624 running,
625 } => Self::Output(OutputEventKind::TestFinished {
626 stress_index: stress_index.map(StressIndexSummary::from),
627 test_instance: test_instance.to_owned(),
628 success_output,
629 failure_output,
630 junit_store_success_output,
631 junit_store_failure_output,
632 run_statuses,
633 current_stats,
634 running,
635 }),
636
637 TestEventKind::InfoStarted { .. }
638 | TestEventKind::InfoResponse { .. }
639 | TestEventKind::InfoFinished { .. }
640 | TestEventKind::InputEnter { .. }
641 | TestEventKind::RunBeginKill { .. } => return None,
642 })
643 }
644}
645
646#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
648#[serde(rename_all = "kebab-case")]
649#[cfg_attr(test, derive(test_strategy::Arbitrary))]
650pub struct StressIndexSummary {
651 pub current: u32,
653 pub total: Option<NonZero<u32>>,
655}
656
657impl From<StressIndex> for StressIndexSummary {
658 fn from(index: StressIndex) -> Self {
659 Self {
660 current: index.current,
661 total: index.total,
662 }
663 }
664}
665
666#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
668#[serde(tag = "type", rename_all = "kebab-case")]
669#[cfg_attr(test, derive(test_strategy::Arbitrary))]
670pub enum StressConditionSummary {
671 Count {
673 count: Option<u32>,
675 },
676 Duration {
678 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
680 duration: Duration,
681 },
682}
683
684impl From<StressCondition> for StressConditionSummary {
685 fn from(condition: StressCondition) -> Self {
686 use crate::runner::StressCount;
687 match condition {
688 StressCondition::Count(count) => Self::Count {
689 count: match count {
690 StressCount::Count { count: n } => Some(n.get()),
691 StressCount::Infinite => None,
692 },
693 },
694 StressCondition::Duration(duration) => Self::Duration { duration },
695 }
696 }
697}
698
699#[derive(Clone, Copy, Debug, PartialEq, Eq)]
704pub(crate) enum OutputKind {
705 Stdout,
707 Stderr,
709 Combined,
711}
712
713impl OutputKind {
714 pub(crate) fn as_str(self) -> &'static str {
716 match self {
717 Self::Stdout => "stdout",
718 Self::Stderr => "stderr",
719 Self::Combined => "combined",
720 }
721 }
722}
723
724#[derive(Clone, Debug, PartialEq, Eq)]
735pub struct OutputFileName(String);
736
737impl OutputFileName {
738 pub(crate) fn from_content(content: &[u8], kind: OutputKind) -> Self {
743 let hash = xxhash_rust::xxh3::xxh3_64(content);
744 Self(format!("{hash:016x}-{}", kind.as_str()))
745 }
746
747 pub fn as_str(&self) -> &str {
749 &self.0
750 }
751
752 fn validate(s: &str) -> bool {
756 if s.contains('/') || s.contains('\\') || s.contains("..") {
757 return false;
758 }
759
760 let valid_suffixes = ["-stdout", "-stderr", "-combined"];
761 for suffix in valid_suffixes {
762 if let Some(hash_part) = s.strip_suffix(suffix)
763 && hash_part.len() == 16
764 && hash_part
765 .chars()
766 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
767 {
768 return true;
769 }
770 }
771
772 false
773 }
774}
775
776impl fmt::Display for OutputFileName {
777 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
778 f.write_str(&self.0)
779 }
780}
781
782impl AsRef<str> for OutputFileName {
783 fn as_ref(&self) -> &str {
784 &self.0
785 }
786}
787
788impl Serialize for OutputFileName {
789 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
790 where
791 S: serde::Serializer,
792 {
793 self.0.serialize(serializer)
794 }
795}
796
797impl<'de> Deserialize<'de> for OutputFileName {
798 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
799 where
800 D: serde::Deserializer<'de>,
801 {
802 let s = String::deserialize(deserializer)?;
803 if Self::validate(&s) {
804 Ok(Self(s))
805 } else {
806 Err(serde::de::Error::custom(format!(
807 "invalid output file name: {s}"
808 )))
809 }
810 }
811}
812
813#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
815#[serde(tag = "status", rename_all = "kebab-case")]
816pub enum ZipStoreOutput {
817 Empty,
819
820 #[serde(rename_all = "kebab-case")]
822 Full {
823 file_name: OutputFileName,
825 },
826
827 #[serde(rename_all = "kebab-case")]
829 Truncated {
830 file_name: OutputFileName,
832 original_size: u64,
834 },
835}
836
837impl ZipStoreOutput {
838 pub fn file_name(&self) -> Option<&OutputFileName> {
840 match self {
841 ZipStoreOutput::Empty => None,
842 ZipStoreOutput::Full { file_name } | ZipStoreOutput::Truncated { file_name, .. } => {
843 Some(file_name)
844 }
845 }
846 }
847}
848
849#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
857#[serde(tag = "kind", rename_all = "kebab-case")]
858#[cfg_attr(test, derive(test_strategy::Arbitrary))]
859pub enum ZipStoreOutputDescription {
860 Split {
862 stdout: Option<ZipStoreOutput>,
864 stderr: Option<ZipStoreOutput>,
866 },
867
868 Combined {
870 output: ZipStoreOutput,
872 },
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use crate::output_spec::RecordingSpec;
879 use test_strategy::proptest;
880
881 #[proptest]
882 fn test_event_summary_roundtrips(value: TestEventSummary<RecordingSpec>) {
883 let json = serde_json::to_string(&value).expect("serialization succeeds");
884 let roundtrip: TestEventSummary<RecordingSpec> =
885 serde_json::from_str(&json).expect("deserialization succeeds");
886 proptest::prop_assert_eq!(value, roundtrip);
887 }
888
889 #[test]
890 fn test_output_file_name_from_content_stdout() {
891 let content = b"hello world";
892 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
893
894 let s = file_name.as_str();
895 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
896 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stdout'");
897
898 let hash_part = &s[..16];
899 assert!(
900 hash_part.chars().all(|c| c.is_ascii_hexdigit()),
901 "hash portion should be hex: {hash_part}"
902 );
903 }
904
905 #[test]
906 fn test_output_file_name_from_content_stderr() {
907 let content = b"error message";
908 let file_name = OutputFileName::from_content(content, OutputKind::Stderr);
909
910 let s = file_name.as_str();
911 assert!(s.ends_with("-stderr"), "should end with -stderr: {s}");
912 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stderr'");
913 }
914
915 #[test]
916 fn test_output_file_name_from_content_combined() {
917 let content = b"combined output";
918 let file_name = OutputFileName::from_content(content, OutputKind::Combined);
919
920 let s = file_name.as_str();
921 assert!(s.ends_with("-combined"), "should end with -combined: {s}");
922 assert_eq!(
923 s.len(),
924 16 + 1 + 8,
925 "should be 16 hex + hyphen + 'combined'"
926 );
927 }
928
929 #[test]
930 fn test_output_file_name_deterministic() {
931 let content = b"deterministic content";
932 let name1 = OutputFileName::from_content(content, OutputKind::Stdout);
933 let name2 = OutputFileName::from_content(content, OutputKind::Stdout);
934 assert_eq!(name1.as_str(), name2.as_str());
935 }
936
937 #[test]
938 fn test_output_file_name_different_content_different_hash() {
939 let content1 = b"content one";
940 let content2 = b"content two";
941 let name1 = OutputFileName::from_content(content1, OutputKind::Stdout);
942 let name2 = OutputFileName::from_content(content2, OutputKind::Stdout);
943 assert_ne!(name1.as_str(), name2.as_str());
944 }
945
946 #[test]
947 fn test_output_file_name_same_content_different_kind() {
948 let content = b"same content";
949 let stdout = OutputFileName::from_content(content, OutputKind::Stdout);
950 let stderr = OutputFileName::from_content(content, OutputKind::Stderr);
951 assert_ne!(stdout.as_str(), stderr.as_str());
952
953 let stdout_hash = &stdout.as_str()[..16];
954 let stderr_hash = &stderr.as_str()[..16];
955 assert_eq!(stdout_hash, stderr_hash);
956 }
957
958 #[test]
959 fn test_output_file_name_empty_content() {
960 let file_name = OutputFileName::from_content(b"", OutputKind::Stdout);
961 let s = file_name.as_str();
962 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
963 assert!(OutputFileName::validate(s), "should be valid: {s}");
964 }
965
966 #[test]
967 fn test_output_file_name_validate_valid_content_addressed() {
968 assert!(OutputFileName::validate("0123456789abcdef-stdout"));
970 assert!(OutputFileName::validate("fedcba9876543210-stderr"));
971 assert!(OutputFileName::validate("aaaaaaaaaaaaaaaa-combined"));
972 assert!(OutputFileName::validate("0000000000000000-stdout"));
973 assert!(OutputFileName::validate("ffffffffffffffff-stderr"));
974 }
975
976 #[test]
977 fn test_output_file_name_validate_invalid_patterns() {
978 assert!(!OutputFileName::validate("0123456789abcde-stdout"));
980 assert!(!OutputFileName::validate("abc-stdout"));
981
982 assert!(!OutputFileName::validate("0123456789abcdef0-stdout"));
984
985 assert!(!OutputFileName::validate("0123456789abcdef-unknown"));
987 assert!(!OutputFileName::validate("0123456789abcdef-out"));
988 assert!(!OutputFileName::validate("0123456789abcdef"));
989
990 assert!(!OutputFileName::validate("0123456789abcdeg-stdout"));
992 assert!(!OutputFileName::validate("0123456789ABCDEF-stdout")); assert!(!OutputFileName::validate("../0123456789abcdef-stdout"));
996 assert!(!OutputFileName::validate("0123456789abcdef-stdout/"));
997 assert!(!OutputFileName::validate("foo/0123456789abcdef-stdout"));
998 assert!(!OutputFileName::validate("..\\0123456789abcdef-stdout"));
999 }
1000
1001 #[test]
1002 fn test_output_file_name_validate_rejects_old_format() {
1003 assert!(!OutputFileName::validate("test-abc123-1-stdout"));
1005 assert!(!OutputFileName::validate("test-abc123-s5-1-stderr"));
1006 assert!(!OutputFileName::validate("script-def456-stdout"));
1007 assert!(!OutputFileName::validate("script-def456-s3-stderr"));
1008 }
1009
1010 #[test]
1011 fn test_output_file_name_serde_round_trip() {
1012 let content = b"test content for serde";
1013 let original = OutputFileName::from_content(content, OutputKind::Stdout);
1014
1015 let json = serde_json::to_string(&original).expect("serialization failed");
1016 let deserialized: OutputFileName =
1017 serde_json::from_str(&json).expect("deserialization failed");
1018
1019 assert_eq!(original.as_str(), deserialized.as_str());
1020 }
1021
1022 #[test]
1023 fn test_output_file_name_deserialize_invalid() {
1024 let json = r#""invalid-file-name""#;
1026 let result: Result<OutputFileName, _> = serde_json::from_str(json);
1027 assert!(
1028 result.is_err(),
1029 "should fail to deserialize invalid pattern"
1030 );
1031
1032 let json = r#""test-abc123-1-stdout""#; let result: Result<OutputFileName, _> = serde_json::from_str(json);
1034 assert!(result.is_err(), "should reject old format");
1035 }
1036
1037 #[test]
1038 fn test_zip_store_output_file_name() {
1039 let content = b"some output";
1040 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
1041
1042 let empty = ZipStoreOutput::Empty;
1043 assert!(empty.file_name().is_none());
1044
1045 let full = ZipStoreOutput::Full {
1046 file_name: file_name.clone(),
1047 };
1048 assert_eq!(
1049 full.file_name().map(|f| f.as_str()),
1050 Some(file_name.as_str())
1051 );
1052
1053 let truncated = ZipStoreOutput::Truncated {
1054 file_name: file_name.clone(),
1055 original_size: 1000,
1056 };
1057 assert_eq!(
1058 truncated.file_name().map(|f| f.as_str()),
1059 Some(file_name.as_str())
1060 );
1061 }
1062}