Back to flin
flin

3 452 tests, zéro échec

Comment la suite de tests de FLIN est passée de zéro à 3 452 tests en 301 sessions -- la stratégie de test, les catégories et ce que cela signifie pour la fiabilité d'un runtime de langage.

Thales & Claude | March 30, 2026 9 min flin
EN/ FR/ ES
flintestingtest-suitequalityzero-failures

Le jour où l'audit s'est conclu, nous avons lancé cargo test une dernière fois. Le nombre retourné était 3 452. Chaque test a réussi. Zéro échec. Zéro ignoré. Zéro résultat instable. Dans un codebase de 186 252 lignes construit en 301 sessions sur 42 jours, chaque vérification automatisée tenait toujours.

Ce nombre n'est pas arrivé par accident. Il est arrivé parce que chaque session qui introduisait une fonctionnalité introduisait aussi les tests pour la vérifier. Il est arrivé parce que chaque session de correction d'audit lançait la suite complète avant et après les changements. Et il est arrivé parce que le système de types de Rust attrape à la compilation les catégories de bugs qui nécessiteraient autrement des milliers de tests supplémentaires dans un langage à typage dynamique.

Voici l'histoire de la suite de tests de FLIN -- comment elle a grandi, ce qu'elle couvre, et ce que 3 452 tests qui passent signifient réellement pour un runtime de langage.

La courbe de croissance

Le nombre de tests de FLIN a grandi avec le nombre de fonctionnalités, mais pas linéairement. Les premières sessions ont produit le plus de tests par fonctionnalité car les composants fondamentaux -- lexer, parser, opérations de base de la VM -- ont la plus haute densité de tests. Un lexer qui reconnaît 80+ types de tokens nécessite au moins 80 tests. Un parser qui gère 40+ types d'instructions nécessite au moins 40 tests. Une table de dispatch d'opcodes avec 170+ opcodes nécessite au moins 170 tests.

Session Range    Focus                 Tests Added    Cumulative
001-050          Lexer, Parser, VM     ~1,200         1,200
051-100          Types, Entities       ~600           1,800
101-150          Web Server, Routes    ~400           2,200
151-200          Database, Security    ~350           2,550
201-250          Audit, Fixes          ~500           3,050
251-301          Polish, Gaps          ~400           3,452

Les sessions intermédiaires (101-200) ajoutaient moins de tests par session car chaque fonctionnalité était plus grande et plus intégrée. Un test de route de serveur web exerce le lexer, le parser, le vérificateur de types, le générateur de code, la VM, le moteur de rendu et le serveur HTTP en une seule fois -- un test couvre plusieurs composants. Les sessions tardives (201+) ont vu une résurgence du nombre de tests car les corrections d'audit nécessitaient une vérification ciblée de comportements spécifiques.

Catégories de tests

La suite de tests est organisée en trois niveaux qui reflètent les conventions de test de Rust :

Tests unitaires (intégrés dans les fichiers source). Ceux-ci testent des fonctions et méthodes individuelles en isolation. Le parser a 29 tests unitaires pour la création d'AST, le formatage d'affichage, les éléments de vue, la détection de composants et les hooks de cycle de vie. Le moteur de rendu a 119 tests unitaires pour la génération HTML, la sérialisation des gestionnaires d'événements, le passage de propriétés de composants et le rendu de layouts.

rust// Example: parser unit test for view element creation
#[test]
fn test_view_element_is_component() {
    let element = ViewElement {
        tag: "Button".to_string(),
        attributes: vec![],
        children: vec![],
        span: Span::default(),
    };
    assert!(element.is_component_tag());

    let element = ViewElement {
        tag: "div".to_string(),
        attributes: vec![],
        children: vec![],
        span: Span::default(),
    };
    assert!(!element.is_component_tag());
}

Tests d'intégration (dans le répertoire tests/). Ceux-ci compilent et exécutent des programmes FLIN complets, vérifiant le comportement de bout en bout. Les tests du flux du serveur de développement de la Session 203 sont des tests d'intégration -- ils créent une VM, injectent un état, exécutent du bytecode et vérifient la persistance de la base de données.

