From 5ea1b5d882e749096fabf1d60bfb89a037c903ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isa=C3=AFe?= Date: Thu, 9 May 2024 10:26:16 +0200 Subject: [PATCH] test: add main structure coverage (#6) * add test file skeleton * write the first basic tests * complete missing parameters test * derive additional traits for params * add inconsistent parameters testing * add all possibility for A_x_x_Rectangle * reorganize tests * replace ClosureExplicit by a macro * replace other tests with macro & rewrite ValuesUniform * add trapezoid tests for integral A rectangles are more precise than trapezoid? * update lint exceptions for tests --- src/parameters.rs | 2 + src/structure/definitions.rs | 2 +- src/structure/tests.rs | 298 ++++++++++++++++++++++++++++++++++- 3 files changed, 299 insertions(+), 3 deletions(-) diff --git a/src/parameters.rs b/src/parameters.rs index cea3558..396273c 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -7,6 +7,7 @@ /// Currently, the supported integration domain can only be one-dimensionnal, described using /// `f64` values (i.e. the type used for further computations). In the future, adding support /// for higher dimension & generic value type can be considered. +#[derive(Debug, Clone)] pub enum DomainDescriptor<'a> { /// List of values taken by the variable on which we integrate. Explicit(&'a [f64]), @@ -35,6 +36,7 @@ pub enum FunctionDescriptor { } /// Numerical integration method enum +#[derive(Debug, Clone, Copy)] pub enum ComputeMethod { /// Rectangle method -- [reference](https://en.wikipedia.org/wiki/Riemann_sum) Rectangle, diff --git a/src/structure/definitions.rs b/src/structure/definitions.rs index 64be02a..b83ab7f 100644 --- a/src/structure/definitions.rs +++ b/src/structure/definitions.rs @@ -7,7 +7,7 @@ use crate::{ComputeMethod, DomainDescriptor, FunctionDescriptor}; // ------ CONTENT /// Integral error -#[derive(Debug)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum IntegraalError { /// One or more parameters are missing. MissingParameters(&'static str), diff --git a/src/structure/tests.rs b/src/structure/tests.rs index ae5035c..bb41ee6 100644 --- a/src/structure/tests.rs +++ b/src/structure/tests.rs @@ -1,4 +1,298 @@ +//! main structure tests + +// ------ IMPORTS + +use crate::{ComputeMethod, DomainDescriptor, FunctionDescriptor, Integraal, IntegraalError}; + +// ------ CONTENT + +// test utils + +const RECTANGLE_TOLERANCE: f64 = 1e-5; +const TRAPEZOID_TOLERANCE: f64 = 1e-5; +const STEP: f64 = 0.001; + +macro_rules! almost_equal { + ($v1: expr, $v2: expr, $tol: ident) => { + ($v1 - $v2).abs() < $tol + }; +} + +macro_rules! generate_sample_descriptors { + ($f: ident, $d: ident, $c: ident) => { + let $f = FunctionDescriptor::Closure(Box::new(|x| x)); + let $d = DomainDescriptor::Explicit(&[]); + let $c = ComputeMethod::Rectangle; + }; +} + +macro_rules! generate_missing { + ($a: ident, $b: ident) => { + let mut integral = Integraal::default(); + integral.$a($a).$b($b); + assert_eq!( + integral.compute(), + Err(IntegraalError::MissingParameters( + "one or more parameter is missing" + )) + ); + }; +} + +// incorrect usages + +#[allow(unused_variables)] +#[test] +fn missing_parameters() { + // missing function descriptor + generate_sample_descriptors!(function, domain, method); + generate_missing!(domain, method); + + // missing domain descriptor + generate_sample_descriptors!(function, domain, method); + generate_missing!(function, method); + + // missing compute method + generate_sample_descriptors!(function, domain, method); + generate_missing!(function, domain); + + // missing all but one + let mut integral = Integraal::default(); + integral.method(method); + assert_eq!( + integral.compute(), + Err(IntegraalError::MissingParameters( + "one or more parameter is missing" + )) + ); +} + +#[test] +fn inconsistent_parameters() { + let method = ComputeMethod::Rectangle; + let function = FunctionDescriptor::Values(vec![1., 1., 1., 1., 1., 1.]); + let domain = vec![0.0, 0.1, 0.2, 0.3, 0.4]; // missing the last x value + let domain = DomainDescriptor::Explicit(&domain); + + let mut integral = Integraal::default(); + integral.method(method).function(function).domain(domain); + assert_eq!( + integral.compute(), + Err(IntegraalError::InconsistentParameters( + "provided function and domain value slices have different lengthes" + )) + ); + + // this is equivalent to the first domain + let domain = DomainDescriptor::Uniform { + start: 0., + step: 0.1, + n_step: 5, + }; + let function = FunctionDescriptor::Values(vec![1., 1., 1., 1., 1., 1.]); + + let mut integral = Integraal::default(); + integral.method(method).function(function).domain(domain); + assert_eq!( + integral.compute(), + Err(IntegraalError::InconsistentParameters( + "provided function and domain value slices have different lengthes" + )) + ); +} + +// correct usages + +// test are groups per module according to the integral & the computation method +// test names follow this pattern: +// + +// integral A +// y = f(x) = sin(x) from 0 to PI + +macro_rules! generate_test { + ($name: ident, $dm: stmt, $fnd: expr, $dmd: expr, $met: expr, $tol: ident) => { + #[allow(non_snake_case)] + #[test] + fn $name() { + $dm + + let functiond = $fnd; + let domaind = $dmd; + let computem = $met; + let mut integraal = Integraal::default(); + let res = integraal + .function(functiond) + .domain(domaind) + .method(computem) + .compute(); + assert!(res.is_ok()); + assert!( + almost_equal!(res.unwrap(), 2.0, $tol), + "left: {} \nright: 2.0", + res.unwrap() + ); + } + }; + ($name: ident, $fnd: expr, $dmd: expr, $met: expr, $tol: ident) => { + #[allow(non_snake_case)] + #[test] + fn $name() { + let functiond = $fnd; + let domaind = $dmd; + let computem = $met; + let mut integraal = Integraal::default(); + let res = integraal + .function(functiond) + .domain(domaind) + .method(computem) + .compute(); + assert!(res.is_ok()); + assert!( + almost_equal!(res.unwrap(), 2.0, $tol), + "left: {} \nright: 2.0", + res.unwrap() + ); + } + }; +} + +#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] +mod a_rectangle { + use super::*; + + generate_test!( + ClosureExplicit, + let domain: Vec = (0..(std::f64::consts::PI * 1000.) as usize) + .map(|step_id| step_id as f64 * STEP) + .collect(), + FunctionDescriptor::Closure(Box::new(f64::sin)), + DomainDescriptor::Explicit(&domain), + ComputeMethod::Rectangle, + RECTANGLE_TOLERANCE + ); + + generate_test!( + ClosureUniform, + FunctionDescriptor::Closure(Box::new(f64::sin)), + DomainDescriptor::Uniform { + start: 0., + step: STEP, + n_step: (1000. * std::f64::consts::PI) as usize, + }, + ComputeMethod::Rectangle, + RECTANGLE_TOLERANCE + ); + + generate_test!( + ValuesExplicit, + let domain: Vec = (0..(std::f64::consts::PI * 1000.) as usize) + .map(|step_id| step_id as f64 * STEP) + .collect(), + FunctionDescriptor::Values(domain.iter().copied().map(f64::sin).collect()), + DomainDescriptor::Explicit(&domain), + ComputeMethod::Rectangle, + RECTANGLE_TOLERANCE + ); + + generate_test!( + ValuesUniform, + FunctionDescriptor::Values( + (0..(1000. * std::f64::consts::PI) as usize) + .map(|step_id| (step_id as f64 * STEP).sin()) + .collect() + ), + DomainDescriptor::Uniform { + start: 0., + step: STEP, + n_step: (1000. * std::f64::consts::PI) as usize, + }, + ComputeMethod::Rectangle, + RECTANGLE_TOLERANCE + ); +} + +#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] +mod a_trapezoid { + use super::*; + + generate_test!( + ClosureExplicit, + let domain: Vec = (0..(std::f64::consts::PI * 1000.) as usize) + .map(|step_id| step_id as f64 * STEP) + .collect(), + FunctionDescriptor::Closure(Box::new(f64::sin)), + DomainDescriptor::Explicit(&domain), + ComputeMethod::Trapezoid, + TRAPEZOID_TOLERANCE + ); + + generate_test!( + ClosureUniform, + FunctionDescriptor::Closure(Box::new(f64::sin)), + DomainDescriptor::Uniform { + start: 0., + step: STEP, + n_step: (1000. * std::f64::consts::PI) as usize, + }, + ComputeMethod::Trapezoid, + TRAPEZOID_TOLERANCE + ); + + generate_test!( + ValuesExplicit, + let domain: Vec = (0..(std::f64::consts::PI * 1000.) as usize) + .map(|step_id| step_id as f64 * STEP) + .collect(), + FunctionDescriptor::Values(domain.iter().copied().map(f64::sin).collect()), + DomainDescriptor::Explicit(&domain), + ComputeMethod::Trapezoid, + TRAPEZOID_TOLERANCE + ); + + generate_test!( + ValuesUniform, + FunctionDescriptor::Values( + (0..(1000. * std::f64::consts::PI) as usize) + .map(|step_id| (step_id as f64 * STEP).sin()) + .collect() + ), + DomainDescriptor::Uniform { + start: 0., + step: STEP, + n_step: (1000. * std::f64::consts::PI) as usize, + }, + ComputeMethod::Trapezoid, + TRAPEZOID_TOLERANCE + ); +} + +// integral B +// y = f(x) = x from -1 to 1 + +#[allow(non_snake_case)] #[test] -fn basic() { - assert_eq!(1 + 1, 2); +fn B_Closure_Explicit_Rectangle() { + let functiond = FunctionDescriptor::Closure(Box::new(|x| x)); + // -1 to 1, with .001 steps + // FIXME + // currently requires one more value because of + // the inconsistent sampling policy + let domain: Vec = (0..=2001) + .map(|step_id| -1. + f64::from(step_id) * STEP) + .collect(); + let domaind = DomainDescriptor::Explicit(&domain); + let computem = ComputeMethod::Rectangle; + let mut integraal = Integraal::default(); + let res = integraal + .function(functiond) + .domain(domaind) + .method(computem) + .compute(); + assert!(res.is_ok()); + assert!( + almost_equal!(res.unwrap(), 0.0, RECTANGLE_TOLERANCE), + "left: {} \nright: 0.0", + res.unwrap() + ); }