Skip to main content

detcore_model/schedule/
boundary.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Explicit slice-boundary protocol. A complete typed stream keeps ordinary
10//! events and actual scheduling boundaries in one causal order.
11//!
12//! A boundary is evidence of an existing scheduling request, not an instruction
13//! or permission to introduce a request. Its identity must come from the actual
14//! semantic callback, before the completed slice is reset. Replay expectations
15//! cannot supply observed identities. The controller must report any error here
16//! as a failed replay, irrespective of the legacy `die_on_desync` setting.
17
18use std::collections::BTreeMap;
19use std::fmt;
20
21use reverie_syscalls::Sysno;
22use serde::Deserialize;
23use serde::Serialize;
24
25use super::InstructionPointer;
26use super::SchedEvent;
27use super::SigWrapper;
28use crate::pid::DetTid;
29use crate::time::LogicalTime;
30
31/// Stable owner identity. These are logical generations, never host PIDs or
32/// addresses. Exec advances the address-space generation; thread creation must
33/// allocate a new thread identity even if a deterministic TID is reused.
34#[derive(
35    Clone,
36    Copy,
37    Debug,
38    Eq,
39    PartialEq,
40    Ord,
41    PartialOrd,
42    Serialize,
43    Deserialize
44)]
45#[serde(deny_unknown_fields)]
46pub struct Owner {
47    /// Current deterministic TID, which Linux can rebind on non-leader exec.
48    pub tid: DetTid,
49    /// Incarnation of the current TID slot, including an exec replacement.
50    pub thread_generation: u64,
51    /// Logical thread's creation TID, retained when exec rebinds its current TID.
52    pub birth_tid: DetTid,
53    /// Creation incarnation; a later thread reusing that TID is distinct.
54    pub birth_generation: u64,
55    /// Creator component of the actual model address-space identity.
56    pub address_space_creator: DetTid,
57    /// Exec-generation component of the actual model address-space identity.
58    pub address_space_generation: u64,
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
62pub enum Phase {
63    Prehook,
64    Posthook,
65    /// Existing no-RCB maximum body, after its time/ordinary-event update.
66    TimerMaximum,
67}
68
69/// The mechanism actually delivered to Detcore. RIP availability does not
70/// distinguish these causes, and stripping a timestamp must not erase them.
71#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
72pub enum Cause {
73    SoftwareCheckin,
74    PmuTimer,
75    NoRcbTimer,
76    ExplicitSchedYield,
77}
78
79/// A semantic entry into Detcore. Hidden backend stops, injected-call retries
80/// and dropped host signals are not entries. A signal delivery number comes
81/// from the already selected logical delivery, not its host arrival time.
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub enum Entry {
85    Syscall {
86        syscall: Sysno,
87        occurrence: u64,
88    },
89    Rdtsc,
90    Cpuid,
91    PmuTimer,
92    NoRcbTimer,
93    ThreadStart,
94    Exec,
95    SignalEntry {
96        signal: SigWrapper,
97        occurrence: u64,
98        announced: bool,
99    },
100    Signal {
101        signal: SigWrapper,
102        occurrence: u64,
103        announced: bool,
104        delivery: u64,
105    },
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct Identity {
111    pub owner: Owner,
112    pub cause: Cause,
113    pub phase: Phase,
114    pub entry: Entry,
115    /// Committed guest branch count within this address-space generation.
116    pub committed_rcbs: u64,
117    /// One-based semantic check-in number, independent of host callbacks.
118    pub checkin: u64,
119    /// One-based completed slice number. Adjacent zero-RCB slices at the same
120    /// check-in have different slice numbers.
121    pub slice: u64,
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct Boundary {
127    pub identity: Identity,
128    /// Always one. A boundary must never be run-length encoded.
129    pub count: u32,
130    pub start_rip: Option<InstructionPointer>,
131    pub end_rip: Option<InstructionPointer>,
132    pub end_time: Option<LogicalTime>,
133}
134
135/// The stored stream index includes boundary records. Keeping a single stream
136/// prevents an inspector from accidentally indexing only guest operations.
137#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
138pub enum Record {
139    Event(SchedEvent),
140    TimesliceEnd(Boundary),
141}
142
143impl Record {
144    /// Match one complete stored record. Only evidence absent from the stored
145    /// record is optional; all semantic identity and count fields are exact.
146    pub fn check_observation(&self, index: usize, observed: &Record) -> Result<(), Error> {
147        let mut comparable = observed.clone();
148        match (self, &mut comparable) {
149            (Record::Event(e), Record::Event(o)) => {
150                if e.end_time.is_none() {
151                    o.end_time = None;
152                }
153                if e.start_rip.is_none() {
154                    o.start_rip = None;
155                }
156                if e.end_rip.is_none() {
157                    o.end_rip = None;
158                }
159            }
160            (Record::TimesliceEnd(e), Record::TimesliceEnd(o)) => {
161                if e.end_time.is_none() {
162                    o.end_time = None;
163                }
164                if e.start_rip.is_none() {
165                    o.start_rip = None;
166                }
167                if e.end_rip.is_none() {
168                    o.end_rip = None;
169                }
170            }
171            _ => {}
172        }
173        if self != &comparable {
174            return Err(Error::Mismatch {
175                index,
176                expected: Box::new(self.clone()),
177                observed: Box::new(observed.clone()),
178            });
179        }
180        Ok(())
181    }
182}
183
184/// A separate envelope prevents an old `PreemptionRecord` reader from silently
185/// ignoring a new metadata field, including for a recording with zero markers.
186/// No implicit conversion to a vector of legacy events is provided.
187#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
188#[serde(try_from = "Wire", into = "Wire")]
189pub struct Schedule {
190    records: Vec<Record>,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195struct Wire {
196    protocol: String,
197    version: u32,
198    record_count: usize,
199    boundary_count: usize,
200    records: Vec<Record>,
201}
202
203const PROTOCOL: &str = "hermit-slice-boundaries";
204const VERSION: u32 = 1;
205
206/// Named failures, carrying the stored stream index where applicable. These
207/// are errors, not recovery hints, skipped edits or successful partial replays.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub enum Error {
210    UnsupportedProtocol {
211        protocol: String,
212        version: u32,
213    },
214    RecordCount {
215        declared: usize,
216        actual: usize,
217    },
218    BoundaryCount {
219        declared: usize,
220        actual: usize,
221    },
222    InvalidBoundary {
223        index: usize,
224        reason: &'static str,
225    },
226    InvalidEvent {
227        index: usize,
228        reason: &'static str,
229    },
230    Mismatch {
231        index: usize,
232        expected: Box<Record>,
233        observed: Box<Record>,
234    },
235    Exhausted {
236        index: usize,
237        observed: Box<Record>,
238    },
239    Incomplete {
240        index: usize,
241        remaining: usize,
242    },
243    UnsupportedEdit {
244        operation: &'static str,
245    },
246}
247
248impl fmt::Display for Error {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(f, "slice-boundary protocol refusal: {self:?}")
251    }
252}
253
254impl std::error::Error for Error {}
255
256impl TryFrom<Wire> for Schedule {
257    type Error = Error;
258
259    fn try_from(wire: Wire) -> Result<Self, Error> {
260        if wire.protocol != PROTOCOL || wire.version != VERSION {
261            return Err(Error::UnsupportedProtocol {
262                protocol: wire.protocol,
263                version: wire.version,
264            });
265        }
266        if wire.record_count != wire.records.len() {
267            return Err(Error::RecordCount {
268                declared: wire.record_count,
269                actual: wire.records.len(),
270            });
271        }
272        let actual = boundary_count(&wire.records);
273        if wire.boundary_count != actual {
274            return Err(Error::BoundaryCount {
275                declared: wire.boundary_count,
276                actual,
277            });
278        }
279        Self::new(wire.records)
280    }
281}
282
283impl From<Schedule> for Wire {
284    fn from(schedule: Schedule) -> Self {
285        Self {
286            protocol: PROTOCOL.into(),
287            version: VERSION,
288            record_count: schedule.records.len(),
289            boundary_count: boundary_count(&schedule.records),
290            records: schedule.records,
291        }
292    }
293}
294
295fn boundary_count(records: &[Record]) -> usize {
296    records
297        .iter()
298        .filter(|record| matches!(record, Record::TimesliceEnd(_)))
299        .count()
300}
301
302impl Boundary {
303    /// Validate the mandatory shape without imposing a stream position. The
304    /// full schedule separately checks ordering and generation transitions.
305    pub fn validate_shape(&self, index: usize) -> Result<(), Error> {
306        let boundary = self;
307        let id = self.identity;
308        let invalid = |reason| Error::InvalidBoundary { index, reason };
309        if boundary.count != 1 {
310            return Err(invalid("boundary count must be one"));
311        }
312        if id.owner.tid.as_raw() <= 0
313            || id.owner.birth_tid.as_raw() <= 0
314            || id.owner.address_space_creator.as_raw() <= 0
315            || id.checkin == 0
316            || id.slice == 0
317        {
318            return Err(invalid("nonpositive owner or zero occurrence"));
319        }
320        match id.entry {
321            Entry::Syscall { occurrence: 0, .. }
322            | Entry::Signal { delivery: 0, .. }
323            | Entry::Signal { occurrence: 0, .. }
324            | Entry::SignalEntry { occurrence: 0, .. } => {
325                return Err(invalid("zero syscall or signal occurrence"));
326            }
327            Entry::Signal { signal, .. } | Entry::SignalEntry { signal, .. }
328                if !(1..=64).contains(&signal.raw()) =>
329            {
330                return Err(invalid("invalid logical signal"));
331            }
332            _ => {}
333        }
334        if (matches!(id.entry, Entry::SignalEntry { .. }) && id.phase != Phase::Prehook)
335            || (matches!(id.entry, Entry::Signal { .. }) && id.phase != Phase::Posthook)
336        {
337            return Err(invalid("signal entry/delivery phase mismatch"));
338        }
339        if id.phase == Phase::TimerMaximum && id.entry != Entry::NoRcbTimer {
340            return Err(invalid(
341                "maximum-body phase requires the actual no-RCB timer",
342            ));
343        }
344        let cause_matches = match id.cause {
345            Cause::PmuTimer => id.entry == Entry::PmuTimer,
346            Cause::NoRcbTimer => id.entry == Entry::NoRcbTimer,
347            Cause::ExplicitSchedYield => matches!(
348                id.entry,
349                Entry::Syscall {
350                    syscall: Sysno::sched_yield,
351                    ..
352                }
353            ),
354            Cause::SoftwareCheckin => !matches!(id.entry, Entry::PmuTimer | Entry::NoRcbTimer),
355        };
356        if !cause_matches {
357            return Err(invalid("cause disagrees with actual semantic entry"));
358        }
359        Ok(())
360    }
361}
362
363impl Schedule {
364    pub fn new(records: Vec<Record>) -> Result<Self, Error> {
365        let mut previous: BTreeMap<Owner, Identity> = BTreeMap::new();
366        let mut latest_owner: BTreeMap<DetTid, Owner> = BTreeMap::new();
367        let mut latest_birth: BTreeMap<(DetTid, u64), Owner> = BTreeMap::new();
368        for (index, record) in records.iter().enumerate() {
369            if let Record::Event(event) = record {
370                if event.dettid.as_raw() <= 0 || event.count == 0 {
371                    return Err(Error::InvalidEvent {
372                        index,
373                        reason: "event owner and count must be positive",
374                    });
375                }
376                continue;
377            }
378            let Record::TimesliceEnd(boundary) = record else {
379                unreachable!();
380            };
381            let id = boundary.identity;
382            let invalid = |reason| Error::InvalidBoundary { index, reason };
383            boundary.validate_shape(index)?;
384            if let Some(last) = latest_owner.get(&id.owner.tid)
385                && (id.owner.thread_generation < last.thread_generation
386                    || (id.owner.thread_generation == last.thread_generation
387                        && (id.owner.birth_tid != last.birth_tid
388                            || id.owner.birth_generation != last.birth_generation
389                            || id.owner.address_space_generation < last.address_space_generation
390                            || (id.owner.address_space_generation
391                                == last.address_space_generation
392                                && id.owner.address_space_creator != last.address_space_creator))))
393            {
394                return Err(invalid("owner generation moved backwards"));
395            }
396            let birth = (id.owner.birth_tid, id.owner.birth_generation);
397            if let Some(last) = latest_birth.get(&birth)
398                && (id.owner.address_space_generation < last.address_space_generation
399                    || (id.owner.address_space_generation == last.address_space_generation
400                        && id.owner != *last))
401            {
402                return Err(invalid(
403                    "logical survivor moved without a new exec incarnation",
404                ));
405            }
406            if let Some(last) = previous.get(&id.owner) {
407                if last.slice.checked_add(1) != Some(id.slice) {
408                    return Err(invalid("missing, duplicated or reordered slice occurrence"));
409                }
410                if id.checkin < last.checkin || id.committed_rcbs < last.committed_rcbs {
411                    return Err(invalid("check-in or guest RCB count moved backwards"));
412                }
413                if id.checkin == last.checkin
414                    && (id.entry != last.entry
415                        || id.phase != last.phase
416                        || id.committed_rcbs != last.committed_rcbs)
417                {
418                    return Err(invalid("one semantic check-in has conflicting identities"));
419                }
420            } else if id.slice != 1 {
421                return Err(invalid("first completed slice must be one"));
422            }
423            previous.insert(id.owner, id);
424            latest_owner.insert(id.owner.tid, id.owner);
425            latest_birth.insert(birth, id.owner);
426        }
427        Ok(Self { records })
428    }
429
430    pub fn records(&self) -> &[Record] {
431        &self.records
432    }
433
434    /// Only the optional outer timestamps are stripped. Semantic identity and
435    /// both RIP observations remain, including in a boundary-only recording.
436    pub fn strip_times(&mut self) {
437        for record in &mut self.records {
438            match record {
439                Record::Event(event) => event.end_time = None,
440                Record::TimesliceEnd(boundary) => boundary.end_time = None,
441            }
442        }
443    }
444
445    /// Boundary-aware schedule editing is not implemented. Refuse before an
446    /// editor receives mutable output or loses the explicit protocol header.
447    /// This applies even when this particular recording contains zero markers.
448    pub fn into_legacy_events(self) -> Result<Vec<SchedEvent>, Error> {
449        Err(Error::UnsupportedEdit {
450            operation: "discard protocol into legacy event vector",
451        })
452    }
453
454    pub fn edit(&mut self, operation: Edit) -> Result<(), Error> {
455        Err(Error::UnsupportedEdit {
456            operation: operation.name(),
457        })
458    }
459
460    pub fn cursor(&self) -> Cursor<'_> {
461        Cursor {
462            schedule: self,
463            next: 0,
464        }
465    }
466}
467
468#[derive(Clone, Copy, Debug)]
469pub enum Edit {
470    Split,
471    Search,
472    Minimize,
473}
474
475impl Edit {
476    fn name(self) -> &'static str {
477        match self {
478            Self::Split => "split boundary schedule",
479            Self::Search => "search boundary schedule",
480            Self::Minimize => "minimize boundary schedule",
481        }
482    }
483}
484
485/// Strict protocol cursor. A failed observation never advances it. There is
486/// deliberately no fast-forward or permissive-desync option. Production
487/// integration must route these errors through the controller refusal path.
488pub struct Cursor<'a> {
489    schedule: &'a Schedule,
490    next: usize,
491}
492
493impl Cursor<'_> {
494    pub fn stored_index(&self) -> usize {
495        self.next
496    }
497
498    pub fn expected(&self) -> Option<&Record> {
499        self.schedule.records.get(self.next)
500    }
501
502    pub fn observe(&mut self, observed: &Record) -> Result<(), Error> {
503        let Some(expected) = self.expected() else {
504            return Err(Error::Exhausted {
505                index: self.next,
506                observed: Box::new(observed.clone()),
507            });
508        };
509        expected.check_observation(self.next, observed)?;
510        self.next += 1;
511        Ok(())
512    }
513
514    pub fn finish(self) -> Result<(), Error> {
515        let remaining = self.schedule.records.len() - self.next;
516        if remaining == 0 {
517            Ok(())
518        } else {
519            Err(Error::Incomplete {
520                index: self.next,
521                remaining,
522            })
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::schedule::Op;
531
532    #[test]
533    fn birth_identity_is_required_and_cannot_move_without_exec() {
534        let first = boundary();
535        let bytes = serde_json::to_value(&first).unwrap();
536        for missing in ["birth_tid", "birth_generation"] {
537            let mut old = bytes.clone();
538            old["identity"]["owner"]
539                .as_object_mut()
540                .unwrap()
541                .remove(missing);
542            assert!(serde_json::from_value::<Boundary>(old).is_err());
543        }
544        let mut changed = first.clone();
545        changed.identity.owner.tid = DetTid::from_raw(4);
546        assert!(
547            Schedule::new(vec![
548                Record::TimesliceEnd(first.clone()),
549                Record::TimesliceEnd(changed.clone())
550            ])
551            .is_err()
552        );
553        changed.identity.owner.address_space_creator = DetTid::from_raw(4);
554        changed.identity.owner.address_space_generation += 1;
555        assert!(
556            Schedule::new(vec![
557                Record::TimesliceEnd(first.clone()),
558                Record::TimesliceEnd(changed.clone())
559            ])
560            .is_ok()
561        );
562        let mut retired = first.clone();
563        retired.identity.slice += 1;
564        assert!(
565            Schedule::new(vec![
566                Record::TimesliceEnd(first),
567                Record::TimesliceEnd(changed),
568                Record::TimesliceEnd(retired)
569            ])
570            .is_err()
571        );
572    }
573
574    fn boundary() -> Boundary {
575        Boundary {
576            identity: Identity {
577                owner: Owner {
578                    tid: DetTid::from_raw(3),
579                    thread_generation: 0,
580                    birth_tid: DetTid::from_raw(3),
581                    birth_generation: 0,
582                    address_space_creator: DetTid::from_raw(3),
583                    address_space_generation: 0,
584                },
585                cause: Cause::SoftwareCheckin,
586                phase: Phase::Posthook,
587                entry: Entry::Syscall {
588                    syscall: Sysno::write,
589                    occurrence: 7,
590                },
591                committed_rcbs: 83,
592                checkin: 10,
593                slice: 1,
594            },
595            count: 1,
596            start_rip: InstructionPointer::new(0x1000),
597            end_rip: InstructionPointer::new(0x1002),
598            end_time: Some(LogicalTime::from_rcbs(83)),
599        }
600    }
601
602    fn one() -> Schedule {
603        Schedule::new(vec![Record::TimesliceEnd(boundary())]).unwrap()
604    }
605
606    #[test]
607    fn envelope_round_trip_preserves_every_field() {
608        let schedule = one();
609        let encoded = serde_json::to_vec(&schedule).unwrap();
610        assert_eq!(
611            serde_json::from_slice::<Schedule>(&encoded).unwrap(),
612            schedule
613        );
614        assert_eq!(
615            serde_json::to_vec(&serde_json::from_slice::<Schedule>(&encoded).unwrap()).unwrap(),
616            encoded
617        );
618    }
619
620    #[test]
621    fn empty_schedule_still_requires_supported_protocol() {
622        let original = Schedule::new(vec![]).unwrap();
623        let value = serde_json::to_value(&original).unwrap();
624        assert_eq!(value["protocol"], PROTOCOL);
625        assert_eq!(value["version"], VERSION);
626        for key in ["protocol", "version", "record_count", "boundary_count"] {
627            let mut bad = value.clone();
628            bad.as_object_mut().unwrap().remove(key);
629            assert!(
630                serde_json::from_value::<Schedule>(bad).is_err(),
631                "missing {key}"
632            );
633        }
634        for bad_version in [0, 2, u32::MAX] {
635            let mut bad = value.clone();
636            bad["version"] = bad_version.into();
637            assert!(serde_json::from_value::<Schedule>(bad).is_err());
638        }
639        let mut bad = value;
640        bad["protocol"] = "unknown".into();
641        assert!(serde_json::from_value::<Schedule>(bad).is_err());
642    }
643
644    #[test]
645    fn deleting_sole_boundary_cannot_select_legacy_replay() {
646        let mut bad = serde_json::to_value(one()).unwrap();
647        bad["records"] = serde_json::json!([]);
648        assert!(serde_json::from_value::<Schedule>(bad.clone()).is_err());
649        bad["record_count"] = 0.into();
650        assert!(serde_json::from_value::<Schedule>(bad.clone()).is_err());
651        bad["boundary_count"] = 0.into();
652        let empty: Schedule = serde_json::from_value(bad).unwrap();
653        assert!(matches!(
654            empty.clone().into_legacy_events(),
655            Err(Error::UnsupportedEdit { .. })
656        ));
657        assert!(matches!(
658            empty.cursor().observe(&Record::TimesliceEnd(boundary())),
659            Err(Error::Exhausted { index: 0, .. })
660        ));
661    }
662
663    #[test]
664    fn malformed_fields_are_not_ignored() {
665        let value = serde_json::to_value(one()).unwrap();
666        for pointer in [
667            "",
668            "/records/0/TimesliceEnd",
669            "/records/0/TimesliceEnd/identity",
670            "/records/0/TimesliceEnd/identity/owner",
671        ] {
672            let mut bad = value.clone();
673            bad.pointer_mut(pointer)
674                .unwrap()
675                .as_object_mut()
676                .unwrap()
677                .insert("future_field".into(), true.into());
678            assert!(
679                serde_json::from_value::<Schedule>(bad).is_err(),
680                "unknown field at {pointer}"
681            );
682        }
683        let mut bad = value.clone();
684        bad["records"][0]["TimesliceEnd"]["identity"]["cause"] = "Unknown".into();
685        assert!(serde_json::from_value::<Schedule>(bad).is_err());
686        let encoded = serde_json::to_vec(&one()).unwrap();
687        assert!(serde_json::from_slice::<Schedule>(&encoded[..encoded.len() - 1]).is_err());
688    }
689
690    #[test]
691    fn counts_and_cause_are_validated_on_deserialization() {
692        for count in [0, 2, u32::MAX] {
693            let mut bad = serde_json::to_value(one()).unwrap();
694            bad["records"][0]["TimesliceEnd"]["count"] = count.into();
695            assert!(serde_json::from_value::<Schedule>(bad).is_err());
696        }
697        for cause in [
698            Cause::PmuTimer,
699            Cause::NoRcbTimer,
700            Cause::ExplicitSchedYield,
701        ] {
702            let mut b = boundary();
703            b.identity.cause = cause;
704            assert!(Schedule::new(vec![Record::TimesliceEnd(b)]).is_err());
705        }
706        for (cause, entry) in [
707            (Cause::PmuTimer, Entry::PmuTimer),
708            (Cause::NoRcbTimer, Entry::NoRcbTimer),
709            (
710                Cause::ExplicitSchedYield,
711                Entry::Syscall {
712                    syscall: Sysno::sched_yield,
713                    occurrence: 1,
714                },
715            ),
716        ] {
717            let mut b = boundary();
718            b.identity.cause = cause;
719            b.identity.entry = entry;
720            assert!(Schedule::new(vec![Record::TimesliceEnd(b)]).is_ok());
721        }
722    }
723
724    #[test]
725    fn every_mandatory_identity_field_is_exact_before_cursor_advance() {
726        let original = boundary();
727        let schedule = one();
728        let mut mutations = vec![];
729        let mut b = original.clone();
730        b.identity.owner.tid = DetTid::from_raw(4);
731        mutations.push(b);
732        let mut b = original.clone();
733        b.identity.owner.thread_generation += 1;
734        mutations.push(b);
735        let mut b = original.clone();
736        b.identity.owner.address_space_generation += 1;
737        mutations.push(b);
738        let mut b = original.clone();
739        b.identity.cause = Cause::ExplicitSchedYield;
740        mutations.push(b);
741        let mut b = original.clone();
742        b.identity.phase = Phase::Prehook;
743        mutations.push(b);
744        let mut b = original.clone();
745        b.identity.committed_rcbs += 1;
746        mutations.push(b);
747        let mut b = original.clone();
748        b.identity.checkin += 1;
749        mutations.push(b);
750        let mut b = original.clone();
751        b.identity.slice += 1;
752        mutations.push(b);
753        let mut b = original.clone();
754        b.identity.entry = Entry::Syscall {
755            syscall: Sysno::read,
756            occurrence: 7,
757        };
758        mutations.push(b);
759        let mut b = original.clone();
760        b.identity.entry = Entry::Syscall {
761            syscall: Sysno::write,
762            occurrence: 8,
763        };
764        mutations.push(b);
765        let mut b = original.clone();
766        b.count = 2;
767        mutations.push(b);
768        let mut b = original.clone();
769        b.end_rip = InstructionPointer::new(0x2000);
770        mutations.push(b);
771        let mut b = original.clone();
772        b.start_rip = None;
773        mutations.push(b);
774        let mut b = original.clone();
775        b.end_time = None;
776        mutations.push(b);
777        let mut cursor = schedule.cursor();
778        for mutation in mutations {
779            assert!(matches!(
780                cursor.observe(&Record::TimesliceEnd(mutation)),
781                Err(Error::Mismatch { index: 0, .. })
782            ));
783            assert_eq!(cursor.stored_index(), 0);
784        }
785        cursor.observe(&Record::TimesliceEnd(original)).unwrap();
786        cursor.finish().unwrap();
787    }
788
789    #[test]
790    fn strip_times_retains_payload_and_rip_evidence() {
791        let mut schedule = one();
792        let before = schedule.clone();
793        schedule.strip_times();
794        let mut expected = boundary();
795        expected.end_time = None;
796        assert_eq!(schedule.records(), &[Record::TimesliceEnd(expected)]);
797        let mut cursor = schedule.cursor();
798        cursor.observe(&before.records()[0]).unwrap();
799        cursor.finish().unwrap();
800        let mut wrong_rcb = boundary();
801        wrong_rcb.identity.committed_rcbs += 1;
802        assert!(
803            schedule
804                .cursor()
805                .observe(&Record::TimesliceEnd(wrong_rcb))
806                .is_err()
807        );
808    }
809
810    #[test]
811    fn adjacent_zero_rcb_boundaries_are_distinct_and_cannot_be_duplicated() {
812        let first = boundary();
813        let mut second = first.clone();
814        second.identity.slice = 2;
815        let schedule = Schedule::new(vec![
816            Record::TimesliceEnd(first.clone()),
817            Record::TimesliceEnd(second.clone()),
818        ])
819        .unwrap();
820        let mut cursor = schedule.cursor();
821        cursor
822            .observe(&Record::TimesliceEnd(first.clone()))
823            .unwrap();
824        assert!(
825            cursor
826                .observe(&Record::TimesliceEnd(first.clone()))
827                .is_err()
828        );
829        assert_eq!(cursor.stored_index(), 1);
830        cursor.observe(&Record::TimesliceEnd(second)).unwrap();
831        cursor.finish().unwrap();
832        assert!(
833            Schedule::new(vec![
834                Record::TimesliceEnd(first.clone()),
835                Record::TimesliceEnd(first)
836            ])
837            .is_err()
838        );
839    }
840
841    #[test]
842    fn missing_and_reordered_slices_are_refused_even_with_equal_rcbs() {
843        let first = boundary();
844        for slice in [0, 1, 3, u64::MAX] {
845            let mut second = first.clone();
846            second.identity.slice = slice;
847            assert!(
848                Schedule::new(vec![
849                    Record::TimesliceEnd(first.clone()),
850                    Record::TimesliceEnd(second)
851                ])
852                .is_err()
853            );
854        }
855    }
856
857    #[test]
858    fn repeated_site_requires_actual_checkin_and_syscall_occurrence() {
859        let first = boundary();
860        let mut second = first.clone();
861        second.identity.slice += 1;
862        second.identity.checkin += 1;
863        second.identity.entry = Entry::Syscall {
864            syscall: Sysno::write,
865            occurrence: 8,
866        };
867        let mut schedule = Schedule::new(vec![
868            Record::TimesliceEnd(first.clone()),
869            Record::TimesliceEnd(second.clone()),
870        ])
871        .unwrap();
872        schedule.strip_times();
873        let mut cursor = schedule.cursor();
874        cursor
875            .observe(&Record::TimesliceEnd(first.clone()))
876            .unwrap();
877        let mut stale = first;
878        stale.identity.slice = 2;
879        assert!(cursor.observe(&Record::TimesliceEnd(stale)).is_err());
880        assert_eq!(cursor.stored_index(), 1);
881        cursor.observe(&Record::TimesliceEnd(second)).unwrap();
882    }
883
884    #[test]
885    fn exec_generation_does_not_accept_an_old_boundary() {
886        let old = boundary();
887        let mut new = old.clone();
888        new.identity.owner.address_space_generation += 1;
889        new.identity.committed_rcbs = 0;
890        new.identity.checkin = 1;
891        let schedule = Schedule::new(vec![
892            Record::TimesliceEnd(old.clone()),
893            Record::TimesliceEnd(new.clone()),
894        ])
895        .unwrap();
896        let mut cursor = schedule.cursor();
897        cursor.observe(&Record::TimesliceEnd(old.clone())).unwrap();
898        assert!(cursor.observe(&Record::TimesliceEnd(old)).is_err());
899        cursor.observe(&Record::TimesliceEnd(new)).unwrap();
900        cursor.finish().unwrap();
901    }
902
903    #[test]
904    fn serialized_stream_cannot_return_to_an_old_generation() {
905        let old = boundary();
906        let mut new = old.clone();
907        new.identity.owner.address_space_generation += 1;
908        new.identity.committed_rcbs = 0;
909        new.identity.checkin = 1;
910        let mut stale = old.clone();
911        stale.identity.slice = 2;
912        assert!(
913            Schedule::new(vec![
914                Record::TimesliceEnd(old),
915                Record::TimesliceEnd(new),
916                Record::TimesliceEnd(stale),
917            ])
918            .is_err()
919        );
920    }
921
922    #[test]
923    fn recorded_cause_is_not_inferred_from_rip_or_rcb() {
924        let mut timer = boundary();
925        timer.identity.cause = Cause::PmuTimer;
926        timer.identity.entry = Entry::PmuTimer;
927        let schedule = Schedule::new(vec![Record::TimesliceEnd(timer)]).unwrap();
928        assert!(
929            schedule
930                .cursor()
931                .observe(&Record::TimesliceEnd(boundary()))
932                .is_err()
933        );
934    }
935
936    #[test]
937    fn missing_extra_and_skipped_records_cannot_report_completion() {
938        assert!(matches!(
939            one().cursor().finish(),
940            Err(Error::Incomplete {
941                index: 0,
942                remaining: 1
943            })
944        ));
945        let schedule = one();
946        let mut cursor = schedule.cursor();
947        cursor.observe(&Record::TimesliceEnd(boundary())).unwrap();
948        assert!(matches!(
949            cursor.observe(&Record::TimesliceEnd(boundary())),
950            Err(Error::Exhausted { index: 1, .. })
951        ));
952        assert_eq!(cursor.stored_index(), 1);
953        let event = Record::Event(SchedEvent::branches(DetTid::from_raw(3), 83));
954        let schedule = Schedule::new(vec![
955            event.clone(),
956            Record::TimesliceEnd(boundary()),
957            event.clone(),
958        ])
959        .unwrap();
960        let mut cursor = schedule.cursor();
961        cursor.observe(&event).unwrap();
962        assert!(cursor.observe(&event).is_err());
963        assert_eq!(cursor.stored_index(), 1);
964        assert!(cursor.finish().is_err());
965    }
966
967    #[test]
968    fn stored_indices_include_boundaries() {
969        let event = Record::Event(SchedEvent::branches(DetTid::from_raw(3), 83));
970        let schedule = Schedule::new(vec![
971            event.clone(),
972            Record::TimesliceEnd(boundary()),
973            event.clone(),
974        ])
975        .unwrap();
976        let mut cursor = schedule.cursor();
977        for (index, record) in schedule.records().iter().enumerate() {
978            assert_eq!(cursor.stored_index(), index);
979            assert_eq!(cursor.expected(), Some(record));
980            cursor.observe(record).unwrap();
981        }
982        assert_eq!(cursor.stored_index(), 3);
983        cursor.finish().unwrap();
984    }
985
986    #[test]
987    fn unsupported_edits_fail_before_output_changes() {
988        for mut schedule in [one(), Schedule::new(vec![]).unwrap()] {
989            let before = serde_json::to_vec(&schedule).unwrap();
990            for edit in [Edit::Split, Edit::Search, Edit::Minimize] {
991                assert!(matches!(
992                    schedule.edit(edit),
993                    Err(Error::UnsupportedEdit { .. })
994                ));
995                assert_eq!(serde_json::to_vec(&schedule).unwrap(), before);
996            }
997            assert!(schedule.into_legacy_events().is_err());
998        }
999    }
1000
1001    #[test]
1002    fn legacy_operation_tags_are_unchanged() {
1003        for (op, json, tag) in [
1004            (Op::Branch, "\"Branch\"", 0),
1005            (Op::Rdtsc, "\"Rdtsc\"", 1),
1006            (Op::Cpuid, "\"Cpuid\"", 2),
1007            (Op::OtherInstructions, "\"OtherInstructions\"", 4),
1008        ] {
1009            assert_eq!(serde_json::to_string(&op).unwrap(), json);
1010            assert_eq!(
1011                bincode::serde::encode_to_vec(op, bincode::config::standard()).unwrap(),
1012                vec![tag]
1013            );
1014        }
1015    }
1016}