Skip to main content

netspeed_cli/domain/
reporting.rs

1//! Result assembly and grading.
2//!
3//! This module handles building test results from individual test runs
4//! and computing grades/ratings.
5
6use crate::grades;
7use crate::profiles::UserProfile;
8use crate::task_runner::TestRunResult;
9use crate::types::{ClientLocation, PhaseResult, ServerInfo, TestPhases, TestResult};
10
11/// Alias for a full test result that can be persisted as a report.
12pub type Report = TestResult;
13
14/// Builder for constructing [`TestResult`] from test runs.
15pub struct TestResultBuilder {
16    server: ServerInfo,
17    ping: Option<f64>,
18    jitter: Option<f64>,
19    packet_loss: Option<f64>,
20    ping_samples: Vec<f64>,
21    download_result: Option<TestRunResult>,
22    upload_result: Option<TestRunResult>,
23    client_ip: Option<String>,
24    client_location: Option<ClientLocation>,
25}
26
27impl TestResultBuilder {
28    pub fn new(server: ServerInfo) -> Self {
29        Self {
30            server,
31            ping: None,
32            jitter: None,
33            packet_loss: None,
34            ping_samples: Vec::new(),
35            download_result: None,
36            upload_result: None,
37            client_ip: None,
38            client_location: None,
39        }
40    }
41
42    pub fn with_ping(
43        mut self,
44        latency: f64,
45        jitter: f64,
46        packet_loss: f64,
47        samples: Vec<f64>,
48    ) -> Self {
49        self.ping = Some(latency);
50        self.jitter = Some(jitter);
51        self.packet_loss = Some(packet_loss);
52        self.ping_samples = samples;
53        self
54    }
55
56    pub fn with_download(mut self, result: TestRunResult) -> Self {
57        self.download_result = Some(result);
58        self
59    }
60
61    pub fn with_upload(mut self, result: TestRunResult) -> Self {
62        self.upload_result = Some(result);
63        self
64    }
65
66    pub fn with_client_ip(mut self, ip: String) -> Self {
67        self.client_ip = Some(ip);
68        self
69    }
70
71    pub fn with_client_location(mut self, location: ClientLocation) -> Self {
72        self.client_location = Some(location);
73        self
74    }
75
76    pub fn build(self) -> TestResult {
77        let default_dl = TestRunResult::default();
78        let default_ul = TestRunResult::default();
79        let dl = self.download_result.as_ref().unwrap_or(&default_dl);
80        let ul = self.upload_result.as_ref().unwrap_or(&default_ul);
81
82        TestResult::from_test_runs(
83            self.server,
84            self.ping,
85            self.jitter,
86            self.packet_loss,
87            &self.ping_samples,
88            dl,
89            ul,
90            self.client_ip,
91            self.client_location,
92        )
93    }
94}
95
96impl Default for TestResult {
97    fn default() -> Self {
98        Self {
99            status: String::new(),
100            version: String::new(),
101            test_id: None,
102            server: ServerInfo {
103                id: String::new(),
104                name: String::new(),
105                sponsor: String::new(),
106                country: String::new(),
107                distance: 0.0,
108            },
109            ping: None,
110            jitter: None,
111            packet_loss: None,
112            download: None,
113            download_peak: None,
114            download_cv: None,
115            upload: None,
116            upload_peak: None,
117            upload_cv: None,
118            download_ci_95: None,
119            upload_ci_95: None,
120            latency_download: None,
121            latency_upload: None,
122            download_samples: None,
123            upload_samples: None,
124            ping_samples: None,
125            timestamp: String::new(),
126            client_ip: None,
127            client_location: None,
128            overall_grade: None,
129            download_grade: None,
130            upload_grade: None,
131            connection_rating: None,
132            phases: TestPhases {
133                ping: PhaseResult::completed(),
134                download: PhaseResult::completed(),
135                upload: PhaseResult::completed(),
136            },
137        }
138    }
139}
140
141pub fn compute_overall_grade(
142    ping: Option<f64>,
143    jitter: Option<f64>,
144    download: Option<f64>,
145    upload: Option<f64>,
146    profile: UserProfile,
147) -> String {
148    grades::grade_overall(ping, jitter, download, upload, profile)
149        .as_str()
150        .to_string()
151}
152
153pub fn compute_download_grade(download_mbps: f64, profile: UserProfile) -> String {
154    grades::grade_download(download_mbps, profile)
155        .as_str()
156        .to_string()
157}
158
159pub fn compute_upload_grade(upload_mbps: f64, profile: UserProfile) -> String {
160    grades::grade_upload(upload_mbps, profile)
161        .as_str()
162        .to_string()
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_result_builder_default() {
171        let result = TestResult::default();
172        assert!(result.server.id.is_empty());
173    }
174
175    #[test]
176    fn test_result_builder_basic() {
177        let server = ServerInfo {
178            id: "123".to_string(),
179            name: "Test Server".to_string(),
180            sponsor: "Test ISP".to_string(),
181            country: "US".to_string(),
182            distance: 100.0,
183        };
184
185        let builder = TestResultBuilder::new(server);
186        let result = builder.build();
187
188        assert_eq!(result.server.id, "123");
189        assert_eq!(result.status, "ok");
190    }
191}