1#[cfg(test)]
18use crate::output_spec::ArbitraryOutputSpec;
19use crate::{
20 config::{elements::JunitFlakyFailStatus, 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 TestSlotAssignment,
29 },
30 },
31 run_mode::NextestRunMode,
32 runner::StressCondition,
33};
34use chrono::{DateTime, FixedOffset};
35use nextest_metadata::MismatchReason;
36use quick_junit::ReportUuid;
37use serde::{Deserialize, Serialize};
38use std::{fmt, num::NonZero, time::Duration};
39
40#[derive(Clone, Debug, Default, Deserialize, Serialize)]
49#[serde(rename_all = "kebab-case")]
50#[non_exhaustive]
51pub struct RecordOpts {
52 #[serde(default)]
54 pub run_mode: NextestRunMode,
55}
56
57impl RecordOpts {
58 pub fn new(run_mode: NextestRunMode) -> Self {
60 Self { run_mode }
61 }
62}
63
64#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
73#[derive(Deserialize, Serialize)]
74#[serde(
75 rename_all = "kebab-case",
76 bound(
77 serialize = "S: SerializableOutputSpec",
78 deserialize = "S: SerializableOutputSpec"
79 )
80)]
81#[cfg_attr(
82 test,
83 derive(test_strategy::Arbitrary),
84 arbitrary(bound(S: ArbitraryOutputSpec))
85)]
86pub struct TestEventSummary<S: OutputSpec> {
87 #[cfg_attr(
89 test,
90 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
91 )]
92 pub timestamp: DateTime<FixedOffset>,
93
94 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
96 pub elapsed: Duration,
97
98 pub kind: TestEventKindSummary<S>,
100}
101
102impl TestEventSummary<LiveSpec> {
103 pub(crate) fn from_test_event(event: TestEvent<'_>) -> Option<Self> {
108 let kind = TestEventKindSummary::from_test_event_kind(event.kind)?;
109 Some(Self {
110 timestamp: event.timestamp,
111 elapsed: event.elapsed,
112 kind,
113 })
114 }
115}
116
117#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
127#[derive(Deserialize, Serialize)]
128#[serde(
129 tag = "type",
130 rename_all = "kebab-case",
131 bound(
132 serialize = "S: SerializableOutputSpec",
133 deserialize = "S: SerializableOutputSpec"
134 )
135)]
136#[cfg_attr(
137 test,
138 derive(test_strategy::Arbitrary),
139 arbitrary(bound(S: ArbitraryOutputSpec))
140)]
141pub enum TestEventKindSummary<S: OutputSpec> {
142 Core(CoreEventKind),
144 Output(OutputEventKind<S>),
146}
147
148#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
154#[serde(tag = "kind", rename_all = "kebab-case")]
155#[cfg_attr(test, derive(test_strategy::Arbitrary))]
156pub enum CoreEventKind {
157 #[serde(rename_all = "kebab-case")]
159 RunStarted {
160 run_id: ReportUuid,
162 profile_name: String,
164 cli_args: Vec<String>,
166 stress_condition: Option<StressConditionSummary>,
168 },
169
170 #[serde(rename_all = "kebab-case")]
172 StressSubRunStarted {
173 progress: StressProgress,
175 },
176
177 #[serde(rename_all = "kebab-case")]
179 SetupScriptStarted {
180 stress_index: Option<StressIndexSummary>,
182 index: usize,
184 total: usize,
186 script_id: ScriptId,
188 program: String,
190 args: Vec<String>,
192 no_capture: bool,
194 },
195
196 #[serde(rename_all = "kebab-case")]
198 SetupScriptSlow {
199 stress_index: Option<StressIndexSummary>,
201 script_id: ScriptId,
203 program: String,
205 args: Vec<String>,
207 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
209 elapsed: Duration,
210 will_terminate: bool,
212 },
213
214 #[serde(rename_all = "kebab-case")]
216 TestStarted {
217 stress_index: Option<StressIndexSummary>,
219 test_instance: OwnedTestInstanceId,
221 slot_assignment: TestSlotAssignment,
223 current_stats: RunStats,
225 running: usize,
227 command_line: Vec<String>,
229 },
230
231 #[serde(rename_all = "kebab-case")]
233 TestSlow {
234 stress_index: Option<StressIndexSummary>,
236 test_instance: OwnedTestInstanceId,
238 retry_data: RetryData,
240 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
242 elapsed: Duration,
243 will_terminate: bool,
245 },
246
247 #[serde(rename_all = "kebab-case")]
249 TestRetryStarted {
250 stress_index: Option<StressIndexSummary>,
252 test_instance: OwnedTestInstanceId,
254 slot_assignment: TestSlotAssignment,
256 retry_data: RetryData,
258 running: usize,
260 command_line: Vec<String>,
262 },
263
264 #[serde(rename_all = "kebab-case")]
266 TestSkipped {
267 stress_index: Option<StressIndexSummary>,
269 test_instance: OwnedTestInstanceId,
271 reason: MismatchReason,
273 },
274
275 #[serde(rename_all = "kebab-case")]
277 RunBeginCancel {
278 setup_scripts_running: usize,
280 running: usize,
282 reason: CancelReason,
284 },
285
286 #[serde(rename_all = "kebab-case")]
288 RunPaused {
289 setup_scripts_running: usize,
291 running: usize,
293 },
294
295 #[serde(rename_all = "kebab-case")]
297 RunContinued {
298 setup_scripts_running: usize,
300 running: usize,
302 },
303
304 #[serde(rename_all = "kebab-case")]
306 StressSubRunFinished {
307 progress: StressProgress,
309 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
311 sub_elapsed: Duration,
312 sub_stats: RunStats,
314 },
315
316 #[serde(rename_all = "kebab-case")]
318 RunFinished {
319 run_id: ReportUuid,
321 #[cfg_attr(
323 test,
324 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
325 )]
326 start_time: DateTime<FixedOffset>,
327 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
329 elapsed: Duration,
330 run_stats: RunFinishedStats,
332 outstanding_not_seen: Option<TestsNotSeenSummary>,
334 },
335}
336
337#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
339#[serde(rename_all = "kebab-case")]
340#[cfg_attr(test, derive(test_strategy::Arbitrary))]
341pub struct TestsNotSeenSummary {
342 pub not_seen: Vec<OwnedTestInstanceId>,
344 pub total_not_seen: usize,
346}
347
348#[derive_where::derive_where(Debug, PartialEq; S::ChildOutputDesc)]
357#[derive(Deserialize, Serialize)]
358#[serde(
359 tag = "kind",
360 rename_all = "kebab-case",
361 bound(
362 serialize = "S: SerializableOutputSpec",
363 deserialize = "S: SerializableOutputSpec"
364 )
365)]
366#[cfg_attr(
367 test,
368 derive(test_strategy::Arbitrary),
369 arbitrary(bound(S: ArbitraryOutputSpec))
370)]
371pub enum OutputEventKind<S: OutputSpec> {
372 #[serde(rename_all = "kebab-case")]
374 SetupScriptFinished {
375 stress_index: Option<StressIndexSummary>,
377 index: usize,
379 total: usize,
381 script_id: ScriptId,
383 program: String,
385 args: Vec<String>,
387 no_capture: bool,
389 run_status: SetupScriptExecuteStatus<S>,
391 },
392
393 #[serde(rename_all = "kebab-case")]
395 TestAttemptFailedWillRetry {
396 stress_index: Option<StressIndexSummary>,
398 test_instance: OwnedTestInstanceId,
400 run_status: ExecuteStatus<S>,
402 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
404 delay_before_next_attempt: Duration,
405 failure_output: TestOutputDisplay,
407 running: usize,
409 },
410
411 #[serde(rename_all = "kebab-case")]
413 TestFinished {
414 stress_index: Option<StressIndexSummary>,
416 test_instance: OwnedTestInstanceId,
418 success_output: TestOutputDisplay,
420 failure_output: TestOutputDisplay,
422 junit_store_success_output: bool,
424 junit_store_failure_output: bool,
426 #[serde(default)]
428 junit_flaky_fail_status: JunitFlakyFailStatus,
429 run_statuses: ExecutionStatuses<S>,
431 current_stats: RunStats,
433 running: usize,
435 },
436}
437
438impl TestEventKindSummary<LiveSpec> {
439 fn from_test_event_kind(kind: TestEventKind<'_>) -> Option<Self> {
440 Some(match kind {
441 TestEventKind::RunStarted {
442 run_id,
443 test_list: _,
444 profile_name,
445 cli_args,
446 stress_condition,
447 } => Self::Core(CoreEventKind::RunStarted {
448 run_id,
449 profile_name,
450 cli_args,
451 stress_condition: stress_condition.map(StressConditionSummary::from),
452 }),
453 TestEventKind::StressSubRunStarted { progress } => {
454 Self::Core(CoreEventKind::StressSubRunStarted { progress })
455 }
456 TestEventKind::SetupScriptStarted {
457 stress_index,
458 index,
459 total,
460 script_id,
461 program,
462 args,
463 no_capture,
464 } => Self::Core(CoreEventKind::SetupScriptStarted {
465 stress_index: stress_index.map(StressIndexSummary::from),
466 index,
467 total,
468 script_id,
469 program,
470 args: args.to_vec(),
471 no_capture,
472 }),
473 TestEventKind::SetupScriptSlow {
474 stress_index,
475 script_id,
476 program,
477 args,
478 elapsed,
479 will_terminate,
480 } => Self::Core(CoreEventKind::SetupScriptSlow {
481 stress_index: stress_index.map(StressIndexSummary::from),
482 script_id,
483 program,
484 args: args.to_vec(),
485 elapsed,
486 will_terminate,
487 }),
488 TestEventKind::TestStarted {
489 stress_index,
490 test_instance,
491 slot_assignment,
492 current_stats,
493 running,
494 command_line,
495 } => Self::Core(CoreEventKind::TestStarted {
496 stress_index: stress_index.map(StressIndexSummary::from),
497 test_instance: test_instance.to_owned(),
498 slot_assignment,
499 current_stats,
500 running,
501 command_line,
502 }),
503 TestEventKind::TestSlow {
504 stress_index,
505 test_instance,
506 retry_data,
507 elapsed,
508 will_terminate,
509 } => Self::Core(CoreEventKind::TestSlow {
510 stress_index: stress_index.map(StressIndexSummary::from),
511 test_instance: test_instance.to_owned(),
512 retry_data,
513 elapsed,
514 will_terminate,
515 }),
516 TestEventKind::TestRetryStarted {
517 stress_index,
518 test_instance,
519 slot_assignment,
520 retry_data,
521 running,
522 command_line,
523 } => Self::Core(CoreEventKind::TestRetryStarted {
524 stress_index: stress_index.map(StressIndexSummary::from),
525 test_instance: test_instance.to_owned(),
526 slot_assignment,
527 retry_data,
528 running,
529 command_line,
530 }),
531 TestEventKind::TestSkipped {
532 stress_index,
533 test_instance,
534 reason,
535 } => Self::Core(CoreEventKind::TestSkipped {
536 stress_index: stress_index.map(StressIndexSummary::from),
537 test_instance: test_instance.to_owned(),
538 reason,
539 }),
540 TestEventKind::RunBeginCancel {
541 setup_scripts_running,
542 current_stats,
543 running,
544 } => Self::Core(CoreEventKind::RunBeginCancel {
545 setup_scripts_running,
546 running,
547 reason: current_stats
548 .cancel_reason
549 .expect("RunBeginCancel event has cancel reason"),
550 }),
551 TestEventKind::RunPaused {
552 setup_scripts_running,
553 running,
554 } => Self::Core(CoreEventKind::RunPaused {
555 setup_scripts_running,
556 running,
557 }),
558 TestEventKind::RunContinued {
559 setup_scripts_running,
560 running,
561 } => Self::Core(CoreEventKind::RunContinued {
562 setup_scripts_running,
563 running,
564 }),
565 TestEventKind::StressSubRunFinished {
566 progress,
567 sub_elapsed,
568 sub_stats,
569 } => Self::Core(CoreEventKind::StressSubRunFinished {
570 progress,
571 sub_elapsed,
572 sub_stats,
573 }),
574 TestEventKind::RunFinished {
575 run_id,
576 start_time,
577 elapsed,
578 run_stats,
579 outstanding_not_seen,
580 } => Self::Core(CoreEventKind::RunFinished {
581 run_id,
582 start_time,
583 elapsed,
584 run_stats,
585 outstanding_not_seen: outstanding_not_seen.map(|t| TestsNotSeenSummary {
586 not_seen: t.not_seen,
587 total_not_seen: t.total_not_seen,
588 }),
589 }),
590
591 TestEventKind::SetupScriptFinished {
592 stress_index,
593 index,
594 total,
595 script_id,
596 program,
597 args,
598 junit_store_success_output: _,
599 junit_store_failure_output: _,
600 no_capture,
601 run_status,
602 } => Self::Output(OutputEventKind::SetupScriptFinished {
603 stress_index: stress_index.map(StressIndexSummary::from),
604 index,
605 total,
606 script_id,
607 program,
608 args: args.to_vec(),
609 no_capture,
610 run_status,
611 }),
612 TestEventKind::TestAttemptFailedWillRetry {
613 stress_index,
614 test_instance,
615 run_status,
616 delay_before_next_attempt,
617 failure_output,
618 running,
619 } => Self::Output(OutputEventKind::TestAttemptFailedWillRetry {
620 stress_index: stress_index.map(StressIndexSummary::from),
621 test_instance: test_instance.to_owned(),
622 run_status,
623 delay_before_next_attempt,
624 failure_output,
625 running,
626 }),
627 TestEventKind::TestFinished {
628 stress_index,
629 test_instance,
630 success_output,
631 failure_output,
632 junit_store_success_output,
633 junit_store_failure_output,
634 junit_flaky_fail_status,
635 run_statuses,
636 current_stats,
637 running,
638 } => Self::Output(OutputEventKind::TestFinished {
639 stress_index: stress_index.map(StressIndexSummary::from),
640 test_instance: test_instance.to_owned(),
641 success_output,
642 failure_output,
643 junit_store_success_output,
644 junit_store_failure_output,
645 junit_flaky_fail_status,
646 run_statuses,
647 current_stats,
648 running,
649 }),
650
651 TestEventKind::InfoStarted { .. }
652 | TestEventKind::InfoResponse { .. }
653 | TestEventKind::InfoFinished { .. }
654 | TestEventKind::InputEnter { .. }
655 | TestEventKind::RunBeginKill { .. } => return None,
656 })
657 }
658}
659
660#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
662#[serde(rename_all = "kebab-case")]
663#[cfg_attr(test, derive(test_strategy::Arbitrary))]
664pub struct StressIndexSummary {
665 pub current: u32,
667 pub total: Option<NonZero<u32>>,
669}
670
671impl From<StressIndex> for StressIndexSummary {
672 fn from(index: StressIndex) -> Self {
673 Self {
674 current: index.current,
675 total: index.total,
676 }
677 }
678}
679
680#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
682#[serde(tag = "type", rename_all = "kebab-case")]
683#[cfg_attr(test, derive(test_strategy::Arbitrary))]
684pub enum StressConditionSummary {
685 Count {
687 count: Option<u32>,
689 },
690 Duration {
692 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
694 duration: Duration,
695 },
696}
697
698impl From<StressCondition> for StressConditionSummary {
699 fn from(condition: StressCondition) -> Self {
700 use crate::runner::StressCount;
701 match condition {
702 StressCondition::Count(count) => Self::Count {
703 count: match count {
704 StressCount::Count { count: n } => Some(n.get()),
705 StressCount::Infinite => None,
706 },
707 },
708 StressCondition::Duration(duration) => Self::Duration { duration },
709 }
710 }
711}
712
713#[derive(Clone, Copy, Debug, PartialEq, Eq)]
718pub(crate) enum OutputKind {
719 Stdout,
721 Stderr,
723 Combined,
725}
726
727impl OutputKind {
728 pub(crate) fn as_str(self) -> &'static str {
730 match self {
731 Self::Stdout => "stdout",
732 Self::Stderr => "stderr",
733 Self::Combined => "combined",
734 }
735 }
736}
737
738#[derive(Clone, Debug, PartialEq, Eq)]
749pub struct OutputFileName(String);
750
751impl OutputFileName {
752 pub(crate) fn from_content(content: &[u8], kind: OutputKind) -> Self {
757 let hash = xxhash_rust::xxh3::xxh3_64(content);
758 Self(format!("{hash:016x}-{}", kind.as_str()))
759 }
760
761 pub fn as_str(&self) -> &str {
763 &self.0
764 }
765
766 fn validate(s: &str) -> bool {
770 if s.contains('/') || s.contains('\\') || s.contains("..") {
771 return false;
772 }
773
774 let valid_suffixes = ["-stdout", "-stderr", "-combined"];
775 for suffix in valid_suffixes {
776 if let Some(hash_part) = s.strip_suffix(suffix)
777 && hash_part.len() == 16
778 && hash_part
779 .chars()
780 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
781 {
782 return true;
783 }
784 }
785
786 false
787 }
788}
789
790impl fmt::Display for OutputFileName {
791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 f.write_str(&self.0)
793 }
794}
795
796impl AsRef<str> for OutputFileName {
797 fn as_ref(&self) -> &str {
798 &self.0
799 }
800}
801
802impl Serialize for OutputFileName {
803 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
804 where
805 S: serde::Serializer,
806 {
807 self.0.serialize(serializer)
808 }
809}
810
811impl<'de> Deserialize<'de> for OutputFileName {
812 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
813 where
814 D: serde::Deserializer<'de>,
815 {
816 let s = String::deserialize(deserializer)?;
817 if Self::validate(&s) {
818 Ok(Self(s))
819 } else {
820 Err(serde::de::Error::custom(format!(
821 "invalid output file name: {s}"
822 )))
823 }
824 }
825}
826
827#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
829#[serde(tag = "status", rename_all = "kebab-case")]
830pub enum ZipStoreOutput {
831 Empty,
833
834 #[serde(rename_all = "kebab-case")]
836 Full {
837 file_name: OutputFileName,
839 },
840
841 #[serde(rename_all = "kebab-case")]
843 Truncated {
844 file_name: OutputFileName,
846 original_size: u64,
848 },
849}
850
851impl ZipStoreOutput {
852 pub fn file_name(&self) -> Option<&OutputFileName> {
854 match self {
855 ZipStoreOutput::Empty => None,
856 ZipStoreOutput::Full { file_name } | ZipStoreOutput::Truncated { file_name, .. } => {
857 Some(file_name)
858 }
859 }
860 }
861}
862
863#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
871#[serde(tag = "kind", rename_all = "kebab-case")]
872#[cfg_attr(test, derive(test_strategy::Arbitrary))]
873pub enum ZipStoreOutputDescription {
874 Split {
876 stdout: Option<ZipStoreOutput>,
878 stderr: Option<ZipStoreOutput>,
880 },
881
882 Combined {
884 output: ZipStoreOutput,
886 },
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use crate::output_spec::RecordingSpec;
893 use test_strategy::proptest;
894
895 #[proptest]
896 fn test_event_summary_roundtrips(value: TestEventSummary<RecordingSpec>) {
897 let json = serde_json::to_string(&value).expect("serialization succeeds");
898 let roundtrip: TestEventSummary<RecordingSpec> =
899 serde_json::from_str(&json).expect("deserialization succeeds");
900 proptest::prop_assert_eq!(value, roundtrip);
901 }
902
903 #[test]
904 fn test_output_file_name_from_content_stdout() {
905 let content = b"hello world";
906 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
907
908 let s = file_name.as_str();
909 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
910 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stdout'");
911
912 let hash_part = &s[..16];
913 assert!(
914 hash_part.chars().all(|c| c.is_ascii_hexdigit()),
915 "hash portion should be hex: {hash_part}"
916 );
917 }
918
919 #[test]
920 fn test_output_file_name_from_content_stderr() {
921 let content = b"error message";
922 let file_name = OutputFileName::from_content(content, OutputKind::Stderr);
923
924 let s = file_name.as_str();
925 assert!(s.ends_with("-stderr"), "should end with -stderr: {s}");
926 assert_eq!(s.len(), 16 + 1 + 6, "should be 16 hex + hyphen + 'stderr'");
927 }
928
929 #[test]
930 fn test_output_file_name_from_content_combined() {
931 let content = b"combined output";
932 let file_name = OutputFileName::from_content(content, OutputKind::Combined);
933
934 let s = file_name.as_str();
935 assert!(s.ends_with("-combined"), "should end with -combined: {s}");
936 assert_eq!(
937 s.len(),
938 16 + 1 + 8,
939 "should be 16 hex + hyphen + 'combined'"
940 );
941 }
942
943 #[test]
944 fn test_output_file_name_deterministic() {
945 let content = b"deterministic content";
946 let name1 = OutputFileName::from_content(content, OutputKind::Stdout);
947 let name2 = OutputFileName::from_content(content, OutputKind::Stdout);
948 assert_eq!(name1.as_str(), name2.as_str());
949 }
950
951 #[test]
952 fn test_output_file_name_different_content_different_hash() {
953 let content1 = b"content one";
954 let content2 = b"content two";
955 let name1 = OutputFileName::from_content(content1, OutputKind::Stdout);
956 let name2 = OutputFileName::from_content(content2, OutputKind::Stdout);
957 assert_ne!(name1.as_str(), name2.as_str());
958 }
959
960 #[test]
961 fn test_output_file_name_same_content_different_kind() {
962 let content = b"same content";
963 let stdout = OutputFileName::from_content(content, OutputKind::Stdout);
964 let stderr = OutputFileName::from_content(content, OutputKind::Stderr);
965 assert_ne!(stdout.as_str(), stderr.as_str());
966
967 let stdout_hash = &stdout.as_str()[..16];
968 let stderr_hash = &stderr.as_str()[..16];
969 assert_eq!(stdout_hash, stderr_hash);
970 }
971
972 #[test]
973 fn test_output_file_name_empty_content() {
974 let file_name = OutputFileName::from_content(b"", OutputKind::Stdout);
975 let s = file_name.as_str();
976 assert!(s.ends_with("-stdout"), "should end with -stdout: {s}");
977 assert!(OutputFileName::validate(s), "should be valid: {s}");
978 }
979
980 #[test]
981 fn test_output_file_name_validate_valid_content_addressed() {
982 assert!(OutputFileName::validate("0123456789abcdef-stdout"));
984 assert!(OutputFileName::validate("fedcba9876543210-stderr"));
985 assert!(OutputFileName::validate("aaaaaaaaaaaaaaaa-combined"));
986 assert!(OutputFileName::validate("0000000000000000-stdout"));
987 assert!(OutputFileName::validate("ffffffffffffffff-stderr"));
988 }
989
990 #[test]
991 fn test_output_file_name_validate_invalid_patterns() {
992 assert!(!OutputFileName::validate("0123456789abcde-stdout"));
994 assert!(!OutputFileName::validate("abc-stdout"));
995
996 assert!(!OutputFileName::validate("0123456789abcdef0-stdout"));
998
999 assert!(!OutputFileName::validate("0123456789abcdef-unknown"));
1001 assert!(!OutputFileName::validate("0123456789abcdef-out"));
1002 assert!(!OutputFileName::validate("0123456789abcdef"));
1003
1004 assert!(!OutputFileName::validate("0123456789abcdeg-stdout"));
1006 assert!(!OutputFileName::validate("0123456789ABCDEF-stdout")); assert!(!OutputFileName::validate("../0123456789abcdef-stdout"));
1010 assert!(!OutputFileName::validate("0123456789abcdef-stdout/"));
1011 assert!(!OutputFileName::validate("foo/0123456789abcdef-stdout"));
1012 assert!(!OutputFileName::validate("..\\0123456789abcdef-stdout"));
1013 }
1014
1015 #[test]
1016 fn test_output_file_name_validate_rejects_old_format() {
1017 assert!(!OutputFileName::validate("test-abc123-1-stdout"));
1019 assert!(!OutputFileName::validate("test-abc123-s5-1-stderr"));
1020 assert!(!OutputFileName::validate("script-def456-stdout"));
1021 assert!(!OutputFileName::validate("script-def456-s3-stderr"));
1022 }
1023
1024 #[test]
1025 fn test_output_file_name_serde_round_trip() {
1026 let content = b"test content for serde";
1027 let original = OutputFileName::from_content(content, OutputKind::Stdout);
1028
1029 let json = serde_json::to_string(&original).expect("serialization failed");
1030 let deserialized: OutputFileName =
1031 serde_json::from_str(&json).expect("deserialization failed");
1032
1033 assert_eq!(original.as_str(), deserialized.as_str());
1034 }
1035
1036 #[test]
1037 fn test_output_file_name_deserialize_invalid() {
1038 let json = r#""invalid-file-name""#;
1040 let result: Result<OutputFileName, _> = serde_json::from_str(json);
1041 assert!(
1042 result.is_err(),
1043 "should fail to deserialize invalid pattern"
1044 );
1045
1046 let json = r#""test-abc123-1-stdout""#; let result: Result<OutputFileName, _> = serde_json::from_str(json);
1048 assert!(result.is_err(), "should reject old format");
1049 }
1050
1051 #[test]
1052 fn test_zip_store_output_file_name() {
1053 let content = b"some output";
1054 let file_name = OutputFileName::from_content(content, OutputKind::Stdout);
1055
1056 let empty = ZipStoreOutput::Empty;
1057 assert!(empty.file_name().is_none());
1058
1059 let full = ZipStoreOutput::Full {
1060 file_name: file_name.clone(),
1061 };
1062 assert_eq!(
1063 full.file_name().map(|f| f.as_str()),
1064 Some(file_name.as_str())
1065 );
1066
1067 let truncated = ZipStoreOutput::Truncated {
1068 file_name: file_name.clone(),
1069 original_size: 1000,
1070 };
1071 assert_eq!(
1072 truncated.file_name().map(|f| f.as_str()),
1073 Some(file_name.as_str())
1074 );
1075 }
1076}