From 3a88ef160c91d6875137e756b050b89dfb545e82 Mon Sep 17 00:00:00 2001 From: Pierre Tenedero Date: Thu, 16 Apr 2026 19:03:22 +0800 Subject: [PATCH] Add test cases for har1 and face-detection --- services/ws-modules/face-detection/src/lib.rs | 5 +- .../face-detection/src/test_face_detection.rs | 104 ++++++++++++++++++ .../ws-modules/face-detection/tests/web.rs | 34 ++++++ services/ws-modules/har1/src/lib.rs | 5 +- services/ws-modules/har1/src/test_har1.rs | 72 ++++++++++++ services/ws-modules/har1/tests/web.rs | 18 +++ 6 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 services/ws-modules/face-detection/src/test_face_detection.rs create mode 100644 services/ws-modules/face-detection/tests/web.rs create mode 100644 services/ws-modules/har1/src/test_har1.rs create mode 100644 services/ws-modules/har1/tests/web.rs diff --git a/services/ws-modules/face-detection/src/lib.rs b/services/ws-modules/face-detection/src/lib.rs index deb5e95..0516faa 100644 --- a/services/ws-modules/face-detection/src/lib.rs +++ b/services/ws-modules/face-detection/src/lib.rs @@ -61,7 +61,7 @@ thread_local! { #[wasm_bindgen(start)] pub fn init() { - tracing_wasm::set_as_global_default(); + let _ = tracing_wasm::try_set_as_global_default(); info!("face detection workflow module initialized"); } @@ -859,3 +859,6 @@ fn canvas_2d_context(canvas: &HtmlCanvasElement) -> Result Result<(), JsValue> { Reflect::set(target, &JsValue::from_str("hidden"), &JsValue::from_bool(hidden)).map(|_| ()) } + +#[cfg(test)] +mod test_face_detection; diff --git a/services/ws-modules/face-detection/src/test_face_detection.rs b/services/ws-modules/face-detection/src/test_face_detection.rs new file mode 100644 index 0000000..e0f33e0 --- /dev/null +++ b/services/ws-modules/face-detection/src/test_face_detection.rs @@ -0,0 +1,104 @@ +use super::*; + +#[test] +fn clamp_bounds_values() { + assert_eq!(clamp(5.0, 0.0, 10.0), 5.0); + assert_eq!(clamp(0.0, 0.0, 10.0), 0.0); + assert_eq!(clamp(10.0, 0.0, 10.0), 10.0); + assert_eq!(clamp(-1.0, 0.0, 10.0), 0.0); + assert_eq!(clamp(11.0, 0.0, 10.0), 10.0); +} + +fn detection(score: f64, box_coords: [f64; 4]) -> Detection { + Detection { + label: "face".into(), + class_index: 0, + score, + box_coords, + } +} + +#[test] +fn iou_uses_inclusive_pixel_coordinates() { + let left = detection(1.0, [0.0, 0.0, 10.0, 10.0]); + let right = detection(1.0, [5.0, 5.0, 15.0, 15.0]); + + let iou = compute_iou(&left, &right); + + assert!((iou - (36.0 / 206.0)).abs() < 1e-6); +} + +#[test] +fn iou_handles_identical_and_non_overlapping_boxes() { + let left = detection(1.0, [0.0, 0.0, 10.0, 10.0]); + let identical = detection(1.0, [0.0, 0.0, 10.0, 10.0]); + let separate = detection(1.0, [20.0, 20.0, 30.0, 30.0]); + let corner_touching = detection(1.0, [10.0, 10.0, 20.0, 20.0]); + + assert!((compute_iou(&left, &identical) - 1.0).abs() < 1e-6); + assert_eq!(compute_iou(&left, &separate), 0.0); + assert!((compute_iou(&left, &corner_touching) - (1.0 / 241.0)).abs() < 1e-6); +} + +#[test] +fn nms_keeps_highest_scored_overlapping_box_and_distant_boxes() { + let detections = vec![ + detection(0.7, [50.0, 50.0, 60.0, 60.0]), + detection(0.8, [1.0, 1.0, 11.0, 11.0]), + detection(0.9, [0.0, 0.0, 10.0, 10.0]), + ]; + + let filtered = apply_nms(detections, 0.5); + + assert_eq!(filtered.len(), 2); + assert_eq!(filtered[0].score, 0.9); + assert_eq!(filtered[1].score, 0.7); +} + +#[test] +fn nms_keeps_boxes_when_iou_equals_threshold() { + let filtered = apply_nms( + vec![ + detection(0.9, [0.0, 0.0, 10.0, 10.0]), + detection(0.8, [0.0, 0.0, 10.0, 10.0]), + ], + 1.0, + ); + + assert_eq!(filtered.len(), 2); +} + +#[test] +fn softmax_handles_empty_equal_and_large_values() { + assert!(softmax(&[]).is_empty()); + + let equal = softmax(&[4.0, 4.0, 4.0, 4.0]); + assert!(equal.iter().all(|value| (*value - 0.25).abs() < 1e-6)); + + let large = softmax(&[1000.0, 1001.0]); + assert_eq!(large.len(), 2); + assert!(large.iter().all(|value| value.is_finite())); + assert!((large.iter().sum::() - 1.0).abs() < 1e-6); + assert!(large[1] > large[0]); +} + +#[test] +fn retinaface_prior_count_matches_model_input_shape() { + let priors = build_retinaface_priors(FACE_INPUT_HEIGHT_F64, FACE_INPUT_WIDTH_F64); + + assert_eq!(priors.len(), 15_960); + assert!((priors[0][0] - (4.0 / FACE_INPUT_WIDTH_F64)).abs() < 1e-6); + assert!((priors[0][1] - (4.0 / FACE_INPUT_HEIGHT_F64)).abs() < 1e-6); + assert!((priors[0][2] - (16.0 / FACE_INPUT_WIDTH_F64)).abs() < 1e-6); + assert!((priors[0][3] - (16.0 / FACE_INPUT_HEIGHT_F64)).abs() < 1e-6); +} + +#[test] +fn retinaface_zero_offsets_decode_to_prior_box() { + let decoded = decode_retinaface_box([0.0, 0.0, 0.0, 0.0], [0.5, 0.5, 0.25, 0.5]); + + assert!((decoded[0] - 0.375).abs() < 1e-6); + assert!((decoded[1] - 0.25).abs() < 1e-6); + assert!((decoded[2] - 0.625).abs() < 1e-6); + assert!((decoded[3] - 0.75).abs() < 1e-6); +} diff --git a/services/ws-modules/face-detection/tests/web.rs b/services/ws-modules/face-detection/tests/web.rs new file mode 100644 index 0000000..e8c1296 --- /dev/null +++ b/services/ws-modules/face-detection/tests/web.rs @@ -0,0 +1,34 @@ +#![cfg(target_arch = "wasm32")] +use et_ws_face_detection::{init, is_running, run, stop}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn init_can_be_called_more_than_once() { + init(); + init(); +} + +#[wasm_bindgen_test] +fn stop_is_idempotent_when_runtime_has_not_started() { + assert!(!is_running()); + stop().expect("stop should succeed when face detection is not running"); + assert!(!is_running()); +} + +#[wasm_bindgen_test] +async fn run_failure_leaves_runtime_stopped() { + let result = run().await; + + match result { + Ok(()) => { + assert!(is_running()); + stop().expect("stop should succeed after a successful run"); + assert!(!is_running()); + } + Err(_) => { + assert!(!is_running()); + } + } +} diff --git a/services/ws-modules/har1/src/lib.rs b/services/ws-modules/har1/src/lib.rs index 06cbfb9..36d2f64 100644 --- a/services/ws-modules/har1/src/lib.rs +++ b/services/ws-modules/har1/src/lib.rs @@ -18,7 +18,7 @@ const HAR_CLASS_LABELS: [&str; 6] = ["class_0", "class_1", "class_2", "class_3", #[wasm_bindgen(start)] pub fn init() { - tracing_wasm::set_as_global_default(); + let _ = tracing_wasm::try_set_as_global_default(); info!("har1 workflow module initialized"); } @@ -528,3 +528,6 @@ async fn sleep_ms(duration_ms: i32) -> Result<(), JsValue> { }); JsFuture::from(promise).await.map(|_| ()) } + +#[cfg(test)] +mod test_har1; diff --git a/services/ws-modules/har1/src/test_har1.rs b/services/ws-modules/har1/src/test_har1.rs new file mode 100644 index 0000000..b5f1e76 --- /dev/null +++ b/services/ws-modules/har1/src/test_har1.rs @@ -0,0 +1,72 @@ +use super::*; + +#[test] +fn softmax_distribution_preserves_order_and_normalizes() { + let logits = vec![2.0, 1.0, 0.1]; + let probs = softmax(&logits); + + assert_eq!(probs.len(), 3); + let sum: f64 = probs.iter().sum(); + assert!((sum - 1.0).abs() < 1e-6); + assert!(probs[0] > probs[1]); + assert!(probs[1] > probs[2]); +} + +#[test] +fn softmax_handles_empty_equal_and_large_values() { + assert!(softmax(&[]).is_empty()); + + let equal = softmax(&[7.0, 7.0, 7.0]); + assert!(equal.iter().all(|value| (*value - (1.0 / 3.0)).abs() < 1e-6)); + + let large = softmax(&[1000.0, 1001.0, 999.0]); + assert_eq!(large.len(), 3); + assert!(large.iter().all(|value| value.is_finite())); + assert!((large.iter().sum::() - 1.0).abs() < 1e-6); + assert!(large[1] > large[0]); + assert!(large[0] > large[2]); +} + +#[test] +fn gravity_and_rotation_conversions_handle_positive_negative_and_zero() { + assert_eq!(to_g(0.0), 0.0); + assert!((to_g(9.80665) - 1.0).abs() < 1e-6); + assert!((to_g(-9.80665) + 1.0).abs() < 1e-6); + + assert_eq!(degrees_to_radians(0.0), 0.0); + assert!((degrees_to_radians(180.0) - std::f64::consts::PI).abs() < 1e-6); + assert!((degrees_to_radians(-90.0) + std::f64::consts::FRAC_PI_2).abs() < 1e-6); +} + +#[test] +fn flatten_samples_preserves_sample_order_and_feature_order() { + let mut samples = VecDeque::new(); + let mut first = [0.0; HAR_FEATURE_COUNT]; + let mut second = [0.0; HAR_FEATURE_COUNT]; + for index in 0..HAR_FEATURE_COUNT { + first[index] = index as f32; + second[index] = (10 + index) as f32; + } + samples.push_back(first); + samples.push_back(second); + + let flattened = flatten_samples(&samples); + + assert_eq!(flattened.len(), 2 * HAR_FEATURE_COUNT); + assert_eq!(&flattened[..HAR_FEATURE_COUNT], &first); + assert_eq!(&flattened[HAR_FEATURE_COUNT..], &second); +} + +#[test] +fn flatten_samples_handles_empty_buffer() { + let samples = VecDeque::new(); + + assert!(flatten_samples(&samples).is_empty()); +} + +#[test] +fn format_number_rejects_non_finite_values() { + assert_eq!(format_number(12.3456, 2), "12.35"); + assert_eq!(format_number(f64::NAN, 2), "n/a"); + assert_eq!(format_number(f64::INFINITY, 2), "n/a"); +} diff --git a/services/ws-modules/har1/tests/web.rs b/services/ws-modules/har1/tests/web.rs new file mode 100644 index 0000000..2703d8f --- /dev/null +++ b/services/ws-modules/har1/tests/web.rs @@ -0,0 +1,18 @@ +#![cfg(target_arch = "wasm32")] +use et_ws_har1::{init, run}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn init_can_be_called_more_than_once() { + init(); + init(); +} + +#[wasm_bindgen_test] +async fn run_reports_environment_error_in_headless_browser() { + let result = run().await; + + assert!(result.is_err()); +}