Files
modeling-app/rust/kcl-lib/src/std/assert.rs
Jonathan Tran ddb034b14d Show KCL backtraces (#7033)
* Add backtrace to errors

* Add display of backtraces with hints

* Change pane badge to only show count of errors

* Fix property name to not collide with Error superclass

* Increase min stack again

* Add e2e test that checks that the diagnostics are created in CodeMirror

* Remove unneeded code

* Change to the new hotness
2025-05-19 18:13:10 +00:00

184 lines
6.5 KiB
Rust

//! Standard library assert functions.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use super::args::TyF64;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue},
std::Args,
};
async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
if !value {
return Err(KclError::Type(KclErrorDetails::new(
format!("assert failed: {}", message),
vec![args.source_range],
)));
}
Ok(())
}
pub async fn assert_is(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let actual = args.get_unlabeled_kw_arg("actual")?;
let error = args.get_kw_arg_opt("error")?;
inner_assert_is(actual, error, &args).await?;
Ok(KclValue::none())
}
/// Check that the provided value is true, or raise a [KclError]
/// with the provided description.
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let actual = args.get_unlabeled_kw_arg("actual")?;
let gt = args.get_kw_arg_opt("isGreaterThan")?;
let lt = args.get_kw_arg_opt("isLessThan")?;
let gte = args.get_kw_arg_opt("isGreaterThanOrEqual")?;
let lte = args.get_kw_arg_opt("isLessThanOrEqual")?;
let eq = args.get_kw_arg_opt("isEqualTo")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let error = args.get_kw_arg_opt("error")?;
inner_assert(actual, gt, lt, gte, lte, eq, tolerance, error, &args).await?;
Ok(KclValue::none())
}
/// Asserts that a value is the boolean value true.
/// ```no_run
/// kclIsFun = true
/// assertIs(kclIsFun)
/// ```
#[stdlib{
name = "assertIs",
keywords = true,
unlabeled_first = true,
args = {
actual = { docs = "Value to check. If this is the boolean value true, assert passes. Otherwise it fails." },
error = { docs = "If the value was false, the program will terminate with this error message" },
}
}]
async fn inner_assert_is(actual: bool, error: Option<String>, args: &Args) -> Result<(), KclError> {
let error_msg = match &error {
Some(x) => x,
None => "should have been true, but it was not",
};
_assert(actual, error_msg, args).await
}
/// Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met.
/// If you provide multiple conditions, they will all be checked and all must be met.
///
/// ```no_run
/// n = 10
/// assert(n, isEqualTo = 10)
/// assert(n, isGreaterThanOrEqual = 0, isLessThan = 100, error = "number should be between 0 and 100")
/// assert(1.0000000000012, isEqualTo = 1, tolerance = 0.0001, error = "number should be almost exactly 1")
/// ```
#[stdlib {
name = "assert",
keywords = true,
unlabeled_first = true,
args = {
actual = { docs = "Value to check. It will be compared with one of the comparison arguments." },
is_greater_than = { docs = "Comparison argument. If given, checks the `actual` value is greater than this." },
is_less_than = { docs = "Comparison argument. If given, checks the `actual` value is less than this." },
is_greater_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is greater than or equal to this." },
is_less_than_or_equal = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this." },
is_equal_to = { docs = "Comparison argument. If given, checks the `actual` value is less than or equal to this.", include_in_snippet = true },
tolerance = { docs = "If `isEqualTo` is used, this is the tolerance to allow for the comparison. This tolerance is used because KCL's number system has some floating-point imprecision when used with very large decimal places." },
error = { docs = "If the value was false, the program will terminate with this error message" },
}
}]
#[allow(clippy::too_many_arguments)]
async fn inner_assert(
actual: TyF64,
is_greater_than: Option<TyF64>,
is_less_than: Option<TyF64>,
is_greater_than_or_equal: Option<TyF64>,
is_less_than_or_equal: Option<TyF64>,
is_equal_to: Option<TyF64>,
tolerance: Option<TyF64>,
error: Option<String>,
args: &Args,
) -> Result<(), KclError> {
// Validate the args
let no_condition_given = [
&is_greater_than,
&is_less_than,
&is_greater_than_or_equal,
&is_less_than_or_equal,
&is_equal_to,
]
.iter()
.all(|cond| cond.is_none());
if no_condition_given {
return Err(KclError::Type(KclErrorDetails::new(
"You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
vec![args.source_range],
)));
}
if tolerance.is_some() && is_equal_to.is_none() {
return Err(KclError::Type(KclErrorDetails::new(
"The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
.to_owned(),
vec![args.source_range],
)));
}
let suffix = if let Some(err_string) = error {
format!(": {err_string}")
} else {
Default::default()
};
let actual = actual.n;
// Run the checks.
if let Some(exp) = is_greater_than {
let exp = exp.n;
_assert(
actual > exp,
&format!("Expected {actual} to be greater than {exp} but it wasn't{suffix}"),
args,
)
.await?;
}
if let Some(exp) = is_less_than {
let exp = exp.n;
_assert(
actual < exp,
&format!("Expected {actual} to be less than {exp} but it wasn't{suffix}"),
args,
)
.await?;
}
if let Some(exp) = is_greater_than_or_equal {
let exp = exp.n;
_assert(
actual >= exp,
&format!("Expected {actual} to be greater than or equal to {exp} but it wasn't{suffix}"),
args,
)
.await?;
}
if let Some(exp) = is_less_than_or_equal {
let exp = exp.n;
_assert(
actual <= exp,
&format!("Expected {actual} to be less than or equal to {exp} but it wasn't{suffix}"),
args,
)
.await?;
}
if let Some(exp) = is_equal_to {
let exp = exp.n;
let tolerance = tolerance.map(|e| e.n).unwrap_or(0.0000000001);
_assert(
(actual - exp).abs() < tolerance,
&format!("Expected {actual} to be equal to {exp} but it wasn't{suffix}"),
args,
)
.await?;
}
Ok(())
}