1use std::path::Path;
14
15use anyhow::Context;
16use anyhow::bail;
17use detcore_model::schedule::boundary::Schedule;
18use serde::Deserialize;
19use serde::Serialize;
20
21use super::PreemptionRecord;
22
23#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27#[serde(try_from = "BoundaryWire", into = "BoundaryWire")]
28pub struct BoundaryRecording {
29 preemptions: PreemptionRecord,
30 schedule: Schedule,
31}
32
33#[derive(Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35struct BoundaryWire {
36 preemptions: PreemptionRecord,
37 schedule: Schedule,
38}
39
40#[derive(Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44struct Envelope {
45 slice_boundary_recording: BoundaryRecording,
46}
47
48#[derive(Deserialize)]
53struct FormatProbe {
54 #[serde(default, deserialize_with = "marker_present")]
56 slice_boundary_recording: bool,
57 #[serde(default, deserialize_with = "marker_present")]
58 incomplete_preemption_record: bool,
59}
60
61fn marker_present<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
62 serde::de::IgnoredAny::deserialize(deserializer).map(|_| true)
63}
64
65impl FormatProbe {
66 fn is_boundary_recording(bytes: &[u8]) -> serde_json::Result<bool> {
67 serde_json::from_slice::<Self>(bytes).map(|probe| probe.slice_boundary_recording)
68 }
69}
70
71pub(super) fn read_settings<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T, String> {
72 let bytes = std::fs::read(path)
73 .map_err(|e| format!("cannot read preemption record {}: {}", path.display(), e))?;
74 let format: FormatProbe = serde_json::from_slice(&bytes)
75 .map_err(|e| format!("cannot parse preemption record {}: {}", path.display(), e))?;
76 if !format.slice_boundary_recording && !format.incomplete_preemption_record {
77 return serde_json::from_slice(&bytes)
80 .map_err(|e| format!("cannot parse preemption record {}: {}", path.display(), e));
81 }
82 let recording = Recording::decode_unchecked_format(&bytes)
85 .map_err(|e| format!("cannot parse preemption record {}: {}", path.display(), e))?;
86 serde_json::from_value(
87 super::record_serde::settings_value(recording.metadata()).map_err(|e| e.to_string())?,
88 )
89 .map_err(|e| format!("cannot parse preemption record {}: {}", path.display(), e))
90}
91
92impl BoundaryRecording {
93 pub fn new(preemptions: PreemptionRecord, schedule: Schedule) -> anyhow::Result<Self> {
99 preemptions
100 .validate_contents()
101 .map_err(anyhow::Error::msg)?;
102 if !preemptions.global.is_empty() {
103 bail!("slice-boundary recording has a second, legacy global event stream");
104 }
105 Ok(Self {
106 preemptions,
107 schedule,
108 })
109 }
110
111 pub fn thread_histories(
114 &self,
115 ) -> std::collections::BTreeMap<crate::types::DetTid, super::ThreadHistory> {
116 self.preemptions.extract_all()
117 }
118
119 pub fn schedule(&self) -> &Schedule {
121 &self.schedule
122 }
123}
124
125impl TryFrom<BoundaryWire> for BoundaryRecording {
126 type Error = anyhow::Error;
127
128 fn try_from(wire: BoundaryWire) -> Result<Self, Self::Error> {
129 Self::new(wire.preemptions, wire.schedule)
130 }
131}
132
133impl From<BoundaryRecording> for BoundaryWire {
134 fn from(recording: BoundaryRecording) -> Self {
135 Self {
136 preemptions: recording.preemptions,
137 schedule: recording.schedule,
138 }
139 }
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
144pub enum Recording {
145 Legacy(PreemptionRecord),
147 Boundaries(BoundaryRecording),
149}
150
151impl Recording {
152 pub fn require_faithful_replay(&self) -> anyhow::Result<()> {
154 self.metadata().require_faithful_replay()
155 }
156
157 pub fn replay_departed_at(&self) -> Option<u64> {
159 self.metadata().replay_departed_at()
160 }
161
162 pub fn require_strict_mode(&self, strict: bool) -> anyhow::Result<()> {
166 match (self, strict) {
167 (Self::Legacy(_), false) | (Self::Boundaries(_), true) => Ok(()),
168 (Self::Legacy(_), true) => {
169 anyhow::bail!("strict schedule boundaries require a complete boundary recording")
170 }
171 (Self::Boundaries(_), false) => {
172 anyhow::bail!("complete boundary recording requires --strict-schedule-boundaries")
173 }
174 }
175 }
176
177 pub fn decode(bytes: &[u8]) -> anyhow::Result<Self> {
182 let recording = Self::decode_unchecked_format(bytes)?;
183 recording
184 .metadata()
185 .validate_format()
186 .map_err(|refusal| anyhow::Error::new(super::RecordFormatRefusal(refusal)))?;
187 Ok(recording)
188 }
189
190 fn decode_unchecked_format(bytes: &[u8]) -> anyhow::Result<Self> {
192 if FormatProbe::is_boundary_recording(bytes)? {
196 let envelope: Envelope = serde_json::from_slice(bytes)?;
197 Ok(Self::Boundaries(envelope.slice_boundary_recording))
198 } else {
199 let legacy: PreemptionRecord = serde_json::from_slice(bytes)?;
200 legacy.validate_contents().map_err(anyhow::Error::msg)?;
201 Ok(Self::Legacy(legacy))
202 }
203 }
204
205 pub fn read(path: &Path) -> anyhow::Result<Self> {
207 Self::decode(&std::fs::read(path).with_context(|| format!("reading {}", path.display()))?)
208 .with_context(|| format!("reading schedule {}", path.display()))
209 }
210
211 pub fn encode(&self) -> anyhow::Result<Vec<u8>> {
213 let mut bytes = match self {
214 Self::Legacy(record) => serde_json::to_vec(record)?,
215 Self::Boundaries(record) => serde_json::to_vec(&Envelope {
216 slice_boundary_recording: record.clone(),
217 })?,
218 };
219 bytes.push(b'\n');
220 Ok(bytes)
221 }
222
223 pub fn write(&self, path: &Path) -> anyhow::Result<()> {
225 let bytes = self.encode()?;
227 std::fs::write(path, bytes).with_context(|| format!("writing {}", path.display()))
228 }
229
230 pub fn into_legacy(self, operation: &'static str) -> anyhow::Result<PreemptionRecord> {
233 match self {
234 Self::Legacy(record) => Ok(record),
235 Self::Boundaries(_) => {
236 Err(detcore_model::schedule::boundary::Error::UnsupportedEdit { operation }.into())
237 }
238 }
239 }
240
241 pub fn strip_times(&mut self) {
244 match self {
245 Self::Legacy(record) => {
246 record.strip_event_times();
247 }
248 Self::Boundaries(record) => record.schedule.strip_times(),
249 }
250 }
251
252 pub fn validate_replay_inputs(
255 &self,
256 source: &str,
257 guest_ids: detcore_model::pid::GuestIds,
258 ) -> Result<(), super::RecordAdmissionRefusal> {
259 self.metadata().validate_replay_inputs(source, guest_ids)
260 }
261
262 pub fn reserved_input(&self) -> &super::ReservedInputMetadata {
264 self.metadata().reserved_input()
265 }
266
267 pub fn guest_ids(&self) -> detcore_model::pid::GuestIds {
269 self.metadata().guest_ids().unwrap_or_default()
270 }
271
272 pub fn epoch(&self) -> Option<chrono::DateTime<chrono::Utc>> {
274 self.metadata().epoch()
275 }
276
277 pub fn pipe_wake_latency(&self) -> Option<u64> {
279 self.metadata().pipe_wake_latency()
280 }
281
282 pub fn signal_state_copy(&self) -> Option<bool> {
284 self.metadata().signal_state_copy()
285 }
286
287 pub fn host_inputs(&self) -> Option<&detcore_model::host_input::HostInputSet> {
289 self.metadata().host_inputs()
290 }
291
292 pub fn time_skips(&self) -> Option<&[super::TimeSkip]> {
294 self.metadata().time_skips()
295 }
296
297 pub fn lists_its_time_skips(&self) -> bool {
300 self.metadata().lists_its_time_skips()
301 }
302
303 pub fn charges_replayed_switch_ins(&self) -> bool {
306 self.metadata().charges_replayed_switch_ins()
307 }
308
309 pub fn format_version(&self) -> Option<u32> {
311 self.metadata().format_version()
312 }
313
314 fn metadata(&self) -> &PreemptionRecord {
315 match self {
316 Self::Legacy(record) => record,
317 Self::Boundaries(record) => &record.preemptions,
318 }
319 }
320}
321
322#[derive(Debug)]
325pub(crate) struct HistoryReader(Recording);
326
327impl HistoryReader {
328 #[cfg(test)]
331 pub(crate) fn read(path: &Path) -> anyhow::Result<Self> {
332 Recording::read(path).map(Self)
333 }
334
335 pub(crate) fn read_for_replay(
338 path: &Path,
339 guest_ids: detcore_model::pid::GuestIds,
340 ) -> anyhow::Result<Self> {
341 let recording = Recording::read(path)?;
342 recording.validate_replay_inputs(
343 &format!("--replay-preemptions-from {}", path.display()),
344 guest_ids,
345 )?;
346 Ok(Self(recording))
347 }
348
349 pub(crate) fn host_inputs(&self) -> Option<&detcore_model::host_input::HostInputSet> {
351 self.0.host_inputs()
352 }
353
354 pub(crate) fn require_legacy_mode(&self) -> anyhow::Result<()> {
357 self.0.require_strict_mode(false)
358 }
359
360 pub(crate) fn require_replay_mode(&self, edited: bool) -> anyhow::Result<()> {
362 self.require_legacy_mode()?;
363 if !edited {
364 self.0.require_faithful_replay()?;
365 }
366 Ok(())
367 }
368
369 pub(crate) fn replay_departed_at(&self) -> Option<u64> {
370 self.0.replay_departed_at()
371 }
372
373 pub(crate) fn thread_initial_priority(
374 &self,
375 tid: &crate::types::DetTid,
376 ) -> Option<crate::scheduler::runqueue::Priority> {
377 self.0
378 .metadata()
379 .per_thread
380 .get(tid)
381 .map(|history| history.initial_priority())
382 }
383
384 pub(crate) fn extract_thread_record(
385 &self,
386 tid: &crate::types::DetTid,
387 ) -> Option<super::ThreadHistory> {
388 self.0.metadata().per_thread.get(tid).cloned()
389 }
390
391 pub(crate) fn callback_checkpoints(
394 &self,
395 ) -> &std::collections::BTreeMap<crate::types::DetTid, Vec<super::callback_history::Checkpoint>>
396 {
397 self.0.metadata().callback_checkpoints()
398 }
399
400 #[cfg(target_arch = "aarch64")]
402 pub(crate) fn store_exclusive_failures(&self) -> u64 {
403 self.0.metadata().store_exclusive_failures()
404 }
405}
406
407impl std::fmt::Display for HistoryReader {
408 fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 let bytes = self.0.encode().map_err(|_| std::fmt::Error)?;
410 output.write_str(std::str::from_utf8(&bytes).map_err(|_| std::fmt::Error)?)
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use detcore_model::schedule::boundary::Boundary;
417 use detcore_model::schedule::boundary::Cause;
418 use detcore_model::schedule::boundary::Entry;
419 use detcore_model::schedule::boundary::Identity;
420 use detcore_model::schedule::boundary::Owner;
421 use detcore_model::schedule::boundary::Phase;
422 use detcore_model::schedule::boundary::Record;
423 use reverie::syscalls::Sysno;
424
425 use super::*;
426 use crate::preemptions::PreemptionReader;
427 use crate::preemptions::PreemptionWriter;
428 use crate::preemptions::strip_times_from_events_file;
429 use crate::types::DetTid;
430 use crate::types::LogicalTime;
431 use crate::types::SchedEvent;
432
433 fn sample(markers: bool) -> Recording {
434 let metadata: PreemptionRecord = serde_json::from_str(
435 &PreemptionWriter::new(None)
436 .with_epoch("2001-02-03T04:05:06Z".parse().unwrap())
437 .with_pipe_wake_latency(Some(0))
438 .with_signal_state_copy(false)
439 .with_expired_wait_runs_charged_once(true)
440 .into_string(),
441 )
442 .unwrap();
443 let records = if markers {
444 vec![Record::TimesliceEnd(Boundary {
445 identity: Identity {
446 owner: Owner {
447 tid: DetTid::from_raw(3),
448 thread_generation: 0,
449 birth_tid: DetTid::from_raw(3),
450 birth_generation: 0,
451 address_space_creator: DetTid::from_raw(3),
452 address_space_generation: 0,
453 },
454 cause: Cause::SoftwareCheckin,
455 phase: Phase::Posthook,
456 entry: Entry::Syscall {
457 syscall: Sysno::write,
458 occurrence: 1,
459 },
460 committed_rcbs: 17,
461 checkin: 1,
462 slice: 1,
463 },
464 count: 1,
465 start_rip: None,
466 end_rip: Some(std::num::NonZeroUsize::new(0x1234).unwrap()),
467 end_time: Some(LogicalTime::from_nanos(123)),
468 })]
469 } else {
470 vec![]
471 };
472 Recording::Boundaries(
473 BoundaryRecording::new(metadata, Schedule::new(records).unwrap()).unwrap(),
474 )
475 }
476
477 #[test]
478 fn actual_legacy_reader_rejects_even_an_empty_boundary_recording() {
479 for markers in [false, true] {
480 let recording = sample(markers);
481 let bytes = recording.encode().unwrap();
482 assert!(serde_json::from_slice::<PreemptionRecord>(&bytes).is_err());
483 assert_eq!(Recording::decode(&bytes).unwrap(), recording);
484 assert!(recording.into_legacy("minimize").is_err());
485 }
486 }
487
488 #[test]
489 fn file_strip_preserves_protocol_metadata_and_mandatory_identity() {
490 let directory = tempfile::tempdir().unwrap();
491 for markers in [false, true] {
492 let input = directory.path().join(format!("{markers}.json"));
493 let original = sample(markers);
494 original.write(&input).unwrap();
495 let original_bytes = std::fs::read(&input).unwrap();
496 let output = strip_times_from_events_file(&input, None).unwrap();
497 let actual = Recording::read(&output).unwrap();
498 let mut expected = original.clone();
499 expected.strip_times();
500 assert_eq!(actual, expected);
501 assert_eq!(std::fs::read(&input).unwrap(), original_bytes);
502 assert_eq!(actual.epoch(), original.epoch());
503 assert_eq!(actual.pipe_wake_latency(), Some(0));
504 assert_eq!(actual.signal_state_copy(), Some(false));
505 assert_eq!(
506 super::super::read_recorded_epoch(&output).unwrap(),
507 original.epoch()
508 );
509 assert_eq!(
510 super::super::read_recorded_pipe_wake_latency(&output).unwrap(),
511 Some(0)
512 );
513 assert_eq!(
514 super::super::read_recorded_signal_state_copy(&output).unwrap(),
515 Some(false)
516 );
517 assert_eq!(
518 super::super::read_recorded_expired_wait_runs_charged_once(&output),
519 Ok(true)
520 );
521 assert!(PreemptionReader::try_new(&output).is_err());
522 assert!(
523 serde_json::from_slice::<PreemptionRecord>(&std::fs::read(output).unwrap())
524 .is_err()
525 );
526 }
527 }
528
529 #[test]
532 fn the_format_is_the_presence_of_the_top_level_marker() {
533 let probe = |text: &str| FormatProbe::is_boundary_recording(text.as_bytes()).unwrap();
534 assert!(!probe(r#"{"per_thread": {}, "global": []}"#));
535 assert!(probe(r#"{"slice_boundary_recording": {"preemptions": 1}}"#));
536 assert!(probe(r#"{"slice_boundary_recording": null}"#));
537 assert!(!probe(
538 r#"{"per_thread": {"3": {"slice_boundary_recording": 1}}, "global": []}"#
539 ));
540 assert!(
541 FormatProbe::is_boundary_recording(
542 br#"{"slice_boundary_recording": null, "slice_boundary_recording": null}"#
543 )
544 .is_err()
545 );
546 assert!(FormatProbe::is_boundary_recording(b"{\"global\": [").is_err());
547 }
548
549 #[test]
550 fn unknown_or_broken_envelope_never_falls_back_to_valid_legacy_fields() {
551 let mut value: serde_json::Value =
552 serde_json::from_slice(&sample(false).encode().unwrap()).unwrap();
553 value["per_thread"] = serde_json::json!({});
554 value["global"] = serde_json::json!([]);
555 assert!(Recording::decode(&serde_json::to_vec(&value).unwrap()).is_err());
556 value["slice_boundary_recording"] = serde_json::Value::Null;
557 assert!(Recording::decode(&serde_json::to_vec(&value).unwrap()).is_err());
558 let body = String::from_utf8(sample(false).encode().unwrap()).unwrap();
559 let duplicated = format!(
560 "{{{}, {}}}",
561 body.trim()[1..body.trim().len() - 1].to_owned(),
562 body.trim()[1..body.trim().len() - 1].to_owned()
563 );
564 assert!(Recording::decode(duplicated.as_bytes()).is_err());
565 let directory = tempfile::tempdir().unwrap();
566 let path = directory.path().join("bad.json");
567 std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
568 assert!(super::super::read_recorded_epoch(&path).is_err());
569 assert!(super::super::read_recorded_pipe_wake_latency(&path).is_err());
570 assert!(super::super::read_recorded_signal_state_copy(&path).is_err());
571 }
572
573 #[test]
577 fn a_refused_format_is_a_refusal_in_either_envelope() {
578 let refused = |value: &serde_json::Value| {
579 Recording::decode(&serde_json::to_vec(value).unwrap())
580 .unwrap_err()
581 .chain()
582 .any(|cause| cause.is::<super::super::RecordFormatRefusal>())
583 };
584 let mut boundary: serde_json::Value =
585 serde_json::from_slice(&sample(false).encode().unwrap()).unwrap();
586 boundary["slice_boundary_recording"]["preemptions"]["format_version"] =
587 serde_json::json!(99);
588 assert!(refused(&boundary));
589 let directory = tempfile::tempdir().unwrap();
590 let path = directory.path().join("refused.json");
591 std::fs::write(&path, serde_json::to_vec(&boundary).unwrap()).unwrap();
592 assert!(super::super::read_recorded_epoch(&path).unwrap().is_some());
593 let mut plain = serde_json::to_value(PreemptionRecord::default()).unwrap();
594 plain["format_version"] = serde_json::json!(99);
595 assert!(refused(&plain));
596 }
597
598 #[test]
599 fn boundary_history_reader_preserves_the_complete_format_and_original_thread_histories() {
600 let dir = tempfile::tempdir().unwrap();
601 let path = dir.path().join("recording.json");
602 let tid = crate::types::DetTid::from_raw(3);
603 let mut writer = PreemptionWriter::new(Some(path.clone()))
604 .with_slice_boundaries()
605 .unwrap();
606 writer.register_thread(tid, crate::scheduler::runqueue::DEFAULT_PRIORITY);
607 let expected_history = writer.inner.per_thread.get(&tid).cloned();
608 writer.flush().unwrap();
609 let before = std::fs::read(&path).unwrap();
610 let history = HistoryReader::read(&path).unwrap();
611 assert!(history.require_legacy_mode().is_err());
612 assert_eq!(
613 history.thread_initial_priority(&tid),
614 Some(crate::scheduler::runqueue::DEFAULT_PRIORITY)
615 );
616 assert_eq!(history.extract_thread_record(&tid), expected_history);
617 assert_eq!(history.to_string().as_bytes(), before);
618 assert!(super::super::PreemptionReader::try_new(&path).is_err());
619 let mut bad: serde_json::Value = serde_json::from_slice(&before).unwrap();
620 bad["slice_boundary_recording"]["schedule"]["version"] = 999.into();
621 std::fs::write(&path, serde_json::to_vec(&bad).unwrap()).unwrap();
622 assert!(HistoryReader::read(&path).is_err());
623 assert_ne!(std::fs::read(&path).unwrap(), before);
624 }
625
626 #[test]
627 fn a_second_event_stream_is_not_accepted_as_metadata() {
628 let metadata =
629 PreemptionRecord::from_sched_events(vec![SchedEvent::branches(DetTid::from_raw(3), 1)]);
630 assert!(BoundaryRecording::new(metadata, Schedule::new(vec![]).unwrap()).is_err());
631 }
632
633 #[test]
634 fn legacy_bytes_and_editor_access_remain_legacy() {
635 let legacy =
636 PreemptionRecord::from_sched_events(vec![SchedEvent::branches(DetTid::from_raw(3), 2)])
637 .with_pipe_wake_latency(Some(7))
638 .with_signal_state_copy(Some(true));
639 let mut expected = serde_json::to_vec(&legacy).unwrap();
640 expected.push(b'\n');
641 let record = Recording::Legacy(legacy.clone());
642 assert_eq!(record.encode().unwrap(), expected);
643 assert_eq!(
644 Recording::decode(&expected)
645 .unwrap()
646 .into_legacy("split")
647 .unwrap(),
648 legacy
649 );
650 }
651
652 #[test]
653 fn legacy_metadata_readers_keep_their_individual_field_contracts() {
654 let directory = tempfile::tempdir().unwrap();
655 let path = directory.path().join("legacy.json");
656 std::fs::write(&path, r#"{"epoch":null,"pipe_wake_latency":"invalid"}"#).unwrap();
657 assert_eq!(super::super::read_recorded_epoch(&path), Ok(None));
658 assert!(super::super::read_recorded_pipe_wake_latency(&path).is_err());
659 assert_eq!(
660 super::super::read_recorded_signal_state_copy(&path),
661 Ok(None)
662 );
663 std::fs::write(&path, r#"{"epoch":null,"epoch":null}"#).unwrap();
664 assert!(super::super::read_recorded_epoch(&path).is_err());
665 }
666
667 #[test]
668 fn writer_keeps_events_markers_and_all_metadata_in_one_stored_stream() {
669 let directory = tempfile::tempdir().unwrap();
670 let path = directory.path().join("recording.json");
671 let mut writer = PreemptionWriter::new(Some(path.clone()))
672 .with_epoch("2001-02-03T04:05:06Z".parse().unwrap())
673 .with_pipe_wake_latency(Some(0))
674 .with_signal_state_copy(false)
675 .with_slice_boundaries()
676 .unwrap();
677 let tid = DetTid::from_raw(3);
678 writer.register_thread(tid, crate::scheduler::runqueue::DEFAULT_PRIORITY);
679 let metadata = writer.inner.clone();
680 let Recording::Boundaries(sample) = sample(true) else {
681 unreachable!()
682 };
683 let Record::TimesliceEnd(boundary) = sample.schedule.records()[0].clone() else {
684 unreachable!()
685 };
686 let first = SchedEvent::branches(tid, 17);
687 let last = SchedEvent::syscall(tid, Sysno::write, crate::types::SyscallPhase::Posthook);
688 writer.insert_schedevent(first.clone());
689 assert_eq!(writer.insert_slice_boundary(boundary.clone()).unwrap(), 1);
690 writer.insert_schedevent(last.clone());
691 writer.flush().unwrap();
692 let bytes = std::fs::read(&path).unwrap();
693 assert_eq!(bytes.last(), Some(&b'\n'));
694 assert!(serde_json::from_slice::<PreemptionRecord>(&bytes).is_err());
695 let Recording::Boundaries(actual) = Recording::decode(&bytes).unwrap() else {
696 panic!("writer flattened the boundary protocol")
697 };
698 assert_eq!(actual.preemptions, metadata);
699 assert_eq!(
700 actual.schedule.records(),
701 &[
702 Record::Event(first),
703 Record::TimesliceEnd(boundary),
704 Record::Event(last),
705 ]
706 );
707 }
708
709 #[test]
710 fn writer_refuses_invalid_order_before_overwriting_the_destination() {
711 let directory = tempfile::tempdir().unwrap();
712 let path = directory.path().join("retained.json");
713 std::fs::write(&path, b"retained original").unwrap();
714 let Recording::Boundaries(sample) = sample(true) else {
715 unreachable!()
716 };
717 let Record::TimesliceEnd(boundary) = sample.schedule.records()[0].clone() else {
718 unreachable!()
719 };
720 let mut writer = PreemptionWriter::new(Some(path.clone()))
721 .with_slice_boundaries()
722 .unwrap();
723 writer.insert_slice_boundary(boundary.clone()).unwrap();
724 writer.insert_slice_boundary(boundary.clone()).unwrap();
725 assert!(writer.flush().is_err());
726 assert_eq!(std::fs::read(&path).unwrap(), b"retained original");
727
728 let mut legacy = PreemptionWriter::new(Some(path.clone()));
729 assert!(legacy.insert_slice_boundary(boundary).is_err());
730 legacy.insert_schedevent(SchedEvent::branches(DetTid::from_raw(3), 1));
731 assert!(legacy.with_slice_boundaries().is_err());
732 assert_eq!(std::fs::read(&path).unwrap(), b"retained original");
733 }
734
735 #[test]
736 fn writer_default_still_emits_exact_legacy_bytes() {
737 let mut writer = PreemptionWriter::new(None);
738 let event = SchedEvent::branches(DetTid::from_raw(3), 7);
739 writer.insert_schedevent(event.clone());
740 assert_eq!(
741 writer.into_string(),
742 PreemptionRecord::from_sched_events(vec![event]).to_string()
743 );
744 }
745
746 #[test]
747 fn zero_count_or_nonpositive_event_owner_is_rejected_on_new_format_read() {
748 for (tid, count) in [(3, 0), (0, 1), (-1, 1)] {
749 let event = SchedEvent::branches(DetTid::from_raw(tid), count);
750 assert!(Schedule::new(vec![Record::Event(event.clone())]).is_err());
751 let mut wire: serde_json::Value =
752 serde_json::from_slice(&sample(false).encode().unwrap()).unwrap();
753 let schedule = &mut wire["slice_boundary_recording"]["schedule"];
754 schedule["record_count"] = 1.into();
755 schedule["records"] = serde_json::json!([Record::Event(event)]);
756 assert!(Recording::decode(&serde_json::to_vec(&wire).unwrap()).is_err());
757 }
758 }
759}
760
761#[cfg(test)]
762mod explicit_mode_tests {
763 use super::*;
764
765 #[test]
766 fn explicit_mode_admission_never_falls_back_or_changes_legacy_policy() {
767 let legacy = Recording::Legacy(PreemptionRecord::default());
768 let typed = Recording::Boundaries(
769 BoundaryRecording::new(PreemptionRecord::default(), Schedule::new(vec![]).unwrap())
770 .unwrap(),
771 );
772 assert!(legacy.require_strict_mode(false).is_ok());
773 assert!(typed.require_strict_mode(true).is_ok());
774 assert!(legacy.require_strict_mode(true).is_err());
775 assert!(typed.require_strict_mode(false).is_err());
776 }
777 #[test]
778 fn ordinary_history_admits_legacy_and_retains_read_error_panics() {
779 let directory = tempfile::tempdir().unwrap();
780 let path = directory.path().join("history.json");
781 std::fs::write(
782 &path,
783 Recording::Legacy(PreemptionRecord::default())
784 .encode()
785 .unwrap(),
786 )
787 .unwrap();
788 assert!(
789 HistoryReader::read(&path)
790 .unwrap()
791 .require_legacy_mode()
792 .is_ok()
793 );
794 for contents in [None, Some("not a JSON recording")] {
795 match contents {
796 None => std::fs::remove_file(&path).unwrap(),
797 Some(value) => std::fs::write(&path, value).unwrap(),
798 }
799 let cfg = crate::Config {
800 replay_preemptions_from: Some(path.clone()),
801 ..crate::Config::default()
802 };
803 assert!(std::panic::catch_unwind(|| crate::GlobalState::for_tests(&cfg)).is_err());
804 }
805 }
806}