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

Small. Fast. Reliable.
Choose any three.
Как SQLite проверен
Оглавление

1. Введение

Надежность SQLite достигаются частично полным и тщательным тестированием.

С version 3.42.0 (2023-05-16) библиотека SQLite состоит приблизительно из 155.8 KSLOC кода C. (KSLOC это тысяча "Source Lines Of Code" или, другими словами, строк кода, исключая пустые строки и комментарии. Для сравнения у проекта есть в 590 раз больше тестового кода и сценариев тестирования, 92053 KSLOC.

1.1. Резюме

  • Четыре независимо развитых испытательных блока
  • 100% тестовое покрытие в развернутой конфигурации
  • Миллионы и миллионы тестовых сценариев
  • Тесты Out-of-memory
  • Тесты I/O error
  • Тесты сбоев и потери питания
  • Тесты Fuzz
  • Тесты граничного значения
  • Отключенные тесты на оптимизацию
  • Регрессионные тесты
  • Уродливые тесты базы данных
  • Широкое применение assert() и проверки на этапе выполнения
  • Анализ Valgrind
  • Неопределенные проверки поведения
  • Контрольные списки

2. Ограничения проверок

Есть четыре независимых испытательных блока, используемые для тестирования основной библиотеки SQLite. Каждый испытательный блок безопасности разрабатывается, сохраняется и управляется отдельно от других.

  1. Тесты TCL это оригинальные тесты для SQLite. Они содержатся в том же самом исходном дереве, как ядро SQLite и как ядро SQLite находится в общественном достоянии. Тесты TCL это основные тесты, используемые во время развития. Тесты TCL написаны, используя TCL scripting language. Сам испытательный блок TCL состоит из 27.2 KSLOC кода C, используемого, чтобы создать интерфейс TCL. Сценарии тестирования содержатся в 1390 файлах всего 23.2 МБ в размере. Есть 51445 отличных тестовых сценариев, но многие тестовые сценарии параметризуются и исполняются многократно (с различными параметрами) так, чтобы на полном тестовом прогоне миллионы отдельных тестов были выполнены.

  2. TH3 блок тестов это ряд собственных тестов, написанных на C, которые обеспечивают 100% тестовое покрытие отделения (и 100% тестовое покрытие MC/DC) основной библиотеки SQLite. Тесты TH3 разработаны, чтобы работать на вложенных и специализированных платформах, которые легко не поддержали бы TCL или другие службы рабочей станции. Тесты TH3 используют только изданные интерфейсы SQLite. TH3 состоит приблизительно из 76.9 МБ или 1055.4 KSLOC кода C, осуществляющего 50362 отличных тестовых сценария. Тесты TH3 в большой степени параметризуются, тем не менее, так полный охват тестовые прогоны приблизительно 2.4 миллиона различных испытательных случаев. Случаи, которые обеспечивают 100% тестовое покрытие отделения, составляют подмножество полного набора тестов TH3. Тест до выпуска делает приблизительно 248.5 миллионов тестов. Дополнительная информация о TH3 есть отдельно.

  3. SQL Logic Test или SLT используется, чтобы управлять огромными числами SQL-операторов для SQLite и нескольких других движков базы данных SQL и проверить, что они все получают те же самые ответы. SLT в настоящее время сравнивает SQLite с PostgreSQL, MySQL, Microsoft SQL Server и Oracle 10g. SLT управляет 7.2 миллионами запросов, включающих 1.12 ГБ данных тестирования.

  4. Движок dbsqlfuzz это собственный fuzz тестер. Другие fuzzer для SQLite видоизменяют входы SQL или файл базы данных. Dbsqlfuzz видоизменяет SQL и файл базы данных в то же время, и таким образом в состоянии достигнуть новых состояний ошибки. Dbsqlfuzz строится, используя libFuzzer framework LLVM с собственным мутатором. Есть 336 файлов семени. dbsqlfuzz fuzzer управляет приблизительно одним миллиардом испытательных мутаций в день. Dbsqlfuzz помогает гарантировать, что SQLite прочен против нападения через злонамеренный SQL или входные базы данных.

В дополнение к четырем главным испытательным блокам безопасности есть несколько других небольших программ.

  1. "speedtest1.c" оценивает исполнение SQLite при типичной рабочей нагрузке.
  2. "mptester.c" это тест напряжения на многократные процессы, одновременно читая и записывая единую базу данных.
  3. "threadtest3.c" это тест напряжения на многократные потоки, используя SQLite одновременно.
  4. "fuzzershell.c" используется, чтобы запустить некоторые тесты fuzz.

Все тесты выше должны работать успешно на многих платформах и под многими конфигурациями времени компиляции перед каждым выпуском SQLite.

До каждой регистрации к исходному дереву SQLite разработчики, как правило, управляют подмножеством (названным "veryquick") тестов Tcl, состоящим приблизительно из 304.7 тысяч тестовых сценариев. Тесты veryquick включают большинство тестов кроме аномалии, fuzz и soak. Идея позади тестов veryquick состоит в том, что они достаточны, чтобы зафиксировать большинство ошибок, но также работают всего несколько минут вместо нескольких часов.

3. Тестирование аномалии

Тестирование аномалии это тесты, разработанные, чтобы проверить правильное поведение SQLite, когда что-то идет не так, как надо. (Относительно) легко построить движок базы данных SQL, который ведет себя правильно на правильно построенных входах на полностью функциональном компьютере. Более трудно построить систему, которая нормально отвечает на недействительные входы и продолжает функционировать после системных сбоев. Тесты аномалии разработаны, чтобы проверить последнее поведение.

3.1. Тесты Out-Of-Memory

SQLite, как все движки SQL, делает широкое применение malloc() (см. отдельный отчет о динамическом выделении памяти в SQLite для дополнительных подробностей). На серверах и рабочих станциях malloc() никогда не терпит неудачу на практике и таким образом, правильная обработка ошибок out-of-memory (OOM) не особенно важна. Но на встроенных устройствах, ошибки OOM пугающе распространены и так как SQLite часто используется на встроенных устройствах, важно, чтобы SQLite был в состоянии изящно обработать ошибки OOM.

Тестирование OOM достигается, моделируя ошибки OOM. SQLite позволяет запросу заменить альтернативой внедрение malloc(), используя sqlite3_config( SQLITE_CONFIG_MALLOC,...). Тесты TCL и TH3 способны к вставке измененной версии malloc(), которая может быть подстроена, чтобы потерпеть неудачу после определенного числа отчислений. Эти инструментованные malloc может собираться, чтобы потерпеть неудачу только однажды, а затем начать работать снова, или продолжать терпеть неудачу после первой неудачи. Тесты OOM сделаны в цикле. На первом повторении инструментованный malloc подстроен, чтобы потерпеть неудачу на первом распределении. Тогда некоторая операция SQLite выполняется, и проверки сделаны, чтобы удостовериться, что SQLite обработал ошибку OOM правильно. Тогда счетчик time-to-failure на инструментованном malloc увеличен на единицу, и тест повторяется. Цикл продолжается до конца всех операционных проходов, никогда не сталкиваясь с моделируемой неудачей OOM. Тесты запущены дважды, однажды с инструментованным набором malloc, чтобы потерпеть неудачу только однажды, и снова с инструментованным набором malloc, чтобы терпеть неудачу непрерывно после первой неудачи.

3.2. Тесты I/O Error

Тестирование I/O error стремится проверить, что SQLite нормально отвечает на сбои операции I/O. Ошибки I/O могли бы следовать из полного диска, работающих со сбоями дисковых аппаратных средств, сетевых отключений электричества, используя сетевую файловую систему, конфигурацию системы или изменения разрешения, которые происходят посреди операции SQL, или других сбоев аппаратной или операционной системы. Безотносительно причины важно, чтобы SQLite были в состоянии правильно ответить на эти ошибки, и тестирование I/O стремится проверить, что это делается.

Тестирование I/O error подобно в понятии тестированию OOM, ошибки I/O моделируются, и проверки осуществлены, чтобы проверить, что SQLite правильно отвечает на моделируемые ошибки. Ошибки I/O моделируются в TCL и в испытательных блоках TH3, вставляя новый объект Virtual File System, который особенно настроен, чтобы моделировать ошибку I/O после некоего количества набора операций I/O. Как с ошибкой OOM при тестировании, ошибочные симуляторы I/O могут собираться потерпеть неудачу только однажды или терпеть неудачу непрерывно после первой неудачи. Тесты запущены в цикле, медленно увеличивая пункт неудачи до выполнения тестового сценария до завершения без ошибки. Циклом управляют дважды, однажды с ошибочным набором симулятора I/O, чтобы моделировать только единственную неудачу и во второй раз, чтобы нарушить все операции I/O после первой неудачи.

В тестах I/O error после того, как отключен механизм моделирования неудачи I/O, база данных исследована, используя PRAGMA integrity_check, чтобы удостовериться, что ошибка I/O не ввела повреждение базы данных.

3.3. Тестирование на сбои

Тестирование на сбои стремится продемонстрировать, что база данных SQLite не повреждена, если прикладная или операционная система потерпит крах или если есть перебой в питании посреди обновления базы данных. Отдельный отчет, названный атомная передача в SQLite описывает защитную меру, которую SQLite принимает, чтобы предотвратить повреждение базы данных после катастрофы. Краш-тесты стремятся проверить, что те защитные меры работают правильно.

Альтернативная Virtual File System вставляется, которая позволяет испытательному блоку моделировать состояние файла базы данных после катастрофы.

В испытательном блоке TCL моделирование катастрофы сделано в отдельном процессе. Главный процесс тестирования порождает дочерний процесс, который управляет некоторой операцией SQLite и беспорядочно терпит крах где-нибудь посреди операции записи. Специальный VFS беспорядочно переупорядочивает и портит несинхронизированные операции записи, чтобы моделировать эффект буферизированных файловых систем. После того, как дочерний процесс вылетает, оригинальный испытательный процесс открывает и читает испытательную базу данных и проверяет, что изменения закончены успешно или были полностью отменены. integrity_check PRAGMA используется, чтобы удостовериться, что никакое повреждение базы данных не происходит.

Блок тестов TH3 должен работать во встроенных системах, у которых не обязательно есть способность породить дочерние процессы, таким образом, это использует an in-memory VFS в памяти, чтобы моделировать катастрофы. VFS в памяти может быть подстроен, чтобы сделать снимок всей файловой системы после заданного количества операций I/O. Краш-тестами управляют в цикле. На каждом повторении пункт, в котором сделан снимок, продвинут до проверяемых операций SQLite. В цикле, после того, как операция SQLite при тесте закончилась, файловая система вернулась к снимку, вводится случайное повреждение файла, которое характерно для видов повреждения, которое ожидают видеть после потери питания. Тогда база данных открыта, и проверки осуществлены, чтобы гарантировать, что это правильно построено и что транзакция дошла до завершения или была полностью отменена. Интерьер цикла повторяется многократно для каждого снимка с различным случайным повреждением каждый раз.

3.4. Составные тесты на неудачу

Наборы тестов для SQLite также исследуют результат укладки многократных отказов. Например, тесты запущены, чтобы гарантировать правильное поведение, когда ошибка I/O или ошибка OOM происходят, пытаясь прийти в себя после предшествующей катастрофы.

4. Тестирование Fuzz

Тестирование Fuzz стремится установить, что SQLite правильно отвечает неправильным, out-of-range или поврежденным вводам.

4.1. SQL Fuzz

SQL fuzz состоит из создания синтаксически корректных, но дико бессмысленных SQL-операторов и передачи к SQLite, чтобы видеть то, что это сделает с ними. Обычно некоторая ошибка возвращена (такая, как "no such table"). Иногда, просто случайно, SQL-оператор оказывается семантически правилен. В этом случае получающимся подготовленным запросом управляют, чтобы удостовериться, что он дает разумный результат.

4.1.1. SQL Fuzz, используя The American Fuzzy Lop Fuzzer

Понятие тестирования fuzz было использовано в течение многих десятилетий, но тестирование не было эффективным способом найти ошибки до 2014, когда Michal Zalewski изобрел первый практический управляемый профилем fuzzer, American Fuzzy Lop или "AFL". В отличие от предшествующих fuzzer, которые вслепую производят случайные входы, AFL инструментует проверяемую программу (изменяя вывод ассемблера из компилятора C) и использует ту инструментовку, чтобы обнаружить, когда вход заставляет программу делать что-то другое, чтобы следовать за новым путем контроля или закрепить цикл различное число раз. Входы, которые вызывают новое поведение, сохранены и далее видоизменены. Таким образом AFL в состоянии "обнаружить" новые поведения программы при тесте, включая поведения, которые никогда не предполагались проектировщиками.

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

Из-за его прошлого успеха AFL стал стандартным компонентом стратегии тестирования SQLite с version 3.8.10 (2015-05-07), пока это не было заменено лучшим fuzzer в version 3.29.0 (2019-07-10).

4.1.2. Google OSS Fuzz

С 2016 команда инженеров в Google начала проект OSS Fuzz. OSS Fuzz применяет fuzzer стиля AFL на инфраструктуре Google. Fuzzer автоматически загружает последние регистрации для участвующих проектов, тестирует их и посылает электронное письмо разработчикам, сообщая о любых проблемах. Когда внесены правки, fuzzer автоматически обнаруживает это и посылает подтверждение по электронной почте разработчикам.

SQLite это один из многих проектов с открытым исходным кодом, которые проверяет OSS Fuzz. test/ossfuzz.c в хранилище SQLite это интерфейс SQLITE к OSS fuzz.

OSS Fuzz больше не находит исторические ошибки в SQLite. Но это все еще действительно иногда находит проблемы в регистрациях новой разработки. Примеры: [1] [2] [3].

4.1.3. dbsqlfuzz fuzzer

Начавшись в конце 2018, SQLite был тестирован с использованием собственного fuzzer, названного "dbsqlfuzz". Dbsqlfuzz строится, используя libFuzzer framework of LLVM.

dbsqlfuzz fuzzer видоизменяет вход SQL и файл базы данных в то же время. Dbsqlfuzz использует свой Structure-Aware Mutator на специализированном входном файле, который определяет входную базу данных и код на SQL, которым будут управлять для той базы данных. Поскольку это видоизменяет входную базу данных и вход SQL в то же время, dbsqlfuzz в состоянии найти некоторые неясные ошибки в SQLite, которые были пропущены предшествующим fuzzer, который видоизменил только входы SQL или только файл базы данных. Разработчики SQLite держат dbsqlfuzz, работающий для ствола приблизительно на 16 ядрах в любом случае. Каждый экземпляр программы dbsqlfuzz в состоянии оценить приблизительно 400 тестовых сценариев в секунду, означая, так что приблизительно 500 миллионов случаев проверяются каждый день.

dbsqlfuzz fuzzer был очень успешен при укреплении кодовой базы SQLite против вредоносной атаки. Так как dbsqlfuzz был добавлен к внутреннему набору тестов SQLite, отчеты об ошибках от внешних fuzzer, таких как OSSFuzz, почти остановились.

Обратите внимание на то, что dbsqlfuzz не основанный на Protobuf осведомленный о структуре fuzzer для SQLite, который используется Хромом и описывается в Structure-Aware Mutator article. Нет никакой связи между этими двумя fuzzer кроме того, что они на основе libFuzzer. The Protobuf fuzzer для SQLite пишется и поддерживается авторами Chromium в Google, а вот dbsqlfuzz пишется и поддерживается авторами SQLite. Наличие многих независимо развитых fuzzer для SQLite хорошо, поскольку это означает, что неясные проблемы, более вероятно, будут раскрыты.

4.1.4. Другой сторонний fuzzer

SQLite, кажется, популярная цель третьих лиц для тестирования. Разработчики слышат о многих попытках тестирования SQLite и они действительно иногда находили отчеты об ошибках, найденных независимыми fuzzer. Все такие отчеты быстро фиксируются, таким образом, продукт улучшен, и все пользовательское сообщество SQLite извлекает выгоду. Этот механизм наличия многих независимых тестеров подобен Linus's law: "given enough eyeballs, all bugs are shallow".

Один особо значимый исследователь это Manuel Rigger в настоящее время (поскольку этот параграф написан 2019-12-21) в ETH Zurich. Большинство fuzzer ищет только ошибки утверждения, катастрофы, неопределенное поведение (UB) или другие легко обнаруженные аномалии. fuzzer доктора Риггера, с другой стороны, в состоянии найти случаи, где SQLite вычисляет неправильный ответ. Риггер нашел много таких случаев. Большинство этих находок это неясные случаи, включающие преобразования типов и преобразования близости, и большое количество находок против невыпущенных особенностей. Тем не менее, его находки все еще важны, поскольку они реальные ошибки, и разработчики SQLite благодарны, что в состоянии определить и решить основные проблемы. Работа Риггера в настоящее время не опубликована. Когда это выпущено, это могло влиять как изобретение Zalewski's AFL и управляемого профилем fuzzing.

4.1.5. Ограничения тестирования fuzzcheck

Исторические тестовые сценарии от AFL, OSS Fuzz и dbsqlfuzz собраны в ряде файлов базы данных в главном исходном дереве SQLite и затем запущены повторно программой "fuzzcheck" каждый раз, когда выполняется "make test". Fuzzcheck управляет всего несколькими тысячами "интересных" случаев из миллиардов случаев, которые различные fuzzer исследовали за эти годы. "Интересные" случаи это такие случаи, которые показывают ранее невидимое поведение. Фактические ошибки, найденные fuzzer, всегда включаются среди интересных тестовых сценариев, но большинство случаев, которыми управляет fuzzcheck, никогда не было фактическими ошибками.

4.1.6. Напряженность между тестированием Fuzz и 100% MC/DC

Тестирование Fuzz и 100% MC/DC в противоречии дург с другом. То есть код, проверенный 100% MC/DC, будет иметь тенденцию быть более уязвимым для проблем, найденных fuzzing, и код, который выступает хорошо во время тестирования fuzz, будет иметь тенденцию иметь (намного) меньше, чем 100% MC/DC. Это вызвано тем, что тестирование MC/DC препятствует защитному коду с недостижимыми отделениями, но без защитного кода fuzzer более вероятно, найдет путь, который вызывает проблемы. Тестирование MC/DC, кажется, работает хорошо на нормы сборки и правила, которые прочны во время нормальной эксплуатации, тогда как тестирование fuzz хорошо для норм и правил, которые прочны против вредоносной атаки.

Конечно, пользователи предпочли бы код, который является прочным в нормальной эксплуатации и стойким к вредоносной атаке. Цель этой секции состоит в том, чтобы просто указать, что выполнение обоих требований в то же время трудно.

Для большой части его истории SQLite был сосредоточен на 100% MC/DC. Сопротивление fuzzing стал беспокойством только с введением AFL в 2014. Некоторое время fuzzer находили много проблем в SQLite. В более свежих годах стратегия тестирования SQLite развилась, чтобы уделить больше внимания тестированию fuzz. Мы все еще поддерживаем 100% MC/DC основного кода, но большая часть тестирования циклов CPU теперь посвящена fuzzing.

В то время как тестирование fuzz и 100% MC/DC находятся в напряженности, они не полностью противоположны. То, что набор тестов SQlite действительно проверяет к 100% MC/DC означает, что, когда fuzzer действительно находят проблемы, те проблемы могут быть решены быстро и с небольшим риском новых ошибок.

4.2. Уродливые файлы базы данных

Есть многочисленные тестовые сценарии, которые проверяют, что SQLite в состоянии иметь дело с уродливыми файлами базы данных. Эти тесты сначала строят правильно построенный файл базы данных, затем добавляют повреждение, изменяя один или несколько байтов в файле некоторыми средствами кроме SQLite. Тогда SQLite используется, чтобы прочитать базу данных. В некоторых случаях изменения байтов вносятся посреди данных. Это заставляет содержание базы данных изменяться, сохраняя базу данных правильно построенной. В других случаях изменяются неиспользованные байты файла, которые не имеют никакого эффекта на целостность базы данных. Интересные случаи это когда байты файла, которые определяют структуру базы данных, изменяются. Уродливые тесты базы данных проверяют, что SQLite находит ошибки формата файла и сообщает о них, используя код возврата SQLITE_CORRUPT, не переполняя буфера или выполняя другие вредные действия.

dbsqlfuzz fuzzer также делает превосходную работу по подтверждению, что SQLite нормально отвечает на уродливые файлы базы данных.

4.3. Тесты граничного значения

SQLite определяет лимиты на свое действие, такие как максимальное количество колонок в таблице, максимальную длину SQL-оператора или максимальное значение целого числа. TCL и наборы тестов TH3 содержат многочисленные тесты, которые выдвигают SQLite на край его определенных пределов и проверяют, что это работает правильно для всех позволенных значений. Дополнительные тесты идут вне определенных пределов и проверяют, что SQLite правильно возвращает ошибки. Исходный код содержит testcase macros, чтобы проверить, что были проверены обе стороны каждой границы.

5. Регрессионное тестирование

Каждый раз, когда об ошибке сообщают, ту ошибку не считают устраненной до введения новых тестовых сценариев, которые показали бы ошибку, и были добавлены к TCL или к наборам тестов TH3. За эти годы это привело к тысячам и тысячам новых тестов. Эти регрессионные тесты гарантируют, что ошибки, которые были исправлены в прошлом, не представлены повторно в будущих версиях SQLite.

6. Автоматическое обнаружение утечки ресурсов

Утечка ресурсов происходит, когда системные ресурсы ассигнуются и никогда не освобождаются. Самые неприятные утечки ресурсов во многих случаях это утечки памяти, когда память ассигнуется, используя malloc(), но никогда не освобождается через free(). Но другие виды ресурсов могут также быть пропущены: дескрипторы файлов, потоки, mutexes и т. д.

TCL и TH3 проверки автоматически контролируют ресурсы системы транспортировки и утечки ресурсов на каждом тесте. Никакая специальная конфигурация или установка не требуются. Испытательные блоки особенно бдительны относительно утечек памяти. Если изменение вызовет утечку памяти, испытательные блоки признают это быстро. SQLite разработан, чтобы никогда не упустить память, даже после исключения, такого как ошибка OOM или ошибка дискового I/O. Испытательные блоки внимательны, чтобы провести в жизнь это.

7. Тестовое покрытие

У ядра SQLite, включая Unix VFS, есть 100% тестовое покрытие отделения под TH3 в его конфигурации по умолчанию, как измерено gcov. Расширения, такие как FTS3 и RTree, исключены из этого анализа.

7.1. Заявление против освещения отделения

Есть много способов измерить тестовое покрытие. Самая популярная метрика это "освещение заявления". Когда вы слышите, что кто-то говорит, что их программа как "тестовое покрытие на "XX% test coverage" без дальнейшего объяснения, они обычно имеют в виду освещение заявления. Освещение заявления измеряет, какой процент строк кода выполняется, по крайней мере, однажды набором тестов.

Освещение отделения более строго, чем освещение заявления. Освещение отделения измеряет количество команд перехода машинного кода, которые оценены, по крайней мере, однажды на обоих направлениях.

Чтобы иллюстрировать различие между освещением заявления и освещением отделения, рассмотрите следующую гипотетическую строку кода C:

if( a>b && c!=25 ){ d++; }

Такая строка кода C могла бы произвести дюжину отдельных команд машинного кода. Если кто-либо из тех инструкций когда-либо оценивается, то мы говорим, что заявление было проверено. Так, например, могло бы иметь место, что условное выражение всегда ложное, и переменная "d" никогда не увеличивается. Несмотря на это, освещение заявления считает эту строку проверенной.

Освещение отделения более строго. С освещением отделения, каждый тест и каждый подблок в рамках заявления рассматривается отдельно. Чтобы достигнуть 100% освещения отделения в примере выше, должно быть по крайней мере три тестовых сценария:

  • a<=b
  • a>b && c==25
  • a>b && c!=25

Любой из вышеупомянутых тестовых сценариев предоставил бы 100% страховую защиту заявления, но все три требуются для 100% освещения отделения. Вообще говоря, 100% освещение отделения подразумевает 100% освещение заявления, но обратное неверно. Чтобы повторно подчеркнуть, испытательный блок TH3 для SQLite обеспечивает более сильную форму тестового покрытия: 100% тестовое покрытие отделения.

7.2. Тестирование освещения защитного кода

Хорошо написанная программа на C будет, как правило, содержать некоторые защитные условные предложения, которые на практике являются всегда верными или всегда ложными. Это приводит к программной дилемме: надо ли удалять защитный код, чтобы получить 100% освещение отделения?

В SQLite ответ на предыдущий вопрос "нет". Для целей тестирования исходный код SQLite определяет макросы под названиями ALWAYS() и NEVER(). Макрос ALWAYS() окружает условия, которые, как ожидают, всегда оценят как верные, и NEVER() окружает условия, которые всегда оцениваются как ложные. Макрос служит комментарием, чтобы указать, что условия это защитный код. В сборках конечных версий макросы переданы:

#define ALWAYS(X)  (X)
#define NEVER(X)   (X)

Во время большей части тестирования, однако, макрос бросит ошибку утверждения, если у их аргумента не будет ожидаемого значения истинности. Это предупреждает разработчиков быстро о неправильных предположениях дизайна.

#define ALWAYS(X)  ((X)?1:assert(0),0)
#define NEVER(X)   ((X)?assert(0),1:0)

Измеряя тестовое покрытие, макрос определяется, чтобы быть постоянным значением истинности так, чтобы он не производил команды перехода ассемблера, а следовательно не играл роли, вычисляя освещение отделения:

#define ALWAYS(X)  (1)
#define NEVER(X)   (0)

Набор тестов разработан, чтобы управляться три раза, однажды для каждого определения ALWAYS() и NEVER(), показанных выше. Все три тестовых прогона должны привести точно к тому же самому результату. Есть тест во время выполнения, используя sqlite3_test_control( SQLITE_TESTCTRL_ALWAYS, ...), который может использоваться, чтобы проверить, что макрос правильно установлен в первую форму (форма передачи) для развертывания.

7.3. Принуждение освещения граничных значений и булевых векторных тестов

Другой макрос, используемый вместе с измерением тестового покрытия, является макросом testcase(). Аргумент это условие, для которого мы хотим тестовые сценарии, которые оцениваются к true и false. В сборках конечных версий макрос testcase() ничего не делает:

#define testcase(X)

Но в измерении освещения макрос testcase() производит код, который оценивает условное выражение в его аргументе. Тогда во время анализа, проверка осуществлена, чтобы гарантировать, чтобы тесты существовали, которые оценивают условное предложение к true и false. Testcase() используется, например, чтобы помочь проверить, что граничные значения проверены. Например:

testcase(a==b);
testcase(a==b+1);
if (a>b && c!=25)
{
   d++;
}

Макрос Testcase также используется, когда два или больше случая оператора переключения идут в тот же самый блок кода, чтобы удостовериться, что код был достигнут всеми случаями:

switch( op )
{
  case OP_Add:
  case OP_Subtract:
  {
    testcase( op==OP_Add );
    testcase( op==OP_Subtract );
    /* ... */
    break;
  }
  /* ... */
}

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

testcase( mask & SQLITE_OPEN_MAIN_DB );
testcase( mask & SQLITE_OPEN_TEMP_DB );
if( (mask & (SQLITE_OPEN_MAIN_DB|SQLITE_OPEN_TEMP_DB))!=0 ){ ... }

Исходный код SQLite содержит 1184 использования макроса testcase().

7.4. Освещение отделения против MC/DC

Два метода измерения тестового покрытия были описаны выше: "заявление" и освещение "отделения". Помимо этих двух есть много других метрик тестового покрытия. Другая популярная метрика это "Измененное освещение условия/решения" или MC/DC. Wikipedia определяет MC/DC так:

  • Каждое решение пробует каждый возможный исход.
  • Каждое условие в решении берет каждый возможный исход.
  • Каждый пункт входа и выхода вызван.
  • Каждое условие в решении, как показывают, независимо затрагивает результат решения.

В языке C, где && и || это операторы "короткого замыкания", MC/DC и освещение отделения это почти то же самое. Главная разница находится в булевых векторных тестах. Можно проверить на любые из нескольких битов в битовом векторе и все еще получить 100% тестовое покрытие отделения даже при том, что второй элемент MC/DC (требование, чтобы каждое условие в решении взяло каждый возможный исход) не мог бы быть удовлетворен.

SQLite использует макрос testcase(), как описано в предыдущем подразделе, чтобы удостовериться, что каждое условие в решении битового вектора берет каждый возможный исход. Таким образом SQLite также достигает 100% MC/DC в дополнение к 100% освещению отделения.

7.5. Измерение освещения отделения

Освещение отделения в SQLite в настоящее время измеряется, используя gcov с опцией "-b". Сначала тестовая программа собрана, используя варианты "-g -fprofile-arcs -ftest-coverage", затем тестовой программой управляют. Далее "gcov -b" управляют, чтобы произвести отчет об освещении. Отчет об освещении многословен и неудобен, чтобы читать, таким образом, этот отчет обрабатывается, используя некоторые простые сценарии, чтобы привести его в более человечески-благоприятный формат. Этот процесс автоматизирован, используя скрипты, конечно.

Обратите внимание на то, что управление SQLite с gcov не является тестом SQLite, это тест набора тестов. gcov, которым управляют, не проверяет SQLite, потому что опции -fprofile-args и -ftest-coverage заставляют компилятор производить различный код. gcov, которым просто управляют, проверяет, что набор тестов обеспечивает 100% тестовое покрытие отделения. gcov, которым управляют, является тестом на тест, метатестом.

После того, как gcov управляли, чтобы проверить 100% тестовое покрытие отделения, тогда тестовая программа повторно собрана, используя параметры компилятора без опций -fprofile-arcs и -ftest-coverage, и тестовая программа запущена повторно. Этот второй пробег и есть фактический тест SQLite.

Важно проверить, что gcov тестовый прогон и второй реальный тестовый прогон оба дают тот же вывод. Любые различия в нем указывают на использование неопределенного поведения в коде SQLite (и следовательно ошибку) или на ошибку в компиляторе. Обратите внимание на то, что SQLite за предыдущее десятилетие столкнулся с ошибками в GCC, Clang и MSVC. Ошибки компилятора, хоть и редки, но действительно происходят, поэтому столь же важно проверить код в поставляемой конфигурации.

7.6. Тестирование мутации

Использование gcov (или подобного), чтобы показать то, что каждая команда перехода взята, по крайней мере, однажды в обоих направлениях, является хорошей мерой качества набора тестов. Но еще лучше показывает, что каждая команда перехода имеет значение в выводе. Другими словами, мы хотим показать не только, что каждая команда перехода проваливаются, оба скачка и, но также и что каждое отделение делает полезную работу и что набор тестов в состоянии обнаружить и проверить ту работу. Когда найдено отделение, которое не имеет значения в выводе, это предполагает, что код, связанный с отделением, может быть удален (сокращение размера библиотеки и возможно ускорение работы) или что набор тестов неверно проверяет опцию, которую реализует отделение.

SQLite стремится проверить, что каждая команда перехода имеет значение, используя тестирование мутации. Скрипт сначала собирает исходный код SQLite на ассемблере (используя, например, опцию -S для gcc). Затем скрипт пошагово проходит ассемблерный код, меняя каждую команду перехода на безусловный переход или на no-op, собирает результат и проверяют, что набор тестов ловит мутацию.

К сожалению, SQLite содержит много команд перехода, которые помогают коду, работать быстрее, не изменяя вывод. Такие отделения производят ложные положительные сигналы во время тестирования мутации. Как пример, допустим, что следующая хэш-функция, которая ускоряет поиск имени таблицы:

55  static unsigned int strHash(const char *z){
56    unsigned int h = 0;
57    unsigned char c;
58    while( (c = (unsigned char)*z++)!=0 ){     /*OPTIMIZATION-IF-TRUE*/
59      h = (h<<3) ^ h ^ sqlite3UpperToLower[c];
60    }
61    return h;
62  }

Если команда перехода, которая осуществляет "c!=0" в строке 58 изменяется в no-op, тогда цикл с условием продолжения образует вечный цикл, и набор тестов потерпит неудачу с тайм-аутом. Но если то отделение будет изменено на безусловный переход, то хэш-функция будет всегда возвращать 0. Проблема состоит в том, что 0 это действительный хэш. Хэш-функция, которая всегда возвращает 0, все еще работает в том смысле, что SQLite все еще всегда получает правильный ответ. Хэш-таблица имени таблицы ухудшается в связанный список и поиски имени таблицы во время парсинга SQL-операторов могли бы быть немного медленнее, но конечным результатом будет то же самое.

Для обхода проблемы комментарии вида "/*OPTIMIZATION-IF-TRUE*/" и "/*OPTIMIZATION-IF-FALSE*/" вставляются в исходный код SQLite, чтобы сказать скрипту тестирования мутации игнорировать некоторые команды перехода.

7.7. Опыт с полным тестовым покрытием

Разработчики SQLite нашли, что тестирование полного охвата это чрезвычайно эффективный метод для выявления и предотвращения ошибок. Поскольку каждая команда перехода в основном коде SQLite покрыта тестовыми сценариями, разработчики могут быть уверены, что у изменений, внесенных в одной части кода, нет непреднамеренных последствий в других частях. Много новых особенностей и повышений производительности, которые были добавлены к SQLite в последние годы, невозможны без доступности тестирования полного охвата.

p>Поддержание 100% MC/DC трудоемкое и отнимающее много времени. Уровень усилий показывает, что тестирование полного охвата, вероятно, не экономически эффективно для типового приложения. Однако мы думаем, что тестирование полного охвата оправдано для очень широко развернутой библиотеки инфраструктуры как SQLite, и специально для библиотеки базы данных, которая по самому ее характеру "помнит" прошлые ошибки.

8. Динамический анализ

Динамический анализ относится к внутренним и внешним проверкам кода SQLite, которые выполнены, в то время как код действует. Динамический анализ, оказалось, был большой помощью в поддержании качества SQLite.

8.1. Assert

Ядро SQLite включает 6754 assert(), которые проверяют предварительные условия функции, инварианты циклов и выходные условия. Assert() это макрос, который является стандартным компонентом ANSI C. Аргумент это булево значение, которое, как предполагается, всегда верно. Если утверждение ложное, программа печатает сообщение об ошибке.

Макрос Assert() отключен, собрав с определенным макросом NDEBUG. В большинстве систем assert позволен по умолчанию. Но в SQLite утверждение столь многочислено и находится в таких критически важных местах, что ядро базы данных работает приблизительно в три раза медленнее, когда assert позволены. Следовательно, умолчание собирает SQLite с выключенным assert. Операторы контроля позволены только, когда SQLite собран с определенным макросом препроцессора SQLITE_DEBUG.

8.2. Valgrind

Valgrind это возможно, самый удивительный и полезный инструмент разработчика в мире. Valgrind это симулятор, он моделирует x86, управляющий x86 Linux binary. Порты Valgrind для платформ кроме Linux находятся в развитии, но сейчас Valgrind работает достоверно только в Linux, что по мнению разработчиков SQLite означает, что Linux должен быть предпочтительной платформой для всей разработки программного обеспечения. Поскольку Valgrind управляет Linux binary, он ищет все виды интересных ошибок, такие как перерасходы множества, чтение от неинициализированной памяти, переполнения стека, утечек памяти и т. д. Valgrind находит проблемы, которые могут легко уменьшиться посредством всего лишь тестового прогона против SQLite. И, когда Valgrind действительно находит ошибку, он может перевести разработчика непосредственно в символический отладчик в точном месте, где ошибка происходит, чтобы облегчить быстрое исправление.

Поскольку это симулятор, управлять binary в Valgrind медленнее, чем управление им на собственном оборудовании. Таким образом, невозможно управлять полным набором тестов SQLite через Valgrind. Однако, тесты veryquick и освещение тестов TH3 выполняются Valgrind для каждого выпуска.

8.3. Memsys2

SQLite содержит подключаемую подсистему выделения памяти. Реализация по умолчанию использует системные malloc() и free(). Однако, если SQLite собран с SQLITE_MEMDEBUG, альтернативная обертка выделения памяти (memsys2) вставляется, которая ищет ошибки распределения памяти во время выполнения. Обертка memsys2 проверяет на утечки памяти, конечно, но также ищет переполнение буфера, использование неинициализированной памяти и пытается использовать память после того, как это было освобождено. Эти же самые проверки также сделаны valgrind (и, действительно, Valgrind делает их лучше), но memsys2 имеет преимущество того, что он намного быстрее, чем Valgrind, что означает, что проверки могут быть сделаны чаще и для более длительных тестов.

8.4. Mutex Assert

SQLite содержит подключаемую mutex-подсистему. В зависимости от вариантов времени компиляции система mutex содержит интерфейсы sqlite3_mutex_held() и sqlite3_mutex_notheld(), которые обнаруживают, проводится ли конкретный mutex вызывающим потоком. Эти два интерфейса используются экстенсивно в assert() в SQLite, чтобы проверить, что mutex проведены и выпущены во все правильные моменты, чтобы перепроверить, что SQLite действительно работает правильно в многопоточных приложениях.

8.5. Тесты журнала

Одна из вещей, которые SQLite делает, чтобы гарантировать, что транзакции атомарны через системные катастрофы и перебои в питании, состоит в том, чтобы написать все изменения в файл журнала отмены до изменения базы данных. Испытательный блок TCL содержит альтернативное внедрение OS backend, который помогает проверить, что это происходит правильно. "journal-test VFS" контролирует весь дисковый I/O между файлом базы данных и журналом отмены проверяя, чтобы удостовериться, что ничто не написано в файл базы данных, что сначала не писалось и синхронизировалось к журналу отмены. Если какие-либо несоответствия найдены, ошибка утверждения поднята.

Тесты журнала это дополнительная перепроверка свыше краш-тестов, чтобы удостовериться, что транзакции SQLite будут атомными через системные катастрофы и перебои в питании.

8.6. Неопределенные проверки поведения

В языке C очень легко написать код, у которого есть "неопределенное" поведение. Это означает, что код мог бы работать во время развития, но дать различный ответ на различной системе или когда повторно собран, используя различные параметры компилятора. Примеры неопределенного и определенного внедрением поведения в ANSI C включают:

  • Переполнение Signed integer. Signed integer overflow не обязательно меняет знак, как большинство людей ожидает.
  • Сдвиг N-bit integer больше, чем на N бит.
  • Сдвиг на отрицательную сумму.
  • Сдвиг не в ту сторону.
  • Применение функции memcpy() при наложении буферов.
  • Порядок оценки аргументов функции.
  • Переменные "char" signed или unsigned.

Так как неопределенное и определенное внедрением поведение непортативное и может легко привести к неправильным ответам, SQLite очень упорно работает, чтобы избежать его. Например, добавляя два значения столбцов целого числа вместе как часть SQL-оператора, SQLite просто не добавляет их вместе с использованиес оператора "+" языка C. Вместо этого это сначала проверяет, чтобы удостовериться, что дополнение не переполнится, и если это так, делает дополнение, используя плавающую точку вместо этого.

Чтобы помочь гарантировать, что SQLite не использует неопределенное поведение, наборы тестов запущены повторно, используя инструментованную сборку, которая пытается обнаружить неопределенное поведение. Например, наборами тестов управляют, используя опцию "-ftrapv" в GCC. И ими управляют снова, используя опцию "-fsanitize=undefined" Clang. А потом еще раз с опцией "/RTC1" MSVC. Тогда наборы тестов запущены повторно, используя такие варианты, как "-funsigned-char" и "-fsigned-char", чтобы удостовериться, что различия во внедрении не имеют значения также. Тесты повторяются на 32-битных и 64-битных системах и на системах с прямым и с обратным порядком байтов, используя множество архитектур ЦП. Кроме того, наборы тестов увеличены со многими тестовыми сценариями, которые сознательно разработаны, чтобы вызвать неопределенное поведение. Например: "SELECT -1*(-9223372036854775808);".

9. Отключенные тесты на оптимизацию

sqlite3_test_control( SQLITE_TESTCTRL_OPTIMIZATIONS, ...) позволяет отобранной оптимизации SQL-оператора быть отключенной во время выполнения. SQLite должен всегда производить точно тот же самый ответ с позволенной и с отключенной оптимизацией, ответ просто будет более быстрый с включенной оптимизацией. Таким образом в производственной среде, каждый всегда оставляет оптимизацию включенной (настройка по умолчанию).

Один метод проверки, используемый SQLite, должен управлять всем набором тестов дважды, однажды с оптимизацией, а во второй раз с выключенной оптимизацией, и проверить, что вывод получен оба раза одинаковый. Это показывает, что оптимизация не вводит ошибки.

Не все тестовые сценарии могут быть обработаны так. Некоторые тестовые сценарии проверяют, что оптимизация действительно уменьшает объем вычисления, считая количество доступов к диску, операций по сортировке, шагов полного просмотра или других шагов обработки, которые происходят во время запросов. Те тестовые сценарии, будет казаться, потерпят неудачу, когда оптимизация будет отключена. Но большинство тестовых сценариев просто проверяет, что правильный ответ был получен, всеми теми случаями можно управлять успешно и без оптимизации, чтобы показать, что оптимизация не вызывает сбои.

10. Контрольные списки

Разработчики SQLite используют контрольный список онлайн, чтобы скоординировать деятельность тестирования и проверить, что каждый выпуск SQLite проходит все тесты. Прошлые контрольные списки сохраняются для исторической справки. Контрольные списки только для чтения для анонимных интернет-пользователей, но разработчики могут авторизоваться и обновить пункты контрольного списка в своих веб-браузерах. Использование контрольных списков для тестирования SQLite и других опытно-конструкторских разработок вызвано The Checklist Manifesto.

Последние контрольные списки содержат приблизительно 200 пунктов, которые индивидуально проверяются для каждого выпуска. Некоторые пункты контрольного списка занимают только несколько секунд, чтобы проверить. Другие включают наборы тестов, которые выполняются в течение многих часов.

Контрольный список выпуска не автоматизирован: разработчики управляют каждым пунктом списка вручную. Мы находим, что важно держать человека в тонусе. Иногда проблемы найдены, управляя пунктом контрольного списка даже при том, что сам тест прошел. Важно иметь человека, рассматривающего испытательный вывод на высшем уровне и постоянно разбирающегося "Это действительно правильно?"

Контрольный список выпуска непрерывно развивается. Поскольку новые проблемы или потенциальные проблемы обнаружены, новые пункты контрольного списка добавляются, чтобы удостовериться, что те проблемы не появляются в последующих версиях. Контрольный список выпуска, оказалось, был неоценимым инструментом, чтобы гарантировать, что ничто не пропущено во время процесса выпуска.

11. Статический анализ

Статический анализ означает анализировать исходный код во время компиляции, чтобы проверить на правильность. Статический анализ включает сообщения предупреждения компилятора и больше всесторонних аналитических движков, таких как Clang Static Analyzer. SQLite собирается без предупреждений на GCC и Clang, используя флаги -Wall и -Wextra в Linux и Mac и в MSVC под Windows. Никакие действительные предупреждения не произведены Clang Static Analyzer "scan-build" (хотя последние версии clang, кажется, производят много ложных положительных срабатываний). Тем не менее, некоторые предупреждения могли бы быть произведены другими статическими анализаторами. Пользователи поощряются не заморачиваться этими предупреждениями, а сосредоточиться на интенсивном тестировании SQLite, описанного выше.

Статический анализ не был полезен в нахождении ошибок в SQLite. Статический анализ нашел несколько ошибок в SQLite, но это исключения. Больше ошибок было введено в SQLite, пытаясь заставить его собираться без предупреждений, чем было найдено статическим анализом.

12. Итог

SQLite open source. Это дает многим людям идею, что это не хорошо проверено как коммерческое программное обеспечение и возможно ненадежно. Но то впечатление ложное. SQLite показал очень высокую надежность в области и очень низкую скорость дефектообразования, особенно рассмотрев, как быстро это развивается. Качество SQLite достигается частично тщательной кодовой разработкой и реализацией. Но обширное тестирование также играет жизненно важную роль в поддержании и улучшении качества SQLite. Этот документ суммировал процедуры тестирования, которым каждый выпуск SQLite подвергается с надеждой на внушение доверия, что SQLite подходит для использования в приложениях для решения ответственных задач.