Ориентация главы

Каждый 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概念

Семейство утверждений и матрица форм тестов

1Позитивные (имя test)testRequire(условие, "пояснение") - отказ печатает сообщение и выходит
2Потоки (_threads)семантика конкурентности по-настоящему - стресс SPSC / гонки отмены / изоляция слота ошибки
3Негативные (_negative)явный отказ нелегальному вводу - утверждение класса и кода ошибки
4Мутация (_mutation)мутация всех байтов + смешение регистров - устойчивость кругового прогона
5OOM (_oom)провальный распределитель внедряется точка за точкой - матрица на 185 файлов
6Фаззинг (_fuzz)синтаксические семена + детерминированный шум - структурированное исследование

testRequire — единая по всей библиотеке утверждениевая функция (тестовый каркас минимален: отказ печатает сообщение + номер строки + ненулевой выход — шлюз верит только коду выхода). Суффиксы форм — именовательная конвенция: в манифесте с одного взгляда виден покрываемый каждым модулем квадрант (карточка queue: test_queue/test_queue_spsc_threads/test_queue_spsc_oom — три квадранта в наличии).

Поточечный OOM: паттерн провального распределителя

1Сборкасобственный распределитель (флаг Fail / квота Limited / остаток Remaining)
2Циклотказ в точке N -> прогон тестируемого потока -> утверждение (отказ в возврате / состояние не изменилось / ошибка = XERR_MEMORY + домен)
3Дополнительное утверждениесам отчёт об ошибке памяти не запрашивает (ошибку надо уметь сообщать и в 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 — прикладная поза провального распределителя:

examples/memory/fail_inject/main.c源码 ↗
























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/fail_inject/main.c -lws2_32 -liphlpapienabled=1fail-after-2: 1st=ok 2nd=NULL triggered=1event[0]: alloccleared: 3rd=ok triggered-still=0reset=1

Что только что произошло. (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:

examples/memory/debug/main.c源码 ↗




















 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/memory/debug/main.c -lws2_32 -liphlpapilive_count=1 live_bytes=64 peak_bytes=64live address=000002bd... size=64 site=examples/memory/debug/main.c:30alloc_count=1 free_count=1 events=2

Что только что произошло. (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-тестами — нет второй реализации.

Типичные ошибки

Ловушка 1: только позитивные тесты:Ловушка 1: только позитивные тесты

Симптом: слили — в проде падает — три пути (гонки конкурентности/уродливый ввод/нехватка памяти) все непроверены.

Причина: позитивные тесты покрывают только квадрант «верное поведение»; четыре квадранта (верное/конкурентное/неверный ввод/без ресурсов) требуют четырёх оружия — последние три и есть источник инцидентов.

错误写法
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 подвешены к тому же модулю в манифесте */
Ловушка 2: фаззинг на настоящей случайности (невоспроизводимо):Ловушка 2: фаззинг на настоящей случайности (невоспроизводимо)

Симптом: CI изредка падает — локальный перезапуск проходит (семя другое), отчёт не может локализовать.

Причина: ценность фаззинга в воспроизводимости — детерминированный шум фиксированного семени отыгрывает каждый отказ. Настоящая случайность — только офлайн-поиск (найдя, переводим в фикс-семенной случай и заносим).

错误写法
srand(time(NULL));            /* каждый раз другое - отказ потерян */
run_fuzz(random_input());
正确写法
uint32 State = UINT32_C(0x9E3779B9);  /* фиксированное семя */
run_fuzz(noise(&State));               /* семя отказа отыгрывается */
Ловушка 3: OOM-тест остановился на «вернул отказ»:Ловушка 3: OOM-тест остановился на «вернул отказ»

Симптом: после 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки