Skip to main content

kcl_lib/std/
assert.rs

1//! Standard library assert functions.
2
3use anyhow::Result;
4
5use super::args::TyF64;
6use crate::{
7    errors::{KclError, KclErrorDetails},
8    execution::{ExecState, KclValue, types::RuntimeType},
9    std::Args,
10};
11
12async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
13    if !value {
14        return Err(KclError::new_type(KclErrorDetails::new(
15            format!("assert failed: {message}"),
16            vec![args.source_range],
17        )));
18    }
19    Ok(())
20}
21
22pub async fn assert_is(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
23    let actual = args.get_unlabeled_kw_arg("actual", &RuntimeType::bool(), exec_state)?;
24    let error = args.get_kw_arg_opt("error", &RuntimeType::string(), exec_state)?;
25    inner_assert_is(actual, error, &args).await?;
26    Ok(KclValue::none())
27}
28
29/// Check that the provided value is true, or raise a [KclError]
30/// with the provided description.
31pub async fn assert(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
32    let actual = args.get_unlabeled_kw_arg("actual", &RuntimeType::num_any(), exec_state)?;
33    let gt = args.get_kw_arg_opt("isGreaterThan", &RuntimeType::num_any(), exec_state)?;
34    let lt = args.get_kw_arg_opt("isLessThan", &RuntimeType::num_any(), exec_state)?;
35    let gte = args.get_kw_arg_opt("isGreaterThanOrEqual", &RuntimeType::num_any(), exec_state)?;
36    let lte = args.get_kw_arg_opt("isLessThanOrEqual", &RuntimeType::num_any(), exec_state)?;
37    let eq = args.get_kw_arg_opt("isEqualTo", &RuntimeType::num_any(), exec_state)?;
38    let neq = args.get_kw_arg_opt("isNotEqualTo", &RuntimeType::num_any(), exec_state)?;
39    let tolerance = args.get_kw_arg_opt("tolerance", &RuntimeType::num_any(), exec_state)?;
40    let error = args.get_kw_arg_opt("error", &RuntimeType::string(), exec_state)?;
41    inner_assert(actual, gt, lt, gte, lte, eq, neq, tolerance, error, &args).await?;
42    Ok(KclValue::none())
43}
44
45async fn inner_assert_is(actual: bool, error: Option<String>, args: &Args) -> Result<(), KclError> {
46    let error_msg = match &error {
47        Some(x) => x,
48        None => "should have been true, but it was not",
49    };
50    _assert(actual, error_msg, args).await
51}
52
53#[allow(clippy::too_many_arguments)]
54async fn inner_assert(
55    actual: TyF64,
56    is_greater_than: Option<TyF64>,
57    is_less_than: Option<TyF64>,
58    is_greater_than_or_equal: Option<TyF64>,
59    is_less_than_or_equal: Option<TyF64>,
60    is_equal_to: Option<TyF64>,
61    is_not_equal_to: Option<TyF64>,
62    tolerance: Option<TyF64>,
63    error: Option<String>,
64    args: &Args,
65) -> Result<(), KclError> {
66    // Validate the args
67    let no_condition_given = [
68        &is_greater_than,
69        &is_less_than,
70        &is_greater_than_or_equal,
71        &is_less_than_or_equal,
72        &is_equal_to,
73        &is_not_equal_to,
74    ]
75    .iter()
76    .all(|cond| cond.is_none());
77    if no_condition_given {
78        return Err(KclError::new_type(KclErrorDetails::new(
79            "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
80            vec![args.source_range],
81        )));
82    }
83
84    if tolerance.is_some() && is_equal_to.is_none() && is_not_equal_to.is_none() {
85        return Err(KclError::new_type(KclErrorDetails::new(
86            "The `tolerance` arg is only used with `isEqualTo` and `isNotEqualTo`. Either remove `tolerance` or add an `isEqualTo` or `isNotEqualTo` arg."
87                .to_owned(),
88            vec![args.source_range],
89        )));
90    }
91
92    let suffix = if let Some(err_string) = error {
93        format!(": {err_string}")
94    } else {
95        Default::default()
96    };
97    let actual = actual.n;
98
99    // Run the checks.
100    if let Some(exp) = is_greater_than {
101        let exp = exp.n;
102        _assert(
103            actual > exp,
104            &format!("Expected {actual} to be greater than {exp} but it wasn't{suffix}"),
105            args,
106        )
107        .await?;
108    }
109    if let Some(exp) = is_less_than {
110        let exp = exp.n;
111        _assert(
112            actual < exp,
113            &format!("Expected {actual} to be less than {exp} but it wasn't{suffix}"),
114            args,
115        )
116        .await?;
117    }
118    if let Some(exp) = is_greater_than_or_equal {
119        let exp = exp.n;
120        _assert(
121            actual >= exp,
122            &format!("Expected {actual} to be greater than or equal to {exp} but it wasn't{suffix}"),
123            args,
124        )
125        .await?;
126    }
127    if let Some(exp) = is_less_than_or_equal {
128        let exp = exp.n;
129        _assert(
130            actual <= exp,
131            &format!("Expected {actual} to be less than or equal to {exp} but it wasn't{suffix}"),
132            args,
133        )
134        .await?;
135    }
136    const DEFAULT_TOLERANCE: f64 = 0.0000000001;
137    if let Some(exp) = is_equal_to {
138        let exp = exp.n;
139        let tolerance = tolerance.as_ref().map(|e| e.n).unwrap_or(DEFAULT_TOLERANCE);
140        _assert(
141            (actual - exp).abs() < tolerance,
142            &format!("Expected {actual} to be equal to {exp} using tolerance {tolerance} but it wasn't{suffix}"),
143            args,
144        )
145        .await?;
146    }
147    if let Some(exp) = is_not_equal_to {
148        let exp = exp.n;
149        let tolerance = tolerance.as_ref().map(|e| e.n).unwrap_or(DEFAULT_TOLERANCE);
150        _assert(
151            (actual - exp).abs() >= tolerance,
152            &format!("Expected {actual} to not be equal to {exp} using tolerance {tolerance} but it was{suffix}"),
153            args,
154        )
155        .await?;
156    }
157    Ok(())
158}