1use super::id::Time;
140use crate::tracing_compat::{info, trace};
141use core::fmt;
142use std::time::Duration;
143
144#[derive(Clone, Copy, PartialEq, Eq)]
150pub struct Budget {
151 pub deadline: Option<Time>,
153 pub poll_quota: u32,
155 pub cost_quota: Option<u64>,
157 pub priority: u8,
159}
160
161impl Budget {
162 pub const INFINITE: Self = Self {
164 deadline: None,
165 poll_quota: u32::MAX,
166 cost_quota: None,
167 priority: 0,
168 };
169
170 pub const ZERO: Self = Self {
172 deadline: Some(Time::ZERO),
173 poll_quota: 0,
174 cost_quota: Some(0),
175 priority: 255,
176 };
177
178 pub const MINIMAL: Self = Self {
184 deadline: None,
185 poll_quota: 100,
186 cost_quota: None,
187 priority: 128,
188 };
189
190 #[inline]
192 #[must_use]
193 pub const fn new() -> Self {
194 Self {
195 deadline: None,
196 poll_quota: u32::MAX,
197 cost_quota: None,
198 priority: 128,
199 }
200 }
201
202 #[inline]
214 #[must_use]
215 pub const fn unlimited() -> Self {
216 Self::INFINITE
217 }
218
219 #[inline]
234 #[must_use]
235 pub const fn with_deadline_secs(secs: u64) -> Self {
236 Self {
237 deadline: Some(Time::from_secs(secs)),
238 poll_quota: u32::MAX,
239 cost_quota: None,
240 priority: 128,
241 }
242 }
243
244 #[inline]
259 #[must_use]
260 pub const fn with_deadline_ns(nanos: u64) -> Self {
261 Self {
262 deadline: Some(Time::from_nanos(nanos)),
263 poll_quota: u32::MAX,
264 cost_quota: None,
265 priority: 128,
266 }
267 }
268
269 #[inline]
276 #[must_use]
277 pub const fn with_deadline(mut self, deadline: Time) -> Self {
278 self.deadline = Some(deadline);
279 self
280 }
281
282 #[inline]
300 #[must_use]
301 pub fn with_timeout(mut self, now: Time, timeout: Duration) -> Self {
302 self.deadline = Some(now + timeout);
303 self
304 }
305
306 #[inline]
308 #[must_use]
309 pub const fn with_poll_quota(mut self, quota: u32) -> Self {
310 self.poll_quota = quota;
311 self
312 }
313
314 #[inline]
316 #[must_use]
317 pub const fn with_cost_quota(mut self, quota: u64) -> Self {
318 self.cost_quota = Some(quota);
319 self
320 }
321
322 #[inline]
324 #[must_use]
325 pub const fn with_priority(mut self, priority: u8) -> Self {
326 self.priority = priority;
327 self
328 }
329
330 #[inline]
334 #[must_use]
335 pub const fn is_exhausted(&self) -> bool {
336 self.poll_quota == 0 || matches!(self.cost_quota, Some(0))
337 }
338
339 #[inline]
341 #[must_use]
342 pub fn is_past_deadline(&self, now: Time) -> bool {
343 self.deadline.is_some_and(|d| now >= d)
344 }
345
346 #[inline]
350 pub fn consume_poll(&mut self) -> Option<u32> {
351 if self.poll_quota > 0 {
352 let old = self.poll_quota;
353 self.poll_quota -= 1;
354 trace!(
355 polls_remaining = self.poll_quota,
356 polls_consumed = 1,
357 "budget poll consumed"
358 );
359 if self.poll_quota == 0 {
360 info!(
361 exhausted_resource = "polls",
362 final_quota = 0,
363 overage_amount = 0,
364 "budget poll quota exhausted"
365 );
366 }
367 Some(old)
368 } else {
369 trace!(
370 polls_remaining = 0,
371 "budget poll consume failed: already exhausted"
372 );
373 None
374 }
375 }
376
377 #[inline]
404 #[must_use]
405 pub fn combine(self, other: Self) -> Self {
406 let combined = Self {
407 deadline: match (self.deadline, other.deadline) {
408 (Some(a), Some(b)) => Some(a.min(b)),
409 (deadline @ Some(_), None) | (None, deadline @ Some(_)) => deadline,
410 (None, None) => None,
411 },
412 poll_quota: self.poll_quota.min(other.poll_quota),
413 cost_quota: match (self.cost_quota, other.cost_quota) {
414 (Some(a), Some(b)) => Some(a.min(b)),
415 (quota @ Some(_), None) | (None, quota @ Some(_)) => quota,
416 (None, None) => None,
417 },
418 priority: self.priority.max(other.priority),
419 };
420
421 let deadline_tightened = match (combined.deadline, self.deadline, other.deadline) {
426 (Some(c), Some(s), _) if c < s => true,
427 (Some(c), _, Some(o)) if c < o => true,
428 (Some(_), None, _) | (Some(_), _, None) => true,
429 _ => false,
430 };
431 let poll_tightened =
432 combined.poll_quota < self.poll_quota || combined.poll_quota < other.poll_quota;
433 let cost_tightened = match (combined.cost_quota, self.cost_quota, other.cost_quota) {
434 (Some(c), Some(s), _) if c < s => true,
435 (Some(c), _, Some(o)) if c < o => true,
436 (Some(_), None, _) | (Some(_), _, None) => true,
437 _ => false,
438 };
439 let priority_tightened =
440 combined.priority > self.priority || combined.priority > other.priority;
441
442 if deadline_tightened || poll_tightened || cost_tightened || priority_tightened {
443 trace!(
444 deadline_tightened,
445 poll_tightened,
446 cost_tightened,
447 self_deadline = ?self.deadline,
448 other_deadline = ?other.deadline,
449 combined_deadline = ?combined.deadline,
450 self_poll_quota = self.poll_quota,
451 other_poll_quota = other.poll_quota,
452 combined_poll_quota = combined.poll_quota,
453 self_cost_quota = ?self.cost_quota,
454 other_cost_quota = ?other.cost_quota,
455 combined_cost_quota = ?combined.cost_quota,
456 self_priority = self.priority,
457 other_priority = other.priority,
458 combined_priority = combined.priority,
459 "budget combined (tightened)"
460 );
461 }
462
463 combined
464 }
465
466 #[inline]
484 #[must_use]
485 pub fn meet(self, other: Self) -> Self {
486 self.combine(other)
487 }
488
489 #[allow(clippy::used_underscore_binding)]
505 #[inline]
506 pub fn consume_cost(&mut self, cost: u64) -> bool {
507 match self.cost_quota {
508 None => {
509 trace!(
510 cost_consumed = cost,
511 cost_remaining = "unlimited",
512 "budget cost consumed (unlimited)"
513 );
514 true }
516 Some(remaining) if remaining >= cost => {
517 let new_remaining = remaining - cost;
518 self.cost_quota = Some(new_remaining);
519 trace!(
520 cost_consumed = cost,
521 cost_remaining = new_remaining,
522 "budget cost consumed"
523 );
524 if new_remaining == 0 {
525 info!(
526 exhausted_resource = "cost",
527 final_quota = 0,
528 overage_amount = 0,
529 "budget cost quota exhausted"
530 );
531 }
532 true
533 }
534 Some(remaining) => {
535 #[cfg(not(feature = "tracing-integration"))]
536 let _ = remaining;
537 trace!(
538 cost_requested = cost,
539 cost_remaining = remaining,
540 "budget cost consume failed: insufficient quota"
541 );
542 false
543 }
544 }
545 }
546
547 #[inline]
564 #[must_use]
565 pub fn remaining_time(&self, now: Time) -> Option<Duration> {
566 self.deadline.and_then(|d| {
567 if now < d {
568 Some(Duration::from_nanos(
569 d.as_nanos().saturating_sub(now.as_nanos()),
570 ))
571 } else {
572 None
573 }
574 })
575 }
576
577 #[inline]
590 #[must_use]
591 pub const fn remaining_polls(&self) -> u32 {
592 self.poll_quota
593 }
594
595 #[inline]
610 #[must_use]
611 pub const fn remaining_cost(&self) -> Option<u64> {
612 self.cost_quota
613 }
614
615 #[inline]
634 #[must_use]
635 pub fn to_timeout(&self, now: Time) -> Option<Duration> {
636 self.remaining_time(now)
637 }
638}
639
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
642pub enum CapabilityBudgetDimension {
643 MemoryBytes,
645 CpuUnits,
647 IoBytes,
649 Cleanup,
651 ArtifactBytes,
653}
654
655impl CapabilityBudgetDimension {
656 #[must_use]
658 pub const fn as_str(self) -> &'static str {
659 match self {
660 Self::MemoryBytes => "memory_bytes",
661 Self::CpuUnits => "cpu_units",
662 Self::IoBytes => "io_bytes",
663 Self::Cleanup => "cleanup",
664 Self::ArtifactBytes => "artifact_bytes",
665 }
666 }
667}
668
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
671pub struct CapabilityBudgetRequirements {
672 pub memory_bytes: bool,
674 pub cpu_units: bool,
676 pub io_bytes: bool,
678 pub cleanup: bool,
680 pub artifact_bytes: bool,
682}
683
684impl CapabilityBudgetRequirements {
685 pub const NONE: Self = Self {
687 memory_bytes: false,
688 cpu_units: false,
689 io_bytes: false,
690 cleanup: false,
691 artifact_bytes: false,
692 };
693
694 #[inline]
696 #[must_use]
697 pub const fn new() -> Self {
698 Self::NONE
699 }
700
701 #[inline]
703 #[must_use]
704 pub const fn require_memory_bytes(mut self) -> Self {
705 self.memory_bytes = true;
706 self
707 }
708
709 #[inline]
711 #[must_use]
712 pub const fn require_cpu_units(mut self) -> Self {
713 self.cpu_units = true;
714 self
715 }
716
717 #[inline]
719 #[must_use]
720 pub const fn require_io_bytes(mut self) -> Self {
721 self.io_bytes = true;
722 self
723 }
724
725 #[inline]
727 #[must_use]
728 pub const fn require_cleanup(mut self) -> Self {
729 self.cleanup = true;
730 self
731 }
732
733 #[inline]
735 #[must_use]
736 pub const fn require_artifact_bytes(mut self) -> Self {
737 self.artifact_bytes = true;
738 self
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum CapabilityBudgetRefusal {
745 MissingRequired(CapabilityBudgetDimension),
747 Exhausted(CapabilityBudgetDimension),
749}
750
751impl fmt::Display for CapabilityBudgetRefusal {
752 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753 match self {
754 Self::MissingRequired(dimension) => {
755 write!(
756 f,
757 "required capability budget missing: {}",
758 dimension.as_str()
759 )
760 }
761 Self::Exhausted(dimension) => {
762 write!(f, "capability budget exhausted: {}", dimension.as_str())
763 }
764 }
765 }
766}
767
768impl std::error::Error for CapabilityBudgetRefusal {}
769
770#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
776pub struct CapabilityBudget {
777 pub memory_bytes: Option<u64>,
779 pub cpu_units: Option<u64>,
781 pub io_bytes: Option<u64>,
783 pub cleanup_budget: Option<Budget>,
785 pub artifact_bytes: Option<u64>,
787}
788
789impl CapabilityBudget {
790 pub const UNSPECIFIED: Self = Self {
792 memory_bytes: None,
793 cpu_units: None,
794 io_bytes: None,
795 cleanup_budget: None,
796 artifact_bytes: None,
797 };
798
799 #[inline]
801 #[must_use]
802 pub const fn new() -> Self {
803 Self::UNSPECIFIED
804 }
805
806 #[inline]
808 #[must_use]
809 pub const fn with_memory_bytes(mut self, bytes: u64) -> Self {
810 self.memory_bytes = Some(bytes);
811 self
812 }
813
814 #[inline]
816 #[must_use]
817 pub const fn with_cpu_units(mut self, units: u64) -> Self {
818 self.cpu_units = Some(units);
819 self
820 }
821
822 #[inline]
824 #[must_use]
825 pub const fn with_io_bytes(mut self, bytes: u64) -> Self {
826 self.io_bytes = Some(bytes);
827 self
828 }
829
830 #[inline]
832 #[must_use]
833 pub const fn with_cleanup_budget(mut self, budget: Budget) -> Self {
834 self.cleanup_budget = Some(budget);
835 self
836 }
837
838 #[inline]
840 #[must_use]
841 pub const fn with_artifact_bytes(mut self, bytes: u64) -> Self {
842 self.artifact_bytes = Some(bytes);
843 self
844 }
845
846 #[inline]
849 #[must_use]
850 pub fn meet(self, child: Self) -> Self {
851 Self {
852 memory_bytes: meet_optional_u64(self.memory_bytes, child.memory_bytes),
853 cpu_units: meet_optional_u64(self.cpu_units, child.cpu_units),
854 io_bytes: meet_optional_u64(self.io_bytes, child.io_bytes),
855 cleanup_budget: match (self.cleanup_budget, child.cleanup_budget) {
856 (Some(parent), Some(child)) => Some(parent.meet(child)),
857 (budget @ Some(_), None) | (None, budget @ Some(_)) => budget,
858 (None, None) => None,
859 },
860 artifact_bytes: meet_optional_u64(self.artifact_bytes, child.artifact_bytes),
861 }
862 }
863
864 #[inline]
871 pub fn plan_child(
872 self,
873 child: Self,
874 requirements: CapabilityBudgetRequirements,
875 ) -> Result<Self, CapabilityBudgetRefusal> {
876 let effective = self.meet(child);
877 effective.validate(requirements)?;
878 Ok(effective)
879 }
880
881 #[inline]
883 pub fn validate(
884 self,
885 requirements: CapabilityBudgetRequirements,
886 ) -> Result<(), CapabilityBudgetRefusal> {
887 validate_required_u64(
888 CapabilityBudgetDimension::MemoryBytes,
889 self.memory_bytes,
890 requirements.memory_bytes,
891 )?;
892 validate_required_u64(
893 CapabilityBudgetDimension::CpuUnits,
894 self.cpu_units,
895 requirements.cpu_units,
896 )?;
897 validate_required_u64(
898 CapabilityBudgetDimension::IoBytes,
899 self.io_bytes,
900 requirements.io_bytes,
901 )?;
902 validate_required_cleanup(self.cleanup_budget, requirements.cleanup)?;
903 validate_required_u64(
904 CapabilityBudgetDimension::ArtifactBytes,
905 self.artifact_bytes,
906 requirements.artifact_bytes,
907 )
908 }
909}
910
911#[inline]
912const fn meet_optional_u64(parent: Option<u64>, child: Option<u64>) -> Option<u64> {
913 match (parent, child) {
914 (Some(parent), Some(child)) => Some(if parent < child { parent } else { child }),
915 (value @ Some(_), None) | (None, value @ Some(_)) => value,
916 (None, None) => None,
917 }
918}
919
920#[inline]
921fn validate_required_u64(
922 dimension: CapabilityBudgetDimension,
923 value: Option<u64>,
924 required: bool,
925) -> Result<(), CapabilityBudgetRefusal> {
926 if !required {
927 return Ok(());
928 }
929
930 match value {
931 None => Err(CapabilityBudgetRefusal::MissingRequired(dimension)),
932 Some(0) => Err(CapabilityBudgetRefusal::Exhausted(dimension)),
933 Some(_) => Ok(()),
934 }
935}
936
937#[inline]
938fn validate_required_cleanup(
939 budget: Option<Budget>,
940 required: bool,
941) -> Result<(), CapabilityBudgetRefusal> {
942 if !required {
943 return Ok(());
944 }
945
946 match budget {
947 None => Err(CapabilityBudgetRefusal::MissingRequired(
948 CapabilityBudgetDimension::Cleanup,
949 )),
950 Some(budget) if budget.is_exhausted() => Err(CapabilityBudgetRefusal::Exhausted(
951 CapabilityBudgetDimension::Cleanup,
952 )),
953 Some(_) => Ok(()),
954 }
955}
956
957impl Default for Budget {
958 fn default() -> Self {
959 Self::new()
960 }
961}
962
963impl fmt::Debug for Budget {
964 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
965 let mut d = f.debug_struct("Budget");
966 if let Some(deadline) = self.deadline {
967 d.field("deadline", &deadline);
968 }
969 if self.poll_quota < u32::MAX {
970 d.field("poll_quota", &self.poll_quota);
971 }
972 if let Some(cost) = self.cost_quota {
973 d.field("cost_quota", &cost);
974 }
975 d.field("priority", &self.priority);
976 d.finish()
977 }
978}
979
980#[derive(Debug, Clone, PartialEq, Eq)]
986pub enum CurveError {
987 EmptySamples,
989 NonMonotone {
991 index: usize,
993 prev: u64,
995 next: u64,
997 },
998}
999
1000#[derive(Debug, Clone, PartialEq, Eq)]
1008pub struct MinPlusCurve {
1009 samples: Vec<u64>,
1010 tail_rate: u64,
1011}
1012
1013impl MinPlusCurve {
1014 #[inline]
1020 pub fn new(samples: Vec<u64>, tail_rate: u64) -> Result<Self, CurveError> {
1021 if samples.is_empty() {
1022 return Err(CurveError::EmptySamples);
1023 }
1024
1025 for idx in 1..samples.len() {
1026 if samples[idx] < samples[idx.saturating_sub(1)] {
1027 return Err(CurveError::NonMonotone {
1028 index: idx,
1029 prev: samples[idx.saturating_sub(1)],
1030 next: samples[idx],
1031 });
1032 }
1033 }
1034
1035 Ok(Self { samples, tail_rate })
1036 }
1037
1038 #[inline]
1043 pub fn from_samples(samples: Vec<u64>) -> Result<Self, CurveError> {
1044 Self::new(samples, 0)
1045 }
1046
1047 #[inline]
1049 #[must_use]
1050 pub fn from_token_bucket(burst: u64, rate: u64, horizon: usize) -> Self {
1051 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
1052 for t in 0..=horizon {
1053 let inc = rate.saturating_mul(t as u64);
1054 samples.push(burst.saturating_add(inc));
1055 }
1056 Self {
1057 samples,
1058 tail_rate: rate,
1059 }
1060 }
1061
1062 #[inline]
1064 #[must_use]
1065 pub fn from_rate_latency(rate: u64, latency: usize, horizon: usize) -> Self {
1066 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
1067 for t in 0..=horizon {
1068 if t <= latency {
1069 samples.push(0);
1070 } else {
1071 let dt = (t - latency) as u64;
1072 samples.push(rate.saturating_mul(dt));
1073 }
1074 }
1075 Self {
1076 samples,
1077 tail_rate: rate,
1078 }
1079 }
1080
1081 #[must_use]
1083 #[inline]
1084 pub fn horizon(&self) -> usize {
1085 self.samples.len().saturating_sub(1)
1086 }
1087
1088 #[must_use]
1090 #[inline]
1091 pub fn tail_rate(&self) -> u64 {
1092 self.tail_rate
1093 }
1094
1095 #[must_use]
1097 #[inline]
1098 pub fn value_at(&self, t: usize) -> u64 {
1099 let horizon = self.horizon();
1100 if t <= horizon {
1101 self.samples[t]
1102 } else {
1103 let extra = (t - horizon) as u64;
1104 self.samples[horizon].saturating_add(self.tail_rate.saturating_mul(extra))
1105 }
1106 }
1107
1108 #[must_use]
1110 #[inline]
1111 pub fn samples(&self) -> &[u64] {
1112 &self.samples
1113 }
1114
1115 #[inline]
1119 #[must_use]
1120 pub fn min_plus_convolution(&self, other: &Self, horizon: usize) -> Self {
1121 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
1122 for t in 0..=horizon {
1123 let mut best = u64::MAX;
1124 for s in 0..=t {
1125 let a = self.value_at(s);
1126 let b = other.value_at(t - s);
1127 let sum = a.saturating_add(b);
1128 if sum < best {
1129 best = sum;
1130 }
1131 }
1132 samples.push(best);
1133 }
1134
1135 Self {
1136 samples,
1137 tail_rate: self.tail_rate.min(other.tail_rate),
1138 }
1139 }
1140}
1141
1142#[derive(Debug, Clone, PartialEq, Eq)]
1144pub struct CurveBudget {
1145 pub arrival: MinPlusCurve,
1147 pub service: MinPlusCurve,
1149}
1150
1151impl CurveBudget {
1152 #[must_use]
1154 #[inline]
1155 pub fn backlog_bound(&self, horizon: usize) -> u64 {
1156 backlog_bound(&self.arrival, &self.service, horizon)
1157 }
1158
1159 #[must_use]
1163 #[inline]
1164 pub fn delay_bound(&self, horizon: usize, max_delay: usize) -> Option<usize> {
1165 delay_bound(&self.arrival, &self.service, horizon, max_delay)
1166 }
1167}
1168
1169#[inline]
1171#[must_use]
1172pub fn backlog_bound(arrival: &MinPlusCurve, service: &MinPlusCurve, horizon: usize) -> u64 {
1173 let mut worst = 0;
1174 for t in 0..=horizon {
1175 let demand = arrival.value_at(t);
1176 let supply = service.value_at(t);
1177 let backlog = demand.saturating_sub(supply);
1178 if backlog > worst {
1179 worst = backlog;
1180 }
1181 }
1182 worst
1183}
1184
1185#[inline]
1189#[must_use]
1190pub fn delay_bound(
1191 arrival: &MinPlusCurve,
1192 service: &MinPlusCurve,
1193 horizon: usize,
1194 max_delay: usize,
1195) -> Option<usize> {
1196 let mut worst_delay = 0;
1197 for t in 0..=horizon {
1198 let demand = arrival.value_at(t);
1199 let mut found = None;
1200 for d in 0..=max_delay {
1201 if service.value_at(t + d) >= demand {
1202 found = Some(d);
1203 break;
1204 }
1205 }
1206 let delay = found?;
1207 if delay > worst_delay {
1208 worst_delay = delay;
1209 }
1210 }
1211 Some(worst_delay)
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 #![allow(
1217 clippy::pedantic,
1218 clippy::nursery,
1219 clippy::expect_fun_call,
1220 clippy::map_unwrap_or,
1221 clippy::cast_possible_wrap,
1222 clippy::future_not_send
1223 )]
1224 use super::*;
1225 use serde_json::json;
1226
1227 fn scrub_budget_event(deadline: Option<Time>) -> &'static str {
1228 if deadline.is_some() {
1229 "[DEADLINE]"
1230 } else {
1231 "[NONE]"
1232 }
1233 }
1234
1235 #[test]
1240 fn infinite_budget_values() {
1241 let b = Budget::INFINITE;
1242 assert_eq!(b.deadline, None);
1243 assert_eq!(b.poll_quota, u32::MAX);
1244 assert_eq!(b.cost_quota, None);
1245 assert_eq!(b.priority, 0);
1246 }
1247
1248 #[test]
1249 fn zero_budget_values() {
1250 let b = Budget::ZERO;
1251 assert_eq!(b.deadline, Some(Time::ZERO));
1252 assert_eq!(b.poll_quota, 0);
1253 assert_eq!(b.cost_quota, Some(0));
1254 assert_eq!(b.priority, 255);
1255 }
1256
1257 #[test]
1258 fn new_returns_default_priority() {
1259 let b = Budget::new();
1260 assert_eq!(b.priority, 128);
1261 assert_ne!(b, Budget::INFINITE);
1262 }
1263
1264 #[test]
1265 fn default_returns_new() {
1266 assert_eq!(Budget::default(), Budget::new());
1267 assert_ne!(Budget::default(), Budget::INFINITE);
1268 }
1269
1270 #[test]
1275 fn with_deadline_sets_deadline() {
1276 let deadline = Time::from_secs(30);
1277 let budget = Budget::new().with_deadline(deadline);
1278 assert_eq!(budget.deadline, Some(deadline));
1279 }
1280
1281 #[test]
1282 fn with_poll_quota_sets_quota() {
1283 let budget = Budget::new().with_poll_quota(42);
1284 assert_eq!(budget.poll_quota, 42);
1285 }
1286
1287 #[test]
1288 fn with_cost_quota_sets_quota() {
1289 let budget = Budget::new().with_cost_quota(1000);
1290 assert_eq!(budget.cost_quota, Some(1000));
1291 }
1292
1293 #[test]
1294 fn with_priority_sets_priority() {
1295 let budget = Budget::new().with_priority(255);
1296 assert_eq!(budget.priority, 255);
1297 }
1298
1299 #[test]
1300 fn builder_chaining() {
1301 let budget = Budget::new()
1302 .with_deadline(Time::from_secs(10))
1303 .with_poll_quota(100)
1304 .with_cost_quota(5000)
1305 .with_priority(200);
1306
1307 assert_eq!(budget.deadline, Some(Time::from_secs(10)));
1308 assert_eq!(budget.poll_quota, 100);
1309 assert_eq!(budget.cost_quota, Some(5000));
1310 assert_eq!(budget.priority, 200);
1311 }
1312
1313 #[test]
1318 fn is_exhausted_false_for_infinite() {
1319 assert!(!Budget::INFINITE.is_exhausted());
1320 }
1321
1322 #[test]
1323 fn is_exhausted_true_for_zero() {
1324 assert!(Budget::ZERO.is_exhausted());
1325 }
1326
1327 #[test]
1328 fn is_exhausted_when_poll_quota_zero() {
1329 let budget = Budget::new().with_poll_quota(0);
1330 assert!(budget.is_exhausted());
1331 }
1332
1333 #[test]
1334 fn is_exhausted_when_cost_quota_zero() {
1335 let budget = Budget::new().with_cost_quota(0);
1336 assert!(budget.is_exhausted());
1337 }
1338
1339 #[test]
1340 fn is_exhausted_false_with_resources() {
1341 let budget = Budget::new().with_poll_quota(10).with_cost_quota(100);
1342 assert!(!budget.is_exhausted());
1343 }
1344
1345 #[test]
1350 fn is_past_deadline_true_when_past() {
1351 let budget = Budget::new().with_deadline(Time::from_secs(5));
1352 let now = Time::from_secs(10);
1353 assert!(budget.is_past_deadline(now));
1354 }
1355
1356 #[test]
1357 fn is_past_deadline_false_when_not_past() {
1358 let budget = Budget::new().with_deadline(Time::from_secs(10));
1359 let now = Time::from_secs(5);
1360 assert!(!budget.is_past_deadline(now));
1361 }
1362
1363 #[test]
1364 fn is_past_deadline_true_at_exact_time() {
1365 let deadline = Time::from_secs(5);
1366 let budget = Budget::new().with_deadline(deadline);
1367 assert!(budget.is_past_deadline(deadline));
1368 }
1369
1370 #[test]
1371 fn is_past_deadline_false_when_no_deadline() {
1372 let budget = Budget::new();
1373 assert!(!budget.is_past_deadline(Time::from_secs(1_000_000)));
1374 }
1375
1376 #[test]
1381 fn consume_poll_decrements() {
1382 let mut budget = Budget::new().with_poll_quota(2);
1383
1384 assert_eq!(budget.consume_poll(), Some(2));
1385 assert_eq!(budget.poll_quota, 1);
1386
1387 assert_eq!(budget.consume_poll(), Some(1));
1388 assert_eq!(budget.poll_quota, 0);
1389
1390 assert_eq!(budget.consume_poll(), None);
1391 assert_eq!(budget.poll_quota, 0);
1392 }
1393
1394 #[test]
1395 fn consume_poll_returns_none_at_zero() {
1396 let mut budget = Budget::new().with_poll_quota(0);
1397 assert_eq!(budget.consume_poll(), None);
1398 assert_eq!(budget.poll_quota, 0);
1399 }
1400
1401 #[test]
1402 fn consume_poll_transitions_to_exhausted() {
1403 let mut budget = Budget::new().with_poll_quota(1);
1404 assert!(!budget.is_exhausted());
1405
1406 budget.consume_poll();
1407 assert!(budget.is_exhausted());
1408 }
1409
1410 #[test]
1415 fn combine_takes_tighter() {
1416 let a = Budget::new()
1417 .with_deadline(Time::from_secs(10))
1418 .with_poll_quota(100)
1419 .with_priority(50);
1420
1421 let b = Budget::new()
1422 .with_deadline(Time::from_secs(5))
1423 .with_poll_quota(200)
1424 .with_priority(100);
1425
1426 let combined = a.combine(b);
1427
1428 assert_eq!(combined.deadline, Some(Time::from_secs(5)));
1430 assert_eq!(combined.poll_quota, 100);
1432 assert_eq!(combined.priority, 100);
1434 }
1435
1436 #[test]
1437 fn combine_deadline_none_with_some() {
1438 let a = Budget::new(); let b = Budget::new().with_deadline(Time::from_secs(5));
1440
1441 assert_eq!(a.combine(b).deadline, Some(Time::from_secs(5)));
1443 assert_eq!(b.combine(a).deadline, Some(Time::from_secs(5)));
1445 }
1446
1447 #[test]
1448 fn combine_deadline_none_with_none() {
1449 let a = Budget::new();
1450 let b = Budget::new();
1451 assert_eq!(a.combine(b).deadline, None);
1452 }
1453
1454 #[test]
1455 fn combine_cost_quota_none_with_some() {
1456 let a = Budget::new(); let b = Budget::new().with_cost_quota(100);
1458
1459 assert_eq!(a.combine(b).cost_quota, Some(100));
1461 assert_eq!(b.combine(a).cost_quota, Some(100));
1462 }
1463
1464 #[test]
1465 fn combine_cost_quota_takes_min() {
1466 let a = Budget::new().with_cost_quota(50);
1467 let b = Budget::new().with_cost_quota(100);
1468
1469 assert_eq!(a.combine(b).cost_quota, Some(50));
1470 assert_eq!(b.combine(a).cost_quota, Some(50));
1471 }
1472
1473 #[test]
1474 fn combine_with_zero_absorbs() {
1475 let any_budget = Budget::new()
1476 .with_deadline(Time::from_secs(100))
1477 .with_poll_quota(1000)
1478 .with_cost_quota(10000)
1479 .with_priority(200);
1480
1481 let combined = any_budget.combine(Budget::ZERO);
1482
1483 assert_eq!(combined.deadline, Some(Time::ZERO));
1485 assert_eq!(combined.poll_quota, 0);
1487 assert_eq!(combined.cost_quota, Some(0));
1489 assert_eq!(combined.priority, 255);
1491 }
1492
1493 #[test]
1494 fn combine_with_infinite_preserves() {
1495 let budget = Budget::new()
1496 .with_deadline(Time::from_secs(10))
1497 .with_poll_quota(100)
1498 .with_cost_quota(1000)
1499 .with_priority(50);
1500
1501 let combined = budget.combine(Budget::INFINITE);
1502
1503 assert_eq!(combined.deadline, Some(Time::from_secs(10)));
1505 assert_eq!(combined.poll_quota, 100);
1506 assert_eq!(combined.cost_quota, Some(1000));
1507 assert_eq!(combined.priority, 50);
1509 }
1510
1511 #[test]
1516 fn capability_budget_child_inherits_parent_and_tightens_overrides() {
1517 let parent = CapabilityBudget::new()
1518 .with_memory_bytes(1_024)
1519 .with_cpu_units(100)
1520 .with_io_bytes(10_000)
1521 .with_cleanup_budget(Budget::new().with_poll_quota(50))
1522 .with_artifact_bytes(4_096);
1523 let child = CapabilityBudget::new()
1524 .with_memory_bytes(2_048)
1525 .with_cpu_units(25)
1526 .with_io_bytes(1_000)
1527 .with_cleanup_budget(Budget::new().with_poll_quota(10));
1528
1529 let effective = parent.meet(child);
1530
1531 assert_eq!(effective.memory_bytes, Some(1_024));
1532 assert_eq!(effective.cpu_units, Some(25));
1533 assert_eq!(effective.io_bytes, Some(1_000));
1534 assert_eq!(
1535 effective.cleanup_budget.map(|budget| budget.poll_quota),
1536 Some(10)
1537 );
1538 assert_eq!(effective.artifact_bytes, Some(4_096));
1539 }
1540
1541 #[test]
1542 fn capability_budget_optional_absent_dimension_admits() {
1543 let requirements = CapabilityBudgetRequirements::NONE;
1544
1545 let effective = CapabilityBudget::new()
1546 .plan_child(CapabilityBudget::new(), requirements)
1547 .expect("optional absent envelopes should admit");
1548
1549 assert_eq!(effective, CapabilityBudget::UNSPECIFIED);
1550 }
1551
1552 #[test]
1553 fn capability_budget_required_absent_dimension_rejects() {
1554 let requirements = CapabilityBudgetRequirements::new().require_memory_bytes();
1555
1556 let err = CapabilityBudget::new()
1557 .plan_child(CapabilityBudget::new(), requirements)
1558 .expect_err("required missing memory budget must reject");
1559
1560 assert_eq!(
1561 err,
1562 CapabilityBudgetRefusal::MissingRequired(CapabilityBudgetDimension::MemoryBytes)
1563 );
1564 }
1565
1566 #[test]
1567 fn capability_budget_required_exhausted_dimension_rejects() {
1568 let requirements = CapabilityBudgetRequirements::new()
1569 .require_cpu_units()
1570 .require_cleanup();
1571 let parent = CapabilityBudget::new()
1572 .with_cpu_units(0)
1573 .with_cleanup_budget(Budget::new().with_poll_quota(10));
1574
1575 let err = parent
1576 .plan_child(CapabilityBudget::new(), requirements)
1577 .expect_err("required exhausted CPU budget must reject");
1578
1579 assert_eq!(
1580 err,
1581 CapabilityBudgetRefusal::Exhausted(CapabilityBudgetDimension::CpuUnits)
1582 );
1583 }
1584
1585 #[test]
1586 fn capability_budget_required_exhausted_cleanup_rejects() {
1587 let requirements = CapabilityBudgetRequirements::new().require_cleanup();
1588 let parent = CapabilityBudget::new().with_cleanup_budget(Budget::new().with_poll_quota(0));
1589
1590 let err = parent
1591 .plan_child(CapabilityBudget::new(), requirements)
1592 .expect_err("required exhausted cleanup budget must reject");
1593
1594 assert_eq!(
1595 err,
1596 CapabilityBudgetRefusal::Exhausted(CapabilityBudgetDimension::Cleanup)
1597 );
1598 }
1599
1600 #[test]
1605 fn debug_shows_constrained_fields() {
1606 let budget = Budget::new()
1607 .with_deadline(Time::from_secs(10))
1608 .with_poll_quota(100)
1609 .with_cost_quota(500);
1610
1611 let debug = format!("{budget:?}");
1612
1613 assert!(debug.contains("deadline"));
1615 assert!(debug.contains("poll_quota"));
1616 assert!(debug.contains("cost_quota"));
1617 assert!(debug.contains("priority"));
1618 }
1619
1620 #[test]
1621 fn debug_omits_unconstrained_fields() {
1622 let budget = Budget::INFINITE;
1623 let debug = format!("{budget:?}");
1624
1625 assert!(!debug.contains("deadline"));
1628 assert!(!debug.contains("poll_quota")); assert!(debug.contains("priority"));
1630 }
1631
1632 #[test]
1637 fn equality_works() {
1638 let a = Budget::new()
1639 .with_deadline(Time::from_secs(10))
1640 .with_poll_quota(100);
1641
1642 let b = Budget::new()
1643 .with_deadline(Time::from_secs(10))
1644 .with_poll_quota(100);
1645
1646 assert_eq!(a, b);
1647 }
1648
1649 #[test]
1650 fn inequality_on_deadline() {
1651 let a = Budget::new().with_deadline(Time::from_secs(10));
1652 let b = Budget::new().with_deadline(Time::from_secs(20));
1653 assert_ne!(a, b);
1654 }
1655
1656 #[test]
1657 fn copy_semantics() {
1658 let a = Budget::new().with_poll_quota(100);
1659 let b = a; assert_eq!(a.poll_quota, b.poll_quota);
1661 let mut c = a;
1663 c.poll_quota = 50;
1664 assert_eq!(a.poll_quota, 100);
1665 assert_eq!(c.poll_quota, 50);
1666 }
1667
1668 #[test]
1673 fn unlimited_returns_infinite() {
1674 assert_eq!(Budget::unlimited(), Budget::INFINITE);
1675 }
1676
1677 #[test]
1678 fn with_deadline_secs_constructor() {
1679 let budget = Budget::with_deadline_secs(30);
1680 assert_eq!(budget.deadline, Some(Time::from_secs(30)));
1681 assert_eq!(budget.poll_quota, u32::MAX);
1682 assert_eq!(budget.cost_quota, None);
1683 assert_eq!(budget.priority, 128);
1684 }
1685
1686 #[test]
1687 fn with_deadline_ns_constructor() {
1688 let budget = Budget::with_deadline_ns(30_000_000_000);
1689 assert_eq!(budget.deadline, Some(Time::from_nanos(30_000_000_000)));
1690 }
1691
1692 #[test]
1693 fn with_timeout_sets_deadline_relative_to_now() {
1694 let now = Time::from_secs(100);
1695 let budget = Budget::new().with_timeout(now, Duration::from_secs(30));
1696
1697 assert_eq!(budget.deadline, Some(Time::from_secs(130)));
1698 assert_eq!(budget.remaining_time(now), Some(Duration::from_secs(30)));
1699 }
1700
1701 #[test]
1702 fn with_timeout_saturates_at_max_time() {
1703 let now = Time::MAX.saturating_sub_nanos(5);
1704 let budget = Budget::new().with_timeout(now, Duration::from_nanos(10));
1705
1706 assert_eq!(budget.deadline, Some(Time::MAX));
1707 }
1708
1709 #[test]
1710 fn with_timeout_keeps_other_constraints() {
1711 let now = Time::from_secs(7);
1712 let budget = Budget::new()
1713 .with_poll_quota(42)
1714 .with_cost_quota(99)
1715 .with_priority(200)
1716 .with_timeout(now, Duration::from_secs(3));
1717
1718 assert_eq!(budget.deadline, Some(Time::from_secs(10)));
1719 assert_eq!(budget.poll_quota, 42);
1720 assert_eq!(budget.cost_quota, Some(99));
1721 assert_eq!(budget.priority, 200);
1722 }
1723
1724 #[test]
1725 fn meet_is_alias_for_combine() {
1726 let a = Budget::new()
1727 .with_deadline(Time::from_secs(10))
1728 .with_poll_quota(100);
1729
1730 let b = Budget::new()
1731 .with_deadline(Time::from_secs(5))
1732 .with_poll_quota(200);
1733
1734 assert_eq!(a.meet(b), a.combine(b));
1735 }
1736
1737 #[test]
1742 fn consume_cost_basic() {
1743 let mut budget = Budget::new().with_cost_quota(100);
1744
1745 assert!(budget.consume_cost(30));
1746 assert_eq!(budget.cost_quota, Some(70));
1747
1748 assert!(budget.consume_cost(70));
1749 assert_eq!(budget.cost_quota, Some(0));
1750
1751 assert!(!budget.consume_cost(1));
1752 assert_eq!(budget.cost_quota, Some(0));
1753 }
1754
1755 #[test]
1756 fn consume_cost_unlimited() {
1757 let mut budget = Budget::new(); assert!(budget.consume_cost(1_000_000));
1759 assert_eq!(budget.cost_quota, None);
1760 }
1761
1762 #[test]
1763 fn consume_cost_exact_amount() {
1764 let mut budget = Budget::new().with_cost_quota(50);
1765 assert!(budget.consume_cost(50));
1766 assert_eq!(budget.cost_quota, Some(0));
1767 }
1768
1769 #[test]
1770 fn consume_cost_transitions_to_exhausted() {
1771 let mut budget = Budget::new().with_cost_quota(10).with_poll_quota(u32::MAX);
1772 assert!(!budget.is_exhausted());
1773
1774 budget.consume_cost(10);
1775 assert!(budget.is_exhausted());
1776 }
1777
1778 #[test]
1783 fn remaining_time_basic() {
1784 let budget = Budget::with_deadline_secs(30);
1785 let now = Time::from_secs(10);
1786
1787 let remaining = budget.remaining_time(now);
1788 assert_eq!(remaining, Some(Duration::from_secs(20)));
1789 }
1790
1791 #[test]
1792 fn remaining_time_no_deadline() {
1793 let budget = Budget::unlimited();
1794 assert_eq!(budget.remaining_time(Time::from_secs(1000)), None);
1795 }
1796
1797 #[test]
1798 fn remaining_time_past_deadline() {
1799 let budget = Budget::with_deadline_secs(10);
1800 assert_eq!(budget.remaining_time(Time::from_secs(15)), None);
1801 }
1802
1803 #[test]
1804 fn remaining_time_at_deadline() {
1805 let budget = Budget::with_deadline_secs(10);
1806 assert_eq!(budget.remaining_time(Time::from_secs(10)), None);
1807 }
1808
1809 #[test]
1810 fn remaining_polls_basic() {
1811 let budget = Budget::new().with_poll_quota(100);
1812 assert_eq!(budget.remaining_polls(), 100);
1813 }
1814
1815 #[test]
1816 fn remaining_polls_unlimited() {
1817 let budget = Budget::unlimited();
1818 assert_eq!(budget.remaining_polls(), u32::MAX);
1819 }
1820
1821 #[test]
1822 fn remaining_cost_basic() {
1823 let budget = Budget::new().with_cost_quota(1000);
1824 assert_eq!(budget.remaining_cost(), Some(1000));
1825 }
1826
1827 #[test]
1828 fn remaining_cost_unlimited() {
1829 let budget = Budget::unlimited();
1830 assert_eq!(budget.remaining_cost(), None);
1831 }
1832
1833 #[test]
1834 fn to_timeout_is_alias_for_remaining_time() {
1835 let budget = Budget::with_deadline_secs(30);
1836 let now = Time::from_secs(10);
1837
1838 assert_eq!(budget.to_timeout(now), budget.remaining_time(now));
1839 }
1840
1841 #[test]
1846 fn min_plus_curve_validation_rejects_non_monotone() {
1847 let err = MinPlusCurve::new(vec![0, 2, 1], 0).expect_err("non-monotone samples");
1848 assert!(matches!(
1849 err,
1850 CurveError::NonMonotone {
1851 index: 2,
1852 prev: 2,
1853 next: 1
1854 }
1855 ));
1856 }
1857
1858 #[test]
1859 fn min_plus_convolution_basic() {
1860 let a = MinPlusCurve::new(vec![0, 1, 2], 1).expect("valid curve");
1861 let b = MinPlusCurve::new(vec![0, 0, 1], 1).expect("valid curve");
1862 let conv = a.min_plus_convolution(&b, 2);
1863 assert_eq!(conv.samples(), &[0, 0, 1]);
1864 }
1865
1866 #[test]
1867 fn min_plus_backlog_delay_demo() {
1868 let arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
1869 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
1870 let budget = CurveBudget { arrival, service };
1871
1872 let backlog = budget.backlog_bound(10);
1873 let delay = budget.delay_bound(10, 10);
1874
1875 assert_eq!(backlog, 9);
1876 assert_eq!(delay, Some(4));
1877 }
1878
1879 #[test]
1912 fn lemma_meet_commutative() {
1913 let a = Budget::new()
1914 .with_deadline(Time::from_secs(10))
1915 .with_poll_quota(100)
1916 .with_cost_quota(500)
1917 .with_priority(50);
1918
1919 let b = Budget::new()
1920 .with_deadline(Time::from_secs(5))
1921 .with_poll_quota(200)
1922 .with_cost_quota(300)
1923 .with_priority(100);
1924
1925 assert_eq!(a.meet(b), b.meet(a));
1926 }
1927
1928 #[test]
1929 fn lemma_meet_commutative_with_none_deadline() {
1930 let a = Budget::new().with_poll_quota(100);
1931 let b = Budget::new()
1932 .with_deadline(Time::from_secs(5))
1933 .with_poll_quota(200);
1934 assert_eq!(a.meet(b), b.meet(a));
1935 }
1936
1937 #[test]
1938 fn lemma_meet_commutative_with_none_cost() {
1939 let a = Budget::new().with_cost_quota(100);
1940 let b = Budget::new(); assert_eq!(a.meet(b), b.meet(a));
1942 }
1943
1944 #[test]
1948 fn lemma_meet_associative() {
1949 let a = Budget::new()
1950 .with_deadline(Time::from_secs(10))
1951 .with_poll_quota(100)
1952 .with_cost_quota(500)
1953 .with_priority(50);
1954
1955 let b = Budget::new()
1956 .with_deadline(Time::from_secs(5))
1957 .with_poll_quota(200)
1958 .with_cost_quota(300)
1959 .with_priority(100);
1960
1961 let c = Budget::new()
1962 .with_deadline(Time::from_secs(8))
1963 .with_poll_quota(150)
1964 .with_cost_quota(400)
1965 .with_priority(75);
1966
1967 assert_eq!(a.meet(b.meet(c)), a.meet(b).meet(c));
1968 }
1969
1970 #[test]
1971 fn lemma_meet_associative_with_none_fields() {
1972 let a = Budget::new().with_deadline(Time::from_secs(10));
1973 let b = Budget::new().with_cost_quota(300);
1974 let c = Budget::new().with_poll_quota(50);
1975
1976 assert_eq!(a.meet(b.meet(c)), a.meet(b).meet(c));
1977 }
1978
1979 #[test]
1983 fn lemma_meet_idempotent() {
1984 let a = Budget::new()
1985 .with_deadline(Time::from_secs(10))
1986 .with_poll_quota(100)
1987 .with_cost_quota(500)
1988 .with_priority(75);
1989 assert_eq!(a.meet(a), a);
1990 }
1991
1992 #[test]
1993 fn lemma_meet_idempotent_infinite() {
1994 assert_eq!(Budget::INFINITE.meet(Budget::INFINITE), Budget::INFINITE);
1995 }
1996
1997 #[test]
1998 fn lemma_meet_idempotent_zero() {
1999 assert_eq!(Budget::ZERO.meet(Budget::ZERO), Budget::ZERO);
2000 }
2001
2002 #[test]
2006 fn lemma_identity_left() {
2007 let a = Budget::new()
2008 .with_deadline(Time::from_secs(10))
2009 .with_poll_quota(100)
2010 .with_cost_quota(500)
2011 .with_priority(50);
2012
2013 let result = Budget::INFINITE.meet(a);
2016 assert_eq!(result.deadline, a.deadline);
2017 assert_eq!(result.poll_quota, a.poll_quota);
2018 assert_eq!(result.cost_quota, a.cost_quota);
2019 assert_eq!(result.priority, a.priority);
2020 }
2021
2022 #[test]
2023 fn lemma_identity_right() {
2024 let a = Budget::new()
2025 .with_deadline(Time::from_secs(10))
2026 .with_poll_quota(100)
2027 .with_cost_quota(500)
2028 .with_priority(200);
2029
2030 let result = a.meet(Budget::INFINITE);
2031 assert_eq!(result.deadline, a.deadline);
2032 assert_eq!(result.poll_quota, a.poll_quota);
2033 assert_eq!(result.cost_quota, a.cost_quota);
2034 }
2035
2036 #[test]
2042 fn lemma_zero_absorbing_quotas() {
2043 let a = Budget::new()
2044 .with_deadline(Time::from_secs(100))
2045 .with_poll_quota(1000)
2046 .with_cost_quota(10000);
2047
2048 let result = a.meet(Budget::ZERO);
2049 assert_eq!(result.deadline, Some(Time::ZERO));
2050 assert_eq!(result.poll_quota, 0);
2051 assert_eq!(result.cost_quota, Some(0));
2052 }
2053
2054 #[test]
2055 fn lemma_zero_absorbing_symmetric() {
2056 let a = Budget::new()
2057 .with_deadline(Time::from_secs(100))
2058 .with_poll_quota(1000)
2059 .with_cost_quota(10000);
2060
2061 let left = a.meet(Budget::ZERO);
2062 let right = Budget::ZERO.meet(a);
2063 assert_eq!(left.deadline, right.deadline);
2064 assert_eq!(left.poll_quota, right.poll_quota);
2065 assert_eq!(left.cost_quota, right.cost_quota);
2066 }
2067
2068 #[test]
2074 fn lemma_meet_monotone_poll() {
2075 let a = Budget::new().with_poll_quota(100);
2076 let b = Budget::new().with_poll_quota(200);
2077 let result = a.meet(b);
2078 assert!(result.poll_quota <= a.poll_quota);
2079 assert!(result.poll_quota <= b.poll_quota);
2080 }
2081
2082 #[test]
2083 fn lemma_meet_monotone_cost() {
2084 let a = Budget::new().with_cost_quota(100);
2085 let b = Budget::new().with_cost_quota(200);
2086 let result = a.meet(b);
2087 assert!(result.cost_quota.unwrap() <= a.cost_quota.unwrap());
2088 assert!(result.cost_quota.unwrap() <= b.cost_quota.unwrap());
2089 }
2090
2091 #[test]
2092 fn lemma_meet_monotone_deadline() {
2093 let a = Budget::new().with_deadline(Time::from_secs(10));
2094 let b = Budget::new().with_deadline(Time::from_secs(20));
2095 let result = a.meet(b);
2096 assert!(result.deadline.unwrap() <= a.deadline.unwrap());
2097 assert!(result.deadline.unwrap() <= b.deadline.unwrap());
2098 }
2099
2100 #[test]
2104 fn lemma_consume_poll_strictly_decreasing() {
2105 let mut budget = Budget::new().with_poll_quota(5);
2106 let mut prev = budget.poll_quota;
2107 while budget.poll_quota > 0 {
2108 let old = budget.consume_poll().expect("quota > 0");
2109 assert_eq!(old, prev);
2110 assert!(budget.poll_quota < prev);
2111 prev = budget.poll_quota;
2112 }
2113 assert_eq!(budget.consume_poll(), None);
2115 }
2116
2117 #[test]
2121 fn lemma_consume_cost_strictly_decreasing() {
2122 let mut budget = Budget::new().with_cost_quota(100);
2123 let initial = budget.cost_quota.unwrap();
2124 assert!(budget.consume_cost(30));
2125 let after = budget.cost_quota.unwrap();
2126 assert!(after < initial);
2127 assert_eq!(after, 70);
2128 }
2129
2130 #[test]
2134 fn lemma_sufficient_budget_enables_progress() {
2135 let mut budget = Budget::new().with_poll_quota(10).with_cost_quota(100);
2136 assert!(!budget.is_exhausted());
2137
2138 assert!(budget.consume_poll().is_some());
2140 assert!(budget.consume_cost(1));
2142 }
2143
2144 #[test]
2145 fn lemma_exhausted_blocks_progress() {
2146 let mut budget = Budget::new().with_poll_quota(0);
2147 assert!(budget.is_exhausted());
2148 assert!(budget.consume_poll().is_none());
2149 }
2150
2151 #[test]
2156 fn lemma_poll_termination() {
2157 let q = 7u32;
2158 let mut budget = Budget::new().with_poll_quota(q);
2159 for _ in 0..q {
2160 assert!(!budget.is_exhausted());
2161 budget.consume_poll().expect("should succeed");
2162 }
2163 assert!(budget.is_exhausted());
2164 assert_eq!(budget.poll_quota, 0);
2165 }
2166
2167 #[test]
2168 fn lemma_cost_termination() {
2169 let mut budget = Budget::new().with_cost_quota(100);
2170 for _ in 0..4 {
2172 assert!(budget.consume_cost(25));
2173 }
2174 assert_eq!(budget.cost_quota, Some(0));
2175 assert!(!budget.consume_cost(1));
2176 }
2177
2178 #[test]
2182 fn lemma_convolution_associative() {
2183 let a = MinPlusCurve::new(vec![0, 1, 3], 2).expect("valid");
2184 let b = MinPlusCurve::new(vec![0, 2, 4], 2).expect("valid");
2185 let c = MinPlusCurve::new(vec![0, 1, 2], 1).expect("valid");
2186 let h = 4;
2187
2188 let ab_c = a.min_plus_convolution(&b, h).min_plus_convolution(&c, h);
2189 let a_bc = a.min_plus_convolution(&b.min_plus_convolution(&c, h), h);
2190
2191 for t in 0..=h {
2192 assert_eq!(
2193 ab_c.value_at(t),
2194 a_bc.value_at(t),
2195 "associativity failed at t={t}"
2196 );
2197 }
2198 }
2199
2200 #[test]
2204 fn lemma_convolution_commutative() {
2205 let a = MinPlusCurve::new(vec![0, 1, 3], 2).expect("valid");
2206 let b = MinPlusCurve::new(vec![0, 2, 4], 2).expect("valid");
2207 let h = 4;
2208
2209 let ab = a.min_plus_convolution(&b, h);
2210 let ba = b.min_plus_convolution(&a, h);
2211
2212 for t in 0..=h {
2213 assert_eq!(
2214 ab.value_at(t),
2215 ba.value_at(t),
2216 "commutativity failed at t={t}"
2217 );
2218 }
2219 }
2220
2221 #[test]
2225 fn lemma_backlog_monotone_arrival() {
2226 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
2227 let small_arrival = MinPlusCurve::from_token_bucket(2, 1, 10);
2228 let large_arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
2229
2230 let small_backlog = backlog_bound(&small_arrival, &service, 10);
2231 let large_backlog = backlog_bound(&large_arrival, &service, 10);
2232
2233 assert!(large_backlog >= small_backlog);
2234 }
2235
2236 #[test]
2240 fn lemma_delay_monotone_arrival() {
2241 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
2242 let small_arrival = MinPlusCurve::from_token_bucket(2, 1, 10);
2243 let large_arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
2244
2245 let small_delay = delay_bound(&small_arrival, &service, 10, 20).unwrap_or(0);
2246 let large_delay = delay_bound(&large_arrival, &service, 10, 20).unwrap_or(0);
2247
2248 assert!(large_delay >= small_delay);
2249 }
2250
2251 #[test]
2256 fn lemma_convolution_tail_rate_is_min() {
2257 let slow = MinPlusCurve::new(vec![0, 1], 1).expect("valid");
2258 let fast = MinPlusCurve::new(vec![0, 3], 3).expect("valid");
2259 let conv = slow.min_plus_convolution(&fast, 10);
2260
2261 assert_eq!(conv.tail_rate(), 1);
2263
2264 let t = 20;
2267 let mut brute = u64::MAX;
2268 for s in 0..=t {
2269 brute = brute.min(slow.value_at(s).saturating_add(fast.value_at(t - s)));
2270 }
2271 assert_eq!(conv.value_at(t), brute);
2272 }
2273
2274 #[test]
2277 fn budget_clone_copy() {
2278 let b = Budget::new().with_poll_quota(42);
2279 let b2 = b; let b3 = b;
2281 assert_eq!(b, b2);
2282 assert_eq!(b2, b3);
2283 }
2284
2285 #[test]
2286 fn curve_error_debug_clone_eq() {
2287 let e1 = CurveError::EmptySamples;
2288 let e2 = e1.clone();
2289 assert_eq!(e1, e2);
2290
2291 let e3 = CurveError::NonMonotone {
2292 index: 2,
2293 prev: 10,
2294 next: 5,
2295 };
2296 let e4 = e3.clone();
2297 assert_eq!(e3, e4);
2298 assert_ne!(e1, e3);
2299 let dbg = format!("{e3:?}");
2300 assert!(dbg.contains("NonMonotone"));
2301 }
2302
2303 #[test]
2304 fn min_plus_curve_debug_clone_eq() {
2305 let c1 = MinPlusCurve::new(vec![0, 1, 2], 1).unwrap();
2306 let c2 = c1.clone();
2307 assert_eq!(c1, c2);
2308 let dbg = format!("{c1:?}");
2309 assert!(dbg.contains("MinPlusCurve"));
2310 }
2311
2312 #[test]
2313 fn curve_budget_debug_clone_eq() {
2314 let arrival = MinPlusCurve::new(vec![0, 2, 4], 2).unwrap();
2315 let service = MinPlusCurve::new(vec![0, 3, 6], 3).unwrap();
2316 let cb = CurveBudget { arrival, service };
2317 let cb2 = cb.clone();
2318 assert_eq!(cb, cb2);
2319 let dbg = format!("{cb:?}");
2320 assert!(dbg.contains("CurveBudget"));
2321 }
2322
2323 #[test]
2324 fn budget_event_snapshot_scrubbed() {
2325 let mut budget = Budget::new()
2326 .with_deadline(Time::from_secs(12))
2327 .with_poll_quota(3)
2328 .with_cost_quota(40)
2329 .with_priority(180);
2330
2331 let before = budget;
2332 let poll_before = budget.consume_poll();
2333 let after_poll = budget;
2334 let cost_ok = budget.consume_cost(15);
2335 let after_cost = budget;
2336
2337 insta::assert_json_snapshot!(
2338 "budget_event_scrubbed",
2339 json!({
2340 "events": [
2341 {
2342 "event": "created",
2343 "deadline": scrub_budget_event(before.deadline),
2344 "poll_quota": before.poll_quota,
2345 "cost_quota": before.cost_quota,
2346 "priority": before.priority,
2347 },
2348 {
2349 "event": "consume_poll",
2350 "returned": poll_before,
2351 "poll_quota_after": after_poll.poll_quota,
2352 },
2353 {
2354 "event": "consume_cost",
2355 "accepted": cost_ok,
2356 "cost_quota_after": after_cost.cost_quota,
2357 "exhausted": after_cost.is_exhausted(),
2358 }
2359 ]
2360 })
2361 );
2362 }
2363}