1use crate::time::CivilDate;
2#[cfg(feature = "diagram-gantt")]
3use crate::time::CivilDateTime;
4#[cfg(any(test, feature = "diagram-gantt"))]
5use crate::time::OffsetDateTime;
6#[cfg(test)]
7use crate::time::UtcOffset;
8use std::cell::RefCell;
9use std::num::NonZeroU64;
10
11const DETERMINISTIC_UNIX_MILLIS: i64 = 0;
12const DETERMINISTIC_OPERATION_SEED: u64 = 0x6D65_726D_616E_0001;
13
14thread_local! {
15 static OPERATION_CONTEXT: RefCell<Option<OperationContext>> = const { RefCell::new(None) };
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RuntimeCapability {
21 SystemClock,
22 SystemTimeZone,
23 SystemRandom,
24 SystemTiming,
25}
26
27impl RuntimeCapability {
28 pub const ALL: [Self; 4] = [
29 Self::SystemClock,
30 Self::SystemTimeZone,
31 Self::SystemRandom,
32 Self::SystemTiming,
33 ];
34
35 pub const fn id(self) -> &'static str {
36 match self {
37 Self::SystemClock => "system-clock",
38 Self::SystemTimeZone => "system-timezone",
39 Self::SystemRandom => "system-random",
40 Self::SystemTiming => "system-timing",
41 }
42 }
43}
44
45const COMPILED_SYSTEM_ADAPTER_IDS: &[&str] = &[
46 #[cfg(feature = "system-clock")]
47 RuntimeCapability::SystemClock.id(),
48 #[cfg(feature = "system-timezone")]
49 RuntimeCapability::SystemTimeZone.id(),
50 #[cfg(feature = "system-random")]
51 RuntimeCapability::SystemRandom.id(),
52 #[cfg(feature = "system-timing")]
53 RuntimeCapability::SystemTiming.id(),
54];
55
56pub const fn compiled_system_adapter_ids() -> &'static [&'static str] {
59 COMPILED_SYSTEM_ADAPTER_IDS
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
64pub enum RuntimePolicyError {
65 #[error("runtime capability `{}` is not compiled into this artifact", .0.id())]
66 MissingCapability(RuntimeCapability),
67 #[error("local UTC offset must be between -1439 and 1439 minutes, got {0}")]
68 InvalidFixedOffset(i32),
69 #[error("system clock instant is outside the supported millisecond range")]
70 SystemClockOutOfRange,
71 #[error("system time-zone adapter failed: {0}")]
72 SystemTimeZone(String),
73 #[error("runtime instant {0} is outside the supported calendar range")]
74 InstantOutOfRange(i64),
75 #[error("fixed_today local datetime {0}T00:00:00 cannot be resolved in the selected time zone")]
76 FixedLocalMidnightOutOfRange(CivilDate),
77 #[error("system random adapter failed: {0}")]
78 SystemRandom(String),
79}
80
81impl RuntimePolicyError {
82 pub const fn missing_capability(&self) -> Option<RuntimeCapability> {
83 match self {
84 Self::MissingCapability(capability) => Some(*capability),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum ClockPolicy {
92 Fixed(i64),
93 System,
94 Captured(i64),
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum SeedPolicy {
99 Fixed(u64),
100 System,
101 Captured(u64),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105enum TimingPolicy {
106 Disabled,
107 System,
108}
109
110#[cfg(feature = "system-timing")]
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112struct SystemTimingAuthority;
113
114#[cfg(not(feature = "system-timing"))]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116enum SystemTimingAuthority {}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct RuntimePolicy {
124 clock: ClockPolicy,
125 local_time_zone: crate::time::LocalTimeZone,
126 fixed_today_local: Option<CivilDate>,
127 seed: SeedPolicy,
128 timing: TimingPolicy,
129}
130
131impl Default for RuntimePolicy {
132 fn default() -> Self {
133 Self::deterministic()
134 }
135}
136
137impl RuntimePolicy {
138 pub const NATIVE_SYSTEM_ADAPTER_IDS: &'static [&'static str] = &[
143 RuntimeCapability::SystemClock.id(),
144 RuntimeCapability::SystemTimeZone.id(),
145 RuntimeCapability::SystemRandom.id(),
146 ];
147
148 pub fn deterministic() -> Self {
150 Self {
151 clock: ClockPolicy::Fixed(DETERMINISTIC_UNIX_MILLIS),
152 local_time_zone: crate::time::LocalTimeZone::utc(),
153 fixed_today_local: None,
154 seed: SeedPolicy::Fixed(DETERMINISTIC_OPERATION_SEED),
155 timing: TimingPolicy::Disabled,
156 }
157 }
158
159 pub fn try_native() -> Result<Self, RuntimePolicyError> {
164 Self::deterministic()
165 .try_with_system_clock()?
166 .try_with_system_time_zone()?
167 .try_with_system_random()
168 }
169
170 pub fn from_operation_context(context: OperationContext) -> Self {
172 let timing = if context.timing.is_some() {
173 TimingPolicy::System
174 } else {
175 TimingPolicy::Disabled
176 };
177 Self {
178 clock: ClockPolicy::Captured(context.unix_millis),
179 local_time_zone: context.local_time_zone,
180 fixed_today_local: context.today_is_fixed.then_some(context.today_local),
181 seed: SeedPolicy::Captured(context.seed),
182 timing,
183 }
184 }
185
186 pub fn with_fixed_unix_millis(mut self, unix_millis: i64) -> Self {
187 self.clock = ClockPolicy::Fixed(unix_millis);
188 self
189 }
190
191 pub fn try_with_system_clock(mut self) -> Result<Self, RuntimePolicyError> {
192 require_system_adapter(
193 RuntimeCapability::SystemClock,
194 cfg!(feature = "system-clock"),
195 )?;
196 self.clock = ClockPolicy::System;
197 Ok(self)
198 }
199
200 pub fn with_fixed_today(mut self, today: Option<CivilDate>) -> Self {
201 self.fixed_today_local = today;
202 self
203 }
204
205 pub fn try_with_fixed_today_at_local_midnight(
212 mut self,
213 today: CivilDate,
214 ) -> Result<Self, RuntimePolicyError> {
215 let local = self
216 .local_time_zone
217 .resolve_local(today.at_midnight())
218 .ok_or(RuntimePolicyError::FixedLocalMidnightOutOfRange(today))?;
219 self.clock = ClockPolicy::Fixed(local.timestamp_millis());
220 self.fixed_today_local = Some(today);
221 Ok(self)
222 }
223
224 pub fn try_with_fixed_local_offset_minutes(
226 mut self,
227 offset_minutes: i32,
228 ) -> Result<Self, RuntimePolicyError> {
229 self.local_time_zone = crate::time::LocalTimeZone::fixed(offset_minutes)?;
230 Ok(self)
231 }
232
233 pub fn with_local_time_zone(mut self, time_zone: crate::time::LocalTimeZone) -> Self {
234 self.local_time_zone = time_zone;
235 self
236 }
237
238 pub fn try_with_system_time_zone(mut self) -> Result<Self, RuntimePolicyError> {
239 self.local_time_zone = crate::time::LocalTimeZone::try_system()?;
240 Ok(self)
241 }
242
243 pub fn with_fixed_seed(mut self, seed: u64) -> Self {
244 self.seed = SeedPolicy::Fixed(seed);
245 self
246 }
247
248 pub fn try_with_system_random(mut self) -> Result<Self, RuntimePolicyError> {
249 require_system_adapter(
250 RuntimeCapability::SystemRandom,
251 cfg!(feature = "system-random"),
252 )?;
253 self.seed = SeedPolicy::System;
254 Ok(self)
255 }
256
257 pub fn try_with_system_timing(mut self) -> Result<Self, RuntimePolicyError> {
259 require_system_adapter(
260 RuntimeCapability::SystemTiming,
261 cfg!(feature = "system-timing"),
262 )?;
263 self.timing = TimingPolicy::System;
264 Ok(self)
265 }
266
267 pub fn fixed_local_offset_minutes(&self) -> Option<i32> {
268 self.local_time_zone.fixed_offset_minutes()
269 }
270
271 pub fn fixed_today(&self) -> Option<CivilDate> {
272 self.fixed_today_local
273 }
274
275 pub fn local_time_zone(&self) -> &crate::time::LocalTimeZone {
276 &self.local_time_zone
277 }
278
279 pub fn begin_operation(&self) -> Result<OperationContext, RuntimePolicyError> {
281 let (unix_millis, clock_source) = match self.clock {
282 ClockPolicy::Fixed(unix_millis) => (unix_millis, RuntimeValueSource::Fixed),
283 ClockPolicy::System => (system_unix_millis()?, RuntimeValueSource::System),
284 ClockPolicy::Captured(unix_millis) => (unix_millis, RuntimeValueSource::Captured),
285 };
286 let local_time_zone = self.local_time_zone.clone();
287 let today_local = match self.fixed_today_local {
288 Some(today) => today,
289 None => local_date_at(unix_millis, &local_time_zone)?,
290 };
291 let (seed, random_source) = match self.seed {
292 SeedPolicy::Fixed(seed) => (seed, RuntimeValueSource::Fixed),
293 SeedPolicy::System => (system_seed()?, RuntimeValueSource::System),
294 SeedPolicy::Captured(seed) => (seed, RuntimeValueSource::Captured),
295 };
296 let timing = match self.timing {
297 TimingPolicy::Disabled => None,
298 TimingPolicy::System => Some(system_timing_authority()?),
299 };
300
301 Ok(OperationContext {
302 unix_millis,
303 clock_source,
304 today_local,
305 today_is_fixed: self.fixed_today_local.is_some(),
306 local_time_zone,
307 seed,
308 random_source,
309 timing,
310 })
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct OperationContext {
317 unix_millis: i64,
318 clock_source: RuntimeValueSource,
319 today_local: CivilDate,
320 today_is_fixed: bool,
321 local_time_zone: crate::time::LocalTimeZone,
322 seed: u64,
323 random_source: RuntimeValueSource,
324 timing: Option<SystemTimingAuthority>,
325}
326
327#[derive(Debug, Clone, Copy)]
332pub struct OperationTiming {
333 authority: SystemTimingAuthority,
334}
335
336#[derive(Debug)]
338pub struct OperationTimer {
339 #[cfg(feature = "system-timing")]
340 started_at: web_time::Instant,
341 #[cfg(not(feature = "system-timing"))]
342 unavailable: std::convert::Infallible,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
347#[error("operation timing was not enabled for this operation")]
348pub struct OperationTimingUnavailable;
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum RuntimeValueSource {
353 Fixed,
354 System,
355 Captured,
356}
357
358impl RuntimeValueSource {
359 pub const fn id(self) -> &'static str {
360 match self {
361 Self::Fixed => "fixed",
362 Self::System => "system",
363 Self::Captured => "captured",
364 }
365 }
366}
367
368impl OperationContext {
369 pub const fn unix_millis(&self) -> i64 {
370 self.unix_millis
371 }
372
373 pub const fn clock_source(&self) -> RuntimeValueSource {
374 self.clock_source
375 }
376
377 pub const fn today_local(&self) -> CivilDate {
378 self.today_local
379 }
380
381 pub const fn today_is_fixed(&self) -> bool {
382 self.today_is_fixed
383 }
384
385 pub fn local_time_zone(&self) -> &crate::time::LocalTimeZone {
386 &self.local_time_zone
387 }
388
389 pub const fn seed(&self) -> u64 {
390 self.seed
391 }
392
393 pub const fn random_source(&self) -> RuntimeValueSource {
394 self.random_source
395 }
396
397 pub fn timing(&self) -> Option<OperationTiming> {
398 self.timing.map(|authority| OperationTiming { authority })
399 }
400
401 pub fn require_timing(&self) -> Result<OperationTiming, OperationTimingUnavailable> {
402 self.timing().ok_or(OperationTimingUnavailable)
403 }
404
405 pub fn derive_u64(&self, domain: &str, ordinal: u64) -> u64 {
411 derive_random_u64(self.seed, domain, ordinal)
412 }
413
414 pub fn derive_nonzero_u64(&self, domain: &str, ordinal: u64) -> NonZeroU64 {
415 NonZeroU64::new(self.derive_u64(domain, ordinal)).unwrap_or(NonZeroU64::MIN)
416 }
417
418 pub fn derive_hex(&self, domain: &str, ordinal: u64, len: usize) -> String {
419 derive_random_hex(self.seed, domain, ordinal, len)
420 }
421}
422
423impl OperationTiming {
424 pub fn start(self) -> OperationTimer {
426 #[cfg(feature = "system-timing")]
427 {
428 let _ = self.authority;
429 OperationTimer {
430 started_at: web_time::Instant::now(),
431 }
432 }
433
434 #[cfg(not(feature = "system-timing"))]
435 {
436 match self.authority {}
437 }
438 }
439}
440
441impl OperationTimer {
442 pub fn elapsed(self) -> std::time::Duration {
444 #[cfg(feature = "system-timing")]
445 {
446 self.started_at.elapsed()
447 }
448
449 #[cfg(not(feature = "system-timing"))]
450 {
451 match self.unavailable {}
452 }
453 }
454}
455
456fn require_system_adapter(
457 capability: RuntimeCapability,
458 available: bool,
459) -> Result<(), RuntimePolicyError> {
460 if available {
461 Ok(())
462 } else {
463 Err(RuntimePolicyError::MissingCapability(capability))
464 }
465}
466
467#[cfg(feature = "system-timing")]
468fn system_timing_authority() -> Result<SystemTimingAuthority, RuntimePolicyError> {
469 Ok(SystemTimingAuthority)
470}
471
472#[cfg(not(feature = "system-timing"))]
473fn system_timing_authority() -> Result<SystemTimingAuthority, RuntimePolicyError> {
474 Err(RuntimePolicyError::MissingCapability(
475 RuntimeCapability::SystemTiming,
476 ))
477}
478
479fn local_date_at(
480 unix_millis: i64,
481 time_zone: &crate::time::LocalTimeZone,
482) -> Result<CivilDate, RuntimePolicyError> {
483 time_zone
484 .at_instant(unix_millis)
485 .map(|local| local.local_datetime().date())
486 .ok_or(RuntimePolicyError::InstantOutOfRange(unix_millis))
487}
488
489#[cfg(feature = "system-clock")]
490fn system_unix_millis() -> Result<i64, RuntimePolicyError> {
491 use std::time::{SystemTime, UNIX_EPOCH};
492
493 let millis = match SystemTime::now().duration_since(UNIX_EPOCH) {
494 Ok(duration) => i128::try_from(duration.as_millis())
495 .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)?,
496 Err(error) => -i128::try_from(error.duration().as_millis())
497 .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)?,
498 };
499 millis
500 .try_into()
501 .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)
502}
503
504#[cfg(not(feature = "system-clock"))]
505fn system_unix_millis() -> Result<i64, RuntimePolicyError> {
506 Err(RuntimePolicyError::MissingCapability(
507 RuntimeCapability::SystemClock,
508 ))
509}
510
511#[cfg(feature = "system-random")]
512fn system_seed() -> Result<u64, RuntimePolicyError> {
513 let mut bytes = [0_u8; size_of::<u64>()];
514 getrandom::fill(&mut bytes)
515 .map_err(|error| RuntimePolicyError::SystemRandom(error.to_string()))?;
516 Ok(u64::from_ne_bytes(bytes))
517}
518
519#[cfg(not(feature = "system-random"))]
520fn system_seed() -> Result<u64, RuntimePolicyError> {
521 Err(RuntimePolicyError::MissingCapability(
522 RuntimeCapability::SystemRandom,
523 ))
524}
525
526pub(crate) fn with_operation_context<R>(context: &OperationContext, f: impl FnOnce() -> R) -> R {
527 OPERATION_CONTEXT.with(|cell| {
528 let previous = cell.replace(Some(context.clone()));
529 struct Restore<'a> {
530 cell: &'a RefCell<Option<OperationContext>>,
531 previous: Option<OperationContext>,
532 }
533 impl Drop for Restore<'_> {
534 fn drop(&mut self) {
535 self.cell.replace(self.previous.take());
536 }
537 }
538 let _restore = Restore { cell, previous };
539 f()
540 })
541}
542
543#[cfg(any(test, feature = "diagram-gantt"))]
544pub(crate) fn today_local() -> CivilDate {
545 active_operation_context().today_local
546}
547
548#[cfg(feature = "diagram-gantt")]
549pub(crate) fn resolve_local_datetime(local: CivilDateTime) -> Option<OffsetDateTime> {
550 active_operation_context()
551 .local_time_zone
552 .resolve_local(local)
553}
554
555#[cfg(feature = "diagram-gantt")]
556pub(crate) fn datetime_to_local(datetime: OffsetDateTime) -> OffsetDateTime {
557 active_operation_context()
558 .local_time_zone
559 .at_instant(datetime.timestamp_millis())
560 .unwrap_or(datetime)
561}
562
563#[cfg(feature = "diagram-gantt")]
564pub(crate) fn datetime_to_local_civil(datetime: OffsetDateTime) -> CivilDateTime {
565 datetime_to_local(datetime).local_datetime()
566}
567
568#[cfg(any(
569 test,
570 feature = "diagram-block",
571 feature = "diagram-git-graph",
572 feature = "diagram-mindmap"
573))]
574pub(crate) fn generated_id_hex(domain: &str, counter: u64, len: usize) -> String {
575 let context = active_operation_context();
576 context.derive_hex(domain, counter, len)
577}
578
579fn derive_random_hex(seed: u64, domain: &str, ordinal: u64, len: usize) -> String {
580 const HEX: &[u8; 16] = b"0123456789abcdef";
581
582 let mut out = String::with_capacity(len);
583 let mut state = derive_random_u64(seed, domain, ordinal);
584 while out.len() < len {
585 state = splitmix64(state);
586 for shift in (0..16).rev() {
587 if out.len() == len {
588 break;
589 }
590 let idx = ((state >> (shift * 4)) & 0xF) as usize;
591 out.push(HEX[idx] as char);
592 }
593 }
594 out
595}
596
597fn derive_random_u64(seed: u64, domain: &str, ordinal: u64) -> u64 {
598 const OPERATION_RANDOM_DOMAIN: u64 = 0x6D65_726D_616E_2D72;
599 let domain_hash = domain
600 .as_bytes()
601 .iter()
602 .fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
603 (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
604 });
605 splitmix64(
606 seed ^ OPERATION_RANDOM_DOMAIN
607 ^ domain_hash.rotate_left(17)
608 ^ ordinal.wrapping_mul(0x9E37_79B9_7F4A_7C15),
609 )
610}
611
612fn splitmix64(state: u64) -> u64 {
613 let mut z = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
614 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
615 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
616 z ^ (z >> 31)
617}
618
619#[cfg(any(
620 test,
621 feature = "diagram-gantt",
622 feature = "diagram-block",
623 feature = "diagram-git-graph",
624 feature = "diagram-mindmap"
625))]
626fn active_operation_context() -> OperationContext {
627 OPERATION_CONTEXT
628 .with(|cell| cell.borrow().clone())
629 .unwrap_or_else(|| {
630 RuntimePolicy::deterministic()
631 .begin_operation()
632 .expect("the deterministic runtime policy is infallible")
633 })
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 fn date(year: i32, month: u32, day: u32) -> CivilDate {
641 CivilDate::new(year, month, day).expect("valid test date")
642 }
643
644 #[test]
645 fn operation_context_restores_after_panic() {
646 let outer = RuntimePolicy::deterministic()
647 .with_fixed_today(Some(date(2026, 7, 18)))
648 .begin_operation()
649 .unwrap();
650 let inner = RuntimePolicy::deterministic()
651 .with_fixed_today(Some(date(2030, 1, 2)))
652 .begin_operation()
653 .unwrap();
654
655 with_operation_context(&outer, || {
656 let panic = std::panic::catch_unwind(|| {
657 with_operation_context(&inner, || panic!("test panic"));
658 });
659 assert!(panic.is_err());
660 assert_eq!(today_local(), outer.today_local());
661 });
662 }
663
664 #[test]
665 fn deterministic_policy_uses_epoch_utc_and_fixed_seed() {
666 let context = RuntimePolicy::deterministic().begin_operation().unwrap();
667
668 assert_eq!(context.unix_millis(), 0);
669 assert_eq!(context.today_local(), date(1970, 1, 1));
670 assert_eq!(context.local_time_zone().fixed_offset_minutes(), Some(0));
671 assert_eq!(context.seed(), DETERMINISTIC_OPERATION_SEED);
672 assert_eq!(context.clock_source(), RuntimeValueSource::Fixed);
673 assert_eq!(context.random_source(), RuntimeValueSource::Fixed);
674 assert!(context.timing().is_none());
675 }
676
677 #[test]
678 fn fixed_today_local_midnight_rejects_unrepresentable_offset_boundary() {
679 let earliest_day = OffsetDateTime::from_unix_millis(i64::MIN, UtcOffset::UTC)
680 .utc_datetime()
681 .date();
682 let error = RuntimePolicy::deterministic()
683 .try_with_fixed_local_offset_minutes(1439)
684 .expect("valid fixed offset")
685 .try_with_fixed_today_at_local_midnight(earliest_day)
686 .expect_err("minimum date at eastern boundary must be rejected");
687
688 assert_eq!(
689 error,
690 RuntimePolicyError::FixedLocalMidnightOutOfRange(earliest_day)
691 );
692 }
693
694 #[test]
695 fn replayed_context_is_attested_as_captured() {
696 let original = RuntimePolicy::deterministic().begin_operation().unwrap();
697 let replayed = RuntimePolicy::from_operation_context(original)
698 .begin_operation()
699 .unwrap();
700
701 assert_eq!(replayed.clock_source(), RuntimeValueSource::Captured);
702 assert_eq!(replayed.random_source(), RuntimeValueSource::Captured);
703 assert!(!replayed.today_is_fixed());
704 }
705
706 #[test]
707 fn replayed_context_preserves_computed_and_fixed_today_semantics() {
708 let computed = RuntimePolicy::deterministic().begin_operation().unwrap();
709 let recomputed = RuntimePolicy::from_operation_context(computed)
710 .try_with_fixed_local_offset_minutes(-60)
711 .unwrap()
712 .begin_operation()
713 .unwrap();
714 assert_eq!(recomputed.today_local(), date(1969, 12, 31));
715 assert!(!recomputed.today_is_fixed());
716
717 let fixed_today = date(2026, 7, 22);
718 let fixed = RuntimePolicy::deterministic()
719 .with_fixed_today(Some(fixed_today))
720 .begin_operation()
721 .unwrap();
722 let replayed_fixed = RuntimePolicy::from_operation_context(fixed)
723 .try_with_fixed_local_offset_minutes(-60)
724 .unwrap()
725 .begin_operation()
726 .unwrap();
727 assert_eq!(replayed_fixed.today_local(), fixed_today);
728 assert!(replayed_fixed.today_is_fixed());
729 }
730
731 #[test]
732 fn operation_seed_is_domain_separated_and_context_owned() {
733 let first = RuntimePolicy::deterministic()
734 .with_fixed_seed(1)
735 .begin_operation()
736 .unwrap();
737 let second = RuntimePolicy::deterministic()
738 .with_fixed_seed(2)
739 .begin_operation()
740 .unwrap();
741
742 let first_id = with_operation_context(&first, || generated_id_hex("test.first", 7, 12));
743 let repeated = with_operation_context(&first, || generated_id_hex("test.first", 7, 12));
744 let second_id = with_operation_context(&second, || generated_id_hex("test.first", 7, 12));
745 let other_domain =
746 with_operation_context(&first, || generated_id_hex("test.second", 7, 12));
747
748 assert_eq!(first_id, repeated);
749 assert_ne!(first_id, second_id);
750 assert_ne!(first_id, other_domain);
751 }
752
753 #[test]
754 fn native_policy_reports_the_first_missing_required_adapter() {
755 let expected = RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS
756 .iter()
757 .map(|id| {
758 RuntimeCapability::ALL
759 .into_iter()
760 .find(|capability| capability.id() == *id)
761 .expect("native policy adapter IDs must use the runtime capability vocabulary")
762 })
763 .map(|capability| {
764 (
765 capability,
766 compiled_system_adapter_ids().contains(&capability.id()),
767 )
768 })
769 .into_iter()
770 .find_map(|(capability, available)| (!available).then_some(capability));
771
772 match expected {
773 Some(capability) => assert_eq!(
774 RuntimePolicy::try_native().unwrap_err(),
775 RuntimePolicyError::MissingCapability(capability)
776 ),
777 None => assert!(RuntimePolicy::try_native().is_ok()),
778 }
779 }
780
781 #[test]
782 fn native_policy_adapter_contract_excludes_timing() {
783 assert_eq!(
784 RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS,
785 ["system-clock", "system-timezone", "system-random"]
786 );
787 assert!(
788 !RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS
789 .contains(&RuntimeCapability::SystemTiming.id())
790 );
791 }
792
793 #[cfg(all(
794 feature = "system-clock",
795 feature = "system-timezone",
796 feature = "system-random"
797 ))]
798 #[test]
799 fn native_policy_does_not_enable_timing_instrumentation() {
800 let context = RuntimePolicy::try_native()
801 .unwrap()
802 .begin_operation()
803 .unwrap();
804
805 assert!(context.timing().is_none());
806 }
807
808 #[test]
809 fn compiled_system_adapter_ids_follow_the_canonical_order() {
810 let expected = RuntimeCapability::ALL
811 .into_iter()
812 .filter(|capability| match capability {
813 RuntimeCapability::SystemClock => cfg!(feature = "system-clock"),
814 RuntimeCapability::SystemTimeZone => cfg!(feature = "system-timezone"),
815 RuntimeCapability::SystemRandom => cfg!(feature = "system-random"),
816 RuntimeCapability::SystemTiming => cfg!(feature = "system-timing"),
817 })
818 .map(RuntimeCapability::id)
819 .collect::<Vec<_>>();
820
821 assert_eq!(compiled_system_adapter_ids(), expected);
822 }
823
824 #[test]
825 fn fixed_offset_changes_the_local_date_without_changing_the_instant() {
826 let west = RuntimePolicy::deterministic()
827 .try_with_fixed_local_offset_minutes(-60)
828 .unwrap()
829 .begin_operation()
830 .unwrap();
831 let utc = RuntimePolicy::deterministic()
832 .try_with_fixed_local_offset_minutes(0)
833 .unwrap()
834 .begin_operation()
835 .unwrap();
836
837 assert_eq!(west.unix_millis(), utc.unix_millis());
838 assert_eq!(west.today_local(), date(1969, 12, 31));
839 assert_eq!(utc.today_local(), date(1970, 1, 1));
840 }
841
842 #[cfg(feature = "system-timing")]
843 #[test]
844 fn explicitly_enabled_system_timing_is_issued_by_the_operation_context() {
845 let context = RuntimePolicy::deterministic()
846 .try_with_system_timing()
847 .unwrap()
848 .begin_operation()
849 .unwrap();
850
851 let timing = context.require_timing().unwrap();
852 let _elapsed = timing.start().elapsed();
853 }
854
855 #[cfg(not(feature = "system-timing"))]
856 #[test]
857 fn deterministic_context_cannot_forge_timing_without_system_timing() {
858 let context = RuntimePolicy::deterministic().begin_operation().unwrap();
859
860 assert_eq!(
861 context.require_timing().unwrap_err(),
862 OperationTimingUnavailable
863 );
864 }
865}