Skip to main content

netspeed_cli/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4#[serde(rename_all = "snake_case")]
5pub enum PhaseState {
6    Completed,
7    Skipped,
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct PhaseResult {
12    pub state: PhaseState,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub reason: Option<String>,
15}
16
17impl PhaseResult {
18    #[must_use]
19    pub fn completed() -> Self {
20        Self {
21            state: PhaseState::Completed,
22            reason: None,
23        }
24    }
25
26    #[must_use]
27    pub fn skipped(reason: impl Into<String>) -> Self {
28        Self {
29            state: PhaseState::Skipped,
30            reason: Some(reason.into()),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct TestPhases {
37    pub ping: PhaseResult,
38    pub download: PhaseResult,
39    pub upload: PhaseResult,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct Server {
44    #[serde(rename = "@id")]
45    pub id: String,
46    #[serde(rename = "@url")]
47    pub url: String,
48    #[serde(rename = "@name")]
49    pub name: String,
50    #[serde(rename = "@sponsor")]
51    pub sponsor: String,
52    #[serde(rename = "@country")]
53    pub country: String,
54    #[serde(rename = "@lat")]
55    pub lat: f64,
56    #[serde(rename = "@lon")]
57    pub lon: f64,
58    #[serde(skip)]
59    pub distance: f64,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct TestResult {
64    pub status: String,
65    pub version: String, // CLI version for API compatibility
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub test_id: Option<String>, // Unique identifier for this test run
68    pub server: ServerInfo,
69    pub ping: Option<f64>,
70    pub jitter: Option<f64>,
71    pub packet_loss: Option<f64>,
72    pub download: Option<f64>,
73    pub download_peak: Option<f64>,
74    pub download_cv: Option<f64>, // coefficient of variation (0-1) for variance
75    pub upload: Option<f64>,
76    pub upload_peak: Option<f64>,
77    pub upload_cv: Option<f64>, // coefficient of variation (0-1) for variance
78    pub download_ci_95: Option<(f64, f64)>, // (lower, upper) 95% CI in Mbps
79    pub upload_ci_95: Option<(f64, f64)>, // (lower, upper) 95% CI in Mbps
80    pub latency_download: Option<f64>,
81    pub latency_upload: Option<f64>,
82    pub download_samples: Option<Vec<f64>>,
83    pub upload_samples: Option<Vec<f64>>,
84    pub ping_samples: Option<Vec<f64>>,
85    pub timestamp: String,
86    pub client_ip: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub client_location: Option<ClientLocation>,
89    // Computed grades for machine-readable output (screen readers, scripts)
90    pub overall_grade: Option<String>,
91    pub download_grade: Option<String>,
92    pub upload_grade: Option<String>,
93    pub connection_rating: Option<String>,
94    pub phases: TestPhases,
95}
96
97/// Builder for constructing `TestResult` — separates construction from data types (SRP).
98///
99/// Uses injected `StatsService` for statistical computations,
100/// enabling alternative algorithms without modifying `TestResult`.
101pub struct TestResultBuilder {
102    server: ServerInfo,
103    ping: Option<f64>,
104    jitter: Option<f64>,
105    packet_loss: Option<f64>,
106    ping_samples: Vec<f64>,
107    download_avg_bps: f64,
108    download_peak_bps: f64,
109    download_samples: Vec<f64>,
110    download_latency: Option<f64>,
111    upload_avg_bps: f64,
112    upload_peak_bps: f64,
113    upload_samples: Vec<f64>,
114    upload_latency: Option<f64>,
115    client_ip: Option<String>,
116    client_location: Option<ClientLocation>,
117}
118
119impl TestResultBuilder {
120    pub fn new(server: ServerInfo) -> Self {
121        Self {
122            server,
123            ping: None,
124            jitter: None,
125            packet_loss: None,
126            ping_samples: Vec::new(),
127            download_avg_bps: 0.0,
128            download_peak_bps: 0.0,
129            download_samples: Vec::new(),
130            download_latency: None,
131            upload_avg_bps: 0.0,
132            upload_peak_bps: 0.0,
133            upload_samples: Vec::new(),
134            upload_latency: None,
135            client_ip: None,
136            client_location: None,
137        }
138    }
139
140    pub fn ping(mut self, ping: f64) -> Self {
141        self.ping = Some(ping);
142        self
143    }
144
145    pub fn jitter(mut self, jitter: f64) -> Self {
146        self.jitter = Some(jitter);
147        self
148    }
149
150    pub fn packet_loss(mut self, loss: f64) -> Self {
151        self.packet_loss = Some(loss);
152        self
153    }
154
155    pub fn ping_samples(mut self, samples: &[f64]) -> Self {
156        self.ping_samples = samples.to_vec();
157        self
158    }
159
160    pub fn download_stats(
161        mut self,
162        avg_bps: f64,
163        peak_bps: f64,
164        samples: &[f64],
165        latency_under_load: Option<f64>,
166    ) -> Self {
167        self.download_avg_bps = avg_bps;
168        self.download_peak_bps = peak_bps;
169        self.download_samples = samples.to_vec();
170        self.download_latency = latency_under_load;
171        self
172    }
173
174    pub fn upload_stats(
175        mut self,
176        avg_bps: f64,
177        peak_bps: f64,
178        samples: &[f64],
179        latency_under_load: Option<f64>,
180    ) -> Self {
181        self.upload_avg_bps = avg_bps;
182        self.upload_peak_bps = peak_bps;
183        self.upload_samples = samples.to_vec();
184        self.upload_latency = latency_under_load;
185        self
186    }
187
188    pub fn client_ip(mut self, ip: impl Into<String>) -> Self {
189        self.client_ip = Some(ip.into());
190        self
191    }
192
193    pub fn client_location(mut self, location: ClientLocation) -> Self {
194        self.client_location = Some(location);
195        self
196    }
197
198    /// Build the `TestResult` using the given stats service for CV/CI computation.
199    pub fn build(self, stats: &dyn StatsService) -> TestResult {
200        fn opt_samples(v: &[f64]) -> Option<Vec<f64>> {
201            if v.is_empty() { None } else { Some(v.to_vec()) }
202        }
203        fn opt_positive(v: f64) -> Option<f64> {
204            if v > 0.0 { Some(v) } else { None }
205        }
206
207        TestResult {
208            status: "ok".to_string(),
209            version: env!("CARGO_PKG_VERSION").to_string(),
210            test_id: Some(uuid_v4()),
211            server: self.server,
212            ping: self.ping,
213            jitter: self.jitter,
214            packet_loss: self.packet_loss,
215            download: opt_positive(self.download_avg_bps),
216            download_peak: opt_positive(self.download_peak_bps),
217            download_cv: stats.coefficient_of_variation(&self.download_samples),
218            upload: opt_positive(self.upload_avg_bps),
219            upload_peak: opt_positive(self.upload_peak_bps),
220            upload_cv: stats.coefficient_of_variation(&self.upload_samples),
221            download_ci_95: stats.confidence_interval_95(&self.download_samples, 1_000_000.0),
222            upload_ci_95: stats.confidence_interval_95(&self.upload_samples, 1_000_000.0),
223            latency_download: self.download_latency,
224            latency_upload: self.upload_latency,
225            download_samples: opt_samples(&self.download_samples),
226            upload_samples: opt_samples(&self.upload_samples),
227            ping_samples: opt_samples(&self.ping_samples),
228            timestamp: chrono::Utc::now().to_rfc3339(),
229            client_ip: self.client_ip,
230            client_location: self.client_location,
231            overall_grade: None,
232            download_grade: None,
233            upload_grade: None,
234            connection_rating: None,
235            phases: TestPhases {
236                ping: PhaseResult::completed(),
237                download: PhaseResult::completed(),
238                upload: PhaseResult::completed(),
239            },
240        }
241    }
242}
243
244impl TestResult {
245    /// Build a `TestResult` from ping test output and download/upload test runs.
246    /// Delegates to `TestResultBuilder` for construction.
247    #[allow(clippy::too_many_arguments)]
248    #[must_use]
249    pub fn from_test_runs(
250        server: ServerInfo,
251        ping: Option<f64>,
252        jitter: Option<f64>,
253        packet_loss: Option<f64>,
254        ping_samples: &[f64],
255        dl: &crate::task_runner::TestRunResult,
256        ul: &crate::task_runner::TestRunResult,
257        client_ip: Option<String>,
258        client_location: Option<ClientLocation>,
259    ) -> Self {
260        let mut builder = TestResultBuilder::new(server)
261            .ping_samples(ping_samples)
262            .download_stats(
263                dl.avg_bps,
264                dl.peak_bps,
265                &dl.speed_samples,
266                dl.latency_under_load,
267            )
268            .upload_stats(
269                ul.avg_bps,
270                ul.peak_bps,
271                &ul.speed_samples,
272                ul.latency_under_load,
273            );
274
275        if let Some(p) = ping {
276            builder = builder.ping(p);
277        }
278        if let Some(j) = jitter {
279            builder = builder.jitter(j);
280        }
281        if let Some(pl) = packet_loss {
282            builder = builder.packet_loss(pl);
283        }
284        if let Some(ip) = client_ip {
285            builder = builder.client_ip(ip);
286        }
287        if let Some(loc) = client_location {
288            builder = builder.client_location(loc);
289        }
290
291        builder.build(&DefaultStats)
292    }
293}
294
295/// Compute coefficient of variation (CV) for a sample set.
296/// Returns None for empty or single-element sets, or when mean is zero.
297fn compute_cv(samples: &[f64]) -> Option<f64> {
298    if samples.len() < 2 {
299        return None;
300    }
301    // Safe: sample counts are small (≤1000), well under 2^53.
302    let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
303    if mean == 0.0 {
304        return None;
305    }
306    // Safe: sample counts are small (≤1000), well under 2^53.
307    let variance: f64 =
308        samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (samples.len() - 1) as f64;
309    let std_dev = variance.sqrt();
310    Some(std_dev / mean)
311}
312
313/// Compute 95% confidence interval for the mean bandwidth.
314/// Returns `(lower, upper)` in Mbps. Uses t-distribution approximation for small samples.
315fn compute_ci_95(samples: &[f64], scale: f64) -> Option<(f64, f64)> {
316    let n = samples.len();
317    if n < 2 {
318        return None;
319    }
320    // Safe: sample counts are small (≤1000), well under 2^53.
321    let mean: f64 = samples.iter().sum::<f64>() / n as f64;
322    if n < 30 {
323        // Small sample: use t ≈ 2.045 (df=29, 95% CI) as conservative estimate
324        // Safe: n is small (≤1000), well under 2^53.
325        let variance: f64 =
326            samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
327        let std_err = variance.sqrt() / (n as f64).sqrt();
328        let margin = 2.045 * std_err;
329        Some(((mean - margin) / scale, (mean + margin) / scale))
330    } else {
331        // Large sample: use z = 1.96
332        // Safe: n is small (≤1000), well under 2^53.
333        let variance: f64 =
334            samples.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
335        let std_err = variance.sqrt() / (n as f64).sqrt();
336        let margin = 1.96 * std_err;
337        Some(((mean - margin) / scale, (mean + margin) / scale))
338    }
339}
340
341/// Trait for statistical computations - enables alternative algorithms.
342pub trait StatsService: Send + Sync {
343    fn coefficient_of_variation(&self, samples: &[f64]) -> Option<f64>;
344    fn confidence_interval_95(&self, samples: &[f64], scale: f64) -> Option<(f64, f64)>;
345}
346
347/// Default statistics implementation using standard formulas.
348pub struct DefaultStats;
349
350impl DefaultStats {
351    pub fn new() -> Self {
352        Self
353    }
354}
355
356impl Default for DefaultStats {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362impl StatsService for DefaultStats {
363    fn coefficient_of_variation(&self, samples: &[f64]) -> Option<f64> {
364        compute_cv(samples)
365    }
366
367    fn confidence_interval_95(&self, samples: &[f64], scale: f64) -> Option<(f64, f64)> {
368        compute_ci_95(samples, scale)
369    }
370}
371
372#[derive(Debug, Clone, Serialize)]
373pub struct ServerInfo {
374    pub id: String,
375    pub name: String,
376    pub sponsor: String,
377    pub country: String,
378    pub distance: f64,
379}
380
381/// Client geographic location derived from speedtest.net config API.
382#[derive(Debug, Clone, Serialize, Default)]
383pub struct ClientLocation {
384    pub lat: f64,
385    pub lon: f64,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub city: Option<String>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub country: Option<String>,
390}
391
392#[derive(Debug, Clone, Serialize)]
393pub struct CsvOutput {
394    pub server_id: String,
395    pub sponsor: String,
396    pub server_name: String,
397    pub timestamp: String,
398    pub distance: f64,
399    pub ping: f64,
400    pub jitter: f64,
401    pub packet_loss: f64,
402    pub download: f64,
403    pub download_peak: f64,
404    pub upload: f64,
405    pub upload_peak: f64,
406    pub ip_address: String,
407}
408
409#[cfg(test)]
410#[allow(clippy::items_after_test_module)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_server_serialization() {
416        let server = Server {
417            id: "1234".to_string(),
418            url: "http://example.com".to_string(),
419            name: "Test Server".to_string(),
420            sponsor: "Test ISP".to_string(),
421            country: "US".to_string(),
422            lat: 40.7128,
423            lon: -74.0060,
424            distance: 100.5,
425        };
426
427        let json = serde_json::to_string(&server).unwrap();
428        // With @ prefix for XML attributes, serde serializes them as normal fields in JSON
429        assert!(json.contains("\"1234\""));
430        assert!(json.contains("\"Test Server\""));
431    }
432
433    #[test]
434    fn test_test_result_serialization() {
435        let result = TestResult {
436            status: "ok".to_string(),
437            version: env!("CARGO_PKG_VERSION").to_string(),
438            test_id: None,
439            server: ServerInfo {
440                id: "1234".to_string(),
441                name: "Test Server".to_string(),
442                sponsor: "Test ISP".to_string(),
443                country: "US".to_string(),
444                distance: 100.5,
445            },
446            ping: Some(15.234),
447            jitter: Some(1.2),
448            packet_loss: Some(0.0),
449            download: Some(150_000_000.0),
450            download_peak: Some(180_000_000.0),
451            upload: Some(50_000_000.0),
452            upload_peak: Some(60_000_000.0),
453            latency_download: Some(25.0),
454            latency_upload: Some(30.0),
455            download_samples: None,
456            upload_samples: None,
457            ping_samples: None,
458            timestamp: "2026-04-04T12:00:00Z".to_string(),
459            client_ip: Some("192.168.1.1".to_string()),
460            client_location: None,
461            download_cv: None,
462            upload_cv: None,
463            download_ci_95: None,
464            upload_ci_95: None,
465            overall_grade: None,
466            download_grade: None,
467            upload_grade: None,
468            connection_rating: None,
469            phases: TestPhases {
470                ping: PhaseResult::completed(),
471                download: PhaseResult::completed(),
472                upload: PhaseResult::completed(),
473            },
474        };
475
476        let json = serde_json::to_string(&result).unwrap();
477        assert!(json.contains("\"status\":\"ok\""));
478        assert!(json.contains("\"ping\":15.234"));
479        assert!(json.contains("\"jitter\":1.2"));
480        assert!(json.contains("\"download\":150000000.0"));
481        assert!(json.contains("\"phases\""));
482    }
483
484    #[test]
485    fn test_csv_output_serialization() {
486        let csv = CsvOutput {
487            server_id: "1234".to_string(),
488            sponsor: "Test ISP".to_string(),
489            server_name: "Test Server".to_string(),
490            timestamp: "2026-04-04T12:00:00Z".to_string(),
491            distance: 100.5,
492            ping: 15.234,
493            jitter: 1.2,
494            packet_loss: 0.0,
495            download: 150_000_000.0,
496            download_peak: 180_000_000.0,
497            upload: 50_000_000.0,
498            upload_peak: 60_000_000.0,
499            ip_address: "192.168.1.1".to_string(),
500        };
501
502        let json = serde_json::to_string(&csv).unwrap();
503        assert!(json.contains("\"server_id\":\"1234\""));
504        assert!(json.contains("\"ping\":15.234"));
505        assert!(json.contains("\"jitter\":1.2"));
506    }
507
508    #[test]
509    fn test_server_clone() {
510        let server = Server {
511            id: "1234".to_string(),
512            url: "http://example.com".to_string(),
513            name: "Test".to_string(),
514            sponsor: "ISP".to_string(),
515            country: "US".to_string(),
516            lat: 40.0,
517            lon: -74.0,
518            distance: 0.0,
519        };
520
521        let cloned = server.clone();
522        assert_eq!(cloned.id, server.id);
523        assert_eq!(cloned.name, server.name);
524    }
525
526    #[test]
527    fn test_phase_result_skipped_serialization() {
528        let phase = PhaseResult::skipped("disabled by user");
529        let json = serde_json::to_string(&phase).unwrap();
530        assert!(json.contains("\"state\":\"skipped\""));
531        assert!(json.contains("\"reason\":\"disabled by user\""));
532    }
533
534    #[test]
535    fn test_uuid_v4_format() {
536        let id = uuid_v4();
537        // UUID v4 format: 8-4-4-4-12 hex characters
538        assert_eq!(id.len(), 36);
539        assert!(id.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
540        assert_eq!(&id[14..15], "4"); // Version 4 marker
541    }
542
543    #[test]
544    fn test_uuid_v4_unique() {
545        let id1 = uuid_v4();
546        let id2 = uuid_v4();
547        assert_ne!(id1, id2);
548    }
549
550    #[test]
551    fn test_uuid_v4_unique_concurrent() {
552        use std::collections::HashSet;
553        use std::sync::{Arc, Mutex};
554        use std::thread;
555
556        let ids: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
557        let threads: Vec<_> = (0..32)
558            .map(|_| {
559                let ids = Arc::clone(&ids);
560                thread::spawn(move || {
561                    let id = uuid_v4();
562                    ids.lock().unwrap().insert(id);
563                })
564            })
565            .collect();
566        for t in threads {
567            t.join().unwrap();
568        }
569        assert_eq!(
570            ids.lock().unwrap().len(),
571            32,
572            "concurrent uuid_v4 produced duplicates"
573        );
574    }
575
576    #[test]
577    fn test_client_location_serialization() {
578        let loc = ClientLocation {
579            lat: 40.7128,
580            lon: -74.0060,
581            city: Some("New York".to_string()),
582            country: Some("US".to_string()),
583        };
584        let json = serde_json::to_string(&loc).unwrap();
585        assert!(json.contains("\"lat\":40.7128"));
586        assert!(json.contains("\"lon\":-74.006"));
587        assert!(json.contains("\"city\":\"New York\""));
588    }
589
590    #[test]
591    fn test_client_location_minimal() {
592        let loc = ClientLocation {
593            lat: 0.0,
594            lon: 0.0,
595            city: None,
596            country: None,
597        };
598        let json = serde_json::to_string(&loc).unwrap();
599        assert!(!json.contains("city"));
600        assert!(!json.contains("country"));
601    }
602
603    #[test]
604    fn test_result_builder_full() {
605        let server = ServerInfo {
606            id: "1".to_string(),
607            name: "Server".to_string(),
608            sponsor: "ISP".to_string(),
609            country: "US".to_string(),
610            distance: 50.0,
611        };
612        let result = TestResultBuilder::new(server)
613            .ping(10.0)
614            .jitter(2.0)
615            .packet_loss(0.5)
616            .ping_samples(&[10.0, 12.0])
617            .download_stats(
618                100_000_000.0,
619                120_000_000.0,
620                &[90_000_000.0, 110_000_000.0],
621                Some(25.0),
622            )
623            .upload_stats(
624                50_000_000.0,
625                60_000_000.0,
626                &[45_000_000.0, 55_000_000.0],
627                Some(30.0),
628            )
629            .client_ip("1.2.3.4")
630            .client_location(ClientLocation {
631                lat: 40.0,
632                lon: -74.0,
633                city: Some("NYC".to_string()),
634                country: Some("US".to_string()),
635            })
636            .build(&DefaultStats);
637
638        assert_eq!(result.status, "ok");
639        assert_eq!(result.ping, Some(10.0));
640        assert_eq!(result.jitter, Some(2.0));
641        assert_eq!(result.packet_loss, Some(0.5));
642        assert_eq!(result.download, Some(100_000_000.0));
643        assert_eq!(result.download_peak, Some(120_000_000.0));
644        assert_eq!(result.upload, Some(50_000_000.0));
645        assert_eq!(result.client_ip, Some("1.2.3.4".to_string()));
646        assert!(result.download_samples.is_some());
647        assert!(result.ping_samples.is_some());
648        assert!(result.download_cv.is_some());
649        assert!(result.download_ci_95.is_some());
650        assert!(result.test_id.is_some());
651    }
652
653    #[test]
654    fn test_result_builder_minimal() {
655        let server = ServerInfo {
656            id: "0".to_string(),
657            name: "".to_string(),
658            sponsor: "".to_string(),
659            country: "".to_string(),
660            distance: 0.0,
661        };
662        let result = TestResultBuilder::new(server).build(&DefaultStats);
663
664        assert_eq!(result.status, "ok");
665        assert!(result.ping.is_none());
666        assert!(result.jitter.is_none());
667        assert!(result.download.is_none());
668        assert!(result.upload.is_none());
669        assert!(result.client_ip.is_none());
670        assert!(result.download_samples.is_none());
671        assert!(result.ping_samples.is_none());
672        assert!(result.download_cv.is_none());
673        assert!(result.upload_cv.is_none());
674    }
675
676    #[test]
677    fn test_stats_service_cv_known_data() {
678        let stats = DefaultStats::new();
679        let cv = stats.coefficient_of_variation(&[10.0, 12.0]);
680        assert!(cv.is_some());
681        let val = cv.unwrap();
682        assert!(val > 0.0);
683    }
684
685    #[test]
686    fn test_stats_service_cv_empty() {
687        let stats = DefaultStats::new();
688        assert!(stats.coefficient_of_variation(&[]).is_none());
689    }
690
691    #[test]
692    fn test_stats_service_cv_single() {
693        let stats = DefaultStats::new();
694        assert!(stats.coefficient_of_variation(&[42.0]).is_none());
695    }
696
697    #[test]
698    fn test_stats_service_cv_zero_mean() {
699        let stats = DefaultStats::new();
700        assert!(stats.coefficient_of_variation(&[0.0, 0.0, 0.0]).is_none());
701    }
702
703    #[test]
704    fn test_stats_service_ci95_known_data() {
705        let stats = DefaultStats::new();
706        let ci = stats.confidence_interval_95(&[100.0, 102.0, 98.0, 101.0, 99.0], 1.0);
707        assert!(ci.is_some());
708        let (lo, hi) = ci.unwrap();
709        assert!(lo < hi);
710        let mid = (lo + hi) / 2.0;
711        assert!((mid - 100.0).abs() < 2.0);
712    }
713
714    #[test]
715    fn test_stats_service_ci95_too_few_samples() {
716        let stats = DefaultStats::new();
717        assert!(stats.confidence_interval_95(&[50.0], 1.0).is_none());
718    }
719}
720
721/// Generate a simple UUID v4-like identifier using timestamp and random bytes.
722/// This is not a standards-compliant UUID but provides uniqueness for test tracking.
723fn uuid_v4() -> String {
724    use std::time::{SystemTime, UNIX_EPOCH};
725    let timestamp = SystemTime::now()
726        .duration_since(UNIX_EPOCH)
727        .unwrap_or_default()
728        .as_nanos();
729    let random: u64 = rand_simple();
730    // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
731    format!(
732        "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
733        (timestamp as u64) & 0xFFFFFFFF,
734        ((random >> 48) & 0xFFFF) as u16,
735        ((random >> 32) & 0xFFF) as u16,
736        ((random >> 16) & 0xFFFF) as u16,
737        random & 0xFFFFFFFFFFFF
738    )
739}
740
741/// Simple pseudo-random number generator based on xorshift.
742/// Not cryptographically secure, but sufficient for test ID generation.
743fn rand_simple() -> u64 {
744    use std::sync::atomic::{AtomicU64, Ordering};
745    static STATE: AtomicU64 = AtomicU64::new(0x123456789ABCDEF0);
746    loop {
747        let prev = STATE.load(Ordering::Relaxed);
748        let mut next = if prev == 0 { 0x123456789ABCDEF0 } else { prev };
749        next ^= next << 13;
750        next ^= next >> 7;
751        next ^= next << 17;
752        if STATE
753            .compare_exchange_weak(prev, next, Ordering::Relaxed, Ordering::Relaxed)
754            .is_ok()
755        {
756            return next;
757        }
758    }
759}