Ориентация главы
Каждый API предыдущих глав охраняет толпа тестов — эта глава о морфологии этой толпы. Тестовая система XRT (подвешена на поле tests манифеста, гоняется build.py на обоих треках) имеет четыре оружия: позитивные тесты (семейство утверждений testRequire(条件, "消息") (условие, "сообщение") — приёмка, лаконичная до одной строки); потоковые тесты (суффикс _threads — семантика конкурентности, измеренная по-настоящему); негативные и мутационные тесты (суффиксы _negative/_mutation — устойчивость к уродливому вводу: круговые прогоны с мутацией всех байтов, граничные семена); поточечные OOM-тесты (суффикс _oom — 185 файлов! Провальный распределитель внедряется точка за точкой, проверяя «отказ не ломает состояние, ошибка сообщает верно»). Фаззинг (fuzz/): входы в стиле LibFuzzer плюс структурированный фаззинг внутри репозитория (детерминированный шум сдвигового регистра с синтаксическими семенами впереди — форма test_http_auth_fuzz). Четыре оружия покрывают четыре квадранта «верное поведение, поведение под конкурентностью, поведение на неверном вводе, поведение без ресурсов».
133.1引入
Почему OOM-тестов 185 файлов? Потому что исчерпание памяти — самый легко пропускаемый путь: разработчик пишет код в предположении успешного выделения (в обычной среде так и есть), и тесты работают на машинах, где памяти в достатке — «очистка после неудачного выделения» становится чистой непроверенной зоной. А именно этот путь прячет больше всего утечек и порчи состояния (наполовину инициализированные структуры, частично построенные контейнеры). Дисциплина XRT — поточечная проверка: провальный распределитель (вроде testMapOomAlloc — «пока разрешено, пересылает вниз; при исчерпании квоты возвращает NULL») заставляет каждое выделение тестируемого кода отказывать по очереди — один раунд на N-й отказ, цикл, пока каждая точка не побывает отказавшей — «отказ выделения не ломает состояние» становится исчерпывающе перечисленным фактом, а не предположением.
Мотив мутационных тестов тот же, но объект другой: устойчивость парсеров. Заголовки HTTP, percent-кодирование, сообщения TLS — реальная сеть приносит любые последовательности байтов; десятков самодельных случаев никогда не хватает. Мутационное тестирование переворачивает каждый байт корректного ввода по одному, затем целые раунды случайного шума — «произвольный ввод не роняет, отказ идёт через структурированную ошибку» доказано машиной. Фаззинг добавляет слой: синтаксические семена (реальные формы протокола) + детерминированная случайная вариация — в CI выкатываются комбинации, до которых человек не додумается.
133.2概念
Семейство утверждений и матрица форм тестов
testRequire — единая по всей библиотеке утверждениевая функция (тестовый каркас минимален: отказ печатает сообщение + номер строки + ненулевой выход — шлюз верит только коду выхода). Суффиксы форм — именовательная конвенция: в манифесте с одного взгляда виден покрываемый каждым модулем квадрант (карточка queue: test_queue/test_queue_spsc_threads/test_queue_spsc_oom — три квадранта в наличии).
Поточечный OOM: паттерн провального распределителя
Глава 6 (отладка памяти) разбирала инструментальную грань провального распределителя (xrtMemDebugFailAfter); тестовая система использует его плюс самописные квотные распределители, покрывая каждый модуль. «Отчёт об ошибке не выделяет память» — повторяющееся утверждение (xrtErrorCreate при отказе сохраняет в слоте категорию OOM — система ошибок жива при истощении ресурсов): тестовая грань обещания «минимальных зависимостей» главы 4.
Мутационные тесты: круговой прогон и стабильность
Две формы: мутация всех байтов (корректный вектор → каждый байт по одному ±1/инвертирован → повторный разбор/декодирование — отказ или успех оба допустимы, но без падений и утечек, а успехи семантически совпадают с оригиналом); шумовые круговые прогоны (детерминированный случайный ввод — например, 6000 раундов percent «любые байты + смешение регистров экранирования стабильно проходят круг»). Детерминированность (фиксированные семена) — ключ: отказы воспроизводимы — найденную фаззингом проблему отыгрывают, сообщив семя.
Фаззинг: семена + шум + инварианты
Структура test_http_auth_fuzz — шаблон: (1) синтаксические семена впереди (массив строк реальных форм протокола — пустая строка / стандартный Basic/Bearer/Digest со сложными параметрами — известные границы проходят первыми); (2) детерминированный шум (случайный вход переменной длины от сдвигового регистра — исследование комбинаций, до которых человек не додумается); (3) утверждения инвариантов (разбор либо успешен, либо структурно отказывает — FuzzAbort с номером строки сообщает место нарушения контракта, «избегая потери диагностики после склейки холодных ветвей оптимизатором» — учтён даже компилятор). Каталог fuzz/ (четыре входа в стиле LibFuzzer у xhttp: http_auth/http_route/http_router/http_sse): постоянные входы для внешних фаззеров — CI быстро гоняет встроенный фаззинг, офлайн глубоко гоняет внешний фаззер, инварианты одни.
133.3示例
Первая полная программа: повседневная грань внедрения сбоев
Программа ниже из examples/memory/fail_inject — прикладная поза провального распределителя:
*/ #include <stdio.h> #include <xrt.h> /* 事件访问器:只打印前几个 alloc 事件的名字。 */ static bool printEvent(const xmemdebugevent* pEvent, ptr pUserData) { size_t* pCount = (size_t*)pUserData; if ( *pCount < 1u && pEvent->Kind == XMEMDEBUG_ALLOC ) { printf("event[%zu]: %s\n", *pCount, xrtMemDebugEventName(pEvent->Kind)); } (*pCount)++; return true; } int main(void) { ptr pFirst; ptr pSecond; ptr pThird; size_t iEvents = 0; (void)xrtMemDebugReset(); (void)xrtMemDebugEnable(true); printf("enabled=%d\n", xrtMemDebugEnabled() ? 1 : 0); /* 注入:第 2 次分配失败。 */ (void)xrtMemDebugFailAfter(1u); pFirst = xrtMalloc(16); pSecond = xrtMalloc(16); printf("fail-after-2: 1st=%s 2nd=%s triggered=%d\n", pFirst != NULL ? "ok" : "NULL", pSecond != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pFirst); /* 事件流:最早的 alloc 事件可按名访问。 */ (void)xrtMemDebugVisit(printEvent, &iEvents); /* 清除未触发注入后再分配恢复成功;已触发的标志保持。 */ xrtMemDebugFailClear(); pThird = xrtMalloc(16); printf("cleared: 3rd=%s triggered-still=%d\n", pThird != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pThird); (void)xrtMemDebugEnable(false); printf("reset=%d\n", xrtMemDebugReset() ? 1 : 0); return 0; }
Что только что произошло. (1) Измеренная семантика xrtMemDebugFailAfter(N): первые N успешны, N+1-й отказывает — в выводе 1st=ok 2nd=NULL и есть FailAfter(1). (2) Поток событий (event[0]: alloc) — упорядоченный доступ к событиям отладочной кучи: внедрённый отказ тоже входит в поток событий (аудит полон). (3) FailClear сбрасывает заодно флаг «уже сработало» (после cleared 3rd=ok и triggered-still=0) — все варианты _oom-тестов построены на этом наборе входов (заголовочный комментарий дословно: тесты и приложения делят одну реализацию — очередное «нет второй реализации»).
Вторая полная программа: локализация утечки
Вторая программа из examples/memory/debug — тройка Reset/Snapshot/VisitLive:
*/ #include <stdio.h> #include <xrt.h> */ static bool printAllocation(const xmemdebugallocation* pAllocation, ptr pUserData) { (void)pUserData; printf("live address=%p size=%zu site=%s:%u\n", pAllocation->Address, pAllocation->Size, pAllocation->File != NULL ? pAllocation->File : "unknown", (unsigned int)pAllocation->Line); return true; } int main(void) { xmemdebugsnapshot tSnapshot; unsigned char* pMemory; /* 起点清零:本范例观察到的所有事件都来自接下来的代码。 */ if ( !xrtMemDebugReset() ) { return 1; } pMemory = (unsigned char*)xrtMalloc(64); if ( pMemory == NULL ) { return 2; } pMemory[0] = 0x5A; /* 泄漏现场:64 字节仍在活动列表,分配点精确到本文件第 30 行。 */ xrtMemDebugSnapshot(&tSnapshot); printf("live_count=%zu live_bytes=%zu peak_bytes=%zu\n", tSnapshot.LiveCount, tSnapshot.LiveBytes, tSnapshot.PeakBytes); (void)xrtMemDebugVisitLive(printAllocation, NULL); /* 释放后再拍快照:Live 归零,累计计数器对账(1 alloc + 1 free)。 */ xrtFree(pMemory); xrtMemDebugSnapshot(&tSnapshot); printf("alloc_count=%llu free_count=%llu events=%zu\n", (unsigned long long)tSnapshot.AllocCount, (unsigned long long)tSnapshot.FreeCount, tSnapshot.EventCount); /* 收尾复位,不影响后续程序段的调试观察。 */ return xrtMemDebugReset() ? 0 : 3; }
Что только что произошло. (1) Стандартная поза локализации утечки (заголовочный комментарий): Reset до подозрительного участка, Snapshot после — LiveCount, не возвращающийся к нулю, плюс site, напечатанный VisitLive (точно до файл:строка), и есть точка утечки. (2) В выводе alloc_count=1 free_count=1, но live_count=1 — живой блок после одного alloc и одного free? Это намеренно оставленный образцом блок (показать, что VisitLive его видит) — мышление сверки счётчиков: три счётчика (выделение/освобождение/живое) перекрёстно проверяют дисциплину распределения. (3) Более сильная способность (карантин, ловящий двойное освобождение/UAF) — в форме JSON-отчёта (образец debug_report) — тесты и эксплуатация делят одни данные.
Третий этаж тестовой пирамиды: граница мутации и фаззинга
Представьте тесты XRT пирамидой: основание — масса позитивных утверждений (десятки testRequire на модуль — поведение прибито пункт за пунктом); середина — расширение по квадрантам (threads/oom — по группе на каждый из двух дорогих квадрантов); вершина — мутация и фаззинг — механическое исследование пространства входов. Границу стоит запомнить: мутация встаёт на якорь известного корректного ввода (переворачивает каждый байт — гарантируя, что «нелегальное рядом с легальным» проходит); фаззинг встаёт на якорь синтаксических шаблонов (случайно собранный похожий на настоящий ввод — исследование области «структурно похоже, но никогда не встречалось»). Первое защищает от регрессий (поломали старые границы), второе — от неизвестного (спрятаны незаписанные границы). Оба живут на детерминированности как линии жизни — это и есть предпосылка входа в CI (минуты прогона, отказ всегда воспроизводим); действительно случайный глубокий поиск остаётся офлайновым внешним фаззерам.
133.4契约
- Именование форм: суффиксы
_threads/_negative/_mutation/_oom/_fuzz— покрываемые квадранты видны в манифесте. - Минимализм утверждений: семейство testRequire — отказ печатает + ненулевой выход; шлюз верит только коду выхода.
- OOM по точкам: провальный распределитель вращается точка за точкой; утверждается отказ в возврате + неизменность состояния + XERR_MEMORY + верный домен.
- Система ошибок жива: отчёт об ошибке в OOM не выделяет память — слот сохраняет категорию OOM.
- Детерминированность мутации: фиксированные семена — отказы воспроизводимы (семя есть удостоверение повтора).
- Инварианты прежде: фаззинг утверждает альтернативу «успех или структурный отказ» — падения/утечки суть нарушения контракта.
- Семена впереди: синтаксические граничные семена перед случайным шумом — известные границы обязаны проходить.
- Двухрежимный фаззинг: встроенный детерминированный (CI) + входы для внешних фаззеров (офлайн-глубина) — инварианты одни.
- Примеры есть тестовое имущество: образцы fail_inject/debug делят входы с _oom-тестами — нет второй реализации.
Типичные ошибки
Симптом: слили — в проде падает — три пути (гонки конкурентности/уродливый ввод/нехватка памяти) все непроверены.
Причина: позитивные тесты покрывают только квадрант «верное поведение»; четыре квадранта (верное/конкурентное/неверный ввод/без ресурсов) требуют четырёх оружия — последние три и есть источник инцидентов.
testRequire(parse(str) == OK, "parse"); /* конкурентность? уродство? OOM? - всё непроверено */
/* хотя бы по группе на квадрант: позитивные + threads + mutation/negative + oom */ testRequire(parse(str) == OK, "parse"); /* tests/xxx/test_xxx_threads.c / _mutation.c / _oom.c подвешены к тому же модулю в манифесте */
Симптом: CI изредка падает — локальный перезапуск проходит (семя другое), отчёт не может локализовать.
Причина: ценность фаззинга в воспроизводимости — детерминированный шум фиксированного семени отыгрывает каждый отказ. Настоящая случайность — только офлайн-поиск (найдя, переводим в фикс-семенной случай и заносим).
srand(time(NULL)); /* каждый раз другое - отказ потерян */ run_fuzz(random_input());
uint32 State = UINT32_C(0x9E3779B9); /* фиксированное семя */ run_fuzz(noise(&State)); /* семя отказа отыгрывается */
Симптом: после OOM состояние испорчено — следующий вызов падает; или категория ошибки неверна (OOM выдан за ошибку параметра).
Причина: полное OOM-утверждение тройное: отказ в возврате + состояние не изменилось (объект продолжает работать или безопасно разрушается) + ошибка верна (XERR_MEMORY + домен). Проверка только возвращаемого значения пропускает половину багов.
testRequire(map_insert(...) == false, "oom fails"); /* состояние? ошибка? - не проверено */
testRequire(map_insert(...) == false, "oom fails"); testRequire(xrtErrorKind(xrtGetError()) == XERR_MEMORY, "kind"); testRequire(map_still_consistent(&Map), "state intact"); /* состояние не изменилось */ testRequire(map_destroy_path_clean(&Map), "可安全清理"); /* финал без второго падения */
Практические задания
build.py --suite queue — наблюдать запуск вариантов threads/oom. Критерии приёмки: для каждого тестового файла можете назвать покрываемый квадрант.
Написать небольшой модуль с выделением (скажем, простой кэш) с позитивным/threads/mutation/oom тестовыми файлами в манифесте. Критерии приёмки: намеренно оставить утечку на OOM-пути — oom-тест её ловит (сверка отладочной кучей).
По образцу test_http_auth_fuzz: синтаксические семена (≥8 реальных форм) + детерминированный шум 5000 раундов + инварианты (разбор успешен или структурно отказывает). Критерии приёмки: внедрить роняющий баг (скажем, чтение за границей) — фаззер ловит за ≤1000 раундов; вывод несёт отыгрываемое семя.
Итоги
| Тема | Кратко |
|---|---|
| Четыре оружия | позитивные (testRequire) / потоки (_threads) / мутация-негатив (_mutation/_negative) / OOM (_oom) |
| Фаззинг | _fuzz встроенный + fuzz/ внешние входы — синтаксические семена впереди + детерминированный шум |
| Паттерн OOM | провальный распределитель вращается по точкам; тройное утверждение (возврат/состояние/ошибка) |
| Выживание ошибки | отчёт в OOM не выделяет память — слот сохраняет категорию |
| Формы мутации | каждый байт перевёрнут + круговые прогоны детерминированного шума |
| Детерминированность | фиксированные семена — отказы воспроизводимы; настоящая случайность только офлайн |
| Инварианты | альтернатива «успех или структурный отказ» — падение/утечка есть нарушение |
| Прикладная грань | образцы fail_inject/debug делят входы с тестами — нет второй реализации |
| Конвенция именования | суффиксы форм — квадранты покрытия видны в манифесте |
| Факт масштаба | 185 файлов _oom — поточечная проверка есть дисциплина, а не лозунг |
📖 Справочники API в этой главе:core (25 записей)memory_debug (19 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки