Kwargs: assert functions (#6406)

Closes https://github.com/KittyCAD/modeling-app/issues/6408
This commit is contained in:
Adam Chalmers
2025-04-22 12:44:52 -05:00
committed by GitHub
parent 8be36d3d16
commit f99e44e371
73 changed files with 4790 additions and 4034 deletions

View File

@ -115,9 +115,9 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// return sumSoFar
/// */
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
/// assert(sum([1, 2, 3]), isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // This example works just like the previous example above, but it uses
@ -126,9 +126,9 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// arr = [1, 2, 3]
/// sum = reduce(arr, 0, (i, result_so_far) => { return i + result_so_far })
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
/// assert(sum, isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // Declare a function that sketches a decagon.
@ -224,7 +224,7 @@ async fn call_reduce_closure(
/// ```no_run
/// arr = [1, 2, 3]
/// new_arr = push(arr, 4)
/// assertEqual(new_arr[3], 4, 0.00001, "4 was added to the end of the array")
/// assert(new_arr[3], isEqualTo = 4, tolerance = 0.1, error = "4 was added to the end of the array")
/// ```
#[stdlib {
name = "push",
@ -260,9 +260,9 @@ pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// ```no_run
/// arr = [1, 2, 3, 4]
/// new_arr = pop(arr)
/// assertEqual(new_arr[0], 1, 0.00001, "1 is the first element of the array")
/// assertEqual(new_arr[1], 2, 0.00001, "2 is the second element of the array")
/// assertEqual(new_arr[2], 3, 0.00001, "3 is the third element of the array")
/// assert(new_arr[0], isEqualTo = 1, tolerance = 0.00001, error = "1 is the first element of the array")
/// assert(new_arr[1], isEqualTo = 2, tolerance = 0.00001, error = "2 is the second element of the array")
/// assert(new_arr[2], isEqualTo = 3, tolerance = 0.00001, error = "3 is the third element of the array")
/// ```
#[stdlib {
name = "pop",

View File

@ -21,135 +21,165 @@ async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError
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 (data, description): (bool, String) = args.get_data()?;
inner_assert(data, &description, &args).await?;
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())
}
/// Check a value at runtime, and raise an error if the argument provided
/// is false.
/// 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
/// myVar = true
/// assert(myVar, "should always be true")
/// 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",
}]
async fn inner_assert(data: bool, message: &str, args: &Args) -> Result<(), KclError> {
_assert(data, message, args).await
}
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (TyF64, TyF64, String) = args.get_data()?;
inner_assert_lt(left.n, right.n, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is less than to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertLessThan(1, 2, "1 is less than 2")
/// ```
#[stdlib {
name = "assertLessThan",
}]
async fn inner_assert_lt(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left < right, message, args).await
}
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (TyF64, TyF64, String) = args.get_data()?;
inner_assert_gt(left.n, right.n, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value equals another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// n = 1.0285
/// o = 1.0286
/// assertEqual(n, o, 0.01, "n is within the given tolerance for o")
/// ```
#[stdlib {
name = "assertEqual",
}]
async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str, args: &Args) -> Result<(), KclError> {
if epsilon <= 0.0 {
Err(KclError::Type(KclErrorDetails {
message: "assertEqual epsilon must be greater than zero".to_owned(),
source_ranges: vec![args.source_range],
}))
} else if (right - left).abs() < epsilon {
Ok(())
} else {
Err(KclError::Type(KclErrorDetails {
message: format!("assert failed because {left} != {right}: {message}"),
source_ranges: vec![args.source_range],
}))
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" },
}
}
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, epsilon, description): (TyF64, TyF64, TyF64, String) = args.get_data()?;
inner_assert_equal(left.n, right.n, epsilon.n, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is greater than another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertGreaterThan(2, 1, "2 is greater than 1")
/// ```
#[stdlib {
name = "assertGreaterThan",
}]
async fn inner_assert_gt(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left > right, message, args).await
}
#[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 {
message: "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
source_ranges: vec![args.source_range],
}));
}
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (TyF64, TyF64, String) = args.get_data()?;
inner_assert_lte(left.n, right.n, &description, &args).await?;
Ok(KclValue::none())
}
if tolerance.is_some() && is_equal_to.is_none() {
return Err(KclError::Type(KclErrorDetails {
message:
"The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
.to_owned(),
source_ranges: vec![args.source_range],
}));
}
/// Check that a numerical value is less than or equal to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertLessThanOrEq(1, 2, "1 is less than 2")
/// assertLessThanOrEq(1, 1, "1 is equal to 1")
/// ```
#[stdlib {
name = "assertLessThanOrEq",
}]
async fn inner_assert_lte(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left <= right, message, args).await
}
let suffix = if let Some(err_string) = error {
format!(": {err_string}")
} else {
Default::default()
};
let actual = actual.n;
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (TyF64, TyF64, String) = args.get_data()?;
inner_assert_gte(left.n, right.n, &description, &args).await?;
Ok(KclValue::none())
}
/// Check that a numerical value is greater than or equal to another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// assertGreaterThanOrEq(2, 1, "2 is greater than 1")
/// assertGreaterThanOrEq(1, 1, "1 is equal to 1")
/// ```
#[stdlib {
name = "assertGreaterThanOrEq",
}]
async fn inner_assert_gte(left: f64, right: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert(left >= right, message, args).await
// 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(())
}

