RussianLDP Рейтинг@Mail.ru
WebMoney: 
WMZ Z294115950220 
WMR R409981405661 
WME E134003968233 
Visa 
4274 3200 2453 6495 

Small. Fast. Reliable.
Choose any three.
Использование assert() в SQLite

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 version 3.22.0 (2018-01-22) содержит 5290 макросов assert(), 839 макросов testcase(), 88 макросов ALWAYS() и 63 макросов NEVER().

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

В SQLite присутствие assert(X) значит, что у разработчиков есть доказательство, что X всегда верно. Читатели могут зависеть от X=true, чтобы помочь им рассуждать о коде. 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) не должно быть рассмотрено как система поддержки против ошибок. 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. Сборка конечных версий используется, чтобы проверить произведенный машинный код.

Все тесты должны дать тот же самый ответ во всех трех сборках. Посмотрите здесь подробности.

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

Тестирование функциональностиПокрытиеРелиз
assert(X)abort() если X = false no-opno-op
ALWAYS(X) abort() если X = falseвсегда true передает значение X
NEVER(X)abort() если X = true всегда falseпередает значение X
testcase(X)no-op делает некоторую безопасную работу, если X = true no-op

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

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

testcase(X) обычно no-op, но для покрытия действительно производит небольшое количество дополнительного кода, который включает код, чтобы по крайней мере проверить, что существуют тестовые сценарии, для которых 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 ссылается на глобальную переменную, которая верна, если файл базы данных мог бы содержать повреждения. Эта переменная верна по умолчанию, так как мы обычно не знаем, цела ли база данных, но во время тестирования, работая с базами данных, которые, как известно, правильно построены, эта глобальная переменная может быть установлена в false. Тогда макрос CORRUPT_DB может использоваться в assert() так, как в https://sqlite.org/src/artifact/18a53540aa3?ln=1679-1680. Те assert() определяют предварительные условия для функций , которые верны для последовательных файлов базы данных, но которые могли бы быть ложными, если файл базы данных поврежден. Знание этих видов условий очень полезно читателям, которые пытаются понять блок кода в изоляции.

ALWAYS(X) и NEVER(X) используются в местах, где мы всегда хотим, чтобы тест произошел даже при том, что разработчики полагают, что значение X точно true или false. Например, 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. Подобные проверки помогают предотвратить ошибки диапазона.