Skip to content

Commit fcb467f

Browse files
Merge pull request #208 from code-inflation/feature/add-unit-tests
Add unit tests for core parsing and calculation logic
2 parents 81d0a00 + 9636468 commit fcb467f

File tree

4 files changed

+443
-1
lines changed

4 files changed

+443
-1
lines changed

src/boxplot.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,85 @@ pub(crate) fn render_plot(minima: f64, q1: f64, median: f64, q3: f64, maxima: f6
4646

4747
plot
4848
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
54+
#[test]
55+
fn test_generate_axis_labels() {
56+
let labels = generate_axis_labels(0.0, 100.0);
57+
assert!(labels.starts_with("0.00"));
58+
assert!(labels.ends_with("100.00"));
59+
assert!(labels.contains("50.00"));
60+
assert_eq!(labels.len(), PLOT_WIDTH);
61+
}
62+
63+
#[test]
64+
fn test_generate_axis_labels_negative() {
65+
let labels = generate_axis_labels(-50.0, 50.0);
66+
assert!(labels.starts_with("-50.00"));
67+
assert!(labels.ends_with("50.00"));
68+
assert!(labels.contains("0.00"));
69+
}
70+
71+
#[test]
72+
fn test_render_plot_basic() {
73+
let plot = render_plot(0.0, 25.0, 50.0, 75.0, 100.0);
74+
75+
// Should contain boxplot characters
76+
assert!(plot.contains('|'));
77+
assert!(plot.contains('-'));
78+
assert!(plot.contains('='));
79+
assert!(plot.contains(':'));
80+
81+
// Should contain axis labels
82+
assert!(plot.contains("0.00"));
83+
assert!(plot.contains("100.00"));
84+
assert!(plot.contains("50.00"));
85+
86+
// Should have newline separating plot from labels
87+
assert!(plot.contains('\n'));
88+
}
89+
90+
#[test]
91+
fn test_render_plot_same_values() {
92+
let plot = render_plot(50.0, 50.0, 50.0, 50.0, 50.0);
93+
94+
// When all values are the same, should still render
95+
assert!(plot.contains('|'));
96+
assert!(plot.contains(':'));
97+
assert!(plot.contains("50.00"));
98+
}
99+
100+
#[test]
101+
fn test_render_plot_structure() {
102+
let plot = render_plot(10.0, 30.0, 50.0, 70.0, 90.0);
103+
let lines: Vec<&str> = plot.split('\n').collect();
104+
105+
// Should have exactly 2 lines: plot and axis labels
106+
assert_eq!(lines.len(), 2);
107+
108+
// First line should be the boxplot
109+
assert!(lines[0].starts_with('|'));
110+
assert!(lines[0].ends_with('|'));
111+
112+
// Second line should be the axis labels
113+
assert!(lines[1].contains("10.00"));
114+
assert!(lines[1].contains("90.00"));
115+
}
116+
117+
#[test]
118+
fn test_render_plot_quartile_ordering() {
119+
let plot = render_plot(0.0, 20.0, 50.0, 80.0, 100.0);
120+
121+
// Find the positions of key characters
122+
let colon_pos = plot.find(':').unwrap();
123+
let first_pipe = plot.find('|').unwrap();
124+
let last_pipe = plot.rfind('|').unwrap();
125+
126+
// Colon (median) should be between the pipes
127+
assert!(colon_pos > first_pipe);
128+
assert!(colon_pos < last_pipe);
129+
}
130+
}

