1#![forbid(unsafe_code)]
33
34use std::collections::VecDeque;
35use std::fmt;
36use web_time::Instant;
37
38use crate::voi_sampling::{VoiConfig, VoiSampler, VoiSummary};
39
40#[derive(Debug, Clone, PartialEq)]
46pub enum TaskEvent {
47 Spawn {
49 task_id: u64,
50 priority: u8,
51 name: Option<String>,
52 },
53 Start { task_id: u64 },
55 Yield { task_id: u64 },
57 Wakeup { task_id: u64, reason: WakeupReason },
59 Complete { task_id: u64 },
61 Failed { task_id: u64, error: String },
63 Cancelled { task_id: u64, reason: CancelReason },
65 PolicyChange {
67 from: SchedulerPolicy,
68 to: SchedulerPolicy,
69 },
70 QueueSnapshot { queued: usize, running: usize },
72 Custom { tag: String, data: String },
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum WakeupReason {
79 Timer,
81 IoReady,
83 Dependency { task_id: u64 },
85 UserAction,
87 Explicit,
89 Other(String),
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub enum CancelReason {
96 UserRequest,
98 Timeout,
100 HazardPolicy { expected_loss: f64 },
102 Shutdown,
104 Other(String),
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum SchedulerPolicy {
111 Fifo,
113 Priority,
115 ShortestFirst,
117 RoundRobin,
119 WeightedFair,
121}
122
123impl fmt::Display for SchedulerPolicy {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::Fifo => write!(f, "fifo"),
127 Self::Priority => write!(f, "priority"),
128 Self::ShortestFirst => write!(f, "shortest_first"),
129 Self::RoundRobin => write!(f, "round_robin"),
130 Self::WeightedFair => write!(f, "weighted_fair"),
131 }
132 }
133}
134
135#[derive(Debug, Clone)]
141pub struct TraceEntry {
142 pub seq: u64,
144 pub tick: u64,
146 pub event: TaskEvent,
148}
149
150impl TraceEntry {
151 pub fn to_jsonl(&self) -> String {
153 let event_type = match &self.event {
154 TaskEvent::Spawn { .. } => "spawn",
155 TaskEvent::Start { .. } => "start",
156 TaskEvent::Yield { .. } => "yield",
157 TaskEvent::Wakeup { .. } => "wakeup",
158 TaskEvent::Complete { .. } => "complete",
159 TaskEvent::Failed { .. } => "failed",
160 TaskEvent::Cancelled { .. } => "cancelled",
161 TaskEvent::PolicyChange { .. } => "policy_change",
162 TaskEvent::QueueSnapshot { .. } => "queue_snapshot",
163 TaskEvent::Custom { .. } => "custom",
164 };
165
166 let details = match &self.event {
167 TaskEvent::Spawn {
168 task_id,
169 priority,
170 name,
171 } => {
172 format!(
173 "\"task_id\":{},\"priority\":{},\"name\":{}",
174 task_id,
175 priority,
176 name.as_ref()
177 .map(|n| format!("\"{}\"", n))
178 .unwrap_or_else(|| "null".to_string())
179 )
180 }
181 TaskEvent::Start { task_id } => format!("\"task_id\":{}", task_id),
182 TaskEvent::Yield { task_id } => format!("\"task_id\":{}", task_id),
183 TaskEvent::Wakeup { task_id, reason } => {
184 let reason_str = match reason {
185 WakeupReason::Timer => "timer".to_string(),
186 WakeupReason::IoReady => "io_ready".to_string(),
187 WakeupReason::Dependency { task_id } => format!("dependency:{}", task_id),
188 WakeupReason::UserAction => "user_action".to_string(),
189 WakeupReason::Explicit => "explicit".to_string(),
190 WakeupReason::Other(s) => format!("other:{}", s),
191 };
192 format!("\"task_id\":{},\"reason\":\"{}\"", task_id, reason_str)
193 }
194 TaskEvent::Complete { task_id } => format!("\"task_id\":{}", task_id),
195 TaskEvent::Failed { task_id, error } => {
196 format!("\"task_id\":{},\"error\":\"{}\"", task_id, error)
197 }
198 TaskEvent::Cancelled { task_id, reason } => {
199 let reason_str = match reason {
200 CancelReason::UserRequest => "user_request".to_string(),
201 CancelReason::Timeout => "timeout".to_string(),
202 CancelReason::HazardPolicy { expected_loss } => {
203 format!("hazard_policy:{:.4}", expected_loss)
204 }
205 CancelReason::Shutdown => "shutdown".to_string(),
206 CancelReason::Other(s) => format!("other:{}", s),
207 };
208 format!("\"task_id\":{},\"reason\":\"{}\"", task_id, reason_str)
209 }
210 TaskEvent::PolicyChange { from, to } => {
211 format!("\"from\":\"{}\",\"to\":\"{}\"", from, to)
212 }
213 TaskEvent::QueueSnapshot { queued, running } => {
214 format!("\"queued\":{},\"running\":{}", queued, running)
215 }
216 TaskEvent::Custom { tag, data } => {
217 format!("\"tag\":\"{}\",\"data\":\"{}\"", tag, data)
218 }
219 };
220
221 format!(
222 "{{\"seq\":{},\"tick\":{},\"event\":\"{}\",{}}}",
223 self.seq, self.tick, event_type, details
224 )
225 }
226}
227
228#[derive(Debug, Clone)]
234pub struct TraceConfig {
235 pub max_entries: usize,
237 pub auto_snapshot: bool,
241 pub snapshot_sampling: Option<VoiConfig>,
243 pub snapshot_change_threshold: usize,
245 pub seed: u64,
247}
248
249impl Default for TraceConfig {
250 fn default() -> Self {
251 Self {
252 max_entries: 10_000,
253 auto_snapshot: false,
254 snapshot_sampling: None,
255 snapshot_change_threshold: 1,
256 seed: 0,
257 }
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct ScheduleTrace {
264 config: TraceConfig,
266 entries: VecDeque<TraceEntry>,
268 seq: u64,
270 tick: u64,
272 snapshot_sampler: Option<VoiSampler>,
274 last_snapshot: Option<(usize, usize)>,
276}
277
278impl ScheduleTrace {
279 #[must_use]
281 pub fn new() -> Self {
282 Self::with_config(TraceConfig::default())
283 }
284
285 #[must_use]
287 pub fn with_config(config: TraceConfig) -> Self {
288 let capacity = if config.max_entries > 0 {
289 config.max_entries
290 } else {
291 1024
292 };
293 let snapshot_sampler = config.snapshot_sampling.clone().map(VoiSampler::new);
294 Self {
295 config,
296 entries: VecDeque::with_capacity(capacity),
297 seq: 0,
298 tick: 0,
299 snapshot_sampler,
300 last_snapshot: None,
301 }
302 }
303
304 pub fn advance_tick(&mut self) {
306 self.tick += 1;
307 }
308
309 pub fn set_tick(&mut self, tick: u64) {
311 self.tick = tick;
312 }
313
314 #[must_use]
316 pub fn tick(&self) -> u64 {
317 self.tick
318 }
319
320 pub fn record(&mut self, event: TaskEvent) {
322 let entry = TraceEntry {
323 seq: self.seq,
324 tick: self.tick,
325 event,
326 };
327 self.seq += 1;
328
329 if self.config.max_entries > 0 && self.entries.len() >= self.config.max_entries {
331 self.entries.pop_front();
332 }
333
334 self.entries.push_back(entry);
335 }
336
337 pub fn record_with_queue_state(&mut self, event: TaskEvent, queued: usize, running: usize) {
339 self.record_with_queue_state_at(event, queued, running, Instant::now());
340 }
341
342 pub fn record_with_queue_state_at(
344 &mut self,
345 event: TaskEvent,
346 queued: usize,
347 running: usize,
348 now: Instant,
349 ) {
350 self.record(event);
351 if self.config.auto_snapshot {
352 self.maybe_snapshot(queued, running, now);
353 }
354 }
355
356 fn maybe_snapshot(&mut self, queued: usize, running: usize, now: Instant) {
358 let should_sample = if let Some(ref mut sampler) = self.snapshot_sampler {
359 let decision = sampler.decide(now);
360 if !decision.should_sample {
361 return;
362 }
363 let violated = self
364 .last_snapshot
365 .map(|(prev_q, prev_r)| {
366 let delta = prev_q.abs_diff(queued) + prev_r.abs_diff(running);
367 delta >= self.config.snapshot_change_threshold
368 })
369 .unwrap_or(false);
370 sampler.observe_at(violated, now);
371 true
372 } else {
373 true
374 };
375
376 if should_sample {
377 self.record(TaskEvent::QueueSnapshot { queued, running });
378 self.last_snapshot = Some((queued, running));
379 }
380 }
381
382 pub fn spawn(&mut self, task_id: u64, priority: u8, name: Option<String>) {
384 self.record(TaskEvent::Spawn {
385 task_id,
386 priority,
387 name,
388 });
389 }
390
391 pub fn start(&mut self, task_id: u64) {
393 self.record(TaskEvent::Start { task_id });
394 }
395
396 pub fn complete(&mut self, task_id: u64) {
398 self.record(TaskEvent::Complete { task_id });
399 }
400
401 pub fn cancel(&mut self, task_id: u64, reason: CancelReason) {
403 self.record(TaskEvent::Cancelled { task_id, reason });
404 }
405
406 #[must_use]
408 pub fn entries(&self) -> &VecDeque<TraceEntry> {
409 &self.entries
410 }
411
412 #[must_use]
414 pub fn len(&self) -> usize {
415 self.entries.len()
416 }
417
418 #[must_use]
420 pub fn is_empty(&self) -> bool {
421 self.entries.is_empty()
422 }
423
424 pub fn clear(&mut self) {
426 self.entries.clear();
427 self.seq = 0;
428 self.last_snapshot = None;
429 if let Some(ref mut sampler) = self.snapshot_sampler {
430 let config = sampler.config().clone();
431 *sampler = VoiSampler::new(config);
432 }
433 }
434
435 #[must_use]
437 pub fn snapshot_sampling_summary(&self) -> Option<VoiSummary> {
438 self.snapshot_sampler.as_ref().map(VoiSampler::summary)
439 }
440
441 #[must_use]
443 pub fn snapshot_sampling_logs_jsonl(&self) -> Option<String> {
444 self.snapshot_sampler
445 .as_ref()
446 .map(VoiSampler::logs_to_jsonl)
447 }
448
449 #[must_use]
451 pub fn to_jsonl(&self) -> String {
452 self.entries
453 .iter()
454 .map(|e| e.to_jsonl())
455 .collect::<Vec<_>>()
456 .join("\n")
457 }
458
459 #[must_use]
463 pub fn checksum(&self) -> u64 {
464 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
466 const FNV_PRIME: u64 = 0x100000001b3;
467
468 let mut hash = FNV_OFFSET;
469
470 for entry in &self.entries {
471 for byte in entry.seq.to_le_bytes() {
473 hash ^= byte as u64;
474 hash = hash.wrapping_mul(FNV_PRIME);
475 }
476
477 for byte in entry.tick.to_le_bytes() {
479 hash ^= byte as u64;
480 hash = hash.wrapping_mul(FNV_PRIME);
481 }
482
483 let event_bytes = self.event_to_bytes(&entry.event);
485 for byte in event_bytes {
486 hash ^= byte as u64;
487 hash = hash.wrapping_mul(FNV_PRIME);
488 }
489 }
490
491 hash
492 }
493
494 #[must_use]
496 pub fn checksum_hex(&self) -> String {
497 format!("{:016x}", self.checksum())
498 }
499
500 fn event_to_bytes(&self, event: &TaskEvent) -> Vec<u8> {
502 let mut bytes = Vec::new();
503
504 match event {
505 TaskEvent::Spawn {
506 task_id, priority, ..
507 } => {
508 bytes.push(0x01);
509 bytes.extend_from_slice(&task_id.to_le_bytes());
510 bytes.push(*priority);
511 }
512 TaskEvent::Start { task_id } => {
513 bytes.push(0x02);
514 bytes.extend_from_slice(&task_id.to_le_bytes());
515 }
516 TaskEvent::Yield { task_id } => {
517 bytes.push(0x03);
518 bytes.extend_from_slice(&task_id.to_le_bytes());
519 }
520 TaskEvent::Wakeup { task_id, .. } => {
521 bytes.push(0x04);
522 bytes.extend_from_slice(&task_id.to_le_bytes());
523 }
524 TaskEvent::Complete { task_id } => {
525 bytes.push(0x05);
526 bytes.extend_from_slice(&task_id.to_le_bytes());
527 }
528 TaskEvent::Failed { task_id, .. } => {
529 bytes.push(0x06);
530 bytes.extend_from_slice(&task_id.to_le_bytes());
531 }
532 TaskEvent::Cancelled { task_id, .. } => {
533 bytes.push(0x07);
534 bytes.extend_from_slice(&task_id.to_le_bytes());
535 }
536 TaskEvent::PolicyChange { from, to } => {
537 bytes.push(0x08);
538 bytes.push(*from as u8);
539 bytes.push(*to as u8);
540 }
541 TaskEvent::QueueSnapshot { queued, running } => {
542 bytes.push(0x09);
543 bytes.extend_from_slice(&(*queued as u64).to_le_bytes());
544 bytes.extend_from_slice(&(*running as u64).to_le_bytes());
545 }
546 TaskEvent::Custom { tag, data } => {
547 bytes.push(0x0A);
548 bytes.extend_from_slice(tag.as_bytes());
549 bytes.push(0x00); bytes.extend_from_slice(data.as_bytes());
551 }
552 }
553
554 bytes
555 }
556}
557
558impl Default for ScheduleTrace {
559 fn default() -> Self {
560 Self::new()
561 }
562}
563
564#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum GoldenCompareResult {
571 Match,
573 Mismatch { expected: u64, actual: u64 },
575 MissingGolden,
577}
578
579impl GoldenCompareResult {
580 #[must_use]
582 pub fn is_match(&self) -> bool {
583 matches!(self, Self::Match)
584 }
585}
586
587#[must_use]
589pub fn compare_golden(trace: &ScheduleTrace, expected: u64) -> GoldenCompareResult {
590 let actual = trace.checksum();
591 if actual == expected {
592 GoldenCompareResult::Match
593 } else {
594 GoldenCompareResult::Mismatch { expected, actual }
595 }
596}
597
598#[derive(Debug, Clone)]
607pub struct IsomorphismProof {
608 pub change_description: String,
610 pub old_checksum: u64,
612 pub new_checksum: u64,
614 pub preserved_invariants: Vec<String>,
616 pub justification: String,
618 pub approved_by: Option<String>,
620 pub approved_at: Option<String>,
622}
623
624impl IsomorphismProof {
625 pub fn new(
627 change_description: impl Into<String>,
628 old_checksum: u64,
629 new_checksum: u64,
630 ) -> Self {
631 Self {
632 change_description: change_description.into(),
633 old_checksum,
634 new_checksum,
635 preserved_invariants: Vec::new(),
636 justification: String::new(),
637 approved_by: None,
638 approved_at: None,
639 }
640 }
641
642 #[must_use]
644 pub fn with_invariant(mut self, invariant: impl Into<String>) -> Self {
645 self.preserved_invariants.push(invariant.into());
646 self
647 }
648
649 #[must_use]
651 pub fn with_justification(mut self, justification: impl Into<String>) -> Self {
652 self.justification = justification.into();
653 self
654 }
655
656 #[must_use]
658 pub fn to_json(&self) -> String {
659 let invariants = self
660 .preserved_invariants
661 .iter()
662 .map(|i| format!("\"{}\"", i))
663 .collect::<Vec<_>>()
664 .join(",");
665
666 let old_checksum = format!("{:016x}", self.old_checksum);
667 let new_checksum = format!("{:016x}", self.new_checksum);
668 let approved_by = self
669 .approved_by
670 .as_ref()
671 .map(|s| format!("\"{}\"", s))
672 .unwrap_or_else(|| "null".to_string());
673 let approved_at = self
674 .approved_at
675 .as_ref()
676 .map(|s| format!("\"{}\"", s))
677 .unwrap_or_else(|| "null".to_string());
678
679 format!(
680 r#"{{"change":"{}","old_checksum":"{}","new_checksum":"{}","invariants":[{}],"justification":"{}","approved_by":{},"approved_at":{}}}"#,
681 self.change_description,
682 old_checksum,
683 new_checksum,
684 invariants,
685 self.justification,
686 approved_by,
687 approved_at,
688 )
689 }
690}
691
692#[derive(Debug, Clone, Default)]
698pub struct TraceSummary {
699 pub total_events: usize,
701 pub spawns: usize,
703 pub completes: usize,
705 pub failures: usize,
707 pub cancellations: usize,
709 pub yields: usize,
711 pub wakeups: usize,
713 pub first_tick: u64,
715 pub last_tick: u64,
717 pub checksum: u64,
719}
720
721impl ScheduleTrace {
722 #[must_use]
724 pub fn summary(&self) -> TraceSummary {
725 let mut summary = TraceSummary {
726 total_events: self.entries.len(),
727 checksum: self.checksum(),
728 ..Default::default()
729 };
730
731 if let Some(first) = self.entries.front() {
732 summary.first_tick = first.tick;
733 }
734 if let Some(last) = self.entries.back() {
735 summary.last_tick = last.tick;
736 }
737
738 for entry in &self.entries {
739 match &entry.event {
740 TaskEvent::Spawn { .. } => summary.spawns += 1,
741 TaskEvent::Complete { .. } => summary.completes += 1,
742 TaskEvent::Failed { .. } => summary.failures += 1,
743 TaskEvent::Cancelled { .. } => summary.cancellations += 1,
744 TaskEvent::Yield { .. } => summary.yields += 1,
745 TaskEvent::Wakeup { .. } => summary.wakeups += 1,
746 _ => {}
747 }
748 }
749
750 summary
751 }
752}
753
754#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
763 fn unit_trace_ordering() {
764 let mut trace = ScheduleTrace::new();
765
766 trace.spawn(1, 0, Some("task_a".to_string()));
767 trace.advance_tick();
768 trace.start(1);
769 trace.advance_tick();
770 trace.complete(1);
771
772 assert_eq!(trace.len(), 3);
773
774 let entries: Vec<_> = trace.entries().iter().collect();
776 assert_eq!(entries[0].seq, 0);
777 assert_eq!(entries[1].seq, 1);
778 assert_eq!(entries[2].seq, 2);
779 assert_eq!(entries[0].tick, 0);
780 assert_eq!(entries[1].tick, 1);
781 assert_eq!(entries[2].tick, 2);
782 }
783
784 #[test]
785 fn unit_trace_hash_stable() {
786 let mut trace1 = ScheduleTrace::new();
788 let mut trace2 = ScheduleTrace::new();
789
790 for trace in [&mut trace1, &mut trace2] {
791 trace.spawn(1, 0, None);
792 trace.advance_tick();
793 trace.start(1);
794 trace.advance_tick();
795 trace.spawn(2, 1, None);
796 trace.advance_tick();
797 trace.complete(1);
798 trace.start(2);
799 trace.advance_tick();
800 trace.cancel(2, CancelReason::UserRequest);
801 }
802
803 assert_eq!(trace1.checksum(), trace2.checksum());
804 assert_eq!(trace1.checksum_hex(), trace2.checksum_hex());
805 }
806
807 #[test]
808 fn unit_hash_differs_on_order_change() {
809 let mut trace1 = ScheduleTrace::new();
810 trace1.spawn(1, 0, None);
811 trace1.spawn(2, 0, None);
812
813 let mut trace2 = ScheduleTrace::new();
814 trace2.spawn(2, 0, None);
815 trace2.spawn(1, 0, None);
816
817 assert_ne!(trace1.checksum(), trace2.checksum());
818 }
819
820 #[test]
821 fn unit_jsonl_format() {
822 let mut trace = ScheduleTrace::new();
823 trace.spawn(1, 0, Some("test".to_string()));
824
825 let jsonl = trace.to_jsonl();
826 assert!(jsonl.contains("\"event\":\"spawn\""));
827 assert!(jsonl.contains("\"task_id\":1"));
828 assert!(jsonl.contains("\"name\":\"test\""));
829 }
830
831 #[test]
832 fn unit_summary_counts() {
833 let mut trace = ScheduleTrace::new();
834
835 trace.spawn(1, 0, None);
836 trace.spawn(2, 0, None);
837 trace.start(1);
838 trace.complete(1);
839 trace.start(2);
840 trace.cancel(2, CancelReason::Timeout);
841
842 let summary = trace.summary();
843 assert_eq!(summary.total_events, 6);
844 assert_eq!(summary.spawns, 2);
845 assert_eq!(summary.completes, 1);
846 assert_eq!(summary.cancellations, 1);
847 }
848
849 #[test]
850 fn unit_golden_compare_match() {
851 let mut trace = ScheduleTrace::new();
852 trace.spawn(1, 0, None);
853 trace.complete(1);
854
855 let expected = trace.checksum();
856 let result = compare_golden(&trace, expected);
857 assert!(result.is_match());
858 }
859
860 #[test]
861 fn unit_golden_compare_mismatch() {
862 let mut trace = ScheduleTrace::new();
863 trace.spawn(1, 0, None);
864
865 let result = compare_golden(&trace, 0xDEADBEEF);
866 assert!(!result.is_match());
867
868 match result {
869 GoldenCompareResult::Mismatch { expected, actual } => {
870 assert_eq!(expected, 0xDEADBEEF);
871 assert_ne!(actual, 0xDEADBEEF);
872 }
873 _ => unreachable!("Expected mismatch"),
874 }
875 }
876
877 #[test]
878 fn unit_isomorphism_proof_json() {
879 let proof = IsomorphismProof::new("Optimized scheduler loop", 0x1234, 0x5678)
880 .with_invariant("All tasks complete in same order")
881 .with_invariant("No task starves")
882 .with_justification("Loop unrolling only affects timing, not ordering");
883
884 let json = proof.to_json();
885 assert!(json.contains("Optimized scheduler loop"));
886 assert!(json.contains("0000000000001234"));
887 assert!(json.contains("0000000000005678"));
888 }
889
890 #[test]
891 fn unit_max_entries_enforced() {
892 let config = TraceConfig {
893 max_entries: 3,
894 ..Default::default()
895 };
896 let mut trace = ScheduleTrace::with_config(config);
897
898 for i in 0..10 {
899 trace.spawn(i, 0, None);
900 }
901
902 assert_eq!(trace.len(), 3);
903
904 let entries: Vec<_> = trace.entries().iter().collect();
906 if let TaskEvent::Spawn { task_id, .. } = &entries[0].event {
907 assert_eq!(*task_id, 7);
908 }
909 }
910
911 #[test]
912 fn unit_clear_resets_state() {
913 let mut trace = ScheduleTrace::new();
914 trace.spawn(1, 0, None);
915 trace.spawn(2, 0, None);
916
917 trace.clear();
918
919 assert!(trace.is_empty());
920 assert_eq!(trace.len(), 0);
921 }
922
923 #[test]
924 fn unit_wakeup_reasons() {
925 let mut trace = ScheduleTrace::new();
926
927 trace.record(TaskEvent::Wakeup {
928 task_id: 1,
929 reason: WakeupReason::Timer,
930 });
931 trace.record(TaskEvent::Wakeup {
932 task_id: 2,
933 reason: WakeupReason::Dependency { task_id: 1 },
934 });
935 trace.record(TaskEvent::Wakeup {
936 task_id: 3,
937 reason: WakeupReason::IoReady,
938 });
939
940 let jsonl = trace.to_jsonl();
941 assert!(jsonl.contains("\"reason\":\"timer\""));
942 assert!(jsonl.contains("\"reason\":\"dependency:1\""));
943 assert!(jsonl.contains("\"reason\":\"io_ready\""));
944 }
945
946 #[test]
947 fn unit_auto_snapshot_with_sampling_records_queue() {
948 let config = TraceConfig {
949 auto_snapshot: true,
950 snapshot_sampling: Some(VoiConfig {
951 max_interval_events: 1,
952 sample_cost: 1.0,
953 ..Default::default()
954 }),
955 snapshot_change_threshold: 1,
956 ..Default::default()
957 };
958 let mut trace = ScheduleTrace::with_config(config);
959 let now = Instant::now();
960
961 trace.record_with_queue_state_at(
962 TaskEvent::Spawn {
963 task_id: 1,
964 priority: 0,
965 name: None,
966 },
967 3,
968 1,
969 now,
970 );
971
972 assert!(
973 trace
974 .entries()
975 .iter()
976 .any(|entry| matches!(entry.event, TaskEvent::QueueSnapshot { .. }))
977 );
978 let summary = trace.snapshot_sampling_summary().expect("sampling enabled");
979 assert_eq!(summary.total_samples, 1);
980 }
981
982 #[test]
983 fn unit_cancel_reasons() {
984 let mut trace = ScheduleTrace::new();
985
986 trace.cancel(1, CancelReason::UserRequest);
987 trace.cancel(2, CancelReason::Timeout);
988 trace.cancel(
989 3,
990 CancelReason::HazardPolicy {
991 expected_loss: 0.75,
992 },
993 );
994
995 let jsonl = trace.to_jsonl();
996 assert!(jsonl.contains("\"reason\":\"user_request\""));
997 assert!(jsonl.contains("\"reason\":\"timeout\""));
998 assert!(jsonl.contains("\"reason\":\"hazard_policy:0.7500\""));
999 }
1000
1001 #[test]
1002 fn unit_policy_change() {
1003 let mut trace = ScheduleTrace::new();
1004
1005 trace.record(TaskEvent::PolicyChange {
1006 from: SchedulerPolicy::Fifo,
1007 to: SchedulerPolicy::Priority,
1008 });
1009
1010 let jsonl = trace.to_jsonl();
1011 assert!(jsonl.contains("\"from\":\"fifo\""));
1012 assert!(jsonl.contains("\"to\":\"priority\""));
1013 }
1014
1015 #[test]
1018 fn trace_config_default_values() {
1019 let config = TraceConfig::default();
1020 assert_eq!(config.max_entries, 10_000);
1021 assert!(!config.auto_snapshot);
1022 assert!(config.snapshot_sampling.is_none());
1023 assert_eq!(config.snapshot_change_threshold, 1);
1024 assert_eq!(config.seed, 0);
1025 }
1026
1027 #[test]
1030 fn schedule_trace_default_impl() {
1031 let trace = ScheduleTrace::default();
1032 assert!(trace.is_empty());
1033 assert_eq!(trace.len(), 0);
1034 assert_eq!(trace.tick(), 0);
1035 }
1036
1037 #[test]
1038 fn with_config_unlimited_entries() {
1039 let config = TraceConfig {
1040 max_entries: 0,
1041 ..Default::default()
1042 };
1043 let mut trace = ScheduleTrace::with_config(config);
1044 for i in 0..50 {
1045 trace.spawn(i, 0, None);
1046 }
1047 assert_eq!(trace.len(), 50);
1048 }
1049
1050 #[test]
1053 fn set_tick_explicit() {
1054 let mut trace = ScheduleTrace::new();
1055 assert_eq!(trace.tick(), 0);
1056 trace.set_tick(42);
1057 assert_eq!(trace.tick(), 42);
1058 trace.advance_tick();
1059 assert_eq!(trace.tick(), 43);
1060 }
1061
1062 #[test]
1063 fn advance_tick_increments() {
1064 let mut trace = ScheduleTrace::new();
1065 trace.advance_tick();
1066 trace.advance_tick();
1067 trace.advance_tick();
1068 assert_eq!(trace.tick(), 3);
1069 }
1070
1071 #[test]
1074 fn record_with_queue_state_no_auto_snapshot() {
1075 let config = TraceConfig {
1076 auto_snapshot: false,
1077 ..Default::default()
1078 };
1079 let mut trace = ScheduleTrace::with_config(config);
1080 let now = Instant::now();
1081 trace.record_with_queue_state_at(TaskEvent::Start { task_id: 1 }, 5, 2, now);
1082 assert_eq!(trace.len(), 1);
1084 assert!(matches!(
1085 trace.entries().front().unwrap().event,
1086 TaskEvent::Start { task_id: 1 }
1087 ));
1088 }
1089
1090 #[test]
1093 fn snapshot_sampling_summary_none_without_sampler() {
1094 let trace = ScheduleTrace::new();
1095 assert!(trace.snapshot_sampling_summary().is_none());
1096 }
1097
1098 #[test]
1099 fn snapshot_sampling_logs_none_without_sampler() {
1100 let trace = ScheduleTrace::new();
1101 assert!(trace.snapshot_sampling_logs_jsonl().is_none());
1102 }
1103
1104 #[test]
1105 fn snapshot_sampling_logs_some_with_sampler() {
1106 let config = TraceConfig {
1107 auto_snapshot: true,
1108 snapshot_sampling: Some(VoiConfig::default()),
1109 ..Default::default()
1110 };
1111 let trace = ScheduleTrace::with_config(config);
1112 assert!(trace.snapshot_sampling_logs_jsonl().is_some());
1113 }
1114
1115 #[test]
1118 fn clear_resets_seq_counter() {
1119 let mut trace = ScheduleTrace::new();
1120 trace.spawn(1, 0, None);
1121 trace.spawn(2, 0, None);
1122 assert_eq!(trace.entries().back().unwrap().seq, 1);
1123
1124 trace.clear();
1125 trace.spawn(3, 0, None);
1126 assert_eq!(trace.entries().front().unwrap().seq, 0);
1128 }
1129
1130 #[test]
1131 fn clear_resets_sampler() {
1132 let config = TraceConfig {
1133 auto_snapshot: true,
1134 snapshot_sampling: Some(VoiConfig {
1135 max_interval_events: 1,
1136 sample_cost: 1.0,
1137 ..Default::default()
1138 }),
1139 ..Default::default()
1140 };
1141 let mut trace = ScheduleTrace::with_config(config);
1142 let now = Instant::now();
1143 trace.record_with_queue_state_at(
1144 TaskEvent::Spawn {
1145 task_id: 1,
1146 priority: 0,
1147 name: None,
1148 },
1149 3,
1150 1,
1151 now,
1152 );
1153 trace.clear();
1154 assert!(trace.is_empty());
1155 let summary = trace.snapshot_sampling_summary().unwrap();
1157 assert_eq!(summary.total_samples, 0);
1158 }
1159
1160 #[test]
1163 fn checksum_empty_trace() {
1164 let trace = ScheduleTrace::new();
1165 assert_eq!(trace.checksum(), 0xcbf29ce484222325);
1167 }
1168
1169 #[test]
1170 fn checksum_hex_format() {
1171 let trace = ScheduleTrace::new();
1172 let hex = trace.checksum_hex();
1173 assert_eq!(hex.len(), 16);
1174 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
1175 }
1176
1177 #[test]
1178 fn checksum_differs_for_different_events() {
1179 let mut t1 = ScheduleTrace::new();
1180 t1.spawn(1, 0, None);
1181
1182 let mut t2 = ScheduleTrace::new();
1183 t2.start(1);
1184
1185 assert_ne!(t1.checksum(), t2.checksum());
1186 }
1187
1188 #[test]
1191 fn golden_missing_golden_variant() {
1192 let result = GoldenCompareResult::MissingGolden;
1193 assert!(!result.is_match());
1194 }
1195
1196 #[test]
1197 fn golden_match_variant() {
1198 assert!(GoldenCompareResult::Match.is_match());
1199 }
1200
1201 #[test]
1204 fn scheduler_policy_display_all_variants() {
1205 assert_eq!(format!("{}", SchedulerPolicy::Fifo), "fifo");
1206 assert_eq!(format!("{}", SchedulerPolicy::Priority), "priority");
1207 assert_eq!(
1208 format!("{}", SchedulerPolicy::ShortestFirst),
1209 "shortest_first"
1210 );
1211 assert_eq!(format!("{}", SchedulerPolicy::RoundRobin), "round_robin");
1212 assert_eq!(
1213 format!("{}", SchedulerPolicy::WeightedFair),
1214 "weighted_fair"
1215 );
1216 }
1217
1218 #[test]
1221 fn summary_yields_wakeups_failures() {
1222 let mut trace = ScheduleTrace::new();
1223 trace.spawn(1, 0, None);
1224 trace.start(1);
1225 trace.record(TaskEvent::Yield { task_id: 1 });
1226 trace.record(TaskEvent::Wakeup {
1227 task_id: 1,
1228 reason: WakeupReason::Timer,
1229 });
1230 trace.record(TaskEvent::Failed {
1231 task_id: 1,
1232 error: "oops".to_string(),
1233 });
1234
1235 let summary = trace.summary();
1236 assert_eq!(summary.yields, 1);
1237 assert_eq!(summary.wakeups, 1);
1238 assert_eq!(summary.failures, 1);
1239 assert_eq!(summary.spawns, 1);
1240 assert_eq!(summary.completes, 0);
1241 assert_eq!(summary.cancellations, 0);
1242 }
1243
1244 #[test]
1245 fn summary_tick_range() {
1246 let mut trace = ScheduleTrace::new();
1247 trace.set_tick(10);
1248 trace.spawn(1, 0, None);
1249 trace.set_tick(50);
1250 trace.complete(1);
1251
1252 let summary = trace.summary();
1253 assert_eq!(summary.first_tick, 10);
1254 assert_eq!(summary.last_tick, 50);
1255 }
1256
1257 #[test]
1258 fn summary_empty_trace() {
1259 let trace = ScheduleTrace::new();
1260 let summary = trace.summary();
1261 assert_eq!(summary.total_events, 0);
1262 assert_eq!(summary.first_tick, 0);
1263 assert_eq!(summary.last_tick, 0);
1264 }
1265
1266 #[test]
1267 fn trace_summary_default() {
1268 let summary = TraceSummary::default();
1269 assert_eq!(summary.total_events, 0);
1270 assert_eq!(summary.spawns, 0);
1271 assert_eq!(summary.checksum, 0);
1272 }
1273
1274 #[test]
1277 fn jsonl_yield_event() {
1278 let mut trace = ScheduleTrace::new();
1279 trace.record(TaskEvent::Yield { task_id: 7 });
1280 let jsonl = trace.to_jsonl();
1281 assert!(jsonl.contains("\"event\":\"yield\""));
1282 assert!(jsonl.contains("\"task_id\":7"));
1283 }
1284
1285 #[test]
1286 fn jsonl_failed_event() {
1287 let mut trace = ScheduleTrace::new();
1288 trace.record(TaskEvent::Failed {
1289 task_id: 3,
1290 error: "timeout".to_string(),
1291 });
1292 let jsonl = trace.to_jsonl();
1293 assert!(jsonl.contains("\"event\":\"failed\""));
1294 assert!(jsonl.contains("\"error\":\"timeout\""));
1295 }
1296
1297 #[test]
1298 fn jsonl_custom_event() {
1299 let mut trace = ScheduleTrace::new();
1300 trace.record(TaskEvent::Custom {
1301 tag: "metric".to_string(),
1302 data: "cpu=42".to_string(),
1303 });
1304 let jsonl = trace.to_jsonl();
1305 assert!(jsonl.contains("\"event\":\"custom\""));
1306 assert!(jsonl.contains("\"tag\":\"metric\""));
1307 assert!(jsonl.contains("\"data\":\"cpu=42\""));
1308 }
1309
1310 #[test]
1311 fn jsonl_queue_snapshot_event() {
1312 let mut trace = ScheduleTrace::new();
1313 trace.record(TaskEvent::QueueSnapshot {
1314 queued: 5,
1315 running: 2,
1316 });
1317 let jsonl = trace.to_jsonl();
1318 assert!(jsonl.contains("\"event\":\"queue_snapshot\""));
1319 assert!(jsonl.contains("\"queued\":5"));
1320 assert!(jsonl.contains("\"running\":2"));
1321 }
1322
1323 #[test]
1324 fn jsonl_cancelled_event() {
1325 let mut trace = ScheduleTrace::new();
1326 trace.cancel(4, CancelReason::Shutdown);
1327 let jsonl = trace.to_jsonl();
1328 assert!(jsonl.contains("\"event\":\"cancelled\""));
1329 assert!(jsonl.contains("\"reason\":\"shutdown\""));
1330 }
1331
1332 #[test]
1333 fn jsonl_cancel_other_reason() {
1334 let mut trace = ScheduleTrace::new();
1335 trace.cancel(5, CancelReason::Other("oom".to_string()));
1336 let jsonl = trace.to_jsonl();
1337 assert!(jsonl.contains("\"reason\":\"other:oom\""));
1338 }
1339
1340 #[test]
1341 fn jsonl_spawn_without_name() {
1342 let mut trace = ScheduleTrace::new();
1343 trace.spawn(1, 3, None);
1344 let jsonl = trace.to_jsonl();
1345 assert!(jsonl.contains("\"name\":null"));
1346 assert!(jsonl.contains("\"priority\":3"));
1347 }
1348
1349 #[test]
1350 fn jsonl_complete_event() {
1351 let mut trace = ScheduleTrace::new();
1352 trace.complete(99);
1353 let jsonl = trace.to_jsonl();
1354 assert!(jsonl.contains("\"event\":\"complete\""));
1355 assert!(jsonl.contains("\"task_id\":99"));
1356 }
1357
1358 #[test]
1359 fn jsonl_start_event() {
1360 let mut trace = ScheduleTrace::new();
1361 trace.start(42);
1362 let jsonl = trace.to_jsonl();
1363 assert!(jsonl.contains("\"event\":\"start\""));
1364 assert!(jsonl.contains("\"task_id\":42"));
1365 }
1366
1367 #[test]
1368 fn jsonl_empty_trace() {
1369 let trace = ScheduleTrace::new();
1370 assert_eq!(trace.to_jsonl(), "");
1371 }
1372
1373 #[test]
1376 fn jsonl_wakeup_user_action() {
1377 let mut trace = ScheduleTrace::new();
1378 trace.record(TaskEvent::Wakeup {
1379 task_id: 1,
1380 reason: WakeupReason::UserAction,
1381 });
1382 let jsonl = trace.to_jsonl();
1383 assert!(jsonl.contains("\"reason\":\"user_action\""));
1384 }
1385
1386 #[test]
1387 fn jsonl_wakeup_explicit() {
1388 let mut trace = ScheduleTrace::new();
1389 trace.record(TaskEvent::Wakeup {
1390 task_id: 1,
1391 reason: WakeupReason::Explicit,
1392 });
1393 let jsonl = trace.to_jsonl();
1394 assert!(jsonl.contains("\"reason\":\"explicit\""));
1395 }
1396
1397 #[test]
1398 fn jsonl_wakeup_other() {
1399 let mut trace = ScheduleTrace::new();
1400 trace.record(TaskEvent::Wakeup {
1401 task_id: 1,
1402 reason: WakeupReason::Other("custom".to_string()),
1403 });
1404 let jsonl = trace.to_jsonl();
1405 assert!(jsonl.contains("\"reason\":\"other:custom\""));
1406 }
1407
1408 #[test]
1411 fn isomorphism_proof_with_approval() {
1412 let mut proof = IsomorphismProof::new("test change", 0xAA, 0xBB);
1413 proof.approved_by = Some("reviewer".to_string());
1414 proof.approved_at = Some("2026-01-01".to_string());
1415
1416 let json = proof.to_json();
1417 assert!(json.contains("\"approved_by\":\"reviewer\""));
1418 assert!(json.contains("\"approved_at\":\"2026-01-01\""));
1419 }
1420
1421 #[test]
1422 fn isomorphism_proof_without_approval() {
1423 let proof = IsomorphismProof::new("refactor", 0x11, 0x22);
1424 let json = proof.to_json();
1425 assert!(json.contains("\"approved_by\":null"));
1426 assert!(json.contains("\"approved_at\":null"));
1427 }
1428
1429 #[test]
1430 fn isomorphism_proof_builder_chain() {
1431 let proof = IsomorphismProof::new("change", 1, 2)
1432 .with_invariant("ordering preserved")
1433 .with_invariant("no data loss")
1434 .with_justification("pure refactor");
1435
1436 assert_eq!(proof.preserved_invariants.len(), 2);
1437 assert_eq!(proof.justification, "pure refactor");
1438 let json = proof.to_json();
1439 assert!(json.contains("ordering preserved"));
1440 assert!(json.contains("no data loss"));
1441 assert!(json.contains("pure refactor"));
1442 }
1443
1444 #[test]
1447 fn trace_entry_jsonl_includes_seq_tick() {
1448 let mut trace = ScheduleTrace::new();
1449 trace.set_tick(7);
1450 trace.spawn(1, 0, None);
1451 let jsonl = trace.to_jsonl();
1452 assert!(jsonl.contains("\"seq\":0"));
1453 assert!(jsonl.contains("\"tick\":7"));
1454 }
1455
1456 #[test]
1459 fn jsonl_multiple_entries_newline_separated() {
1460 let mut trace = ScheduleTrace::new();
1461 trace.spawn(1, 0, None);
1462 trace.start(1);
1463 trace.complete(1);
1464
1465 let jsonl = trace.to_jsonl();
1466 let lines: Vec<_> = jsonl.lines().collect();
1467 assert_eq!(lines.len(), 3);
1468 }
1469
1470 #[test]
1473 fn auto_snapshot_no_violation_below_threshold() {
1474 let config = TraceConfig {
1475 auto_snapshot: true,
1476 snapshot_sampling: Some(VoiConfig {
1477 max_interval_events: 1,
1478 sample_cost: 1.0,
1479 ..Default::default()
1480 }),
1481 snapshot_change_threshold: 10,
1482 ..Default::default()
1483 };
1484 let mut trace = ScheduleTrace::with_config(config);
1485 let now = Instant::now();
1486
1487 trace.record_with_queue_state_at(
1489 TaskEvent::Spawn {
1490 task_id: 1,
1491 priority: 0,
1492 name: None,
1493 },
1494 5,
1495 1,
1496 now,
1497 );
1498 trace.record_with_queue_state_at(TaskEvent::Start { task_id: 1 }, 6, 1, now);
1500
1501 let summary = trace.snapshot_sampling_summary().unwrap();
1503 assert_eq!(summary.total_samples, 2);
1504 }
1505
1506 #[test]
1509 fn checksum_includes_policy_change() {
1510 let mut t1 = ScheduleTrace::new();
1511 t1.record(TaskEvent::PolicyChange {
1512 from: SchedulerPolicy::Fifo,
1513 to: SchedulerPolicy::Priority,
1514 });
1515
1516 let mut t2 = ScheduleTrace::new();
1517 t2.record(TaskEvent::PolicyChange {
1518 from: SchedulerPolicy::Priority,
1519 to: SchedulerPolicy::Fifo,
1520 });
1521
1522 assert_ne!(t1.checksum(), t2.checksum());
1523 }
1524
1525 #[test]
1526 fn checksum_includes_custom_event_data() {
1527 let mut t1 = ScheduleTrace::new();
1528 t1.record(TaskEvent::Custom {
1529 tag: "a".to_string(),
1530 data: "1".to_string(),
1531 });
1532
1533 let mut t2 = ScheduleTrace::new();
1534 t2.record(TaskEvent::Custom {
1535 tag: "b".to_string(),
1536 data: "1".to_string(),
1537 });
1538
1539 assert_ne!(t1.checksum(), t2.checksum());
1540 }
1541}