Skip to content

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/. Это означает:

  1. Модульные тесты стандартной библиотеки (std.math / std.list / std.dict / std.convert / std.io) невозможно писать на YaoXiang
  2. Покрытие модульными тестами каждого модуля стандартной библиотеки заблокировано, поскольку отсутствует доступная тестовая инфраструктура
  3. Регрессионные тесты языковых возможностей (например, изменение семантики 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}"│
└──────────────────────────────────────────────────────────────┘

Основные принципы ​

  1. Фреймворк тестирования — не свойство компилятора, а инструмент CLI — yaoxiang run уже умеет «выполнять тесты»; yaoxiang test лишь запускает все файлы и показывает отчёт
  2. Нулевые изменения компилятора — не вводится сканирование аннотаций @test, секции метаданных в байткоде, специальных точек входа в исполнителе
  3. Самозагрузка — модуль std.test реализован на чистом YaoXiang, базовые возможности предоставляются std.assert / std.result
  4. Тестовый файл — обычный файл .yx — файл выполняется как дочерний процесс, exit code определяет общий результат прохождения/провала
  5. Провал утверждения — это значение, а не событие процесса — тестовая функция возвращает 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):

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 человекочитаемые строки прогресса выводятся потоково по порядку завершения (без чередования блоков), JSON files сортируется по пути file для стабильности вывода (дружелюбно к diff в CI)
  • Массив tests внутри файла per-test берётся из сбора сьютом §7 и вступает в силу после реализации модели значения
  • Официальный CI (.github/workflows/ci.yml job 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:

toml
[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) ​

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) встраиваются в бинарник на этапе сборки:

rust
// build.rs или скрипт сборки, генерируется автоматически
pub const STD_YX_FILES: &[(&str, &str)] = &[
    ("std/test.yx", r#"..."#),  // исходный текст
    // в будущем — больше
];

Точкой подключения служит модульная система (RFC-029, полностью реализовано 2026-08-02): Registry одновременно хранит нативные и исходные модули, за многофайловую оркестровку отвечает orchestrator. Порядок разрешения use std.test:

  1. Сначала поиск нативного модуля Rust (существующий механизм, например std.assert)
  2. Если не найден — поиск во встроенных STD_YX_FILES; при попадании исходный модуль инжектируется в orchestrator по виртуальному пути (например, <std>/test.yx) как seed-модуль и проходит стандартный фронтенд-конвейер (parse → typecheck → IR)
  3. Если не найден — обнаружение через файловую систему (пользовательские модули)

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 эквивалентно, ветка байткода не меняется.

Этап обнаружения:

  1. Если указаны [ПУТИ], используются они напрямую
  2. Иначе читается [tool.test].patterns из yaoxiang.toml
  3. Если конфигурация отсутствует, по умолчанию tests/**/*.yx
  4. Применяется фильтр --filter (по вхождению в имя файла)
  5. Область обнаружения — это уровни тестирования (§9): patterns по умолчанию покрывают только языковой корпус; слой библиотечных тестов (например, src/std/tests/) обнаруживается через явный путь или конфигурацию пакета и не подмешивается в сканирование по умолчанию

Этап выполнения:

  1. Для каждого файла выполнение разветвляется по заголовочной директиве (грамматика директив см. §8.2, парсится через src/util/test_markers.rs, общий с yx_runner):
    • Поведенческие тесты: дочерний процесс yaoxiang run <file> (ошибки времени выполнения по умолчанию несут позицию в исходнике и кадры стека — debug_map генерируется по умолчанию, трассировка стека выводит file:line:col); объявление // mode: указывает режим --runtime дочернего процесса
    • Класс отказа на этапе компиляции: один шаг yaoxiang check <file>
    • Класс сбоя на этапе выполнения: два шага — check (должен пройти) + run (должен провалиться)
  2. Файлы с // skip: <причина> пропускаются, учитываются в skipped отчёта
  3. Сравнение вердикта с ожидаемым кодом — по матрице §8.2; ошибка разбора директивы — не выполнять, сразу FAIL (отказ на этапе конструирования)
  4. Захват stdout/stderr для отчёта
  5. По умолчанию последовательно; при --parallel (или [tool.test].parallel) запускается пул worker'ов по числу доступных ядер, каждый файл по-прежнему — отдельный дочерний процесс — skip/invalid обрабатываются первыми в порядке обнаружения, результаты выполнения выводятся потоково по порядку завершения, JSON сортируется по пути (Phase 3, поставка 2026-09-07)
  6. При --fail-fast планирование новых файлов останавливается сразу при первом FAIL; в параллельном режиме файлы в работе завершаются и учитываются

6. Изоляция тестов ​

Изоляция тестов естественным образом обеспечивается границей процессов:

  • Каждый тестовый файл выполняется в отдельном дочернем процессе
  • Каждый дочерний процесс имеет собственные Heap, Frame, NativeContext
  • Паника в одном тестовом файле не влияет на остальные
  • Дополнительный механизм изолированного контекста Heap не требуется
  • Параллельное выполнение (Phase 3) не расширяет границу изоляции: рабочая директория (CWD) между дочерними процессами общая, параллельные тесты не должны занимать одинаковые пути в CWD — тесты с файловым I/O используют уникальные имена файлов и убирают их по завершении

7. Сьют и множественные тесты (value-модель) ​

Тестовый файл может содержать несколько тестов. Организация внутри файла (реализовано 2026-09-03):

yaoxiang
// 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, тест выражает ожидаемый провал обычным утверждением:

yaoxiang
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 строк, строгое совпадение токенов):