src/lib.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,129 @@ fn parse_payload_size(input_string: &str) -> Result<PayloadSize, String> {
106106
fn parse_output_format(input_string: &str) -> Result<OutputFormat, String> {
107107
OutputFormat::from(input_string.to_string())
108108
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
114+
#[test]
115+
fn test_output_format_from_valid_inputs() {
116+
assert_eq!(OutputFormat::from("csv".to_string()), Ok(OutputFormat::Csv));
117+
assert_eq!(OutputFormat::from("CSV".to_string()), Ok(OutputFormat::Csv));
118+
assert_eq!(
119+
OutputFormat::from("json".to_string()),
120+
Ok(OutputFormat::Json)
121+
);
122+
assert_eq!(
123+
OutputFormat::from("JSON".to_string()),
124+
Ok(OutputFormat::Json)
125+
);
126+
assert_eq!(
127+
OutputFormat::from("json-pretty".to_string()),
128+
Ok(OutputFormat::JsonPretty)
129+
);
130+
assert_eq!(
131+
OutputFormat::from("json_pretty".to_string()),
132+
Ok(OutputFormat::JsonPretty)
133+
);
134+
assert_eq!(
135+
OutputFormat::from("JSON-PRETTY".to_string()),
136+
Ok(OutputFormat::JsonPretty)
137+
);
138+
assert_eq!(
139+
OutputFormat::from("stdout".to_string()),
140+
Ok(OutputFormat::StdOut)
141+
);
142+
assert_eq!(
143+
OutputFormat::from("STDOUT".to_string()),
144+
Ok(OutputFormat::StdOut)
145+
);
146+
}
147+
148+
#[test]
149+
fn test_output_format_from_invalid_inputs() {
150+
assert!(OutputFormat::from("invalid".to_string()).is_err());
151+
assert!(OutputFormat::from("xml".to_string()).is_err());
152+
assert!(OutputFormat::from("".to_string()).is_err());
153+
assert!(OutputFormat::from("json_invalid".to_string()).is_err());
154+
155+
let error_msg = OutputFormat::from("invalid".to_string()).unwrap_err();
156+
assert_eq!(
157+
error_msg,
158+
"Value needs to be one of csv, json or json-pretty"
159+
);
160+
}
161+
162+
#[test]
163+
fn test_output_format_display() {
164+
assert_eq!(format!("{}", OutputFormat::Csv), "Csv");
165+
assert_eq!(format!("{}", OutputFormat::Json), "Json");
166+
assert_eq!(format!("{}", OutputFormat::JsonPretty), "JsonPretty");
167+
assert_eq!(format!("{}", OutputFormat::StdOut), "StdOut");
168+
assert_eq!(format!("{}", OutputFormat::None), "None");
169+
}
170+
171+
#[test]
172+
fn test_cli_options_should_download() {
173+
let mut options = SpeedTestCLIOptions {
174+
nr_tests: 10,
175+
nr_latency_tests: 25,
176+
max_payload_size: speedtest::PayloadSize::M25,
177+
output_format: OutputFormat::StdOut,
178+
verbose: false,
179+
ipv4: None,
180+
ipv6: None,
181+
disable_dynamic_max_payload_size: false,
182+
download_only: false,
183+
upload_only: false,
184+
completion: None,
185+
};
186+
187+
// Default: both download and upload
188+
assert!(options.should_download());
189+
assert!(options.should_upload());
190+
191+
// Download only
192+
options.download_only = true;
193+
assert!(options.should_download());
194+
assert!(!options.should_upload());
195+
196+
// Upload only
197+
options.download_only = false;
198+
options.upload_only = true;
199+
assert!(!options.should_download());
200+
assert!(options.should_upload());
201+
}
202+
203+
#[test]
204+
fn test_cli_options_should_upload() {
205+
let mut options = SpeedTestCLIOptions {
206+
nr_tests: 10,
207+
nr_latency_tests: 25,
208+
max_payload_size: speedtest::PayloadSize::M25,
209+
output_format: OutputFormat::StdOut,
210+
verbose: false,
211+
ipv4: None,
212+
ipv6: None,
213+
disable_dynamic_max_payload_size: false,
214+
download_only: false,
215+
upload_only: false,
216+
completion: None,
217+
};
218+
219+
// Default: both download and upload
220+
assert!(options.should_upload());
221+
assert!(options.should_download());
222+
223+
// Upload only
224+
options.upload_only = true;
225+
assert!(options.should_upload());
226+
assert!(!options.should_download());
227+
228+
// Download only
229+
options.upload_only = false;
230+
options.download_only = true;
231+
assert!(!options.should_upload());
232+
assert!(options.should_download());
233+
}
234+
}

src/measurements.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,90 @@ pub(crate) fn format_bytes(bytes: usize) -> String {
201201
_ => format!("{bytes} bytes"),
202202
}
203203
}
204+
205+
#[cfg(test)]
206+
mod tests {
207+
use super::*;
208+
209+
#[test]
210+
fn test_format_bytes() {
211+
assert_eq!(format_bytes(500), "500 bytes");
212+
assert_eq!(format_bytes(1_000), "1KB");
213+
assert_eq!(format_bytes(100_000), "100KB");
214+
assert_eq!(format_bytes(999_999), "999KB");
215+
assert_eq!(format_bytes(1_000_000), "1MB");
216+
assert_eq!(format_bytes(25_000_000), "25MB");
217+
assert_eq!(format_bytes(100_000_000), "100MB");
218+
assert_eq!(format_bytes(999_999_999), "999MB");
219+
assert_eq!(format_bytes(1_000_000_000), "1000000000 bytes");
220+
}
221+
222+
#[test]
223+
fn test_measurement_display() {
224+
let measurement = Measurement {
225+
test_type: TestType::Download,
226+
payload_size: 1_000_000,
227+
mbit: 50.5,
228+
};
229+
230+
let display_str = format!("{}", measurement);
231+
assert!(display_str.contains("Download"));
232+
assert!(display_str.contains("1MB"));
233+
assert!(display_str.contains("50.5"));
234+
}
235+
236+
#[test]
237+
fn test_calc_stats_empty() {
238+
assert_eq!(calc_stats(vec![]), None);
239+
}
240+
241+
#[test]
242+
fn test_calc_stats_single_value() {
243+
let result = calc_stats(vec![10.0]).unwrap();
244+
assert_eq!(result, (10.0, 10.0, 10.0, 10.0, 10.0, 10.0));
245+
}
246+
247+
#[test]
248+
fn test_calc_stats_two_values() {
249+
let result = calc_stats(vec![10.0, 20.0]).unwrap();
250+
assert_eq!(result.0, 10.0); // min
251+
assert_eq!(result.4, 20.0); // max
252+
assert_eq!(result.2, 15.0); // median
253+
assert_eq!(result.5, 15.0); // avg
254+
}
255+
256+
#[test]
257+
fn test_calc_stats_multiple_values() {
258+
let result = calc_stats(vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
259+
assert_eq!(result.0, 1.0); // min
260+
assert_eq!(result.4, 5.0); // max
261+
assert_eq!(result.2, 3.0); // median
262+
assert_eq!(result.5, 3.0); // avg
263+
}
264+
265+
#[test]
266+
fn test_calc_stats_unsorted() {
267+
let result = calc_stats(vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap();
268+
assert_eq!(result.0, 1.0); // min
269+
assert_eq!(result.4, 5.0); // max
270+
assert_eq!(result.2, 3.0); // median
271+
assert_eq!(result.5, 3.0); // avg
272+
}
273+
274+
#[test]
275+
fn test_median_odd_length() {
276+
assert_eq!(median(&[1.0, 2.0, 3.0]), 2.0);
277+
assert_eq!(median(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
278+
}
279+
280+
#[test]
281+
fn test_median_even_length() {
282+
assert_eq!(median(&[1.0, 2.0]), 1.5);
283+
assert_eq!(median(&[1.0, 2.0, 3.0, 4.0]), 2.5);
284+
}
285+
286+
#[test]
287+
fn test_median_single_value() {
288+
assert_eq!(median(&[5.0]), 5.0);
289+
}
290+
}

0 commit comments

Comments
 (0)