Spec-Zone.ru › SQLite

Использование assert() в SQLite

Содержание
1. Assert() и аналогичные макросы в SQLite
1.1. Философия assert()
1.2. Разное поведение в зависимости от типа сборки
2. Примеры

1. Assert() и аналогичные макросы в SQLite

Макрос assert(X) является частью стандартного C в заголовочном файле <assert.h>. SQLite добавляет три других макроса, похожих на assert(): NEVER(X), ALWAYS(X) и testcase(X).

  • assert(X) → Утверждение assert(X) указывает, что условие X всегда истинно. Другими словами, X — это инвариант. Макрос assert(X) работает как процедура, не возвращая никакого значения.

  • ALWAYS(X) → Функция ALWAYS(X) указывает, что условие X всегда истинно, насколько известно разработчикам, но нет доказательства истинности X, или доказательство сложное и подвержено ошибкам, или доказательство зависит от деталей реализации, которые, вероятно, изменятся в будущем. ALWAYS(X) ведет себя как функция, возвращающая булево значение X, и предназначена для использования в условном операторе «if».

  • NEVER(X) → Функция NEVER(X) указывает, что условие X никогда не истинно. Это отрицательный аналог функции ALWAYS(X).

  • testcase(X) → Утверждение testcase(X) указывает, что X иногда истинно, а иногда ложно. Другими словами, testcase(X) указывает, что X определенно не является инвариантом. Поскольку SQLite использует 100% тестирование MC/DC, наличие макроса testcase(X) указывает на то, что не только возможно, что X может быть истинным или ложным, но и есть тестовые случаи, демонстрирующие это.

Версия SQLite 3.22.0 (2018-01-22) содержит 5290 макросов assert(), 839 макросов testcase(), 88 макросов ALWAYS() и 63 макроса NEVER().

1.1. Философия assert()

В SQLite наличие assert(X) означает, что у разработчиков есть доказательство того, что X всегда истинно. Читатели могут полагаться на то, что X истинно, чтобы помочь им понять код. Assert(X) — это сильное утверждение об истинности X. Никаких сомнений.

Макросы ALWAYS(X) и NEVER(X) — это более слабое утверждение об истинности X. Наличие ALWAYS(X) или NEVER(X) означает, что разработчики считают, что X всегда или никогда не истинно, но доказательств нет, или доказательство сложное и подвержено ошибкам, или доказательство зависит от других аспектов системы, которые, вероятно, изменятся.

В других системах иногда assert(X) используется так же, как ALWAYS(X) или NEVER(X) в SQLite. Разработчики добавят assert(X) как молчаливое признание, что они не полностью уверены, что X всегда истинно. Мы считаем, что такое использование assert(X) неправильно и нарушает намерение и цель наличия assert(X) в C в первую очередь. Assert(X) не должен рассматриваться как страховка или залог защиты от ошибок. Assert(X) также не подходит для защиты от ошибок. В этих случаях следует использовать макрос ALWAYS(X) или NEVER(X), или что-то подобное, потому что ALWAYS(X) или NEVER(X) будут следовать за кодом, чтобы реально справиться с проблемой, когда вывод программистов окажется неверным. Поскольку код, следующий за ALWAYS(X) или NEVER(X), не протестирован, это должно быть что-то очень простое, например, оператор «return», который легко проверяется визуально.

Поскольку assert() часто и неправильно используется, некоторые теоретики и разработчики языков программирования относятся к нему с недоверием. Например, разработчики языка программирования Go намеренно опустили встроенную функцию assert(). Они считают, что вред, наносимый неправильным использованием assert(), перевешивает преимущества его включения в качестве встроенной функции языка. Разработчики SQLite не согласны. Фактически, первоначальная цель этой статьи — противостоять распространенному мнению о том, что assert() вреден. На нашем опыте SQLite было бы гораздо сложнее разрабатывать, тестировать и поддерживать без assert().

1.2. Разное поведение в зависимости от типа сборки

Для проверки программного обеспечения SQLite используются три разные сборки.

  1. Функциональная сборка используется для проверки исходного кода.
  2. Сборка для проверки покрытия используется для проверки набора тестов, чтобы подтвердить, что набор тестов обеспечивает 100% MC/DC.
  3. Сборка релиза используется для проверки сгенерированного машинного кода.

Все тесты должны давать одинаковый результат во всех трех сборках. Подробности см. в документе "Как тестируется SQLite".

Разные макросы, похожие на assert(), ведут себя по-разному в зависимости от того, как построено SQLite.

Функциональное тестирование Тестирование покрытия Релиз
assert(X) abort() если X ложно бездействие бездействие
ALWAYS(X) abort() если X ложно всегда истинно пропускает значение X
NEVER(X) abort() если X истинно всегда ложно пропускает значение X
testcase(X) бездействие выполняет некоторую безвредную работу, если X истинно бездействие