text
// 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", GCC dg-error, Clang expected-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 — ветка исходного кода CLI Run делегируется в run_project (предусловие для многофайлового запуска)
  • src/main.rs — новая подкоманда Test
  • src/std/test.yx — новый модуль на чистом YaoXiang
  • build.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, тестовый файл — обычный .yx2026-07-26Нулевые изменения компилятора, дочерний процесс уже даёт изоляцию
Способ утвержденийМодуль std.test — функции на чистом YaoXiang2026-07-26Самозагрузка, без нативного кода
Модель выполнения тестовДочерний процесс yaoxiang run <file> + exit code2026-07-26Изоляция на уровне процесса, нулевые изменения компилятора
Загрузка стандартной библиотекиСейчас встраивается в бинарник, в будущем — файловая система2026-07-26Привязка к версии, доступность в однофайловом режиме
Тип параметров утвержденийНеаннотированные параметры (Any), без зависимости от системы дженериков2026-08-02Синтаксис типа ? не существует; на Any эмпирически работают сравнение и интерполяция
Многофайловый запускCLI run делегирует в run_project (orchestrator) как предусловие2026-08-02Модель дочернего процесса наследует возможности CLI; обнаружение по use по требованию вырождается в чистую оптимизацию производительности
Источник позиций в отчётеОшибки времени выполнения по умолчанию несут позицию2026-09-07debug_map генерируется по умолчанию, трассировка стека выводит file:line:col; принадлежность кадров, прошедших через встроенный модуль (std.test), этим не гарантируется, относится к RFC-034
Расслоение негативных тестовУровень значения общий / структурированная метка для провала компиляции в runner (только внутренне) / жёсткий провал относится к Result-изации2026-09-02Value-модель закреплена; заменяет неявное соглашение [test:error]
Множественные тесты в файлеValue-стандартная модель: тестовые функции возвращают Result, сьют собирает per-test вердикты2026-09-02Без catch, без покадрового входа (вход только для внутренних сценариев)
Код ErrorError получает машиночитаемое поле code2026-09-02Поддержка утверждений по коду ошибки; коды времени компиляции сверяются runner'ом
Форма библиотеки утвержденийПоставлено 7 функций value-семантики, контракт Result(Void, String); переходная abort-версия удалена2026-09-03Void — канонический 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; главная составляющая затрат — полная компиляция каждого файла (таблица рисков), параллелизм смягчает только сторону процессов — настоящим смягчением будет кеширование/слайсинг тестового цикла

Ссылки ​