runmat_runtime/builtins/diagnostics/
assert.rs

1//! MATLAB-compatible `assert` builtin that mirrors MATLAB diagnostic semantics.
2
3use runmat_builtins::{ComplexTensor, Tensor, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::format::format_variadic;
7use crate::builtins::common::gpu_helpers;
8use crate::builtins::common::spec::{
9    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10    ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12#[cfg(feature = "doc_export")]
13use crate::register_builtin_doc_text;
14use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
15
16const DEFAULT_IDENTIFIER: &str = "MATLAB:assertion:failed";
17const DEFAULT_MESSAGE: &str = "Assertion failed.";
18const INVALID_CONDITION_IDENTIFIER: &str = "MATLAB:assertion:invalidCondition";
19const INVALID_INPUT_IDENTIFIER: &str = "MATLAB:assertion:invalidInput";
20const MIN_INPUT_IDENTIFIER: &str = "MATLAB:minrhs";
21const MIN_INPUT_MESSAGE: &str = "Not enough input arguments.";
22
23#[cfg(feature = "doc_export")]
24pub const DOC_MD: &str = r#"---
25title: "assert"
26category: "diagnostics"
27keywords: ["assert", "diagnostics", "validation", "error", "gpu"]
28summary: "Throw a MATLAB-style error when a logical or numeric condition evaluates to false."
29references: []
30gpu_support:
31  elementwise: false
32  reduction: false
33  precisions: []
34  broadcasting: "none"
35  notes: "Conditions are evaluated on the host. GPU tensors are gathered before the logical test, and fall back to the default CPU implementation."
36fusion:
37  elementwise: false
38  reduction: false
39  max_inputs: 0
40  constants: "inline"
41requires_feature: null
42tested:
43  unit: "builtins::diagnostics::assert::tests"
44  integration: "builtins::diagnostics::assert::tests::assert_gpu_tensor_passes"
45---
46
47# What does the `assert` function do in MATLAB / RunMat?
48`assert(cond, ...)` aborts execution with a MATLAB-compatible error when `cond` is false or contains any zero/NaN elements. When the condition is true (or an empty array), execution continues with no output. RunMat mirrors MATLAB’s identifier normalisation, message formatting, and argument validation rules.
49
50## How does the `assert` function behave in MATLAB / RunMat?
51- The first argument must be logical or numeric (real or complex). Scalars must evaluate to true; arrays must contain only nonzero, non-NaN elements (complex values fail when both real and imaginary parts are zero or contain NaNs). Empty inputs pass automatically.
52- `assert(cond)` raises `MATLAB:assertion:failed` with message `Assertion failed.` when `cond` is false.
53- `assert(cond, msg, args...)` formats `msg` with `sprintf`-compatible conversions using any additional arguments.
54- `assert(cond, id, msg, args...)` uses a custom message identifier (normalised to `MATLAB:*` when missing a namespace) and the formatted message text.
55- Arguments are validated strictly: identifiers and message templates must be string scalars or character vectors, and malformed format strings raise `MATLAB:assertion:invalidInput`.
56- Conditions supplied as gpuArray values are gathered to host memory prior to evaluation so that MATLAB semantics continue to apply.
57
58## `assert` Function GPU Execution Behaviour
59`assert` is a control-flow builtin. RunMat gathers GPU-resident tensors (including logical gpuArrays) to host memory before evaluating the condition. No GPU kernels are launched, and the acceleration provider metadata is marked as a gather-immediately operation so execution always follows the MATLAB-compatible CPU path. Residency metadata is preserved so subsequent statements observe the same values they would have seen without the assertion.
60
61## Examples of using the `assert` function in MATLAB / RunMat
62
63### Checking that all elements are nonzero
64```matlab
65A = [1 2 3];
66assert(all(A));
67```
68This runs without output because every element of `A` is nonzero.
69
70### Verifying array bounds during development
71```matlab
72idx = 12;
73assert(idx >= 1 && idx <= numel(signal), ...
74       "Index %d is outside [1, %d].", idx, numel(signal));
75```
76If `idx` falls outside the valid range, RunMat throws `MATLAB:assertion:failed` with the formatted bounds message.
77
78### Attaching a custom identifier for tooling
79```matlab
80assert(det(M) ~= 0, "runmat:demo:singularMatrix", ...
81       "Matrix must be nonsingular (determinant is zero).");
82```
83When the matrix is singular, the assertion fails with identifier `runmat:demo:singularMatrix`, allowing downstream tooling to catch it precisely.
84
85### Guarding GPU computations without manual gathering
86```matlab
87G = gpuArray(rand(1024, 1));
88assert(all(G > 0), "All entries must be positive.");
89```
90The gpuArray is gathered automatically before evaluation; no manual `gather` call is required.
91
92### Converting NaN checks into assertion failures
93```matlab
94avg = mean(samples);
95assert(~isnan(avg), "Average must be finite.");
96```
97If `avg` evaluates to `NaN`, RunMat raises an error so the calling code cannot continue with invalid state.
98
99### Ensuring structure fields exist before use
100```matlab
101assert(isfield(cfg, "rate"), ...
102       "runmat:config:missingField", ...
103       "Configuration missing required field '%s'.", "rate");
104```
105Missing fields trigger `runmat:config:missingField`, making it easy to spot configuration mistakes early.
106
107### Detecting invalid enumeration values early
108```matlab
109valid = ["nearest", "linear", "spline"];
110assert(any(mode == valid), ...
111       "Invalid interpolation mode '%s'.", mode);
112```
113Passing an unsupported option raises a descriptive error so callers can correct the mode value.
114
115### Validating dimensions before expensive work
116```matlab
117assert(size(A, 2) == size(B, 1), ...
118       "runmat:demo:dimensionMismatch", ...
119       "Inner dimensions must agree (size(A,2)=%d, size(B,1)=%d).", ...
120       size(A, 2), size(B, 1));
121```
122If the dimensions disagree, the assertion stops execution before any costly matrix multiplication is attempted.
123
124## FAQ
1251. **What types can I pass as the condition?** Logical scalars/arrays and numeric scalars/arrays are accepted. Character arrays, strings, cells, structs, and complex values raise `MATLAB:assertion:invalidCondition`.
1262. **How are NaN values treated?** Any `NaN` element causes the assertion to fail, matching MATLAB’s requirement that all elements are non-NaN and nonzero.
1273. **Do empty arrays pass the assertion?** Yes. Empty logical or numeric arrays are treated as true.
1284. **Can I omit the namespace in the message identifier?** Yes. RunMat prefixes unqualified identifiers with `MATLAB:` to match MATLAB behaviour.
1295. **What happens if my format string is malformed?** The builtin raises `MATLAB:assertion:invalidInput` describing the formatting issue.
1306. **Does `assert` run on the GPU?** No. GPU tensors are gathered automatically and evaluated on the CPU to preserve MATLAB semantics.
1317. **Can I use strings for messages and identifiers?** Yes. Both character vectors and string scalars are accepted for identifiers and message templates.
1328. **What value does `assert` return when the condition is true?** Like MATLAB, `assert` has no meaningful return value. RunMat returns `0.0` internally to satisfy the runtime but nothing is produced in MATLAB code.
1339. **How do I disable assertions in production code?** Wrap the condition in an `if` statement controlled by your own flag; MATLAB (and RunMat) always evaluates `assert`.
13410. **How do I distinguish assertion failures from other errors?** Provide a custom identifier (for example `runmat:module:assertFailed`) and catch it in a `try`/`catch` block.
135
136## See Also
137[error](./error), [warning](./warning), [isnan](../logical/tests/isnan), [sprintf](../strings/core/sprintf)
138
139## Source & Feedback
140- Full source: [`crates/runmat-runtime/src/builtins/diagnostics/assert.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/diagnostics/assert.rs)
141- Report issues: https://github.com/runmat-org/runmat/issues/new/choose
142"#;
143
144pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
145    name: "assert",
146    op_kind: GpuOpKind::Custom("control"),
147    supported_precisions: &[],
148    broadcast: BroadcastSemantics::None,
149    provider_hooks: &[],
150    constant_strategy: ConstantStrategy::InlineLiteral,
151    residency: ResidencyPolicy::GatherImmediately,
152    nan_mode: ReductionNaN::Include,
153    two_pass_threshold: None,
154    workgroup_size: None,
155    accepts_nan_mode: false,
156    notes: "Control-flow builtin; GPU tensors are gathered to host memory before evaluation.",
157};
158
159register_builtin_gpu_spec!(GPU_SPEC);
160
161pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
162    name: "assert",
163    shape: ShapeRequirements::Any,
164    constant_strategy: ConstantStrategy::InlineLiteral,
165    elementwise: None,
166    reduction: None,
167    emits_nan: false,
168    notes: "Control-flow builtin with no fusion support.",
169};
170
171register_builtin_fusion_spec!(FUSION_SPEC);
172
173#[cfg(feature = "doc_export")]
174register_builtin_doc_text!("assert", DOC_MD);
175
176#[runtime_builtin(
177    name = "assert",
178    category = "diagnostics",
179    summary = "Throw a MATLAB-style error when a logical or numeric condition evaluates to false.",
180    keywords = "assert,diagnostics,validation,error",
181    accel = "metadata"
182)]
183fn assert_builtin(args: Vec<Value>) -> Result<Value, String> {
184    if args.is_empty() {
185        return Err(build_error(MIN_INPUT_IDENTIFIER, MIN_INPUT_MESSAGE));
186    }
187
188    let mut iter = args.into_iter();
189    let condition_raw = iter.next().expect("checked length above");
190    let rest: Vec<Value> = iter.collect();
191
192    let condition = normalize_condition_value(condition_raw)?;
193    match evaluate_condition(condition)? {
194        ConditionOutcome::Pass => Ok(Value::Num(0.0)),
195        ConditionOutcome::Fail => {
196            let payload = failure_payload(&rest)?;
197            Err(build_error(&payload.identifier, &payload.message))
198        }
199    }
200}
201
202fn normalize_condition_value(condition: Value) -> Result<Value, String> {
203    match condition {
204        Value::GpuTensor(handle) => {
205            let gpu_value = Value::GpuTensor(handle);
206            gpu_helpers::gather_value(&gpu_value)
207                .map_err(|e| build_error(INVALID_INPUT_IDENTIFIER, &format!("assert: {e}")))
208        }
209        other => Ok(other),
210    }
211}
212
213#[derive(Copy, Clone, Debug, PartialEq, Eq)]
214enum ConditionOutcome {
215    Pass,
216    Fail,
217}
218
219fn evaluate_condition(value: Value) -> Result<ConditionOutcome, String> {
220    match value {
221        Value::Bool(flag) => Ok(if flag {
222            ConditionOutcome::Pass
223        } else {
224            ConditionOutcome::Fail
225        }),
226        Value::Int(int_value) => {
227            if int_value.to_i64() != 0 {
228                Ok(ConditionOutcome::Pass)
229            } else {
230                Ok(ConditionOutcome::Fail)
231            }
232        }
233        Value::Num(num) => {
234            if num.is_nan() || num == 0.0 {
235                Ok(ConditionOutcome::Fail)
236            } else {
237                Ok(ConditionOutcome::Pass)
238            }
239        }
240        Value::Complex(re, im) => {
241            if complex_element_passes(re, im) {
242                Ok(ConditionOutcome::Pass)
243            } else {
244                Ok(ConditionOutcome::Fail)
245            }
246        }
247        Value::LogicalArray(array) => {
248            if array.data.iter().all(|&bit| bit != 0) {
249                Ok(ConditionOutcome::Pass)
250            } else {
251                Ok(ConditionOutcome::Fail)
252            }
253        }
254        Value::Tensor(tensor) => evaluate_tensor_condition(&tensor),
255        Value::ComplexTensor(tensor) => evaluate_complex_tensor(&tensor),
256        Value::GpuTensor(_) => {
257            unreachable!("gpu tensors are gathered in normalize_condition_value")
258        }
259        _ => Err(build_error(
260            INVALID_CONDITION_IDENTIFIER,
261            "assert: first input must be logical or numeric.",
262        )),
263    }
264}
265
266fn evaluate_tensor_condition(tensor: &Tensor) -> Result<ConditionOutcome, String> {
267    if tensor.data.is_empty() {
268        return Ok(ConditionOutcome::Pass);
269    }
270    for value in &tensor.data {
271        if value.is_nan() || *value == 0.0 {
272            return Ok(ConditionOutcome::Fail);
273        }
274    }
275    Ok(ConditionOutcome::Pass)
276}
277
278fn evaluate_complex_tensor(tensor: &ComplexTensor) -> Result<ConditionOutcome, String> {
279    if tensor.data.is_empty() {
280        return Ok(ConditionOutcome::Pass);
281    }
282    for &(re, im) in &tensor.data {
283        if !complex_element_passes(re, im) {
284            return Ok(ConditionOutcome::Fail);
285        }
286    }
287    Ok(ConditionOutcome::Pass)
288}
289
290fn complex_element_passes(re: f64, im: f64) -> bool {
291    if re.is_nan() || im.is_nan() {
292        return false;
293    }
294    re != 0.0 || im != 0.0
295}
296
297struct FailurePayload {
298    identifier: String,
299    message: String,
300}
301
302fn failure_payload(args: &[Value]) -> Result<FailurePayload, String> {
303    if args.is_empty() {
304        return Ok(FailurePayload {
305            identifier: DEFAULT_IDENTIFIER.to_string(),
306            message: DEFAULT_MESSAGE.to_string(),
307        });
308    }
309
310    let candidate = &args[0];
311    let treat_as_identifier = args.len() >= 2 && value_is_identifier(candidate);
312
313    if treat_as_identifier {
314        if args.len() < 2 {
315            return Err(build_error(
316                INVALID_INPUT_IDENTIFIER,
317                "assert: message text must follow the message identifier.",
318            ));
319        }
320        let identifier = identifier_from_value(candidate)?;
321        let template = message_from_value(&args[1])?;
322        let formatting_args: &[Value] = if args.len() > 2 { &args[2..] } else { &[] };
323        let message = format_message(&template, formatting_args)?;
324        Ok(FailurePayload {
325            identifier,
326            message,
327        })
328    } else {
329        let template = message_from_value(candidate)?;
330        let formatting_args: &[Value] = if args.len() > 1 { &args[1..] } else { &[] };
331        let message = format_message(&template, formatting_args)?;
332        Ok(FailurePayload {
333            identifier: DEFAULT_IDENTIFIER.to_string(),
334            message,
335        })
336    }
337}
338
339fn value_is_identifier(value: &Value) -> bool {
340    if let Some(text) = string_scalar_opt(value) {
341        is_message_identifier(&text) || looks_like_unqualified_identifier(&text)
342    } else {
343        false
344    }
345}
346
347fn identifier_from_value(value: &Value) -> Result<String, String> {
348    let text = string_scalar_from_value(
349        value,
350        "assert: message identifier must be a string scalar or character vector.",
351    )?;
352    if text.trim().is_empty() {
353        return Err(build_error(
354            INVALID_INPUT_IDENTIFIER,
355            "assert: message identifier must be nonempty.",
356        ));
357    }
358    Ok(normalize_identifier(&text))
359}
360
361fn message_from_value(value: &Value) -> Result<String, String> {
362    string_scalar_from_value(
363        value,
364        "assert: message text must be a string scalar or character vector.",
365    )
366}
367
368fn format_message(template: &str, args: &[Value]) -> Result<String, String> {
369    format_variadic(template, args)
370        .map_err(|err| build_error(INVALID_INPUT_IDENTIFIER, &format!("assert: {err}")))
371}
372
373fn build_error(identifier: &str, message: &str) -> String {
374    let ident = normalize_identifier(identifier);
375    format!("{ident}: {message}")
376}
377
378fn normalize_identifier(raw: &str) -> String {
379    let trimmed = raw.trim();
380    if trimmed.is_empty() {
381        DEFAULT_IDENTIFIER.to_string()
382    } else if trimmed.contains(':') {
383        trimmed.to_string()
384    } else {
385        format!("MATLAB:{trimmed}")
386    }
387}
388
389fn is_message_identifier(text: &str) -> bool {
390    let trimmed = text.trim();
391    if trimmed.is_empty() || !trimmed.contains(':') {
392        return false;
393    }
394    trimmed
395        .chars()
396        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
397}
398
399fn looks_like_unqualified_identifier(text: &str) -> bool {
400    let trimmed = text.trim();
401    if trimmed.is_empty() || trimmed.contains(char::is_whitespace) {
402        return false;
403    }
404    trimmed
405        .chars()
406        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.'))
407}
408
409fn string_scalar_from_value(value: &Value, context: &str) -> Result<String, String> {
410    match value {
411        Value::String(text) => Ok(text.clone()),
412        Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
413        Value::CharArray(char_array) if char_array.rows == 1 => {
414            Ok(char_array.data.iter().collect::<String>())
415        }
416        _ => Err(build_error(INVALID_INPUT_IDENTIFIER, context)),
417    }
418}
419
420fn string_scalar_opt(value: &Value) -> Option<String> {
421    match value {
422        Value::String(text) => Some(text.clone()),
423        Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
424        Value::CharArray(char_array) if char_array.rows == 1 => {
425            Some(char_array.data.iter().collect())
426        }
427        _ => None,
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use crate::builtins::common::test_support;
435    use runmat_builtins::{ComplexTensor, IntValue, LogicalArray, Tensor};
436
437    #[test]
438    fn assert_true_passes() {
439        let result = assert_builtin(vec![Value::Bool(true)]).expect("assert should pass");
440        assert_eq!(result, Value::Num(0.0));
441    }
442
443    #[test]
444    fn assert_empty_tensor_passes() {
445        let tensor = Tensor::new(Vec::new(), vec![0, 3]).unwrap();
446        assert_builtin(vec![Value::Tensor(tensor)]).expect("assert should pass");
447    }
448
449    #[test]
450    fn assert_empty_logical_passes() {
451        let logical = LogicalArray::new(Vec::new(), vec![0]).unwrap();
452        assert_builtin(vec![Value::LogicalArray(logical)]).expect("assert should pass");
453    }
454
455    #[test]
456    fn assert_false_uses_default_message() {
457        let err = assert_builtin(vec![Value::Bool(false)]).expect_err("assert should fail");
458        assert!(err.starts_with(DEFAULT_IDENTIFIER));
459        assert!(err.contains(DEFAULT_MESSAGE));
460    }
461
462    #[test]
463    fn assert_handles_numeric_tensor() {
464        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
465        assert_builtin(vec![Value::Tensor(tensor)]).expect("assert should pass");
466    }
467
468    #[test]
469    fn assert_detects_zero_in_tensor() {
470        let tensor = Tensor::new(vec![1.0, 0.0, 3.0], vec![3, 1]).unwrap();
471        let err = assert_builtin(vec![Value::Tensor(tensor)]).expect_err("assert should fail");
472        assert!(err.starts_with(DEFAULT_IDENTIFIER));
473    }
474
475    #[test]
476    fn assert_detects_nan() {
477        let err = assert_builtin(vec![Value::Num(f64::NAN)]).expect_err("assert should fail");
478        assert!(err.starts_with(DEFAULT_IDENTIFIER));
479    }
480
481    #[test]
482    fn assert_complex_scalar_passes() {
483        assert_builtin(vec![Value::Complex(0.0, 2.0)]).expect("assert should pass");
484    }
485
486    #[test]
487    fn assert_complex_scalar_failure() {
488        let err = assert_builtin(vec![Value::Complex(0.0, 0.0)]).expect_err("assert should fail");
489        assert!(err.starts_with(DEFAULT_IDENTIFIER));
490    }
491
492    #[test]
493    fn assert_complex_tensor_failure() {
494        let tensor = ComplexTensor::new(vec![(1.0, 0.0), (0.0, 0.0)], vec![2, 1]).expect("tensor");
495        let err =
496            assert_builtin(vec![Value::ComplexTensor(tensor)]).expect_err("assert should fail");
497        assert!(err.starts_with(DEFAULT_IDENTIFIER));
498    }
499
500    #[test]
501    fn assert_accepts_custom_message() {
502        let err = assert_builtin(vec![
503            Value::Bool(false),
504            Value::from("Vector length must be positive."),
505        ])
506        .expect_err("assert should fail");
507        assert!(err.contains("Vector length must be positive."));
508    }
509
510    #[test]
511    fn assert_supports_message_formatting() {
512        let err = assert_builtin(vec![
513            Value::Bool(false),
514            Value::from("Expected positive value, got %d."),
515            Value::Int(IntValue::I32(-4)),
516        ])
517        .expect_err("assert should fail");
518        assert!(err.contains("Expected positive value, got -4."));
519    }
520
521    #[test]
522    fn assert_supports_custom_identifier() {
523        let err = assert_builtin(vec![
524            Value::Bool(false),
525            Value::from("runmat:tests:failed"),
526            Value::from("Failure %d occurred."),
527            Value::Int(IntValue::I32(3)),
528        ])
529        .expect_err("assert should fail");
530        assert!(err.starts_with("runmat:tests:failed"));
531        assert!(err.contains("Failure 3 occurred."));
532    }
533
534    #[test]
535    fn assert_unqualified_identifier_prefixed() {
536        let err = assert_builtin(vec![
537            Value::Bool(false),
538            Value::from("customAssertionFailed"),
539            Value::from("runtime failure"),
540        ])
541        .expect_err("assert should fail");
542        assert!(err.starts_with("MATLAB:customAssertionFailed"));
543    }
544
545    #[test]
546    fn assert_rejects_invalid_condition_type() {
547        let err = assert_builtin(vec![Value::from("invalid")]).expect_err("assert should error");
548        assert!(err.starts_with(INVALID_CONDITION_IDENTIFIER));
549    }
550
551    #[test]
552    fn assert_gpu_tensor_passes() {
553        test_support::with_test_provider(|provider| {
554            let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
555            let view = runmat_accelerate_api::HostTensorView {
556                data: &tensor.data,
557                shape: &tensor.shape,
558            };
559            let handle = provider.upload(&view).expect("upload");
560            let result = assert_builtin(vec![Value::GpuTensor(handle)]).expect("assert");
561            assert_eq!(result, Value::Num(0.0));
562        });
563    }
564
565    #[test]
566    fn assert_invalid_message_type_errors() {
567        let err = assert_builtin(vec![Value::Bool(false), Value::Num(5.0)])
568            .expect_err("assert should error");
569        assert!(err.starts_with(INVALID_INPUT_IDENTIFIER));
570    }
571
572    #[test]
573    fn assert_formatting_error_propagates() {
574        let err = assert_builtin(vec![
575            Value::Bool(false),
576            Value::from("number %d must be > 0"),
577        ])
578        .expect_err("assert should fail");
579        assert!(err.starts_with(INVALID_INPUT_IDENTIFIER));
580        assert!(err.contains("sprintf"));
581    }
582
583    #[test]
584    fn assert_gpu_tensor_failure() {
585        test_support::with_test_provider(|provider| {
586            let tensor = Tensor::new(vec![1.0, 0.0, 3.0], vec![3, 1]).unwrap();
587            let view = runmat_accelerate_api::HostTensorView {
588                data: &tensor.data,
589                shape: &tensor.shape,
590            };
591            let handle = provider.upload(&view).expect("upload");
592            let err = assert_builtin(vec![Value::GpuTensor(handle)]).expect_err("assert");
593            assert!(err.starts_with(DEFAULT_IDENTIFIER));
594        });
595    }
596
597    #[test]
598    fn assert_logical_array_failure() {
599        let logical = LogicalArray::new(vec![1, 0], vec![2]).unwrap();
600        let err =
601            assert_builtin(vec![Value::LogicalArray(logical)]).expect_err("assert should fail");
602        assert!(err.starts_with(DEFAULT_IDENTIFIER));
603    }
604
605    #[test]
606    fn assert_requires_condition_argument() {
607        let err = assert_builtin(Vec::new()).expect_err("assert should error");
608        assert!(err.starts_with(MIN_INPUT_IDENTIFIER));
609        assert!(err.contains(MIN_INPUT_MESSAGE));
610    }
611
612    #[test]
613    #[cfg(feature = "wgpu")]
614    fn assert_wgpu_tensor_failure_matches_cpu() {
615        use runmat_accelerate::backend::wgpu::provider::{
616            register_wgpu_provider, WgpuProviderOptions,
617        };
618
619        if register_wgpu_provider(WgpuProviderOptions::default()).is_err() {
620            return;
621        }
622        let Some(provider) = runmat_accelerate_api::provider() else {
623            return;
624        };
625
626        let tensor = Tensor::new(vec![1.0, 0.0], vec![2, 1]).unwrap();
627        let view = runmat_accelerate_api::HostTensorView {
628            data: &tensor.data,
629            shape: &tensor.shape,
630        };
631        let handle = provider.upload(&view).expect("upload");
632        let err = assert_builtin(vec![Value::GpuTensor(handle)]).expect_err("assert should fail");
633        assert!(err.starts_with(DEFAULT_IDENTIFIER));
634    }
635
636    #[test]
637    #[cfg(feature = "doc_export")]
638    fn doc_examples_present() {
639        let blocks = test_support::doc_examples(DOC_MD);
640        assert!(!blocks.is_empty());
641    }
642}