RFC-036: Фреймворк тестирования std.test и команда yaoxiang test
Резюме
Введение в YaoXiang стандартного фреймворка тестирования — модуля std.test и подкоманды CLI yaoxiang test. Тестовые файлы представляют собой обычные файлы .yx; общий результат прохождения/провала определяется по exit code дочернего процесса; внутри файла допускается несколько тестовых функций — провал утверждения выражается значением Err (value-семантика), а сьют собирает per-test вердикты (§7). Модуль std.test реализован на чистом YaoXiang и является первой dogfooding-библиотекой. yaoxiang test — это инструмент CLI, а не свойство компилятора: никаких изменений в парсере, IR, байткоде или исполнителе не требуется.
Мотивация
Зачем нужен фреймворк тестирования?
В настоящее время тестовое покрытие YaoXiang полагается на #[test] на стороне Rust и интеграционные тесты в tests/. Это означает:
- Модульные тесты стандартной библиотеки (std.math / std.list / std.dict / std.convert / std.io) невозможно писать на YaoXiang
- Покрытие модульными тестами каждого модуля стандартной библиотеки заблокировано, поскольку отсутствует доступная тестовая инфраструктура
- Регрессионные тесты языковых возможностей (например, изменение семантики spawn в RFC-032) лишены средств автоматизации
Ключевые ограничения
- Железное правило 17 ключевых слов: не вводить никаких новых ключевых слов или синтаксических конструкций
- Нулевые изменения компилятора: не трогать парсер, IR, байткод, исполнитель
- Приоритет самозагрузки: тестовая библиотека написана на YaoXiang — первая dogfooding-библиотека
Архитектура
┌──────────────────────────────────────────────────────────────┐
│ yaoxiang test │
│ │
│ Слой CLI: yaoxiang test [--filter --fail-fast --json ...] │
│ │ │
│ Слой обнаружения: читает yaoxiang.toml → [tool.test] patterns│
│ по умолчанию: tests/**/*.yx │
│ │ │
│ Слой выполнения: для каждого файла: yaoxiang run <file> │
│ проверяет exit code → последовательное выполнение│
│ │ │
│ Слой отчёта: PASS/FAIL → сводка │
│ поддержка --json / --verbose / --fail-fast │
│ │
│ Слой утверждений: std.test (чистый YaoXiang, самозагрузка) │
│ нижний уровень: std.assert.assert │
│ диагностика: f"Expected {expected}, got {actual}"│
└──────────────────────────────────────────────────────────────┘Основные принципы
- Фреймворк тестирования — не свойство компилятора, а инструмент CLI —
yaoxiang runуже умеет «выполнять тесты»;yaoxiang testлишь запускает все файлы и показывает отчёт - Нулевые изменения компилятора — не вводится сканирование аннотаций
@test, секции метаданных в байткоде, специальных точек входа в исполнителе - Самозагрузка — модуль
std.testреализован на чистом YaoXiang, базовые возможности предоставляютсяstd.assert/std.result - Тестовый файл — обычный файл
.yx— файл выполняется как дочерний процесс, exit code определяет общий результат прохождения/провала - Провал утверждения — это значение, а не событие процесса — тестовая функция возвращает
Result, провал утверждения выражается какErr, сьют собирает per-test вердикты (§7); process-level abort относится только к рантайм-стражам и не используется для тестовых утверждений
Детальное проектирование
1. Дизайн CLI
yaoxiang test [ОПЦИИ] [ПУТИ]
Аргументы:
[ПУТИ]... Указанные тестовые файлы или каталоги (по умолчанию: из yaoxiang.toml, иначе tests/)
Опции:
--filter <NAME> Запускать только те тесты, имя файла которых содержит <NAME>
--fail-fast Остановиться при первом провале
--verbose, -v Показывать подробный stdout/stderr каждого теста
--list Только перечислить тестовые файлы, не запускать
--no-progress Не показывать вывод прогресса (заголовок и строки PASS); детали FAIL и сводка сохраняются (сценарий CI)
--json Выводить результаты в формате JSON (для интеграции с CI)
--parallel Параллельное выполнение (по одному worker на ядро; OR с [tool.test].parallel)Формат вывода
Вывод по умолчанию (per-test вердикты собираются сьютом внутри файла, см. §7):
Running 3 test files...
tests/math_test.yx ........................ PASS (0.002s)
tests/list_test.yx ........................ FAIL (0.003s)
`-- [FAIL] push_grows_len: Expected 3, got 2
`-- [ ok ] pop_returns_last
Results: 2 files passed, 1 file failed, 0 skipped (0.006s)
Categories: 2 behavior, 0 compile-error, 0 runtime-errorВывод JSON (--json):
{
"summary": {
"total": 3,
"passed": 2,
"failed": 1,
"skipped": 0,
"by_kind": { "behavior": 3, "compile-error": 0, "runtime-error": 0, "invalid": 0 },
"time_secs": 0.006
},
"files": [
{ "file": "tests/math_test.yx", "kind": "behavior", "passed": true, "time_secs": 0.002 },
{
"file": "tests/list_test.yx",
"kind": "behavior",
"passed": false,
"time_secs": 0.003,
"exit_code": 1,
"stderr": "error [E1024]: one is not two",
"tests": [
{ "name": "push_grows_len", "passed": false, "error": "Expected 3, got 2" },
{ "name": "pop_returns_last", "passed": true }
]
}
]
}- Проваленные файлы дополнительно несут
exit_codeиstderr(диагностика дочернего процесса с удалёнными ANSI-последовательностями, для разбора причин в CI); при комбинации--verboseи--jsonвсе файлы несутstdout/stderr --no-progressподавляет только вывод прогресса (заголовок и строки PASS) — детали FAIL и сводка выводятся всегда, провал не может пройти незамеченным;--listвыводит по одной строке с путём к тестовому файлу, не выполняя их- Каждый файл сопровождается полем
kind(behavior / compile-error / runtime-error / invalid, см. §8.2), summary включаетby_kindс количеством выполнений (фиксированные четыре ключа, без skipped); человекочитаемая сводка включает строкуCategories:с распределением по категориям - При
--parallelчеловекочитаемые строки прогресса выводятся потоково по порядку завершения (без чередования блоков), JSONfilesсортируется по путиfileдля стабильности вывода (дружелюбно к diff в CI) - Массив
testsвнутри файла per-test берётся из сбора сьютом §7 и вступает в силу после реализации модели значения - Официальный CI (
.github/workflows/ci.ymljob test) использует именно это: на стороне cargo запускается--test integration(интеграция CLI) и--test yx_runner(страж двойного корня корпуса), затемyaoxiang test --json --parallel— послойное выполнение; режим по умолчанию (языковой корпус) и явныйsrc/std/tests(слой библиотеки) порождают каждый свой отчёт; сводная таблица (total / passed / failed / skipped / time_secs и by_kind) записывается в job summary, для проваленных файлов печатаютсяkind/exit_code/stderrдля разбора причин, любой сьют с ненулевым выходом — красный статус
2. Конфигурация yaoxiang.toml
Размещается в секции [tool.test], в соответствии с соглашением о сторонних расширениях [tool.*] из RFC-015:
[project]
name = "my-project"
[tool.test]
patterns = ["tests/**/*.yx"]
exclude = ["tests/fixtures/**"] # совпадения исключаются из набора обнаружения (также удаляются из --list)
parallel = true # параллельное выполнение (OR с флагом --parallel)- По умолчанию
patterns = ["tests/**/*.yx"]— пользователь получает рабочий вариант из коробки без настройки excludeимеет ту же форму, что иpatterns(литеральный путь илиroot/**…, всегда матчинг по префиксу пути); excluded означает «не тест»; фикстуры, которым нужно поведение времени выполнения, запускаются напрямую черезyaoxiang run- Режим одного файла (
yaoxiang test foo.yx) запускается напрямую, без чтения конфигурации — при явно заданных paths ключиexclude/parallelне действуют (за исключением флага) - В будущем возможно выделение в отдельный репозиторий (расположение
[tool.test]не меняется)
3. Модуль std.test (чистый YaoXiang)
// std/test.yx — библиотека тестовых утверждений на чистом YaoXiang (стандартная форма value-семантики, реализовано 2026-09-03)
// Первая dogfooding-библиотека: тестовая библиотека YaoXiang написана на YaoXiang.
use std.result
assert_eq: (a: Any, b: Any) -> Result(Void, String) = (a, b) => {
if a == b { return result.ok(void) }
return result.err(f"Expected {b}, got {a}")
}
assert_ne: (a: Any, b: Any) -> Result(Void, String) = (a, b) => {
if a != b { return result.ok(void) }
return result.err(f"Expected not equal to {b}, got {a}")
}
assert_true: (cond: Bool) -> Result(Void, String) = (cond) => {
if cond { return result.ok(void) }
return result.err(f"Expected true, got {cond}")
}
// assert_not и assert_false — то же тело; assert_err / assert_err_code см. §8.1- Функции утверждений используют value-семантику: возвращают
Result(Void, String), провал выражается какErr(диагностическое сообщение), без abort процесса — сьют §7 на этом основании собирает per-test вердикты. Process-level abort-семантикаstd.assert.assertсохранена для рантайм-стражей и не входит в путь тестовых утверждений. Полезная нагрузка Ok —Void(unit согласно type-system.md;()— пустой Tuple, не путать — закреплено 2026-09-03) - Семейство из 7 функций (поставлено 2026-09-03, переходная abort-версия удалена): value-семантические
assert_eq/assert_ne/assert_true/assert_false+assert_not(то же тело, что и assert_false, зарезервировано под форму!assert) +assert_err(§8.1)assert_err_code(утверждение кода ошибки, §8.1)assert_approx_eq(a: Float, b: Float, eps: Float)(Phase 3, поставка 2026-09-07): проверка условия|a - b| <= eps, eps задаётся вызывающей стороной явно — допуск является частью контракта теста, без скрытых значений по умолчанию; отрицательный eps — Err уже в точке объявления, NaN — всегда Err
assert_eq/assert_neиспользуют аннотацию параметров как Any —==/!=и интерполяция f-string нормально работают на Any и не зависят от системы дженериков. Обратите внимание, что параметры должны быть аннотированы явно: неаннотированные параметры не проходят проверку вызова&Result(T, E)нативного дженерика (подтверждено зондом R1)assert_false/assert_notиспользуют выражение отрицания черезcond == false(унарный синтаксисnotещё не реализован, после стабилизации возможен переход; унарная форма!assertимеет ту же зависимость, см. §8.1)- Форма тела блока + явный
return: тип then-ветви if-выражения отбрасывается при проверке, if-выражение с Result в обеих ветвях — слепая зона проверки, реализация обходит это std.testне зависит от какого-либо нативного кода, реализован на чистом YaoXiang
4. Механизм загрузки стандартной библиотеки (ключевое решение)
Phase 1: встраивание в бинарник
std/test.yx (и все будущие модули стандартной библиотеки, написанные на YaoXiang) встраиваются в бинарник на этапе сборки:
// build.rs или скрипт сборки, генерируется автоматически
pub const STD_YX_FILES: &[(&str, &str)] = &[
("std/test.yx", r#"..."#), // исходный текст
// в будущем — больше
];Точкой подключения служит модульная система (RFC-029, полностью реализовано 2026-08-02): Registry одновременно хранит нативные и исходные модули, за многофайловую оркестровку отвечает orchestrator. Порядок разрешения use std.test:
- Сначала поиск нативного модуля Rust (существующий механизм, например
std.assert) - Если не найден — поиск во встроенных
STD_YX_FILES; при попадании исходный модуль инжектируется в orchestrator по виртуальному пути (например,<std>/test.yx) как seed-модуль и проходит стандартный фронтенд-конвейер (parse → typecheck → IR) - Если не найден — обнаружение через файловую систему (пользовательские модули)
use std.assert внутри встроенного исходного модуля нормально резолвится resolver'ом в нативный registry — нативные и исходные модули сосуществуют в Registry, межвидовые зависимости работают естественно. Встроенные модули компилируются по требованию: попадают в конвейер только при импорте.
Преимущества:
- В режиме одного файла
use std.testтоже работает - Версия стандартной библиотеки жёстко привязана к бинарнику, рассинхронизация невозможна
- Пользователю не нужно настраивать путь к стандартной библиотеке
Будущее: стандартная библиотека в файловой системе
Когда проектный режим YaoXiang созреет, стандартная библиотека будет переведена в форму файловой системы. Подробности см. в обновлении RFC-014.
5. Обнаружение и выполнение
Предусловие (резолюция ревью 2026-08-02): CLI run подключается к orchestrator. Текущая реализация CLI run использует однофайловый конвейер (run_file_with_diagnostics) и не может разрешать импорты пользовательских модулей; но модель дочерних процессов yaoxiang test наследует возможности CLI, и импорт файлом тестов модулей проекта — ключевой сценарий. Поэтому в Phase 1 ветка исходного кода CLI Run сначала делегируется в run_project (orchestrator, рекурсивное обнаружение каталогов); обнаружение по use по требованию затем накладывается как чисто оптимизация производительности. Для однофайлового случая без import поведение orchestrator эквивалентно, ветка байткода не меняется.
Этап обнаружения:
- Если указаны
[ПУТИ], используются они напрямую - Иначе читается
[tool.test].patternsизyaoxiang.toml - Если конфигурация отсутствует, по умолчанию
tests/**/*.yx - Применяется фильтр
--filter(по вхождению в имя файла) - Область обнаружения — это уровни тестирования (§9): patterns по умолчанию покрывают только языковой корпус; слой библиотечных тестов (например,
src/std/tests/) обнаруживается через явный путь или конфигурацию пакета и не подмешивается в сканирование по умолчанию
Этап выполнения:
- Для каждого файла выполнение разветвляется по заголовочной директиве (грамматика директив см. §8.2, парсится через
src/util/test_markers.rs, общий с yx_runner):- Поведенческие тесты: дочерний процесс
yaoxiang run <file>(ошибки времени выполнения по умолчанию несут позицию в исходнике и кадры стека — debug_map генерируется по умолчанию, трассировка стека выводитfile:line:col); объявление// mode:указывает режим--runtimeдочернего процесса - Класс отказа на этапе компиляции: один шаг
yaoxiang check <file> - Класс сбоя на этапе выполнения: два шага —
check(должен пройти) +run(должен провалиться)
- Поведенческие тесты: дочерний процесс
- Файлы с
// skip: <причина>пропускаются, учитываются в skipped отчёта - Сравнение вердикта с ожидаемым кодом — по матрице §8.2; ошибка разбора директивы — не выполнять, сразу FAIL (отказ на этапе конструирования)
- Захват stdout/stderr для отчёта
- По умолчанию последовательно; при
--parallel(или[tool.test].parallel) запускается пул worker'ов по числу доступных ядер, каждый файл по-прежнему — отдельный дочерний процесс — skip/invalid обрабатываются первыми в порядке обнаружения, результаты выполнения выводятся потоково по порядку завершения, JSON сортируется по пути (Phase 3, поставка 2026-09-07) - При
--fail-fastпланирование новых файлов останавливается сразу при первом FAIL; в параллельном режиме файлы в работе завершаются и учитываются
6. Изоляция тестов
Изоляция тестов естественным образом обеспечивается границей процессов:
- Каждый тестовый файл выполняется в отдельном дочернем процессе
- Каждый дочерний процесс имеет собственные Heap, Frame, NativeContext
- Паника в одном тестовом файле не влияет на остальные
- Дополнительный механизм изолированного контекста Heap не требуется
- Параллельное выполнение (Phase 3) не расширяет границу изоляции: рабочая директория (CWD) между дочерними процессами общая, параллельные тесты не должны занимать одинаковые пути в CWD — тесты с файловым I/O используют уникальные имена файлов и убирают их по завершении
7. Сьют и множественные тесты (value-модель)
Тестовый файл может содержать несколько тестов. Организация внутри файла (реализовано 2026-09-03):
// tests/list_test.yx
use std.list
use std.result
use std.test
push_grows_len: () -> Result(Void, String) = () => {
xs = [1]
extended = list.push(xs, 2)
test.assert_eq(list.len(extended), 2)
}
pop_returns_last: () -> Result(Void, String) = () => {
mut xs = [1, 2]
last = list.pop(xs)
test.assert_eq(last, 2)
}
main: () -> Void = {
test.suite([
("push_grows_len", () => push_grows_len()),
("pop_returns_last", () => pop_returns_last()),
])
}- Каждый тест — функция без аргументов, возвращающая
Result(Void, String); провал утверждения выражается какErr(value-семантическое семейство §3), процесс не прерывается — последующие тесты выполняются как обычно test.suiteвызывает по очереди и собирает: если результат теста не Ok, записываются имя и диагностика, Ok — молча; после прохождения всех любой Err приводит к аборту черезstd.assert.assertс приложенными деталями провала (N of M test(s) failed+ для каждого пункта[FAIL] имя: диагностика) — exit code файла не равен 0 (правило §5 без изменений). Здесь abort — это рантайм-страж тестового бинарника, а не путь утверждений; полный Ok — тихий выход 0- Тестовые функции верхнего уровня добавляются в список в виде замыканий (
("name", () => test_fn())): использование имени функции верхнего уровня в качестве значения пока не поддерживается (ограничение IR,E3006) — вызов глобальной функции из тела замыкания не затрагивается - Runner видит только файл, сканирования на уровне функций нет: per-test вердикты полностью собираются сьютом, внутренняя структура файла прозрачна для runner — принцип нулевых изменений компилятора не нарушается
- Явно не используется: внутрипроцессная граница catch (железное правило 17 ключевых слов); вход в отдельные функции из runner (только для внутренних сценариев §8.2 вроде провала компиляции)
- Форма API закреплена (2026-09-03):
suite(tests: List((String, () -> Result(Void, String)))) -> Void; дубликаты имён не обнаруживаются (имя используется только для отображения в отчёте);--filterфильтрует по имени файла и не различает имена тестов внутри сьюта
8. Три уровня негативных тестов (ожидаемый провал)
Негативные тесты разделяются по уровню возникновения сбоя, каждый уровень — на своём месте:
8.1 Уровень значения (общий, для пользователей)
Тестируемая операция возвращает Result, тест выражает ожидаемый провал обычным утверждением:
r = range.iter(invalid_range)
test.assert_err(r)
e = result.unwrap_err(r)
test.assert_eq(result.code(e), "E6009")
// или одной обёрткой (код существует только на носителе Error в std, E строго Error):
test.assert_err_code(r, "E6009")assert_not/assert_err/assert_err_codeпоставлены вместе с value-семантическим семейством (2026-09-03, §3); унарная форма!assertожидает реализации синтаксисаnot(то же ограничение, что иcond == falseуassert_false)- Утверждение по коду ошибки полагается на то, что значение
Errorнесёт машиночитаемое полеcode— реализовано:Error = { code, message }(нативнаяerror_new(code, message)), чтение черезresult.unwrap_err(r)для извлечения носителя + аксессорыresult.code(e)/result.message(e)(предполагавшиеся на этапе проектирования именованиеerror_new_with_code, экспорт констант кодов и доступ к полюerr.codeне были приняты — в языке нет доступа к полям Struct, константы кодов не экспортируются) - По мере продвижения Result-изации операции, которые могут провалиться, одна за другой возвращают
Result, файловые негативные метки в корпусе мигрируют в файловые утверждения
8.2 Файловые негативные директивы (только для внутреннего использования разработчиками языка)
Компиляция — это «всё или ничего» для файла, невозможно выразить «эта строка не должна компилироваться» внутри файла; сбой на этапе выполнения тоже требует файлового выражения (например, сьют содержит тесты, которые должны провалиться). Заголовок файла объявляет ожидание структурированной директивой, runner классифицирует и выносит вердикт по категории ожидания (классификация закреплена 2026-09-03; грамматика директив закреплена и реализована 2026-09-06).
Грамматика заголовочных директив (// key: value, в пределах первых 16 строк, строгое совпадение токенов):
// expect: compile-error E1002 [E1003 ...] тест отказа на этапе компиляции
// expect: runtime-error E6003 [...] тест сбоя на этапе выполнения
// skip: <причина> пропустить выполнение, учесть в skipped
// mode: embedded|standard|full режим --runtime дочернего процесса (потребляется только шагом run)- Отсутствие директивы
expect:= поведенческий тест.expect:— единственное объявление ожидания — 2026-09-06 закреплён отказ от булева флага[test:error]и китайского прозаического «预期:» с извлечением кодов: булев флаг и строка ожидания — это два слабо связанных факта, и опора на дисциплину неизбежно ведёт к расхождению; строгая токенная грамматика на английском позволяет runner'у разбирать механически (kind + код — фиксированные токены, любой лишний токен после кода — ошибка разбора), ошибка разбора = не выполнять, сразу FAIL — тихого пути деградации при ошибочной директиве нет (отказ на этапе конструирования) - Класс ошибки компиляции: один шаг
check— должен провалиться, и вывод должен содержать все[EXXXX]; успешная компиляция = FAIL (не сообщили о том, что должны были), отказ с неверным кодом = FAIL. Синтаксические ошибки (E1xxx, фаза парсера) и семантические (E2xxx+) не выделены в отдельные категории — сам ожидаемый код фиксирует фазу - Класс ошибки выполнения: двухшаговый вердикт —
checkдолжен пройти (компиляция чиста),runдолжен провалиться и вывод должен содержать все[EXXXX]. Взрыв на этапе компиляции = FAIL (ключевое правило, обратное классу ошибки компиляции, предотвращающее пропуск «компиляция случайно прошла, выполнение случайно провалилось») - Согласовано с индустрией: Rust compiletest
//~ ERROR, Go// ERROR "regexp", GCCdg-error, Clangexpected-error— все объявляют ожидания внутри комментариев фикстуры и проводят двустороннее сравнение в harness; они используют привязку к строке, потому что их мультидиагностические компиляторы должны различать множество ожиданий в одном файле; наш компилятор останавливается на первой ошибке, один файл — одна диагностика, файловый уровень согласован с реалией компилятора — после реализации восстановления ошибок в грамматику можно добавить привязку к строке (файловые заголовки + ожидания на уровне функций в Cranelift filetests — такой же смешанный формат) - Известный долг рендеринга: диагностика фазы парсинга сейчас выводится в форме Debug (
code: "E0012"вместо[E0012]); сканер кода принимает обе формы; после унификации рендеринга диагностики строгая форма будет возвращена - Служит только корпусу данного репозитория, не является частью пользовательского фреймворка тестирования; соглашение о двойном runner закрыто (2026-09-03): yx_runner (cargo test) и
yaoxiang testсовместно используютsrc/util/test_markers.rsдля разбора заголовочных директив, соглашение о каталоге 06-compile-errors упразднено. Слой отчёта выдаёт подсчёт по категориям: человекочитаемая сводка включает строкуCategories:, JSON summary включаетby_kind(behavior / compile-error / runtime-error / invalid, skipped считается отдельно), каждый файл сопровождается полемkind
8.3 Жёсткий сбой во время выполнения (относится к Result-изации)
Отдельный механизм не предусмотрен — операции, которые могут провалиться, по направлению языка возвращают Result, тесты единообразно выражаются через §8.1. Process-level abort (например, нарушение утверждения, ошибка параметров во время выполнения) по мере Result-изации постепенно сворачивается в значение, фреймворк тестирования не предоставляет для него специальной семантики. (Замечание: файловая метка «ошибка выполнения» из §8.2 — это канал runner'а для файловой верификации операций, которые пока не Result-изируемы, что не противоречит направлению данного раздела — последний является конечной целью, первый — переходным каналом)
9. Расслоение системы тестирования: языковой корпус и библиотечные тесты (закреплено 2026-09-03)
Тесты разделяются на два слоя по объекту тестирования, у каждого своя принадлежность и сопровождающие; система меток (§8.2) и библиотека утверждений (§3) — общие для обоих уровней:
Первый слой: языковой корпус пригодности (tests/yaoxiang/)
- Объект тестирования — сам язык — парсер, система типов, модули, конкурентность, владение, отказы на этапе компиляции, семантика времени выполнения; каталоги организованы по разделам языковой спецификации
- std в корпусе выступает только как инструмент утверждений (
std.assert/std.test) и никогда не тестируется — поведение API библиотеки не относится к пригодности языка - Внутри корпуса по §8 поток делится на три категории вердиктов: поведенческие тесты / тесты отказа на этапе компиляции / тесты сбоя на этапе выполнения
Второй слой: библиотечные тесты (идут вместе с библиотекой)
- Объект тестирования — публичный контракт API библиотеки (например, поведение
list.push, семантикаresult.code) - Тесты пишутся внутри пакета самой библиотеки: для std пакет — это
src/std/, её тесты на уровне yx относятся кsrc/std/tests/(рядом с реализацией; каталог сосуществует с модульными тестами Rust, типы файлов не пересекаются); тесты самойstd.testтоже здесь (тестируем std.test с помощью std.test, самозагрузочный цикл) - Будущие пользовательские пакеты следуют тому же соглашению: тесты внутри пакета, обнаруживаются по
[tool.test]пакета (макет тестового расположения RFC-014 уже обкатан) - Обнаружение не входит в patterns по умолчанию (по умолчанию
tests/**/*.yxпокрывает только языковой слой): слой библиотечных тестов обнаруживается через явный путь (yaoxiang test src/std/tests) или конфигурацию пакета; CI запускает послойно
Заметка о миграции: миграция завершена (2026-09-06) — 19 файлов из прежнего tests/yaoxiang/07-std/ после индивидуального разбора все оказались библиотечными тестами (объект тестирования — API-контракт модуля std; роль таких языковых возможностей, как распространение ?, автоматическое заимствование, инстанциация дженериков, в них — носитель, а не объект тестирования), целиком перенесены в src/std/tests/ с удалением каталога 07-std; yx_runner переходит на обнаружение двух корней (tests/yaoxiang/ + src/std/tests/), patterns по умолчанию не содержат библиотечный слой (интеграционный тест закрепляет этот контракт). Языковой корпус с тех пор не содержит тестов std-API.
Связь с существующими системами
| Элемент | Связь |
|---|---|
Rust #[test] | Не трогается, внутренние тесты компилятора остаются на Rust |
Существующие интеграционные тесты .yx (tests/yaoxiang/) | Обнаруживаются и выполняются yaoxiang test |
std.assert.assert(cond) | Сохранена для рантайм-стражей; value-семантическое семейство std.test построено поверх std.result (§3, §7) |
| Модульная система (RFC-029) | Встроенные исходные модули подключаются через Registry/orchestrator; подключение CLI run к orchestrator — предусловие |
Рефакторинг корпуса (io.println → assert.assert) | Полностью совпадает с направлением yaoxiang test |
Аннотации @ | Не используются, @test не вводится |
Стратегия реализации
Phase 1: основной функционал
Объём изменений:
src/util/diagnostic/mod.rs/src/main.rs— ветка исходного кода CLIRunделегируется вrun_project(предусловие для многофайлового запуска)src/main.rs— новая подкомандаTestsrc/std/test.yx— новый модуль на чистом YaoXiangbuild.rs— встраиваниеstd/*.yxв бинарник- orchestrator / Registry — поддержка загрузки модулей
.yxпо виртуальному пути из встроенных источников - RFC-015, разбор конфигурации — секция
[tool.test] - Выполнение через дочерний процесс + отчёт
Поставка:
yaoxiang testбазово работоспособен- 4 функции утверждений в
std.test - Обнаружение по умолчанию
tests/**/*.yx - Последовательное выполнение + формат вывода по умолчанию
Phase 2: доработка
- Параметры
--filter/--fail-fast/--verbose - Вывод
--json(интеграция с CI) - Опция
--list - Опция
--no-progress
Phase 3: продвинутый уровень (поставлено 2026-09-07)
- Параллельное выполнение
--parallel(пул worker'ов + отдельный дочерний процесс на файл; ключ[tool.test].parallelдействует так же) - Конфигурация
[tool.test].exclude(исключение по префиксу,--listтакже исключает) assert_approx_eq(утверждение с явным eps для Float, §3)
Риски и смягчение
| Риск | Вероятность | Смягчение |
|---|---|---|
f"..." падает на интерполяции в Any | Нет | 2026-08-02 подтверждено эмпирически (Int/String работают) |
Разбор yaoxiang.toml отсутствует в текущем CLI | Низкая | Простое расширение, не затрагивает ядро |
| Подключение CLI run к orchestrator вносит регрессию поведения | Низкая | Для однофайлового пути без import поведение эквивалентно; orchestrator покрыт интеграционными тестами |
Встраивание исходников .yx в бинарник увеличивает размер | Низкая | Исходники .yx крайне малы, пренебрежимо |
| Длительность тестового цикла растёт с корпусом | Высокая | Главная составляющая — полная компиляция каждого файла (185 файлов, замерено 11.3 с), не запуск дочернего процесса; --parallel смягчает только сторону процессов, стоимость компиляции требует кеширования/слайсинга тестового цикла |
Открытые вопросы
- [x] Корректно ли разрешается ссылка
use std.assertвstd/test.yx? — Решено (2026-08-02). После реализации модульной системы (RFC-029) нативные и исходные модули сосуществуют в Registry, resolver разрешает единообразно, межвидовые зависимости работают естественно - [x] Вносит ли дженериковый
to_stringдляf"..."в выводе тестов новые ограничения типов? — Решено (2026-08-02). Эмпирически подтверждено: на неаннотированных параметрах (Any)==/!=и интерполяция f-string работают (проверено на Int/String), новые ограничения не вводятся - [x] Допустимы ли дженериковые параметры
?? — Решено (2026-08-02): синтаксис типа?в настоящее время не существует (и будет молча проглочен, заведён отдельный issue), в Phase 1 функции утверждений используют неаннотированные параметры и не зависят от системы дженериков
Записи о проектных решениях
| Решение | Вердикт | Дата | Обоснование |
|---|---|---|---|
| Способ маркировки тестов | Без аннотаций @test, тестовый файл — обычный .yx | 2026-07-26 | Нулевые изменения компилятора, дочерний процесс уже даёт изоляцию |
| Способ утверждений | Модуль std.test — функции на чистом YaoXiang | 2026-07-26 | Самозагрузка, без нативного кода |
| Модель выполнения тестов | Дочерний процесс yaoxiang run <file> + exit code | 2026-07-26 | Изоляция на уровне процесса, нулевые изменения компилятора |
| Загрузка стандартной библиотеки | Сейчас встраивается в бинарник, в будущем — файловая система | 2026-07-26 | Привязка к версии, доступность в однофайловом режиме |
| Тип параметров утверждений | Неаннотированные параметры (Any), без зависимости от системы дженериков | 2026-08-02 | Синтаксис типа ? не существует; на Any эмпирически работают сравнение и интерполяция |
| Многофайловый запуск | CLI run делегирует в run_project (orchestrator) как предусловие | 2026-08-02 | Модель дочернего процесса наследует возможности CLI; обнаружение по use по требованию вырождается в чистую оптимизацию производительности |
| Источник позиций в отчёте | Ошибки времени выполнения по умолчанию несут позицию | 2026-09-07 | debug_map генерируется по умолчанию, трассировка стека выводит file:line:col; принадлежность кадров, прошедших через встроенный модуль (std.test), этим не гарантируется, относится к RFC-034 |
| Расслоение негативных тестов | Уровень значения общий / структурированная метка для провала компиляции в runner (только внутренне) / жёсткий провал относится к Result-изации | 2026-09-02 | Value-модель закреплена; заменяет неявное соглашение [test:error] |
| Множественные тесты в файле | Value-стандартная модель: тестовые функции возвращают Result, сьют собирает per-test вердикты | 2026-09-02 | Без catch, без покадрового входа (вход только для внутренних сценариев) |
| Код Error | Error получает машиночитаемое поле code | 2026-09-02 | Поддержка утверждений по коду ошибки; коды времени компиляции сверяются runner'ом |
| Форма библиотеки утверждений | Поставлено 7 функций value-семантики, контракт Result(Void, String); переходная abort-версия удалена | 2026-09-03 | Void — канонический unit (() — пустой Tuple, не путать); вложенная позиция как Any жёсткая, неаннотированные параметры не проходят проверку нативного дженерика — параметры должны быть аннотированы явно (подтверждено зондом R1) |
| Расслоение системы тестирования | Языковой корпус (tests/yaoxiang/) и библиотечные тесты (идут с библиотекой, std → src/std/tests/) разделены на два слоя; std в корпусе выступает только как инструмент утверждений | 2026-09-03 | Объект тестирования определяет принадлежность и сопровождающего; библиотечные тесты идут с пакетом — обкатка для RFC-014 |
| Классификация негативных меток | Разделение по категориям ожидания: класс ошибки компиляции — check должен провалиться, класс ошибки выполнения — check должен пройти + run должен провалиться; отчёт выдаёт подсчёт по категориям | 2026-09-03 (реализовано 09-06) | Смешанные категории приведут к пропуску «компиляция случайно прошла, выполнение случайно провалилось»; ожидаемый код фиксирует фазу, синтаксические ошибки не выделены в отдельную категорию |
| Грамматика заголовочных директив | Ожидание объявляется структурированной директивой на английском (// expect: / // skip: / // mode:, строгая токенная грамматика, ошибка разбора = сразу FAIL); отказ от булева флага [test:error] и китайского прозаического «预期:» с извлечением кодов | 2026-09-06 | Ожидание — это атрибут содержимого фикстуры, объявление внутри фикстуры согласовано с индустрией (compiletest / Go / GCC / Clang поступают так же), центральный реестр неизбежно гниёт; булев флаг + строка ожидания как два факта, связанных дисциплиной, — поверхность дефектов; структурированная грамматика позволяет runner'у выносить механический вердикт без участия человека |
| Модель параллельного выполнения | --parallel поднимает пул worker'ов по числу ядер (каждый файл по-прежнему — отдельный дочерний процесс), skip/invalid обрабатываются первыми в порядке обнаружения, результаты выполнения выводятся потоково по порядку завершения, JSON сортируется по пути; --fail-fast останавливает планирование (файлы в работе завершаются и учитываются); CWD остаётся общей, граница изоляции не расширяется | 2026-09-07 | В модели дочерних процессов параллелизм = порождение процессов через планировщик потоков ОС, без конкурентности на уровне yx; главная составляющая затрат — полная компиляция каждого файла (таблица рисков), параллелизм смягчает только сторону процессов — настоящим смягчением будет кеширование/слайсинг тестового цикла |
Ссылки
- RFC-014: Проектирование системы пакетов — структура каталогов стандартной библиотеки
- RFC-015: Система конфигурации — секция конфигурации
[tool.test] - RFC-030: Механизм утверждений assert — базовая зависимость
- Механизм Rust
#[test]— справочное проектирование
