1use super::WorkType;
22
23mod mempolicy;
24mod sched;
25mod work;
26mod workload;
27
28pub use mempolicy::{MemPolicy, MpolFlags};
29pub use sched::{AluWidth, FutexLockMode, ReapMode, SchedClass, SchedPolicy, WakeMechanism};
30pub use work::WorkSpec;
31pub(crate) use work::validate_task_comm_string;
32pub use workload::WorkloadConfig;
33
34pub(crate) mod humantime_serde_helper {
47 use std::time::Duration;
48
49 pub fn serialize<S: serde::Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
50 s.serialize_str(&humantime::format_duration(*d).to_string())
51 }
52
53 pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
54 let s = <String as serde::Deserialize>::deserialize(d)?;
55 humantime::parse_duration(&s).map_err(serde::de::Error::custom)
56 }
57}
58
59pub mod defaults {
69 pub const BURSTY_BURST_DURATION: std::time::Duration = std::time::Duration::from_millis(50);
71 pub const BURSTY_SLEEP_DURATION: std::time::Duration = std::time::Duration::from_millis(100);
72 pub const PIPE_IO_BURST_ITERS: u64 = 1024;
74 pub const FUTEX_PING_PONG_SPIN_ITERS: u64 = 1024;
76 pub const CACHE_PRESSURE_SIZE_KIB: usize = 32;
78 pub const CACHE_PRESSURE_STRIDE: usize = 64;
79 pub const CACHE_YIELD_SIZE_KIB: usize = 32;
80 pub const CACHE_YIELD_STRIDE: usize = 64;
81 pub const CACHE_PIPE_SIZE_KIB: usize = 32;
82 pub const CACHE_PIPE_BURST_ITERS: u64 = 1024;
83 pub const FUTEX_FAN_OUT_FAN_OUT: usize = 4;
85 pub const FUTEX_FAN_OUT_SPIN_ITERS: u64 = 1024;
86 pub const AFFINITY_CHURN_SPIN_ITERS: u64 = 1024;
88 pub const CROSS_AFFINITY_CHURN_SPIN_ITERS: u64 = 1024;
90 pub const POLICY_CHURN_SPIN_ITERS: u64 = 1024;
92 pub const FAN_OUT_COMPUTE_FAN_OUT: usize = 4;
94 pub const FAN_OUT_COMPUTE_CACHE_FOOTPRINT_KIB: usize = 256;
95 pub const FAN_OUT_COMPUTE_OPERATIONS: usize = 5;
96 pub const FAN_OUT_COMPUTE_SLEEP_USEC: u64 = 100;
97 pub const PAGE_FAULT_CHURN_REGION_KIB: usize = 4096;
99 pub const PAGE_FAULT_CHURN_TOUCHES_PER_CYCLE: usize = 256;
100 pub const PAGE_FAULT_CHURN_SPIN_ITERS: u64 = 64;
101 pub const MUTEX_CONTENTION_CONTENDERS: usize = 4;
103 pub const MUTEX_CONTENTION_HOLD_ITERS: u64 = 256;
104 pub const MUTEX_CONTENTION_WORK_ITERS: u64 = 1024;
105 pub const THUNDERING_HERD_WAITERS: usize = 7;
107 pub const THUNDERING_HERD_BATCHES: u64 = 1_000;
108 pub const THUNDERING_HERD_INTER_BATCH_MS: u64 = 5;
109 pub const PRIORITY_INVERSION_HIGH_COUNT: usize = 1;
111 pub const PRIORITY_INVERSION_MEDIUM_COUNT: usize = 1;
112 pub const PRIORITY_INVERSION_LOW_COUNT: usize = 1;
113 pub const PRIORITY_INVERSION_HOLD_ITERS: u64 = 4096;
114 pub const PRIORITY_INVERSION_WORK_ITERS: u64 = 1024;
115 pub const PRIORITY_INVERSION_PI_MODE: super::FutexLockMode = super::FutexLockMode::Plain;
116 pub const PRODUCER_CONSUMER_PRODUCERS: usize = 2;
118 pub const PRODUCER_CONSUMER_CONSUMERS: usize = 1;
119 pub const PRODUCER_CONSUMER_PRODUCE_RATE_HZ: u64 = 1_000;
120 pub const PRODUCER_CONSUMER_CONSUME_ITERS: u64 = 4_096;
121 pub const PRODUCER_CONSUMER_QUEUE_DEPTH_TARGET: u64 = 1024;
122 pub const RT_STARVATION_RT_WORKERS: usize = 1;
124 pub const RT_STARVATION_CFS_WORKERS: usize = 1;
125 pub const RT_STARVATION_RT_PRIORITY: i32 = 50;
126 pub const RT_STARVATION_BURST_ITERS: u64 = 1024;
127 pub const ASYMMETRIC_WAKER_BURST_ITERS: u64 = 1024;
129 pub const WAKE_CHAIN_DEPTH: usize = 4;
131 pub const WAKE_CHAIN_WAKE: super::WakeMechanism = super::WakeMechanism::Pipe;
132 pub const WAKE_CHAIN_WORK_PER_HOP: std::time::Duration = std::time::Duration::from_micros(100);
133 pub const NUMA_WORKING_SET_SWEEP_REGION_KIB: usize = 4_096;
135 pub const NUMA_WORKING_SET_SWEEP_SWEEP_PERIOD_MS: u64 = 100;
136 pub const CGROUP_CHURN_GROUPS: usize = 2;
138 pub const CGROUP_CHURN_CYCLE_MS: u64 = 100;
139 pub const CGROUP_ATTACH_STORM_DEST: &str = "dest";
143 pub const SIGNAL_STORM_SIGNALS_PER_ITER: u64 = 16;
145 pub const SIGNAL_STORM_WORK_ITERS: u64 = 1024;
146 pub const PREEMPT_STORM_CFS_WORKERS: usize = 2;
148 pub const PREEMPT_STORM_RT_BURST_ITERS: u64 = 1024;
149 pub const PREEMPT_STORM_RT_SLEEP_US: u64 = 1_000;
150 pub const EPOLL_STORM_PRODUCERS: usize = 1;
152 pub const EPOLL_STORM_CONSUMERS: usize = 2;
153 pub const EPOLL_STORM_EVENTS_PER_BURST: u64 = 32;
154 pub const NUMA_MIGRATION_CHURN_PERIOD_MS: u64 = 100;
156 pub const IDLE_CHURN_BURST_DURATION: std::time::Duration = std::time::Duration::from_millis(1);
158 pub const IDLE_CHURN_SLEEP_DURATION: std::time::Duration = std::time::Duration::from_millis(5);
159 pub const IDLE_CHURN_PRECISE_TIMING: bool = false;
164 pub const TIMER_LATENCY_INTERVAL_US: u64 = 1000;
169 pub const NET_TRAFFIC_INTERVAL_US: u64 = 0;
173 pub const NET_TRAFFIC_FRAME_BYTES: u16 = 60;
176 pub const IRQ_WAKE_INTERVAL_US: u64 = 1000;
183 pub const IRQ_WAKE_FRAME_BYTES: u16 = 60;
186 pub const ALU_HOT_WIDTH: super::AluWidth = super::AluWidth::Widest;
192 pub const IPC_VARIANCE_HOT_ITERS: u64 = 100_000;
198 pub const IPC_VARIANCE_COLD_ITERS: u64 = 1024;
203 pub const IPC_VARIANCE_PERIOD_ITERS: u64 = 64;
208}
209
210pub(crate) fn resolve_work_type(
218 base: &WorkType,
219 override_wt: Option<&WorkType>,
220 swappable: bool,
221 num_workers: usize,
222) -> WorkType {
223 if !swappable {
224 return base.clone();
225 }
226 match override_wt {
227 Some(wt) => {
228 if let Some(gs) = wt.worker_group_size()
229 && !num_workers.is_multiple_of(gs)
230 {
231 return base.clone();
232 }
233 wt.clone()
234 }
235 None => base.clone(),
236 }
237}
238
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
284#[serde(rename_all = "snake_case")]
285pub enum CloneMode {
286 #[default]
290 Fork,
291 Thread,
299}
300
301#[cfg(test)]
302mod tests {
303 use super::super::AffinityIntent;
304 use super::super::types::WorkType;
305 use super::*;
306 use std::collections::BTreeSet;
307 use std::time::Duration;
308
309 #[test]
310 fn sched_policy_debug_shows_variant_and_priority() {
311 let s = format!("{:?}", SchedPolicy::Fifo(50));
312 assert!(s.contains("Fifo"), "must show variant name");
313 assert!(s.contains("50"), "must show priority value");
314 let s = format!("{:?}", SchedPolicy::RoundRobin(99));
315 assert!(s.contains("RoundRobin"), "must show variant name");
316 assert!(s.contains("99"), "must show priority value");
317 let s1 = format!("{:?}", SchedPolicy::Fifo(1));
319 let s10 = format!("{:?}", SchedPolicy::Fifo(10));
320 assert_ne!(
321 s1, s10,
322 "different priorities must produce different debug output"
323 );
324 }
325 #[test]
326 fn sched_policy_copy_preserves_priority() {
327 let a = SchedPolicy::Fifo(42);
328 let b = a; match b {
330 SchedPolicy::Fifo(p) => assert_eq!(p, 42),
331 _ => panic!("copy must preserve variant and priority"),
332 }
333 }
334 #[test]
337 fn sched_policy_fifo_constructor() {
338 match SchedPolicy::fifo(50) {
339 SchedPolicy::Fifo(p) => assert_eq!(p, 50),
340 _ => panic!("expected Fifo"),
341 }
342 }
343 #[test]
344 fn sched_policy_rr_constructor() {
345 match SchedPolicy::round_robin(25) {
346 SchedPolicy::RoundRobin(p) => assert_eq!(p, 25),
347 _ => panic!("expected RoundRobin"),
348 }
349 }
350 #[test]
353 fn mempolicy_default_node_set_empty() {
354 assert!(MemPolicy::Default.node_set().is_empty());
355 }
356 #[test]
357 fn mempolicy_local_node_set_empty() {
358 assert!(MemPolicy::Local.node_set().is_empty());
359 }
360 #[test]
361 fn mempolicy_bind_node_set() {
362 let p = MemPolicy::Bind([0, 2].into_iter().collect());
363 assert_eq!(p.node_set(), [0, 2].into_iter().collect());
364 }
365 #[test]
366 fn mempolicy_preferred_node_set() {
367 let p = MemPolicy::Preferred(1);
368 assert_eq!(p.node_set(), [1].into_iter().collect());
369 }
370 #[test]
371 fn mempolicy_interleave_node_set() {
372 let p = MemPolicy::Interleave([0, 1, 3].into_iter().collect());
373 assert_eq!(p.node_set(), [0, 1, 3].into_iter().collect());
374 }
375 #[test]
376 fn mempolicy_preferred_many_node_set() {
377 let p = MemPolicy::preferred_many([0, 2]);
378 assert_eq!(p.node_set(), [0, 2].into_iter().collect());
379 }
380 #[test]
381 fn mempolicy_weighted_interleave_node_set() {
382 let p = MemPolicy::weighted_interleave([1, 3]);
383 assert_eq!(p.node_set(), [1, 3].into_iter().collect());
384 }
385 #[test]
386 fn mempolicy_validate_bind_empty() {
387 let err = MemPolicy::Bind(BTreeSet::new()).validate().unwrap_err();
388 assert!(
389 err.contains("Bind") && err.contains("NUMA node"),
390 "diagnostic must name the variant and required content: {err}",
391 );
392 assert!(
398 err.contains("MemPolicy::bind("),
399 "diagnostic must name the recommended constructor: {err}",
400 );
401 }
402 #[test]
403 fn mempolicy_validate_interleave_empty() {
404 let err = MemPolicy::Interleave(BTreeSet::new())
405 .validate()
406 .unwrap_err();
407 assert!(
408 err.contains("Interleave") && err.contains("NUMA node"),
409 "diagnostic must name the variant and required content: {err}",
410 );
411 assert!(
412 err.contains("MemPolicy::interleave("),
413 "diagnostic must name the recommended constructor: {err}",
414 );
415 }
416 #[test]
417 fn mempolicy_validate_preferred_many_empty() {
418 let err = MemPolicy::PreferredMany(BTreeSet::new())
419 .validate()
420 .unwrap_err();
421 assert!(
422 err.contains("PreferredMany") && err.contains("NUMA node"),
423 "diagnostic must name the variant and required content: {err}",
424 );
425 assert!(
426 err.contains("MemPolicy::preferred_many("),
427 "diagnostic must name the recommended constructor: {err}",
428 );
429 }
430 #[test]
431 fn mempolicy_validate_weighted_interleave_empty() {
432 let err = MemPolicy::WeightedInterleave(BTreeSet::new())
433 .validate()
434 .unwrap_err();
435 assert!(
436 err.contains("WeightedInterleave") && err.contains("NUMA node"),
437 "diagnostic must name the variant and required content: {err}",
438 );
439 assert!(
440 err.contains("MemPolicy::weighted_interleave("),
441 "diagnostic must name the recommended constructor: {err}",
442 );
443 assert!(
451 !err.contains("MemPolicy::Interleave(["),
452 "diagnostic must not suggest the non-compiling capital-I Interleave variant with a literal array: {err}",
453 );
454 }
455 #[test]
456 fn mempolicy_validate_preferred_many_ok() {
457 assert!(MemPolicy::preferred_many([0]).validate().is_ok());
458 }
459 #[test]
460 fn mempolicy_validate_weighted_interleave_ok() {
461 assert!(MemPolicy::weighted_interleave([0, 1]).validate().is_ok());
462 }
463
464 #[test]
465 fn workload_config_validate_accepts_default() {
466 WorkloadConfig::default()
467 .validate()
468 .expect("WorkloadConfig::default must self-validate (mem_policy=Default)");
469 }
470
471 #[test]
472 fn workload_config_validate_rejects_invalid_primary_mempolicy() {
473 let cfg = WorkloadConfig::default().mem_policy(MemPolicy::Bind(BTreeSet::new()));
474 let err = cfg
475 .validate()
476 .expect_err("empty Bind nodemask on primary must reject");
477 let msg = err.to_string();
478 assert!(
479 msg.contains("primary") && msg.contains("Bind") && msg.contains("NUMA node"),
480 "diagnostic must name the slot (primary), the variant (Bind), and the constraint (NUMA node): got {msg}",
481 );
482 }
483
484 #[test]
485 fn workload_config_validate_rejects_invalid_composed_mempolicy() {
486 let bad = WorkSpec::default()
487 .work_type(WorkType::SpinWait)
488 .mem_policy(MemPolicy::Interleave(BTreeSet::new()));
489 let cfg = WorkloadConfig::default().composed(vec![bad]);
490 let err = cfg
491 .validate()
492 .expect_err("empty Interleave nodemask on composed[0] must reject");
493 let msg = err.to_string();
494 assert!(
495 msg.contains("composed[0]")
496 && msg.contains("group_idx 1")
497 && msg.contains("Interleave"),
498 "diagnostic must name composed[0] + group_idx 1 + Interleave: got {msg}",
499 );
500 }
501
502 #[test]
503 fn workload_config_validate_accepts_valid_composed_mempolicy() {
504 let ok = WorkSpec::default()
505 .work_type(WorkType::SpinWait)
506 .mem_policy(MemPolicy::Bind([0].into_iter().collect()));
507 let cfg = WorkloadConfig::default().composed(vec![ok]);
508 cfg.validate()
509 .expect("non-empty Bind on composed[0] must validate");
510 }
511
512 #[test]
524 fn workload_config_validate_short_circuits_first_invalid_composed() {
525 let valid_spec = WorkSpec::default()
526 .work_type(WorkType::SpinWait)
527 .mem_policy(MemPolicy::Bind([0].into_iter().collect()));
528 let invalid_bind = WorkSpec::default()
529 .work_type(WorkType::SpinWait)
530 .mem_policy(MemPolicy::Bind(BTreeSet::new()));
531 let invalid_interleave = WorkSpec::default()
532 .work_type(WorkType::SpinWait)
533 .mem_policy(MemPolicy::Interleave(BTreeSet::new()));
534 let cfg =
535 WorkloadConfig::default().composed(vec![valid_spec, invalid_bind, invalid_interleave]);
536 let err = cfg
537 .validate()
538 .expect_err("multi-composed with invalid entries must reject");
539 let msg = err.to_string();
540 assert!(
541 msg.contains("composed[1]"),
542 "diagnostic must name the FIRST invalid composed entry (composed[1]): got {msg}",
543 );
544 assert!(
545 msg.contains("Bind"),
546 "diagnostic must name the first failing variant (Bind): got {msg}",
547 );
548 assert!(
558 !msg.contains("composed[2]"),
559 "short-circuit must not surface the second invalid entry (composed[2]): got {msg}",
560 );
561 }
562 #[test]
563 fn mpol_flags_union() {
564 let f = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
565 assert_eq!(f.bits(), (1 << 15) | (1 << 13));
566 }
567 #[test]
568 fn mpol_flags_none_is_zero() {
569 assert_eq!(MpolFlags::NONE.bits(), 0);
570 }
571 #[test]
572 fn work_mpol_flags_builder() {
573 let w = WorkSpec::default().mpol_flags(MpolFlags::STATIC_NODES);
574 assert_eq!(w.mpol_flags, MpolFlags::STATIC_NODES);
575 }
576 #[test]
577 fn mpol_flags_contains_identity() {
578 assert!(MpolFlags::NONE.contains(MpolFlags::NONE));
579 assert!(MpolFlags::STATIC_NODES.contains(MpolFlags::STATIC_NODES));
580 let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
581 assert!(composite.contains(composite));
582 }
583 #[test]
584 fn mpol_flags_contains_superset_is_true_for_subset() {
585 let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
586 assert!(composite.contains(MpolFlags::STATIC_NODES));
587 assert!(composite.contains(MpolFlags::NUMA_BALANCING));
588 }
589 #[test]
590 fn mpol_flags_contains_subset_is_false_for_superset() {
591 let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
592 assert!(!MpolFlags::STATIC_NODES.contains(composite));
593 assert!(!MpolFlags::NUMA_BALANCING.contains(composite));
594 }
595 #[test]
596 fn mpol_flags_contains_empty_is_always_true() {
597 assert!(MpolFlags::NONE.contains(MpolFlags::NONE));
600 assert!(MpolFlags::STATIC_NODES.contains(MpolFlags::NONE));
601 let composite = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
602 assert!(composite.contains(MpolFlags::NONE));
603 }
604 #[test]
605 fn mpol_flags_none_does_not_contain_any_set_flag() {
606 assert!(!MpolFlags::NONE.contains(MpolFlags::STATIC_NODES));
607 assert!(!MpolFlags::NONE.contains(MpolFlags::RELATIVE_NODES));
608 assert!(!MpolFlags::NONE.contains(MpolFlags::NUMA_BALANCING));
609 }
610 #[test]
611 fn mpol_flags_contains_rejects_disjoint_flag() {
612 assert!(!MpolFlags::STATIC_NODES.contains(MpolFlags::NUMA_BALANCING));
615 assert!(!MpolFlags::NUMA_BALANCING.contains(MpolFlags::STATIC_NODES));
616 }
617 #[test]
618 fn mpol_flags_contains_rejects_partial_overlap() {
619 let a = MpolFlags::STATIC_NODES | MpolFlags::NUMA_BALANCING;
622 let b = MpolFlags::RELATIVE_NODES | MpolFlags::NUMA_BALANCING;
623 assert!(!a.contains(b));
624 assert!(!b.contains(a));
625 }
626 #[test]
629 fn clone_mode_default_is_fork() {
630 assert!(matches!(CloneMode::default(), CloneMode::Fork));
633 }
634 #[test]
635 fn workload_config_default_clone_mode_is_fork() {
636 let c = WorkloadConfig::default();
637 assert!(matches!(c.clone_mode, CloneMode::Fork));
638 }
639 #[test]
640 fn workload_config_clone_mode_builder() {
641 let cfg = WorkloadConfig::default().clone_mode(CloneMode::Thread);
642 assert!(matches!(cfg.clone_mode, CloneMode::Thread));
643 }
644 #[test]
645 fn work_mem_policy_builder() {
646 let w = WorkSpec::default().mem_policy(MemPolicy::Bind([0].into_iter().collect()));
647 assert!(matches!(w.mem_policy, MemPolicy::Bind(_)));
648 }
649 #[test]
650 fn work_default_mempolicy_is_default() {
651 let w = WorkSpec::default();
652 assert!(matches!(w.mem_policy, MemPolicy::Default));
653 }
654 #[test]
655 fn workload_config_default_mempolicy() {
656 let wl = WorkloadConfig::default();
657 assert!(matches!(wl.mem_policy, MemPolicy::Default));
658 }
659 #[test]
663 fn workload_config_default_matcher_fields_are_none() {
664 let wl = WorkloadConfig::default();
665 assert!(wl.comm.is_none());
666 assert!(wl.uid.is_none());
667 assert!(wl.gid.is_none());
668 assert!(wl.numa_node.is_none());
669 }
670 #[test]
671 fn workload_config_matcher_field_builders() {
672 let wl = WorkloadConfig::default()
673 .comm("ktstr-worker")
674 .uid(1001)
675 .gid(1002)
676 .numa_node(0);
677 assert_eq!(wl.comm.as_deref(), Some("ktstr-worker"));
678 assert_eq!(wl.uid, Some(1001));
679 assert_eq!(wl.gid, Some(1002));
680 assert_eq!(wl.numa_node, Some(0));
681 }
682 #[test]
686 fn workload_config_default_roundtrips() {
687 let cfg = WorkloadConfig::default();
688 let json = serde_json::to_string(&cfg).unwrap();
689 let back: WorkloadConfig = serde_json::from_str(&json).unwrap();
690 let json2 = serde_json::to_string(&back).unwrap();
692 assert_eq!(json, json2);
693 }
694
695 #[test]
698 fn resolve_work_type_not_swappable() {
699 let base = WorkType::SpinWait;
700 let over = WorkType::YieldHeavy;
701 let result = resolve_work_type(&base, Some(&over), false, 4);
702 assert!(matches!(result, WorkType::SpinWait));
703 }
704 #[test]
705 fn resolve_work_type_swappable_applies_override() {
706 let base = WorkType::SpinWait;
707 let over = WorkType::YieldHeavy;
708 let result = resolve_work_type(&base, Some(&over), true, 4);
709 assert!(matches!(result, WorkType::YieldHeavy));
710 }
711 #[test]
712 fn resolve_work_type_swappable_no_override() {
713 let base = WorkType::SpinWait;
714 let result = resolve_work_type(&base, None, true, 4);
715 assert!(matches!(result, WorkType::SpinWait));
716 }
717 #[test]
718 fn resolve_work_type_group_size_mismatch() {
719 let base = WorkType::SpinWait;
720 let over = WorkType::pipe_io(100); let result = resolve_work_type(&base, Some(&over), true, 3); assert!(matches!(result, WorkType::SpinWait));
723 }
724 #[test]
725 fn resolve_work_type_group_size_match() {
726 let base = WorkType::SpinWait;
727 let over = WorkType::pipe_io(100); let result = resolve_work_type(&base, Some(&over), true, 4); assert!(matches!(result, WorkType::PipeIo { .. }));
730 }
731
732 #[test]
735 fn work_builder_chain() {
736 let w = WorkSpec::default()
737 .workers(8)
738 .work_type(WorkType::bursty(
739 Duration::from_millis(10),
740 Duration::from_millis(20),
741 ))
742 .sched_policy(SchedPolicy::Batch)
743 .affinity(AffinityIntent::SingleCpu)
744 .nice(7);
745 assert_eq!(w.num_workers, Some(8));
746 if let WorkType::Bursty {
747 burst_duration,
748 sleep_duration,
749 } = w.work_type
750 {
751 assert_eq!(burst_duration, Duration::from_millis(10));
752 assert_eq!(sleep_duration, Duration::from_millis(20));
753 } else {
754 panic!("expected Bursty variant; got {:?}", w.work_type);
755 }
756 assert!(matches!(w.sched_policy, SchedPolicy::Batch));
757 assert!(matches!(w.affinity, AffinityIntent::SingleCpu));
758 assert_eq!(w.nice, Some(7));
759 }
760 #[test]
761 fn work_default_values() {
762 let w = WorkSpec::default();
763 assert_eq!(w.num_workers, None);
764 assert!(matches!(w.work_type, WorkType::SpinWait));
765 assert!(matches!(w.sched_policy, SchedPolicy::Normal));
766 assert!(matches!(w.affinity, AffinityIntent::Inherit));
767 assert_eq!(w.nice, None);
770 }
771
772 #[test]
778 fn sched_policy_constructors_usable_in_const_context() {
779 const F: SchedPolicy = SchedPolicy::fifo(50);
780 const RR: SchedPolicy = SchedPolicy::round_robin(99);
781 const DL: SchedPolicy = SchedPolicy::deadline(
782 Duration::from_millis(10),
783 Duration::from_millis(20),
784 Duration::from_millis(30),
785 );
786 assert!(matches!(F, SchedPolicy::Fifo(50)));
787 assert!(matches!(RR, SchedPolicy::RoundRobin(99)));
788 assert!(matches!(
789 DL,
790 SchedPolicy::Deadline {
791 runtime,
792 deadline,
793 period
794 } if runtime == Duration::from_millis(10)
795 && deadline == Duration::from_millis(20)
796 && period == Duration::from_millis(30)
797 ));
798 }
799
800 #[test]
806 fn sched_policy_default_is_normal_and_serde_roundtrip_per_variant() {
807 let d: SchedPolicy = Default::default();
808 assert!(matches!(d, SchedPolicy::Normal));
809
810 let variants = [
811 SchedPolicy::Normal,
812 SchedPolicy::Batch,
813 SchedPolicy::Idle,
814 SchedPolicy::Fifo(50),
815 SchedPolicy::RoundRobin(99),
816 SchedPolicy::Deadline {
817 runtime: Duration::from_millis(10),
818 deadline: Duration::from_millis(20),
819 period: Duration::from_millis(30),
820 },
821 SchedPolicy::Ext,
822 ];
823 for original in &variants {
824 let bytes = serde_json::to_vec(original).expect("serialize");
825 let restored: SchedPolicy = serde_json::from_slice(&bytes).expect("deserialize");
826 assert_eq!(restored, *original, "roundtrip drift for {original:?}");
827 }
828 }
829
830 #[test]
835 fn sched_class_ext_maps_to_sched_policy_ext() {
836 assert_eq!(SchedClass::Ext.to_policy(), SchedPolicy::Ext);
837 }
838}