View File

@ -22,7 +22,7 @@ pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
///
/// ```no_run
/// n = int(ceil(5/2))
/// assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
/// assert(n, isEqualTo = 3, error = "5/2 = 2.5, rounded up makes 3")
/// // Draw n cylinders.
/// startSketchOn('XZ')
/// |> circle(center = [0, 0], radius = 2 )

View File

@ -36,12 +36,12 @@ pub async fn rem(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
/// If `num` is negative, the result will be too.
///
/// ```no_run
/// assertEqual(rem( 7, divisor = 4), 3, 0.01, "remainder is 3" )
/// assertEqual(rem(-7, divisor = 4), -3, 0.01, "remainder is -3")
/// assertEqual(rem( 7, divisor = -4), 3, 0.01, "remainder is 3" )
/// assertEqual(rem( 6, divisor = 2.5), 1, 0.01, "remainder is 1" )
/// assertEqual(rem( 6.5, divisor = 2.5), 1.5, 0.01, "remainder is 1.5" )
/// assertEqual(rem( 6.5, divisor = 2), 0.5, 0.01, "remainder is 0.5" )
/// assert(rem( 7, divisor = 4), isEqualTo = 3, error = "remainder is 3")
/// assert(rem(-7, divisor = 4), isEqualTo = -3, error = "remainder is -3")
/// assert(rem( 7, divisor = -4), isEqualTo = 3, error = "remainder is 3")
/// assert(rem( 6, divisor = 2.5), isEqualTo = 1, error = "remainder is 1")
/// assert(rem( 6.5, divisor = 2.5), isEqualTo = 1.5, error = "remainder is 1.5")
/// assert(rem( 6.5, divisor = 2), isEqualTo = 0.5, error = "remainder is 0.5")
/// ```
#[stdlib {
name = "rem",

View File

@ -137,11 +137,7 @@ lazy_static! {
Box::new(crate::std::units::FromCm),
Box::new(crate::std::units::FromYd),
Box::new(crate::std::assert::Assert),
Box::new(crate::std::assert::AssertEqual),
Box::new(crate::std::assert::AssertLessThan),
Box::new(crate::std::assert::AssertGreaterThan),
Box::new(crate::std::assert::AssertLessThanOrEq),
Box::new(crate::std::assert::AssertGreaterThanOrEq),
Box::new(crate::std::assert::AssertIs),
Box::new(crate::std::transform::Scale),
Box::new(crate::std::transform::Translate),
Box::new(crate::std::transform::Rotate),