1use 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}