По умолчанию assert(X) в стандартном C включен для сборок релиза. Это разумный по умолчанию. Однако в кодовой базе SQLite есть много утверждений assert() в чувствительных к производительности частях кода. Оставление assert(X) включенным приводит к замедлению работы SQLite примерно в три раза. Кроме того, SQLite стремится обеспечить 100% MC/DC в составе поставки, что, очевидно, невозможно, если включены утверждения assert(X). По этим причинам assert(X) — это бездействие для сборок релиза в SQLite.

Макросы ALWAYS(X) и NEVER(X) ведут себя как assert(X) во время функционального тестирования, поскольку разработчики хотят немедленно получить оповещение о проблеме, если значение X отличается от ожидаемого. Но для поставки ALWAYS(X) и NEVER(X) являются простыми пропускающими макросами, которые обеспечивают защиту от ошибок. Для тестирования покрытия ALWAYS(X) и NEVER(X) — это жестко запрограммированные булевы значения, чтобы они не приводили к генерации недостижимого машинного кода.

Макрос testcase(X) обычно является бездействием, но для сборки тестового покрытия он генерирует небольшое количество дополнительного кода, включающего по крайней мере одну ветвь, чтобы проверить, что существуют тестовые случаи, для которых X является как истинным, так и ложным.

2. Примеры

Утверждение assert() часто используется для проверки предварительных условий внутренних функций и методов. Пример: https://sqlite.org/src/artifact/c1e97e4c6f?ln=1048. Это считается лучше, чем просто указание предварительного условия в комментарии к заголовку, поскольку assert() фактически выполняется. В высокотестируемой программе, такой как SQLite, читатель знает, что предварительное условие истинно для всех сотен миллионов тестовых случаев, выполняемых над SQLite, поскольку оно было подтверждено assert(). В отличие от этого, текстовое предварительное условие в комментарии к заголовку не тестируется. Оно могло быть истинным при написании кода, но кто скажет, что оно по-прежнему истинно сейчас?

Иногда SQLite использует assert() с возможностью вычисления на этапе компиляции. Рассмотрим код по адресу https://sqlite.org/src/artifact/c1e97e4c6f?ln=2130-2138. Четыре утверждения assert() проверяют значения констант на этапе компиляции, чтобы читатель мог быстро проверить корректность оператора if, не заглядывая в отдельный заголовочный файл с константными значениями.

Иногда утверждения assert() на этапе компиляции используются для проверки правильности компиляции SQLite. Например, код по адресу https://sqlite.org/src/artifact/c1e97e4c6f?ln=157 проверяет, что макрос препроцессора SQLITE_PTRSIZE правильно задан для целевой архитектуры.

Макрос CORRUPT_DB используется во многих утверждениях assert(). В функциональных сборках CORRUPT_DB ссылается на глобальную переменную, которая истинна, если файл базы данных может содержать повреждения. Эта переменная по умолчанию истинна, поскольку мы обычно не знаем, содержит ли база данных повреждения или нет, но во время тестирования при работе с базами данных, которые, как известно, хорошо сформированы, эту глобальную переменную можно установить в ложь. Тогда макрос CORRUPT_DB может использоваться в утверждениях assert(), как показано по адресу https://sqlite.org/src/artifact/18a53540aa3?ln=1679-1680. Эти assert() указывают предварительные условия для процедуры, которые истинны для согласованных файлов баз данных, но могут быть ложными, если файл базы данных поврежден. Знание этого рода условий очень полезно для читателей, которые пытаются понять блок кода в изоляции.

Функции ALWAYS(X) и NEVER(X) используются в тех местах, где мы всегда хотим выполнить тест, даже если разработчики считают, что значение X всегда истинно или ложно. Например, процедура sqlite3BtreeCloseCursor() должна удалить закрывающий курсор из связанного списка всех курсоров. Мы знаем, что курсор находится в списке, поэтому цикл должен завершиться оператором «break», но удобно использовать тест ALWAYS(X) по адресу https://sqlite.org/src/artifact/18a53540aa3?ln=4371 для предотвращения выхода за пределы связанного списка в случае возникновения ошибки в другой части кода, которая повредила связанный список.

Иногда ALWAYS(X) или NEVER(X) проверяют предварительные условия, которые могут измениться, если другие части кода изменятся тонким образом. По адресу https://sqlite.org/src/artifact/18a53540aa3?ln=5512-5516 у нас есть тест для двух предварительных условий, которые истинны только из-за ограниченной области использования функции sqlite3BtreeRowCountEst(). Будущие улучшения SQLite могут использовать sqlite3BtreeRowCountEst() новыми способами, где эти предварительные условия больше не выполняются, и макросы NEVER() быстро оповестят разработчиков об этом, когда ситуация возникнет. Но если по какой-либо причине предварительные условия не будут выполнены в сборке релиза, программа все равно будет действовать разумно и не произведет недопустимый доступ к памяти.

Макрос testcase() часто используется для проверки того, что граничные случаи неравенства сравнения проверяются. Например, по адресу https://sqlite.org/src/artifact/18a53540aa3?ln=5766. Такого рода проверки помогают предотвратить ошибки «плюс-минус один».

Эта страница была в последний раз изменена 08.01.2022 05:02:57 UTC

SQLite is in the Public Domain.
https://sqlite.org/assert.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API