# руда-тест-утилиты
Общие строительные блоки для тестов ядра в Ruda: построители тестовых
тензоров, сравнение эталонов на стороне хоста и унифицированный модуль рендеринга,
который красиво печатает тензоры (или сравнивает их) в рамках одной конфигурации.
---
## Конфигурация: `ruda-test.toml`
Настройте политику тестирования и тензорную печать с помощью файла `ruda-test.toml` в
корне рабочей области. При первом доступе загрузчик переходит из текущего рабочего
каталога до тех пор, пока не найдет файл, а затем кэширует конфигурацию.
Конфигурация состоит из двух разделов:
```toml
[test]
[print]
enabled = false # toggle all printing
view = "table" # "table" | "lines"
force-fail = true # reject passing outcomes in correct/strict print mode
fail-only = false # diff: only render cells where Δ > ε
show-expected = false # diff: render `got/expected` per cell (else just `got`)
filter = "" # per-axis filter, same DSL as the slice helper
```
**Весь конвейер подчиняется одному правилу:** если `enabled = false` , ничего не печатается. Установите для него
значение `true` , запустите тест и посмотрите, как рендерятся ваши тензоры. Вот и все.
### `[test] policy`
|`correct`|принять|не удалось|принять|
|`strict`|принять|не удалось|не удалось|
|`fail-if-run`|не удалось|принять|принять|
При включенной печати и `force-fail = true` политики `correct` и `strict`
отклоняют результаты передачи и ошибки компиляции, даже если рендеринг
был пропущен. Политика `fail-if-run` не изменяется при этом параметре.
---
## Рендеринг: один путь для всего
И `assert_equals_approx(actual, expected, ε)` , и бесплатный `print_tensors(label, &[&a, &b], Some(ε))` проходят через
один и тот же рендерер. Не существует «пути
различий» и «путь с красивой печатью»; сравнение фактического и
ожидаемого и красивая печать двух несвязанных тензоров одинаковой
формы — это буквально один и тот же вызов.
Правила:
- Один тензор → только значения, без цвета.
- Два тензора **одного ранга и формы** → конечные значения окрашены в зеленый цвет.
, если `Δ ≤ max(ε, ε × |expected|)`, и красный в противном случае. С `show-expected = true` ячейка показывает
`got/expected`; иначе просто `got`.
- Два тензора **разного ранга или формы** → молча пропускаются.
Средство визуализации никогда не паникует из-за неправильного ввода.
- Ранг фильтра печати ≠ ранг тензора → рендеринг пропускается. Несоответствующий
возвращает `ValidationResult::Error`.
```rust
use ruda_test_utils::print_tensors;
// Single tensor — table or lines per [print] view, no color.
print_tensors("input", &[&host], None);
// Two tensors — colored diff. Same path used by assert_equals_approx.
print_tensors("a vs b", &[&a, &b], Some(1e-3));
```
В представлении таблицы никогда не отображаются числа Δ/ε (информацию
несет цвет ячейки). В представлении линий они всегда отображаются.
### Пример представления таблицы (с `show-expected = true`)
```
=== diff shape=[2, 3] ===
| 0 1 2
----+------------------------------------------------------
```
### Вид таблицы + `fail-only = true`
```
=== diff shape=[2, 3] ===
| 0 1 2
----+---------------------------
```
### Просмотр линий + `fail-only = true`
```
=== diff shape=[2, 3] ===
[1, 0] | 4.000000 | 3.000000 | 1.000000 | 0.003000 | FAIL ← red
[1, 1] | 5.000000 | 4.000000 | 1.000000 | 0.004000 | FAIL ← red
[1, 2] | 6.000000 | 5.000000 | 1.000000 | 0.005000 | FAIL ← red
```
---
## Синтаксис фильтра
Используется как `[print] filter` , так и
`assert_equals_approx_in_slice` . Список тусклых записей, разделенных запятыми:
- `.` — подстановочный знак (любой индекс в этом диапазоне)
- `N` — единый индекс
- `M-K` — диапазон включительно
Пример для 4-D тензора: `.,.,10-20,30` выбирает все элементы, где dim
2 находится в `10..=20` , а dim 3 — это
точно `30` . Ранг фильтра должен быть равен рангу тензора.
Из Rust:
```rust
use ruda_test_utils::{DimFilter, assert_equals_approx_in_slice};
// Vec<Range<usize>> works (half-open, like Rust slices).
assert_equals_approx_in_slice(&actual, &expected, 0.001, vec![0..1, 0..3]);
// Or build the canonical TensorFilter explicitly.
let filter = vec![
DimFilter::Exact(0),
DimFilter::Range { start: 0, end: 2 }, // inclusive: 0..=2
];
assert_equals_approx_in_slice(&actual, &expected, 0.001, filter);
```
`parse_tensor_filter("0,0-2")` анализирует строку DSL в строку `TensorFilter`.
---
## Сообщения об ошибках
`assert_equals_approx` возвращает `ValidationResult` , собирая до **8**
несоответствий плюс совокупную статистику. Вызов `.as_test_outcome().enforce()` применяет
политику тестирования и вызывает панику при отклонении результата:
```
Test failed: Got incorrect results: 17/4096 elements mismatched
[0, 5]: got 1.234, expected 1.220, |Δ|=0.014 > ε=0.001
...
... and 9 more
```
Когда печать включена, вывод каждого элемента поступает на стандартный вывод;
тревожное сообщение сохраняет только совокупный заголовок, поэтому оно не дублирует дамп.
---
## Выполнение тестов
Запустите из корня рабочей области RUDA, выбрав одну среду выполнения теста:
```sh
cargo test --locked -p ruda-test-utils --test lib --features ruda-test-runtime/cuda
```
Функция времени выполнения может быть `cpu`, `cuda`, `hip` или `wgpu`.
---
## Создание тестовых входов
Два эквивалентных способа построения тестового тензора:
```rust
use ruda_kernel::dsl::prelude::RudaPrimitive;
use ruda_test_utils::{TestInput, StrideSpec, DataKind, Distribution};
// Long-form constructor.
let (handle, host) = TestInput::new(
client.clone(),
[4, 4],
f32::as_type_native_unchecked().storage_type(),
StrideSpec::RowMajor,
DataKind::Random {
seed: 0,
distribution: Distribution::Uniform(-1.0, 1.0),
},
)
.generate_with_f32_host_data();
// Fluent builder — `dtype` defaults to f32, `stride` defaults to RowMajor.
let (handle, host) = TestInput::builder(client.clone(), [4, 4])
.uniform( 0, -1.0, 1.0)
.generate_with_f32_host_data();
```
Настройки Builder (все необязательно):
|`.dtype(d)`|`f32`|Переопределить ввод dtype.|
|`.stride(spec)`|`StrideSpec::RowMajor`|Переопределить схему шага.|
Финализаторы Builder (каждый возвращает `TestInput`, готовый к генерации):
|`.arange()`|`Arange { scale: None }`|
|`.arange_scaled(s)`|`Arange { scale: Some(s) }`|
|`.eye()`|`Eye`|
|`.zeros()`|`Zeros`|
|`.uniform(seed, lo, hi)`|`Random { Uniform(lo, hi) }`|
|`.bernoulli(seed, p)`|`Random { Bernoulli(p) }`|
|`.normal(seed, mean, std)`|`Random { Normal { mean, std } }`|
|`.random(seed, dist)`|`Random { dist }`|
|`.linspace(start, end)`|`Custom { data }` с N равномерно распределенными значениями из `start..=end`|
|`.custom(data)`|`Custom { data }`|
После финализатора вызовите любой из:
`.generate()` , `.generate_with_f32_host_data()` , `.generate_with_bool_host_data()` ,
`.generate_test_tensor()` , `.f32_host_data()` , `.bool_host_data()` .