- SQLite maintient un haut niveau de fiabilité et de robustesse grâce à un système de tests automatisés extrêmement rigoureux, avec 590 fois plus de code de test que de code produit
- Quatre harness de test indépendants (TCL, TH3, SQL Logic Test, dbsqlfuzz) valident la bibliothèque principale et exécutent des centaines de millions de tests
- Des tests de situations anormales (OOM, erreurs d’E/S, simulation de crash) et des tests de fuzzing permettent de vérifier un fonctionnement stable même face à des entrées invalides et à des défaillances système
- Des procédures de validation multicouches sont maintenues, notamment 100 % de couverture de branches et MC/DC, détection des fuites de ressources, Valgrind, analyse statique et checklist
- Grâce à cette approche de test systématique, SQLite est considéré comme une base de données open source offrant une fiabilité et une qualité de niveau base de données commerciale
1. Vue d’ensemble
- La fiabilité et la robustesse de SQLite proviennent d’un processus de test minutieux
- À la version 3.42.0, SQLite se compose d’environ 155.8 KSLOC de code C et de 92053.1 KSLOC de code de test
- Le système de test comprend 4 harness indépendants, une couverture de branches à 100 % et des millions de cas de test
- Il inclut notamment des tests OOM, d’erreurs d’E/S, de crash, de fuzzing, de valeurs limites, de régression, de fichiers DB anormaux et de désactivation des optimisations
2. Harness de test
- TCL Tests
- Ensemble de tests public principalement utilisé pendant le développement de SQLite
- Composé de 27.2 KSLOC de code C et de 1390 fichiers de scripts (23.2MB)
- Environ 50 000 cas de test, et plusieurs millions d’exécutions lors d’un run complet grâce à la paramétrisation
- TH3
- Ensemble de tests commercial en C atteignant 100 % de couverture de branches et MC/DC
- Fonctionne aussi en environnement embarqué, avec 1055.4 KSLOC et environ 50 000 cas
- Environ 2,4 millions d’exécutions pour un test complet de couverture, et 248 millions de soak tests avant publication
- SQL Logic Test (SLT)
- Compare les résultats de SQLite avec PostgreSQL, MySQL, SQL Server et Oracle 10g
- Composé de 7,2 millions de requêtes et de 1.12GB de données
- dbsqlfuzz
- Fuzzer basé sur libFuzzer qui fait muter simultanément le SQL et les fichiers de base de données
- Effectue environ 1 milliard de tests de mutation par jour pour vérifier la robustesse face à des entrées malveillantes
- Outils supplémentaires
- speedtest1.c, mptester.c, threadtest3.c, fuzzershell.c, jfuzz, etc.
- Tous les tests doivent passer sur plusieurs plateformes et configurations de compilation avant qu’une release soit possible
3. Tests de situations anormales
- Tests OOM
- Simulent des échecs de
malloc() pour vérifier la capacité de récupération en cas de manque de mémoire
- Répétés en incrémentant le compteur du point d’échec
- Tests d’erreurs d’E/S
- Utilisent un système de fichiers virtuel (VFS) pour simuler des erreurs disque
- Vérifient l’intégrité des données après erreur avec
PRAGMA integrity_check
- Tests de crash
- Simulent des coupures d’alimentation et des crashs du système d’exploitation
- Le harness TCL s’appuie sur des processus enfants, TH3 sur un VFS en mémoire
- Vérifient qu’une transaction est soit totalement annulée, soit totalement terminée
- Tests de pannes combinées
- Vérifient aussi les cas où un OOM ou une erreur d’E/S survient en chaîne après un crash
4. Tests de fuzzing
- SQL Fuzz
- Génère du SQL syntaxiquement valide mais anormal pour vérifier la réaction de SQLite
- American Fuzzy Lop (AFL)
- Fuzzer guidé par profil introduit en 2014 pour explorer de nouveaux chemins de contrôle
- A découvert de nombreux échecs d’assert, crashs et résultats erronés dans SQLite
- Google OSS Fuzz
- Fuzzing automatisé exécuté sur l’infrastructure de Google depuis 2016
- Détecte des problèmes intermittents sur les nouveaux commits
- dbsqlfuzz / jfuzz
- Introduits comme fuzzers internes à partir de 2018, avec mutation simultanée du SQL et des fichiers DB
- Plus de 500 millions de tests par jour, ce qui a presque fait disparaître les rapports de bugs issus de fuzzers externes
- Depuis 2024, jfuzz ajoute la validation des entrées JSONB
- Fuzzers tiers et fuzzcheck
- Des chercheurs externes (par ex. Manuel Rigger) ont découvert de nombreux cas de calculs erronés
- L’utilitaire fuzzcheck revalide plusieurs milliers d’anciens cas de fuzz jugés « intéressants »
- La tension entre MC/DC et fuzz testing
- MC/DC pousse à minimiser le code défensif, tandis que le fuzzing nécessite ce code défensif
- SQLite combine les deux approches pour conserver un code robuste face aux entrées normales comme malveillantes
5. Tests de régression
- Tout bug signalé est obligatoirement ajouté sous forme de nouveau cas de test après correction
- L’objectif est d’éviter la réapparition d’anciens bugs
6. Détection automatique des fuites de ressources
- Les harness TCL et TH3 surveillent automatiquement les fuites de mémoire, de fichiers, de threads et de mutex
- Même après un OOM ou une erreur d’E/S, aucune fuite mémoire ne doit subsister
7. Couverture de test
- Le cœur de SQLite atteint 100 % de couverture de branches selon TH3
- Hors extensions comme FTS3 et RTree
- Statement vs Branch Coverage
- La couverture de branches est plus stricte que la couverture d’instructions, car elle valide chaque embranchement dans les deux sens
- Couverture du code défensif
- Les macros ALWAYS() et NEVER() explicitent les conditions défensives
- Les tests sont répétés avec trois formes de définition pour vérifier la cohérence
- Tests de valeurs limites et de vecteurs booléens
- La macro testcase() vérifie à la fois les résultats positifs et négatifs des conditions
- 1184 appels à testcase() sont utilisés
- Atteinte de MC/DC
- La macro testcase() permet de vérifier l’impact indépendant de chaque condition
- Mesure basée sur gcov
- La couverture est mesurée avec les options
-fprofile-arcs -ftest-coverage
- La comparaison des résultats permet de détecter des bugs compilateur ou des comportements indéfinis
- Mutation Testing
- Modifie les instructions de branchement pour vérifier que les tests détectent ces changements
- Les branches d’optimisation (
/*OPTIMIZATION-IF-TRUE*/) sont traitées comme exception
- Retour d’expérience sur la couverture complète
- Le test de toutes les branches minimise les effets de bord lors des modifications du code
- Le coût de maintenance est élevé, mais il est justifié pour une bibliothèque d’infrastructure largement déployée
8. Analyse dynamique
- Assert()
- 6754 assertions vérifient les préconditions, postconditions et invariants de boucle
- Actives uniquement dans les builds
SQLITE_DEBUG
- Valgrind
- Détecte les erreurs mémoire, les débordements de pile et les accès à de la mémoire non initialisée
- Avant chaque release, les tests veryquick et TH3 sont exécutés sous Valgrind
- Memsys2
- Dans les builds
SQLITE_MEMDEBUG, un wrapper est inséré pour surveiller les erreurs mémoire
- Permet des vérifications répétées plus rapides qu’avec Valgrind
- Mutex Asserts
sqlite3_mutex_held() et autres vérifient la synchronisation multithread
- Journal Tests
- Vérifient que le journal de rollback est écrit avant la DB, garantissant l’atomicité des transactions
- Undefined Behavior Checks
- Détectent les comportements indéfinis avec
-ftrapv, -fsanitize=undefined, /RTC1, etc.
- Répétés en 32/64 bits, selon l’endianness et sur diverses architectures CPU
9. Tests avec optimisations désactivées
sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS) permet de désactiver les optimisations
- Les résultats doivent être identiques avec ou sans optimisations
- Certains tests de mesure de performance font exception
10. Checklist
- Avant chaque release, une checklist manuelle d’environ 200 éléments est vérifiée
- Certains points prennent quelques secondes, d’autres plusieurs heures
- En cas de problème détecté, un nouvel élément est ajouté immédiatement pour améliorer le processus en continu
11. Analyse statique
- Compilation sans avertissement avec GCC, Clang et MSVC
- Aucun avertissement valide non plus avec Clang Static Analyzer
- L’analyse statique a une efficacité limitée pour détecter les bugs réels
12. Résumé
- Bien qu’open source, SQLite maintient une qualité de niveau commercial et un faible taux de défauts
- Des tests rigoureux et la conception du code en sont les facteurs clés
- Chaque release passe par les procédures ci-dessus pour fournir un moteur de base de données fiable même dans des environnements critiques
Aucun commentaire pour le moment.