Skip to content

Commit 5da12cf

Browse files
authored
test: add comprehensive test coverage for Rust modules (#49)
- Add extensive unit tests for mathematics module (mean, median functions) - Add comprehensive test coverage for dispersion module (variance, stdev, percentile) - Include edge case tests for empty arrays, NaN, infinity, and large datasets - Add rb-sys-test-helpers dependency for Rust testing infrastructure - Integrate Rust testing into GitHub Actions CI workflow - Remove unused build.rs file and update Cargo.toml dependencies
1 parent afcb96f commit 5da12cf

File tree

6 files changed

+409
-9
lines changed

6 files changed

+409
-9
lines changed

.github/workflows/branch-protection.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
runs-on: ${{ matrix.os }}
2121
steps:
2222
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
23-
- uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990
23+
- uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26
2424
with:
2525
ruby-version: ${{ matrix.ruby }}
2626
- run: gem install bundler --no-document

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/ruby_native_statistics/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ edition = "2024"
77
crate-type = ["cdylib"]
88

99
[build-dependencies]
10-
rb-sys-env = "0.2.2"
10+
rb-sys-env = "0.2"
11+
12+
[dev-dependencies]
13+
rb-sys-env = { version = "0.1" }
14+
rb-sys-test-helpers = { version = "0.2" }
1115

1216
[dependencies]
1317
magnus = { version = "0.7.1" }

ext/ruby_native_statistics/build.rs

Lines changed: 0 additions & 6 deletions
This file was deleted.

ext/ruby_native_statistics/src/dispersion.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ fn calculate_percentile(array: &mut [f64], percentile: f64) -> Result<f64, Dispe
111111
Ok((h - h_floor as f64) * (array[h_floor] - array[h_floor - 1]) + array[h_floor - 1])
112112
}
113113
}
114+
114115
pub fn percentile(rb_self: RArray, percentile: f64) -> Result<f64, DispersionError> {
115116
let mut array = rb_self
116117
.to_vec::<f64>()
@@ -126,3 +127,193 @@ pub fn percentile(rb_self: RArray, percentile: f64) -> Result<f64, DispersionErr
126127

127128
calculate_percentile(&mut array, percentile)
128129
}
130+
131+
#[cfg(test)]
132+
mod tests {
133+
use super::*;
134+
135+
#[test]
136+
fn test_calculate_mean() {
137+
assert_eq!(calculate_mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
138+
assert_eq!(calculate_mean(&[10.0]), 10.0);
139+
assert_eq!(calculate_mean(&[1.5, 2.5]), 2.0);
140+
assert_eq!(calculate_mean(&[-1.0, 0.0, 1.0]), 0.0);
141+
assert_eq!(calculate_mean(&[2.5, 7.5, 15.0, 5.0]), 7.5);
142+
}
143+
144+
#[test]
145+
fn test_distance_from_mean() {
146+
// For [1, 2, 3], mean = 2, distances = [1, 0, 1], sum of squares = 2
147+
assert_eq!(distance_from_mean(&[1.0, 2.0, 3.0]), 2.0);
148+
149+
// For single element, distance should be 0
150+
assert_eq!(distance_from_mean(&[5.0]), 0.0);
151+
152+
// For [0, 0, 0], all distances are 0
153+
assert_eq!(distance_from_mean(&[0.0, 0.0, 0.0]), 0.0);
154+
155+
// For [-2, 0, 2], mean = 0, distances = [4, 0, 4], sum = 8
156+
assert_eq!(distance_from_mean(&[-2.0, 0.0, 2.0]), 8.0);
157+
}
158+
159+
#[test]
160+
fn test_calculate_variance_sample() {
161+
// Sample variance: divide by n-1
162+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
163+
let expected = 2.5; // distance_from_mean = 10, n-1 = 4, variance = 2.5
164+
assert_eq!(calculate_variance(&data, false), expected);
165+
166+
// Two elements
167+
let data = [1.0, 3.0];
168+
let expected = 2.0; // distance_from_mean = 2, n-1 = 1, variance = 2.0
169+
assert_eq!(calculate_variance(&data, false), expected);
170+
171+
// All same values
172+
let data = [5.0, 5.0, 5.0];
173+
assert_eq!(calculate_variance(&data, false), 0.0);
174+
}
175+
176+
#[test]
177+
fn test_calculate_variance_population() {
178+
// Population variance: divide by n
179+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
180+
let expected = 2.0; // distance_from_mean = 10, n = 5, variance = 2.0
181+
assert_eq!(calculate_variance(&data, true), expected);
182+
183+
// Single element
184+
let data = [10.0];
185+
assert_eq!(calculate_variance(&data, true), 0.0);
186+
187+
// Two elements
188+
let data = [1.0, 3.0];
189+
let expected = 1.0; // distance_from_mean = 2, n = 2, variance = 1.0
190+
assert_eq!(calculate_variance(&data, true), expected);
191+
}
192+
193+
#[test]
194+
fn test_calculate_stdev_sample() {
195+
// Sample standard deviation is sqrt of sample variance
196+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
197+
let expected = 2.5_f64.sqrt(); // sample variance = 2.5
198+
assert_eq!(calculate_stdev(&data, false), expected);
199+
200+
// All same values
201+
let data = [7.0, 7.0, 7.0, 7.0];
202+
assert_eq!(calculate_stdev(&data, false), 0.0);
203+
}
204+
205+
#[test]
206+
fn test_calculate_stdev_population() {
207+
// Population standard deviation is sqrt of population variance
208+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
209+
let expected = 2.0_f64.sqrt(); // population variance = 2.0
210+
assert_eq!(calculate_stdev(&data, true), expected);
211+
212+
// Single element
213+
let data = [42.0];
214+
assert_eq!(calculate_stdev(&data, true), 0.0);
215+
}
216+
217+
#[test]
218+
fn test_calculate_percentile_basic() {
219+
let mut data = [1.0, 2.0, 3.0, 4.0, 5.0];
220+
221+
// 0th percentile (minimum)
222+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
223+
224+
// 50th percentile (median)
225+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 3.0);
226+
227+
// 100th percentile (maximum)
228+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
229+
}
230+
231+
#[test]
232+
fn test_calculate_percentile_interpolation() {
233+
let mut data = [1.0, 2.0, 3.0, 4.0];
234+
235+
// 25th percentile: h = (4-1)*0.25 + 1 = 1.75
236+
// Interpolate between index 0 (value 1) and index 1 (value 2)
237+
// Result = 0.75 * (2-1) + 1 = 1.75
238+
assert_eq!(calculate_percentile(&mut data, 0.25).unwrap(), 1.75);
239+
240+
// 75th percentile: h = (4-1)*0.75 + 1 = 3.25
241+
// Interpolate between index 2 (value 3) and index 3 (value 4)
242+
// Result = 0.25 * (4-3) + 3 = 3.25
243+
assert_eq!(calculate_percentile(&mut data, 0.75).unwrap(), 3.25);
244+
}
245+
246+
#[test]
247+
fn test_calculate_percentile_single_element() {
248+
let mut data = [42.0];
249+
250+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 42.0);
251+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 42.0);
252+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 42.0);
253+
}
254+
255+
#[test]
256+
fn test_calculate_percentile_unsorted_data() {
257+
let mut data = [5.0, 1.0, 3.0, 2.0, 4.0];
258+
259+
// Should sort internally and return correct percentiles
260+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
261+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 3.0);
262+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
263+
}
264+
265+
#[test]
266+
fn test_calculate_percentile_with_duplicates() {
267+
let mut data = [1.0, 2.0, 2.0, 3.0, 4.0];
268+
269+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
270+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 4.0);
271+
272+
// 50th percentile should handle duplicates correctly
273+
let result = calculate_percentile(&mut data, 0.5).unwrap();
274+
assert!(result >= 2.0 && result <= 3.0);
275+
}
276+
277+
#[test]
278+
fn test_calculate_percentile_negative_numbers() {
279+
let mut data = [-5.0, -2.0, 0.0, 2.0, 5.0];
280+
281+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), -5.0);
282+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 0.0);
283+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
284+
}
285+
286+
#[test]
287+
fn test_variance_and_stdev_consistency() {
288+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
289+
290+
// Sample variance and stdev should be consistent
291+
let sample_var = calculate_variance(&data, false);
292+
let sample_stdev = calculate_stdev(&data, false);
293+
assert!((sample_stdev * sample_stdev - sample_var).abs() < 1e-14);
294+
295+
// Population variance and stdev should be consistent
296+
let pop_var = calculate_variance(&data, true);
297+
let pop_stdev = calculate_stdev(&data, true);
298+
assert!((pop_stdev * pop_stdev - pop_var).abs() < 1e-14);
299+
}
300+
301+
#[test]
302+
fn test_mathematical_properties() {
303+
let data = [2.0, 4.0, 6.0, 8.0, 10.0];
304+
let mean = calculate_mean(&data);
305+
306+
// Mean should be the average
307+
assert_eq!(mean, 6.0);
308+
309+
// Population variance should be less than sample variance (when n > 1)
310+
let pop_var = calculate_variance(&data, true);
311+
let sample_var = calculate_variance(&data, false);
312+
assert!(pop_var < sample_var);
313+
314+
// Population stdev should be less than sample stdev
315+
let pop_stdev = calculate_stdev(&data, true);
316+
let sample_stdev = calculate_stdev(&data, false);
317+
assert!(pop_stdev < sample_stdev);
318+
}
319+
}

0 commit comments

Comments
 (0)