rust// Example: integration test for entity persistence across restarts
#[test]
fn test_recovery_between_vms() {
    let db_path = tempdir().unwrap();

    // VM1: create and save an entity
    {
        let mut vm = VM::new_with_storage(db_path.path());
        vm.register_entity("Todo", &["title", "done"]);
        vm.save_entity("Todo", &[
            ("title", Value::Text("Buy milk".into())),
            ("done", Value::Bool(false)),
        ]).unwrap();
    }
    // VM1 is dropped -- simulates server shutdown

    // VM2: verify entity survived
    {
        let mut vm = VM::new_with_storage(db_path.path());
        vm.register_entity("Todo", &["title", "done"]);
        let todos = vm.query_all("Todo").unwrap();
        assert_eq!(todos.len(), 1);
        assert_eq!(
            vm.get_field(&todos[0], "title").unwrap(),
            Value::Text("Buy milk".into())
        );
    }
}

Tests de stress du parser (ligne 8866+ dans parser.rs). Le fichier du parser contient environ 600 assertions basées sur panic dans sa section de test. Celles-ci sont délibérément agressives -- elles testent qu'une syntaxe FLIN spécifique produit des structures AST spécifiques, et toute déviation provoque un échec immédiat du test. Les appels panic dans la section de test (que l'audit a initialement signalés) sont des assertions de test Rust standard, pas des problèmes dans le code de production.

Ce que les tests couvrent

La couverture de la suite de tests couvre chaque sous-système majeur :

Couverture du lexer. Chaque type de token dans le vocabulaire de 84 mots-clés est testé. Les modes d'interpolation de templates (chaîne, attribut, vue), la gestion du contenu brut pour les balises style/script/pre/code, les transitions de la machine à états à trois modes, et les cas limites comme les accolades imbriquées dans les expressions d'attributs.

Couverture du parser. Les 40+ types d'instructions, les 35+ types d'expressions, les 7 types de patterns, les 16 variantes d'annotations de types et les 50+ validateurs de champs. Les tests du parser vérifient à la fois l'analyse correcte et la récupération d'erreurs -- ce qui se passe lorsque l'entrée est malformée.

Couverture de la VM. Le dispatch d'opcodes pour tous les opcodes implémentés, le comportement des fonctions natives pour les opérations sur les chaînes/maths/dates/listes, le ramasse-miettes sous pression d'allocation et la gestion de la portée pour les closures et les upvalues.

Couverture du moteur de rendu. La génération HTML pour chaque type d'élément, la sérialisation des gestionnaires d'événements pour les éléments natifs et composants, le rendu conditionnel et en boucle, la composition de slots et de layouts, et la recherche de traductions.

Couverture de la base de données. Les opérations CRUD pour tous les types d'entités, la persistance et récupération WAL, l'atomicité des transactions, le versionnage par voyage dans le temps, et le filtrage de requêtes avec divers opérateurs.

rust// Example: VM test for CreateMap with both string representations
#[test]
fn test_create_map_value_text_keys() {
    let mut vm = VM::new();
    let source = r#"
        translations = {
            en: { "hello": "Hello" },
            fr: { "hello": "Bonjour" }
        }
    "#;
    let result = vm.execute(source).unwrap();

    // Verify both Value::Text and Value::Object keys work
    let map = vm.get_global("translations").unwrap();
    let en = vm.map_get(&map, "en").unwrap();
    let hello = vm.map_get(&en, "hello").unwrap();
    assert_eq!(hello, Value::Text("Hello".into()));
}

Ce que les tests ne couvrent pas

L'honnêteté sur la couverture de tests exige de reconnaître les lacunes. La suite de tests de FLIN a trois angles morts significatifs à la fin de l'audit :

Pas de tests de fuzzing. Le parser et le lexer n'ont pas été soumis à la génération d'entrées aléatoires. Le fuzzing exercerait les chemins de récupération d'erreurs et les cas limites que les tests écrits par des humains manquent. Pour un langage qui traite des entrées développeur non fiables, c'est une lacune à combler.

