ellip 1.2.3

Elliptic integrals for Rust
Documentation
/*
 * Ellip is licensed under The 3-Clause BSD, see LICENSE.
 * Copyright 2025 Sira Pornsiriprasert <code@psira.me>
 */

#[macro_export]
macro_rules! compare_test_data {
    ($func: expr, $cases :expr, $t: ident, $rtol:expr, $atol:expr) => {
        let mut test_fail = 0;
        let mut worst_line = 0;
        let mut worst_params: Vec<$t> = Vec::new();
        let mut worst_errors: [$t;5] = [0.0; 5];

        for (i, case) in $cases.iter().enumerate() {
            let inp = &case.inputs;
            let exp = case.expected;
            let result = $func(&inp);

            if exp.is_nan() && result.is_nan() {
                continue;
            }

            if result.is_nan() {
                eprintln! ("Test failed on case {}: input = {:?}, expected = {:?}, got = NAN",
                    i + 1,
                    inp,
                    exp,
                );
                test_fail += 1;
            }

            if !result.is_finite() {
                if result != exp {
                    eprintln! ("Test failed on case {}: input = {:?}, expected = {:?}, got = {:?}",
                        i + 1,
                        inp,
                        exp,
                        result);
                    test_fail += 1;
                }
            }

            let error = (result - exp).abs();
            let tol = $t::from($atol) + $t::from($rtol) * exp.abs();
            if error > tol {
                let rel = error / exp.abs();
                test_fail += 1;
                if rel > worst_errors[1] {
                    worst_line = i + 1;
                    worst_errors = [error, rel, tol, exp, result];
                    worst_params = inp.clone();
                }
                eprintln!(
                    "Test failed on case {}: input = {:?}, expected = {:?}, got = {:?} \n error = {:?}, rel = {:?}, abs = {:?}, rtol = {:?}, atol = {:?}\n",
                    i + 1,
                    inp,
                    exp,
                    result,
                    error,
                    rel,
                    tol,
                    $rtol,
                    $atol,
                );
            }
        }

        if test_fail > 0 {
            panic!(
                "Failed {}/{} cases. Worst on case {}: input = {:?}, expected = {:?}, got = {:?} \n error = {:?}, rel = {:?}, abs = {:?}, rtol = {:?}, atol = {:?}",
                test_fail,
                $cases.len(),
                worst_line,
                worst_params,
                worst_errors[3],
                worst_errors[4],
                worst_errors[0],
                worst_errors[1],
                worst_errors[2],
                $rtol,
                $atol,
            );
        }
    };
}

#[macro_export]
macro_rules! compare_test_data_boost {
    ($filename:expr, $func:expr, $rtol:expr) => {
        compare_test_data_boost!($filename, $func, f64, $rtol, 0.0)
    };
    ($filename:expr, $func:expr, $rtol:expr, atol: $atol:expr) => {
        compare_test_data_boost!($filename, $func, f64, $rtol, $atol)
    };
    ($filename:expr, $func:expr, $n_args:tt, $rtol:expr) => {{
        use ellip_dev_utils::func_wrapper;
        func_wrapper!($func, k, $n_args);
        compare_test_data_boost!($filename, wrapped_func, f64, $rtol, 0.0)
    }};
    ($filename:expr, $func:expr, $t:ident, $rtol:expr, $atol:expr) => {{
        use ellip_dev_utils::parser;
        use std::path::Path;

        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests/data/boost")
            .join($filename);
        match parser::read_boost_data(&path.to_str().unwrap()) {
            Ok(cases) => {
                crate::compare_test_data!($func, cases, $t, $rtol, $atol);
            }
            Err(err) => panic!(
                "Failed to read reference dataset {}: {}",
                path.display(),
                err
            ),
        };
    }};
}

#[macro_export]
macro_rules! compare_test_data_wolfram {
    ($filename:expr, $func:expr, $n_args:tt, $rtol:expr, atol: $atol:expr) => {{
        use ellip_dev_utils::func_wrapper;
        func_wrapper!($func, $n_args);
        compare_test_data_wolfram!(
            "./tests/data/wolfram",
            $filename,
            wrapped_func,
            f64,
            $rtol,
            $atol
        )
    }};
    ($filename:expr, $func:expr, $n_args:tt, $rtol:expr) => {{
        use ellip_dev_utils::func_wrapper;
        func_wrapper!($func, $n_args);
        compare_test_data_wolfram!(
            "./tests/data/wolfram",
            $filename,
            wrapped_func,
            f64,
            $rtol,
            0.0
        )
    }};
    ($path:expr, $filename:expr, $func:expr, $n_args:tt, $rtol:expr) => {{
        use ellip_dev_utils::func_wrapper;
        func_wrapper!($func, $n_args);
        compare_test_data_wolfram!($path, $filename, wrapped_func, f64, $rtol, 0.0)
    }};
    ($path:expr, $filename:expr, $func:expr, $t:ident, $rtol:expr, $atol:expr) => {{
        {
            use ellip_dev_utils::parser;
            use std::path::Path;

            let path = Path::new(env!("CARGO_MANIFEST_DIR"))
                .join($path)
                .join($filename);
            match parser::read_wolfram_data(&path.to_str().unwrap()) {
                Ok(cases) => {
                    crate::compare_test_data!($func, cases, $t, $rtol, $atol);
                }
                Err(err) => panic!(
                    "Failed to read reference dataset {}: {}",
                    path.display(),
                    err
                ),
            };
        }
    }};
}

#[macro_export]
macro_rules! assert_close {
    ($expected: expr, $actual: expr, $rtol: expr) => {
        $crate::util::assert_close($actual, $expected, $rtol);
    };
}

#[cfg(feature = "test_force_fail")]
#[macro_export]
macro_rules! test_force_unreachable {
    ($($inner:tt)*) => {
        mod tests {
            use super::*;

            #[test]
            fn force_unreachable() {
                $($inner)*
            }
        }
    };
}

pub fn linspace(start: f64, end: f64, num: usize) -> Vec<f64> {
    if num < 2 {
        return vec![start];
    }

    let step = (end - start) / (num - 1) as f64;
    let mut result = Vec::with_capacity(num);

    for i in 0..num {
        result.push(start + step * i as f64);
    }

    result
}

#[cfg(all(test, not(feature = "test_force_fail")))]
mod assertion_regression_tests {
    // Regression for audit finding A15: https://github.com/p-sira/ellip/pull/129
    #[test]
    fn macro_rejects_negative_reference_errors_and_nan() {
        assert!(std::panic::catch_unwind(|| {
            crate::assert_close!(-1.0_f64, 100.0_f64, 1e-15);
        })
        .is_err());
        assert!(std::panic::catch_unwind(|| {
            crate::assert_close!(f64::NAN, 1.0_f64, 1e-15);
        })
        .is_err());
        crate::assert_close!(-1.0_f64, -1.0_f64, 0.0);
        crate::assert_close!(0.0_f64, 0.0_f64, 0.0);
    }
}

#[cfg(all(test, not(feature = "test_force_fail")))]
mod dataset_regression_tests {
    // Regression for audit finding A16: https://github.com/p-sira/ellip/pull/130
    fn identity(input: &[f64]) -> f64 {
        input[0]
    }

    // Regression for audit finding A16: https://github.com/p-sira/ellip/pull/130
    #[test]
    #[should_panic(expected = "missing-audit-a16.txt")]
    fn missing_boost_dataset_fails() {
        crate::compare_test_data_boost!("missing-audit-a16.txt", identity, 1e-15);
    }

    // Regression for audit finding A16: https://github.com/p-sira/ellip/pull/130
    #[test]
    #[should_panic(expected = "missing-audit-a16.csv")]
    fn missing_wolfram_dataset_fails() {
        crate::compare_test_data_wolfram!(
            "tests/data/wolfram",
            "missing-audit-a16.csv",
            identity,
            f64,
            1e-15,
            0.0
        );
    }

    // Regression for audit finding A16: https://github.com/p-sira/ellip/pull/130
    #[test]
    fn datasets_load_outside_checkout() {
        let status = std::process::Command::new(std::env::current_exe().unwrap())
            .args(["--exact", "carlson::elliprc::tests::test_elliprc"])
            .current_dir(std::env::temp_dir())
            .status()
            .unwrap();
        assert!(status.success());
    }
}