1use std::collections::{HashMap, VecDeque};
8use std::sync::RwLock;
9use std::sync::atomic::AtomicBool;
10use std::time::{Duration, Instant};
11
12use serde::Serialize;
13
14#[derive(Debug, Clone, Serialize)]
16pub struct CommandTimingStats {
17 pub command: String,
19 pub count: u64,
21 pub min_ms: f64,
23 pub max_ms: f64,
25 pub avg_ms: f64,
27 pub p95_ms: f64,
29 pub total_ms: f64,
31}
32
33#[derive(Debug, Default)]
35pub struct TimingSamples {
36 pub samples: Vec<Duration>,
38}
39
40impl TimingSamples {
41 pub fn record(&mut self, duration: Duration) {
43 self.samples.push(duration);
44 }
45
46 #[must_use]
48 pub fn stats(&self, command: &str) -> CommandTimingStats {
49 if self.samples.is_empty() {
50 return CommandTimingStats {
51 command: command.to_string(),
52 count: 0,
53 min_ms: 0.0,
54 max_ms: 0.0,
55 avg_ms: 0.0,
56 p95_ms: 0.0,
57 total_ms: 0.0,
58 };
59 }
60 let mut sorted: Vec<f64> = self
61 .samples
62 .iter()
63 .map(|d| d.as_secs_f64() * 1000.0)
64 .collect();
65 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
66
67 let count = sorted.len() as u64;
68 let total: f64 = sorted.iter().sum();
69 let min = sorted[0];
70 let max = sorted[sorted.len() - 1];
71 let avg = total / sorted.len() as f64;
72 let p95_idx = ((sorted.len() as f64) * 0.95).ceil() as usize;
73 let p95 = sorted[p95_idx.min(sorted.len() - 1)];
74
75 CommandTimingStats {
76 command: command.to_string(),
77 count,
78 min_ms: (min * 100.0).round() / 100.0,
79 max_ms: (max * 100.0).round() / 100.0,
80 avg_ms: (avg * 100.0).round() / 100.0,
81 p95_ms: (p95 * 100.0).round() / 100.0,
82 total_ms: (total * 100.0).round() / 100.0,
83 }
84 }
85}
86
87pub struct CommandTimings {
89 inner: RwLock<HashMap<String, TimingSamples>>,
90}
91
92impl CommandTimings {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 inner: RwLock::new(HashMap::new()),
98 }
99 }
100
101 pub fn record(&self, command: &str, duration: Duration) {
103 let mut map = self
104 .inner
105 .write()
106 .unwrap_or_else(std::sync::PoisonError::into_inner);
107 map.entry(command.to_string()).or_default().record(duration);
108 }
109
110 #[must_use]
112 pub fn all_stats(&self) -> Vec<CommandTimingStats> {
113 let map = self
114 .inner
115 .read()
116 .unwrap_or_else(std::sync::PoisonError::into_inner);
117 let mut stats: Vec<CommandTimingStats> =
118 map.iter().map(|(name, s)| s.stats(name)).collect();
119 stats.sort_by(|a, b| {
120 b.total_ms
121 .partial_cmp(&a.total_ms)
122 .unwrap_or(std::cmp::Ordering::Equal)
123 });
124 stats
125 }
126
127 #[must_use]
129 pub fn stats_for(&self, command: &str) -> Option<CommandTimingStats> {
130 let map = self
131 .inner
132 .read()
133 .unwrap_or_else(std::sync::PoisonError::into_inner);
134 map.get(command).map(|s| s.stats(command))
135 }
136
137 pub fn clear(&self) {
139 let mut map = self
140 .inner
141 .write()
142 .unwrap_or_else(std::sync::PoisonError::into_inner);
143 map.clear();
144 }
145}
146
147impl Default for CommandTimings {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[derive(Debug, Clone, Serialize)]
157pub enum FaultType {
158 Delay {
160 delay_ms: u64,
162 },
163 Error {
165 message: String,
167 },
168 Drop,
170 Corrupt,
172}
173
174#[derive(Debug, Clone, Serialize)]
176pub struct FaultConfig {
177 pub command: String,
179 pub fault_type: FaultType,
181 pub trigger_count: u64,
183 pub max_triggers: u64,
185 #[serde(skip)]
187 pub created_at: Instant,
188}
189
190impl FaultConfig {
191 #[must_use]
193 pub fn should_trigger(&self) -> bool {
194 self.max_triggers == 0 || self.trigger_count < self.max_triggers
195 }
196}
197
198pub struct FaultRegistry {
200 inner: RwLock<HashMap<String, FaultConfig>>,
201}
202
203impl FaultRegistry {
204 #[must_use]
206 pub fn new() -> Self {
207 Self {
208 inner: RwLock::new(HashMap::new()),
209 }
210 }
211
212 pub fn inject(&self, config: FaultConfig) {
214 let mut map = self
215 .inner
216 .write()
217 .unwrap_or_else(std::sync::PoisonError::into_inner);
218 map.insert(config.command.clone(), config);
219 }
220
221 pub fn check_and_trigger(&self, command: &str) -> Option<FaultType> {
224 let mut map = self
225 .inner
226 .write()
227 .unwrap_or_else(std::sync::PoisonError::into_inner);
228 if let Some(config) = map.get_mut(command)
229 && config.should_trigger()
230 {
231 config.trigger_count += 1;
232 return Some(config.fault_type.clone());
233 }
234 None
235 }
236
237 #[must_use]
239 pub fn list(&self) -> Vec<FaultConfig> {
240 let map = self
241 .inner
242 .read()
243 .unwrap_or_else(std::sync::PoisonError::into_inner);
244 map.values().cloned().collect()
245 }
246
247 pub fn clear(&self, command: &str) -> bool {
249 let mut map = self
250 .inner
251 .write()
252 .unwrap_or_else(std::sync::PoisonError::into_inner);
253 map.remove(command).is_some()
254 }
255
256 pub fn clear_all(&self) -> usize {
258 let mut map = self
259 .inner
260 .write()
261 .unwrap_or_else(std::sync::PoisonError::into_inner);
262 let count = map.len();
263 map.clear();
264 count
265 }
266}
267
268impl Default for FaultRegistry {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274#[derive(Debug, Clone, Serialize, PartialEq)]
278pub enum JsonShape {
279 Null,
281 Bool,
283 Number,
285 String,
287 Array(Box<Self>),
289 Object(HashMap<String, Self>),
291}
292
293impl JsonShape {
294 #[must_use]
296 pub fn from_value(value: &serde_json::Value) -> Self {
297 match value {
298 serde_json::Value::Null => Self::Null,
299 serde_json::Value::Bool(_) => Self::Bool,
300 serde_json::Value::Number(_) => Self::Number,
301 serde_json::Value::String(_) => Self::String,
302 serde_json::Value::Array(arr) => {
303 let elem = arr.first().map_or(Self::Null, Self::from_value);
304 Self::Array(Box::new(elem))
305 }
306 serde_json::Value::Object(obj) => {
307 let fields: HashMap<String, Self> = obj
308 .iter()
309 .map(|(k, v)| (k.clone(), Self::from_value(v)))
310 .collect();
311 Self::Object(fields)
312 }
313 }
314 }
315
316 #[must_use]
318 pub fn type_name(&self) -> &'static str {
319 match self {
320 Self::Null => "null",
321 Self::Bool => "bool",
322 Self::Number => "number",
323 Self::String => "string",
324 Self::Array(_) => "array",
325 Self::Object(_) => "object",
326 }
327 }
328}
329
330#[derive(Debug, Clone, Serialize)]
332pub struct ContractBaseline {
333 pub command: String,
335 pub args: serde_json::Value,
337 pub shape: JsonShape,
339 pub sample: String,
341 pub recorded_at: String,
343}
344
345#[derive(Debug, Clone, Serialize)]
347pub struct ContractDrift {
348 pub command: String,
350 pub new_fields: Vec<String>,
352 pub removed_fields: Vec<String>,
354 pub type_changes: Vec<TypeChange>,
356 pub shape_matches: bool,
358}
359
360#[derive(Debug, Clone, Serialize)]
362pub struct TypeChange {
363 pub path: String,
365 pub baseline_type: String,
367 pub current_type: String,
369}
370
371#[must_use]
373pub fn diff_shapes(baseline: &JsonShape, current: &JsonShape, prefix: &str) -> ContractDrift {
374 let mut new_fields = Vec::new();
375 let mut removed_fields = Vec::new();
376 let mut type_changes = Vec::new();
377
378 diff_shapes_inner(
379 baseline,
380 current,
381 prefix,
382 &mut new_fields,
383 &mut removed_fields,
384 &mut type_changes,
385 );
386
387 let shape_matches =
388 new_fields.is_empty() && removed_fields.is_empty() && type_changes.is_empty();
389 ContractDrift {
390 command: prefix.to_string(),
391 new_fields,
392 removed_fields,
393 type_changes,
394 shape_matches,
395 }
396}
397
398fn diff_shapes_inner(
399 baseline: &JsonShape,
400 current: &JsonShape,
401 prefix: &str,
402 new_fields: &mut Vec<String>,
403 removed_fields: &mut Vec<String>,
404 type_changes: &mut Vec<TypeChange>,
405) {
406 match (baseline, current) {
407 (JsonShape::Object(b_fields), JsonShape::Object(c_fields)) => {
408 for (key, b_shape) in b_fields {
409 let path = if prefix.is_empty() {
410 key.clone()
411 } else {
412 format!("{prefix}.{key}")
413 };
414 if let Some(c_shape) = c_fields.get(key) {
415 diff_shapes_inner(
416 b_shape,
417 c_shape,
418 &path,
419 new_fields,
420 removed_fields,
421 type_changes,
422 );
423 } else {
424 removed_fields.push(path);
425 }
426 }
427 for key in c_fields.keys() {
428 if !b_fields.contains_key(key) {
429 let path = if prefix.is_empty() {
430 key.clone()
431 } else {
432 format!("{prefix}.{key}")
433 };
434 new_fields.push(path);
435 }
436 }
437 }
438 (JsonShape::Array(b_elem), JsonShape::Array(c_elem)) => {
439 let path = format!("{prefix}[]");
440 diff_shapes_inner(
441 b_elem,
442 c_elem,
443 &path,
444 new_fields,
445 removed_fields,
446 type_changes,
447 );
448 }
449 (b, c) if b.type_name() != c.type_name() => {
450 type_changes.push(TypeChange {
451 path: prefix.to_string(),
452 baseline_type: b.type_name().to_string(),
453 current_type: c.type_name().to_string(),
454 });
455 }
456 _ => {}
457 }
458}
459
460pub struct ContractStore {
462 inner: RwLock<HashMap<String, ContractBaseline>>,
463}
464
465impl ContractStore {
466 #[must_use]
468 pub fn new() -> Self {
469 Self {
470 inner: RwLock::new(HashMap::new()),
471 }
472 }
473
474 pub fn record(&self, baseline: ContractBaseline) {
476 let mut map = self
477 .inner
478 .write()
479 .unwrap_or_else(std::sync::PoisonError::into_inner);
480 map.insert(baseline.command.clone(), baseline);
481 }
482
483 #[must_use]
485 pub fn get(&self, command: &str) -> Option<ContractBaseline> {
486 let map = self
487 .inner
488 .read()
489 .unwrap_or_else(std::sync::PoisonError::into_inner);
490 map.get(command).cloned()
491 }
492
493 #[must_use]
495 pub fn all(&self) -> Vec<ContractBaseline> {
496 let map = self
497 .inner
498 .read()
499 .unwrap_or_else(std::sync::PoisonError::into_inner);
500 map.values().cloned().collect()
501 }
502
503 pub fn clear(&self) -> usize {
505 let mut map = self
506 .inner
507 .write()
508 .unwrap_or_else(std::sync::PoisonError::into_inner);
509 let count = map.len();
510 map.clear();
511 count
512 }
513}
514
515impl Default for ContractStore {
516 fn default() -> Self {
517 Self::new()
518 }
519}
520
521#[derive(Debug, Clone, Serialize)]
525pub struct StartupPhase {
526 pub name: String,
528 pub duration_ms: f64,
530 pub cumulative_ms: f64,
532}
533
534pub struct StartupTimeline {
536 start: Instant,
537 phases: RwLock<Vec<(String, Instant)>>,
538}
539
540impl StartupTimeline {
541 #[must_use]
543 pub fn new() -> Self {
544 Self {
545 start: Instant::now(),
546 phases: RwLock::new(Vec::new()),
547 }
548 }
549
550 pub fn mark(&self, name: &str) {
552 let mut phases = self
553 .phases
554 .write()
555 .unwrap_or_else(std::sync::PoisonError::into_inner);
556 phases.push((name.to_string(), Instant::now()));
557 }
558
559 #[must_use]
561 pub fn report(&self) -> Vec<StartupPhase> {
562 let phases = self
563 .phases
564 .read()
565 .unwrap_or_else(std::sync::PoisonError::into_inner);
566 let mut result = Vec::new();
567 let mut prev = self.start;
568
569 for (name, instant) in phases.iter() {
570 let duration = instant.duration_since(prev);
571 let cumulative = instant.duration_since(self.start);
572 result.push(StartupPhase {
573 name: name.clone(),
574 duration_ms: (duration.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
575 cumulative_ms: (cumulative.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
576 });
577 prev = *instant;
578 }
579 result
580 }
581
582 #[must_use]
584 pub fn total_ms(&self) -> f64 {
585 let phases = self
586 .phases
587 .read()
588 .unwrap_or_else(std::sync::PoisonError::into_inner);
589 if let Some((_, last)) = phases.last() {
590 (last.duration_since(self.start).as_secs_f64() * 1000.0 * 100.0).round() / 100.0
591 } else {
592 0.0
593 }
594 }
595}
596
597impl Default for StartupTimeline {
598 fn default() -> Self {
599 Self::new()
600 }
601}
602
603#[derive(Debug, Clone, Serialize)]
607pub struct CapturedTauriEvent {
608 pub name: String,
610 pub payload: String,
612 pub timestamp: String,
614}
615
616const DEFAULT_EVENT_BUS_CAPACITY: usize = 1000;
617
618#[derive(Clone)]
620pub struct EventBusMonitor {
621 inner: std::sync::Arc<RwLock<VecDeque<CapturedTauriEvent>>>,
622 capacity: usize,
623}
624
625impl EventBusMonitor {
626 #[must_use]
628 pub fn new(capacity: usize) -> Self {
629 Self {
630 inner: std::sync::Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
631 capacity,
632 }
633 }
634
635 pub fn push(&self, event: CapturedTauriEvent) {
637 let mut buf = self
638 .inner
639 .write()
640 .unwrap_or_else(std::sync::PoisonError::into_inner);
641 if buf.len() >= self.capacity {
642 buf.pop_front();
643 }
644 buf.push_back(event);
645 }
646
647 #[must_use]
649 pub fn events(&self) -> Vec<CapturedTauriEvent> {
650 self.inner
651 .read()
652 .unwrap_or_else(std::sync::PoisonError::into_inner)
653 .iter()
654 .cloned()
655 .collect()
656 }
657
658 #[must_use]
660 pub fn len(&self) -> usize {
661 self.inner
662 .read()
663 .unwrap_or_else(std::sync::PoisonError::into_inner)
664 .len()
665 }
666
667 #[must_use]
669 pub fn is_empty(&self) -> bool {
670 self.len() == 0
671 }
672
673 pub fn clear(&self) -> usize {
675 let mut buf = self
676 .inner
677 .write()
678 .unwrap_or_else(std::sync::PoisonError::into_inner);
679 let count = buf.len();
680 buf.clear();
681 count
682 }
683}
684
685impl Default for EventBusMonitor {
686 fn default() -> Self {
687 Self::new(DEFAULT_EVENT_BUS_CAPACITY)
688 }
689}
690
691#[derive(Debug, Clone, Serialize)]
695pub struct TrackedTaskInfo {
696 pub name: String,
698 pub spawned_at: String,
700 pub is_finished: bool,
702 pub uptime_secs: u64,
704}
705
706struct TrackedTaskEntry {
707 name: String,
708 spawned_at: Instant,
709 spawned_at_wall: String,
710 finished: std::sync::Arc<AtomicBool>,
711}
712
713pub struct TaskTracker {
715 tasks: RwLock<Vec<TrackedTaskEntry>>,
716}
717
718impl TaskTracker {
719 #[must_use]
721 pub fn new() -> Self {
722 Self {
723 tasks: RwLock::new(Vec::new()),
724 }
725 }
726
727 pub fn track(&self, name: &str) -> std::sync::Arc<AtomicBool> {
729 let finished = std::sync::Arc::new(AtomicBool::new(false));
730 let entry = TrackedTaskEntry {
731 name: name.to_string(),
732 spawned_at: Instant::now(),
733 spawned_at_wall: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
734 finished: finished.clone(),
735 };
736 self.tasks
737 .write()
738 .unwrap_or_else(std::sync::PoisonError::into_inner)
739 .push(entry);
740 finished
741 }
742
743 #[must_use]
745 pub fn list(&self) -> Vec<TrackedTaskInfo> {
746 let tasks = self
747 .tasks
748 .read()
749 .unwrap_or_else(std::sync::PoisonError::into_inner);
750 tasks
751 .iter()
752 .map(|t| TrackedTaskInfo {
753 name: t.name.clone(),
754 spawned_at: t.spawned_at_wall.clone(),
755 is_finished: t.finished.load(std::sync::atomic::Ordering::Relaxed),
756 uptime_secs: t.spawned_at.elapsed().as_secs(),
757 })
758 .collect()
759 }
760
761 #[must_use]
763 pub fn active_count(&self) -> usize {
764 let tasks = self
765 .tasks
766 .read()
767 .unwrap_or_else(std::sync::PoisonError::into_inner);
768 tasks
769 .iter()
770 .filter(|t| !t.finished.load(std::sync::atomic::Ordering::Relaxed))
771 .count()
772 }
773}
774
775impl Default for TaskTracker {
776 fn default() -> Self {
777 Self::new()
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 #[test]
786 fn event_bus_push_and_read() {
787 let bus = EventBusMonitor::new(3);
788 assert!(bus.is_empty());
789 bus.push(CapturedTauriEvent {
790 name: "test".to_string(),
791 payload: "{}".to_string(),
792 timestamp: "2026-01-01T00:00:00Z".to_string(),
793 });
794 assert_eq!(bus.len(), 1);
795 assert_eq!(bus.events()[0].name, "test");
796 }
797
798 #[test]
799 fn event_bus_ring_buffer_eviction() {
800 let bus = EventBusMonitor::new(2);
801 for i in 0..5 {
802 bus.push(CapturedTauriEvent {
803 name: format!("event_{i}"),
804 payload: String::new(),
805 timestamp: String::new(),
806 });
807 }
808 assert_eq!(bus.len(), 2);
809 assert_eq!(bus.events()[0].name, "event_3");
810 assert_eq!(bus.events()[1].name, "event_4");
811 }
812
813 #[test]
814 fn event_bus_clear() {
815 let bus = EventBusMonitor::new(10);
816 bus.push(CapturedTauriEvent {
817 name: "a".to_string(),
818 payload: String::new(),
819 timestamp: String::new(),
820 });
821 assert_eq!(bus.clear(), 1);
822 assert!(bus.is_empty());
823 }
824
825 #[test]
826 fn task_tracker_lifecycle() {
827 let tracker = TaskTracker::new();
828 let flag = tracker.track("mcp_server");
829 let tasks = tracker.list();
830 assert_eq!(tasks.len(), 1);
831 assert_eq!(tasks[0].name, "mcp_server");
832 assert!(!tasks[0].is_finished);
833 assert_eq!(tracker.active_count(), 1);
834
835 flag.store(true, std::sync::atomic::Ordering::Relaxed);
836 let tasks = tracker.list();
837 assert!(tasks[0].is_finished);
838 assert_eq!(tracker.active_count(), 0);
839 }
840
841 #[test]
842 fn timing_samples_basic() {
843 let mut samples = TimingSamples::default();
844 samples.record(Duration::from_millis(10));
845 samples.record(Duration::from_millis(20));
846 samples.record(Duration::from_millis(30));
847 let stats = samples.stats("test_cmd");
848 assert_eq!(stats.count, 3);
849 assert!((stats.min_ms - 10.0).abs() < 1.0);
850 assert!((stats.max_ms - 30.0).abs() < 1.0);
851 assert!((stats.avg_ms - 20.0).abs() < 1.0);
852 }
853
854 #[test]
855 fn timing_samples_empty() {
856 let samples = TimingSamples::default();
857 let stats = samples.stats("empty");
858 assert_eq!(stats.count, 0);
859 assert_eq!(stats.min_ms, 0.0);
860 }
861
862 #[test]
863 fn command_timings_thread_safe() {
864 let timings = CommandTimings::new();
865 timings.record("cmd_a", Duration::from_millis(5));
866 timings.record("cmd_a", Duration::from_millis(15));
867 timings.record("cmd_b", Duration::from_millis(100));
868
869 let all = timings.all_stats();
870 assert_eq!(all.len(), 2);
871 assert_eq!(all[0].command, "cmd_b");
872
873 let a = timings.stats_for("cmd_a").unwrap();
874 assert_eq!(a.count, 2);
875 }
876
877 #[test]
878 fn fault_registry_lifecycle() {
879 let registry = FaultRegistry::new();
880 registry.inject(FaultConfig {
881 command: "slow_cmd".to_string(),
882 fault_type: FaultType::Delay { delay_ms: 500 },
883 trigger_count: 0,
884 max_triggers: 2,
885 created_at: Instant::now(),
886 });
887
888 assert!(registry.check_and_trigger("slow_cmd").is_some());
889 assert!(registry.check_and_trigger("slow_cmd").is_some());
890 assert!(registry.check_and_trigger("slow_cmd").is_none());
891
892 assert_eq!(registry.list().len(), 1);
893 assert!(registry.clear("slow_cmd"));
894 assert_eq!(registry.list().len(), 0);
895 }
896
897 #[test]
898 fn fault_registry_unlimited() {
899 let registry = FaultRegistry::new();
900 registry.inject(FaultConfig {
901 command: "always_fail".to_string(),
902 fault_type: FaultType::Error {
903 message: "injected".to_string(),
904 },
905 trigger_count: 0,
906 max_triggers: 0,
907 created_at: Instant::now(),
908 });
909
910 for _ in 0..100 {
911 assert!(registry.check_and_trigger("always_fail").is_some());
912 }
913 }
914
915 #[test]
916 fn json_shape_extraction() {
917 let value = serde_json::json!({
918 "name": "test",
919 "count": 42,
920 "active": true,
921 "items": [{"id": 1}],
922 "meta": null
923 });
924 let shape = JsonShape::from_value(&value);
925 match &shape {
926 JsonShape::Object(fields) => {
927 assert_eq!(fields.len(), 5);
928 assert_eq!(*fields.get("name").unwrap(), JsonShape::String);
929 assert_eq!(*fields.get("count").unwrap(), JsonShape::Number);
930 assert_eq!(*fields.get("active").unwrap(), JsonShape::Bool);
931 assert_eq!(*fields.get("meta").unwrap(), JsonShape::Null);
932 }
933 _ => panic!("expected object"),
934 }
935 }
936
937 #[test]
938 fn contract_diff_detects_changes() {
939 let baseline = serde_json::json!({"name": "old", "count": 1});
940 let current = serde_json::json!({"name": "new", "count": "not_a_number", "extra": true});
941
942 let b_shape = JsonShape::from_value(&baseline);
943 let c_shape = JsonShape::from_value(¤t);
944 let drift = diff_shapes(&b_shape, &c_shape, "test_cmd");
945
946 assert!(!drift.shape_matches);
947 assert_eq!(drift.new_fields, vec!["test_cmd.extra"]);
948 assert_eq!(drift.type_changes.len(), 1);
949 assert_eq!(drift.type_changes[0].path, "test_cmd.count");
950 }
951
952 #[test]
953 fn contract_store_crud() {
954 let store = ContractStore::new();
955 let baseline = ContractBaseline {
956 command: "get_user".to_string(),
957 args: serde_json::json!({}),
958 shape: JsonShape::Object(HashMap::new()),
959 sample: "{}".to_string(),
960 recorded_at: "2026-05-26".to_string(),
961 };
962 store.record(baseline);
963 assert!(store.get("get_user").is_some());
964 assert_eq!(store.all().len(), 1);
965 assert_eq!(store.clear(), 1);
966 assert!(store.get("get_user").is_none());
967 }
968
969 #[test]
970 fn startup_timeline_records_phases() {
971 let timeline = StartupTimeline::new();
972 std::thread::sleep(Duration::from_millis(5));
973 timeline.mark("phase_1");
974 std::thread::sleep(Duration::from_millis(5));
975 timeline.mark("phase_2");
976
977 let report = timeline.report();
978 assert_eq!(report.len(), 2);
979 assert_eq!(report[0].name, "phase_1");
980 assert!(report[1].cumulative_ms >= report[0].cumulative_ms);
981 assert!(timeline.total_ms() > 0.0);
982 }
983}