Tests de concurrence limités. Le serveur web et les modules WebSocket ont des tests fonctionnels mais pas de tests de charge. Sous le traitement concurrent de requêtes, les patterns d'accès à l'état partagé pourraient révéler des conditions de course que les tests séquentiels ne peuvent pas détecter.

Pas de tests basés sur les propriétés. Les opérations qui devraient satisfaire des propriétés algébriques (comme parse(format(ast)) == ast ou deserialize(serialize(value)) == value) sont testées avec des exemples spécifiques plutôt qu'avec des frameworks de tests basés sur les propriétés comme proptest. Les tests basés sur les propriétés fourniraient des garanties plus fortes sur ces invariants.

La suite de tests comme documentation

Au-delà de la vérification, la suite de tests de FLIN sert de documentation exécutable. Lorsque l'audit avait besoin de comprendre comment une fonctionnalité était censée fonctionner, les tests fournissaient la réponse faisant autorité. Les journaux de session décrivaient l'intention ; les tests décrivaient le comportement.

Ce double rôle est particulièrement important pour un runtime de langage car la spécification et l'implémentation peuvent diverger. Quand c'est le cas, la question « lequel est correct ? » n'a qu'une seule réponse fiable : les tests. Si les tests passent et que le comportement correspond aux attentes des tests, l'implémentation est correcte indépendamment de ce que dit la spécification. Si les tests doivent être mis à jour, la spécification doit l'être aussi.

rust// Tests as documentation: how does Entity.where() work?
#[test]
fn test_entity_where_filters_correctly() {
    let mut vm = setup_todo_vm();

    // Add test data
    vm.save_entity("Todo", &[
        ("title", Value::Text("Done task".into())),
        ("done", Value::Bool(true)),
    ]).unwrap();
    vm.save_entity("Todo", &[
        ("title", Value::Text("Open task".into())),
        ("done", Value::Bool(false)),
    ]).unwrap();

    // where(done == false) should return only open tasks
    let result = vm.execute(r#"
        open = Todo.where(done == false)
    "#).unwrap();

    let open = vm.get_global("open").unwrap();
    assert_eq!(vm.list_len(&open), 1);
}

Ce test vous dit tout ce qu'il faut savoir sur Entity.where() : il accepte une comparaison de champ, il retourne une liste filtrée, et il évalue le prédicat contre les champs de chaque entité. Aucune documentation en prose ne pourrait être plus précise ou plus digne de confiance.

3 452 et ce n'est pas fini

Le nombre 3 452 n'est pas un compte final. C'est un instantané de janvier 2026. Chaque session future qui ajoutera une fonctionnalité ajoutera des tests. Chaque rapport de bug produira un test de régression avant la correction. L'audit lui-même a ajouté des dizaines de tests pour des chemins de code précédemment non testés.

Mais 3 452 tests passant simultanément, après 301 sessions de développement, est une déclaration sur l'intégrité du codebase. Cela dit que les décisions fondamentales -- utiliser Rust, maintenir la discipline de test, lancer la suite complète après chaque changement -- étaient correctes. Cela dit qu'un runtime de langage construit en 42 jours peut être aussi bien testé qu'un construit sur des années, si la culture du test est ancrée dès la Session 1.

Et cela dit que lorsque l'audit a trouvé 30 TODO et 5 panics en production, il les a trouvés sur fond de milliers de comportements vérifiés. Les défauts étaient l'exception, pas la règle. Le codebase était sain.

Le prochain article examine ce que l'audit nous a appris sur la pratique plus large de la construction d'un langage de programmation -- les leçons architecturales, les aperçus de processus et les principes que nous emporterions dans la prochaine phase de FLIN.


Ceci est la partie 152 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.

Navigation de la série : - [151] Audit de la persistance de la base de données - [152] 3 452 tests, zéro échec (vous êtes ici) - [153] Ce que l'audit nous a appris sur la construction d'un langage

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles