luk036/dedup-cpp-tests

Trouver et supprimer les tests unitaires C++ redondants, puis combler les lacunes de couverture en analysant les rapports gcov et en ajoutant des tests ciblés – le tout vérifié par une comparaison avant/après de la couverture.

Qu'est-ce que dedup-cpp-tests ?

Cette compétence automatise l’optimisation des suites de tests C++ en deux modes complémentaires : **DEDUP** (dédoublonnage) et **GAP-FILL** (comblement des lacunes). Le mode DEDUP identifie les fichiers de test redondants en comparant les scénarios de test valeur par valeur, élimine ceux dont plus de 90 % des cas sont couverts ailleurs, et vérifie que la couverture par ligne de chaque en-tête reste strictement identique avant/après via **gcov**. Le mode GAP-FILL analyse le rapport de couverture après dédoublonnage (ou directement dans un projet existant) pour détecter les fichiers source compilés sans tests, les en-têtes avec moins de 90 % de couverture, et les branches non testées, puis génère des tests unitaires ciblés qui augmentent concrètement la couverture. La compétence impose des règles strictes : ne jamais dupliquer des tests paramétrés ou génératifs, toujours reconstruire à partir d’un état propre avant chaque mesure, utiliser des valeurs attendues codées en dur (pas recalculées par le code testé), et prioriser les fichiers compilés sur les en-têtes. Elle supporte GCC/gcov, Clang/llvm-cov, et peut être adaptée à MSVC via OpenCppCoverage, avec une intégration CMake pour la détection automatique des fichiers.

Compatible avec~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-cpp-tests

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Ce que je fais

J'ai deux modes de fonctionnement qui fonctionnent ensemble :

  1. DEDUP — Identifier et supprimer les fichiers/scénarios de test redondants où un autre fichier couvre déjà les mêmes chemins de code avec les mêmes valeurs d'entrée. Vérifié via la comparaison de couverture gcov (avant vs. après).

  2. GAP-FILL — Après le dédoublonnage, analyser le rapport de couverture pour trouver le code non testé ou insuffisamment testé (en-têtes et sources compilés), puis écrire des tests unitaires ciblés pour combler ces lacunes. Vérifié via l'amélioration de la couverture gcov (avant vs. après).

Les deux modes partagent un flux de travail commun de construction de base et de mesure de couverture.

Quand m'utiliser

Pour DEDUP :

  • Votre suite de tests C++ a grandi de manière organique avec plusieurs fichiers couvrant le même domaine
  • Des fichiers de test de comblement (*_gaps.cpp) ont été ajoutés à côté de fichiers de tests complets (*_comprehensive.cpp)
  • Vous avez hérité d'une base de code avec des scénarios de test dupliqués
  • Vous souhaitez réduire le temps de test CI sans perdre de couverture

Pour GAP-FILL :

  • Vous avez des fichiers sources .cpp compilés sans fichier de test correspondant
  • Le rapport de couverture montre des en-têtes avec moins de 90 % de couverture de lignes
  • Vous venez de supprimer des tests redondants et souhaitez réinvestir le temps de test économisé dans le comblement des lacunes
  • Vous devez augmenter la couverture avant une échéance de publication

Ne m'utilisez PAS quand :

  • Les tests sont paramétrés/génératifs testant la même propriété (il s'agit d'une redondance intentionnelle)
  • Le projet n'a pas d'outillage de couverture configuré
  • Vous avez seulement besoin d'ajouter des tests sans d'abord vérifier la redondance

Comment je fonctionne

Phase 0 : Comprendre le paysage des tests

Lire le répertoire de test et le CMakeLists.txt :

test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp              # sources compilés

Vérifier la présence de :

  • Paires de motifs : *_gaps.cpp + *_comprehensive.cpp, *_basic.cpp + *_detailed.cpp
  • Fichiers .cpp compilés dans source/ sans test_*.cpp correspondant
  • En-têtes dans include/ sans fichier de test dédié
  • Framework de test (doctest, Catch2, Google Test, etc.) et globbing CMake

Noter :

  • Le framework de test et les macros utilisés (TEST_CASE, CHECK_EQ, etc.)
  • Si CMakeLists.txt utilise file(GLOB ...) ou des listes de sources explicites
  • Le modèle de drapeau de couverture (généralement -DENABLE_TEST_COVERAGE=ON)

Phase 1 : Détecter la redondance au niveau des FICHIERS

Pour chaque fichier candidat, comparer ses scénarios de test avec tous les autres fichiers de test de la suite.

Méthode de comparaison concrète :

  1. Lire les deux fichiers côte à côte. Regrouper les scénarios de test par ce qu'ils exercent :

    • Fonctions libres (gcd, lcm, etc.)
    • Méthodes de classe (constructeurs, opérateurs, accesseurs, mutateurs)
    • Instanciations et spécialisations de templates
    • Couverture des cas limites (zéro, infini, NaN, valeurs aux limites)
  2. Vérifier la duplication au niveau des valeurs. Si les deux fichiers testent la même fonction avec des valeurs identiques (par exemple gcd_recur(12, 8) = 4 dans les deux), ce scénario est redondant. Les variations mineures (nombres de Fibonacci dans un fichier mais pas dans l'autre) ne sont PAS redondantes — ce sont des entrées uniques.

  3. Marquer un fichier comme « redondant » si plus de 90 % de ses scénarios de test apparaissent dans un autre fichier avec une couverture identique ou plus complète. Les moins de 10 % de scénarios uniques restants (variations triviales) peuvent être supprimés en toute sécurité.

Phase 2 : Construire la référence (AVANT la suppression)

Construire la suite de tests avec instrumentation de couverture :

OutilConfiguration
GCC + gcovcmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
Clang + llvm-covcmake -S test -B build/cov -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" -G "Ninja"
MSVCUtiliser OpenCppCoverage ou la couverture intégrée de VS Enterprise

Puis construire et exécuter :

cmake --build build/cov
./build/cov/<TestExecutable>

Générer la couverture pour les en-têtes de bibliothèque

cd build/cov

# Pour chaque en-tête de bibliothèque, exécuter gcov sur un fichier objet de test qui l'inclut
<path>/include/<project>/<header>.hpp

# Pour chaque source compilé, exécuter gcov sur l'objet de la bibliothèque
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp

Enregistrer les métriques de référence :

En-têtes :
  array_like.hpp :    100%  (28/28)
  bpqueue.hpp :       100%  (73/73)
  ...
Sources compilés :
  ell1d.cpp :           0%   (0/23)   ← NON TESTÉ !
  ldlt_mgr.cpp :      100%  (10/10)
  ...

Cas de test : N  |  Assertions : M  |  Statut : SUCCÈS

Enregistrer également les lignes exactes non couvertes pour chaque fichier (les lignes #####: dans la sortie .gcov).

Phase 3 : Supprimer les fichiers redondants (mode DEDUP)

Remove-Item -Force test/source/test_*_gaps.cpp    # PowerShell
git status test/
git diff --stat

Phase 4 : Reconstruire et collecter les métriques APRÈS-DEDUP

# Reconfigurer (CMake GLOB peut détecter les fichiers manquants)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

Comparer la couverture avec la référence. Si la couverture d'un en-tête diminue ou si les lignes non couvertes changent, annuler immédiatement — le fichier n'était PAS redondant.

Critères de verdict pour DEDUP :

VérificationExigence
Tous les tests passentDoit passer (avant et après)
Couverture de ligne par en-têteExactement égale avant et après
Lignes non couvertesEnsemble identique de lignes non couvertes
Nombre de tests diminuéAttendu — cas de test supprimés
Nombre d'assertions diminuéAttendu — assertions supprimées

Phase 5 : Analyser les lacunes de couverture (mode GAP-FILL)

Si après le dédoublonnage (ou en partant de zéro) le rapport de couverture montre des lacunes, identifier le code non testé :

Lacune de type A : Source compilé avec une couverture nulle

Vérifier source/*.cpp par rapport à test/source/*.cpp. Tout .cpp dans source/ sans test_*.cpp correspondant est probablement non testé.

# Trouver les fichiers sources sans couverture de test
Get-ChildItem source/*.cpp | ForEach-Object {
    $base = $_.Name -replace '\.cpp$'
    $test = "test/source/test_$base.cpp"
    if (-not (Test-Path $test)) { Write-Host "NON TESTÉ : $($_.Name)" }
}

Lacune de type B : En-têtes de bibliothèque avec une faible couverture

Regarder le rapport gcov pour les en-têtes avec moins de 90 % de couverture de lignes ou « aucune ligne exécutable ». Les en-têtes lourds en templates peuvent attribuer la couverture au fichier .cpp — vérifier les deux.

Lacune de type C : Fonctions/branches dans des fichiers couverts

Les lignes non couvertes (#####: dans gcov) dans des fichiers partiellement couverts révèlent des branches spécifiques qui ne sont pas exercées. Ce sont les cibles de plus grande valeur.

Phase 6 : Écrire des tests ciblés

6a : Lire le code source non testé

Lire le fichier source complètement pour comprendre :

  • L'API de la classe/fonction (constructeurs, méthodes, paramètres)
  • Toutes les branches conditionnelles (if/else, cas switch, retours anticipés)
  • Les cas limites (entrées nulles/nulles, valeurs aux limites, conditions d'erreur)
  • Le comportement mathématique ou algorithmique

6b : Concevoir des cas de test qui couvrent chaque branche

Pour chaque méthode, énumérer les scénarios de test nécessaires :

MéthodeConditionEntréeComportement attendu
update(cut)beta > taug=1, beta=10 avec _r=5Retourne NoSoln, état inchangé
update(cut)beta < -taug=1, beta=-10 avec _r=5Retourne NoEffect, état inchangé
update(cut)|beta| <= tau, g>0g=1, beta=2 avec _r=5Retourne Success, nouveau centre à l+_r
update(cut)|beta| <= tau, g<0g=-1, beta=2 avec _r=5Retourne Success, nouveau centre à l+_r
update_central_cut(cut)g > 0g=1, beta=0Divise _r par deux, déplace le centre à gauche
update_central_cut(cut)g < 0g=-1, beta=0Divise _r par deux, déplace le centre à droite

Inclure les cas limites :

  • Intervalles de largeur nulle
  • Valeurs aux limites (beta == tau, beta == -tau)
  • Gradient nul (si applicable)
  • Opérations répétées/enchaînées pour vérifier les transitions d'état

6c : Écrire le fichier de test

Écrire un nouveau fichier test_<module>.cpp dans test/source/ en suivant les modèles de test existants du projet :

#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS

#include <doctest/doctest.h>

#include <ellalgo/<module>.hpp>   // en-tête testé
#include <ellalgo/ell_config.hpp>  // si nécessaire pour les définitions de types
#include <utility>                 // si nécessaire pour std::pair

TEST_CASE("<Module> : construction") {
    auto x = <Module>(...);
    CHECK_EQ(x.some_method(), expected_value);
}

TEST_CASE("<Module> : méthode avec chemin de succès") {
    auto x = <Module>(...);
    auto status = x.update(...);
    CHECK_EQ(status, CutStatus::Success);
    CHECK_EQ(x.state(), expected_state);
}

TEST_CASE("<Module> : méthode avec chemin d'échec") {
    auto x = <Module>(...);
    auto status = x.update(...);  // déclenche la branche beta > tau
    CHECK_EQ(status, CutStatus::NoSoln);
}

TEST_CASE("<Module> : cas limites") {
    // Intervalle de largeur nulle, conditions aux limites, etc.
}

TEST_CASE("<Module> : opérations enchaînées") {
    // Plusieurs mises à jour en séquence
}

Règles pour les valeurs de test :

  • Utiliser des valeurs attendues codées en dur (pas calculées à partir du même code testé)
  • Annoter chaque valeur attendue avec la dérivation mathématique dans un commentaire (par exemple, // tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5)
  • Tester chaque branche if au moins une fois
  • Tester chaque cas switch au moins une fois
  • Tester au moins une opération enchaînée/séquentielle pour vérifier la persistance de l'état

Phase 7 : Reconstruire et collecter les métriques APRÈS-GAP-FILL

cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

# Régénérer les rapports de couverture
gcov -o CMakeFiles/<Tests>.dir/source/test_<module>.cpp.o <path>/include/<project>/<module>.hpp
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<module>.cpp.o <path>/source/<module>.cpp

Phase 8 : Rapporter l'amélioration de la couverture

| Métrique | Avant | Après | Δ |
|----------|-------|-------|---|
| Cas de test | N | N' | +Δ |
| Assertions | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (compilé) | 0% | 100% | +100% |
| Tous les autres fichiers | ≥90% | ≥90% | 0% |

Lacunes de couverture comblées : ✅

RÈGLES CRITIQUES

Règle 1 : La couverture est la vérité, pas le nombre de tests

Un fichier de test est redondant seulement si sa suppression laisse la couverture de ligne de chaque en-tête de bibliothèque inchangée. Si la couverture diminue, le fichier couvrait des chemins de code uniques. Annuler immédiatement.

Lors de l'ajout de tests, un nouveau test n'est réussi que s'il augmente la couverture du fichier cible. Si la couverture du fichier cible n'augmente pas, le test n'exerce pas le code — revoir la logique du test.

Règle 2 : Vérifier les lignes non couvertes, pas seulement le pourcentage

Deux exécutions peuvent montrer « 98,06 % » mais avoir des lignes non couvertes différentes. Toujours comparer l'ensemble exact des lignes #####: dans la sortie du fichier .gcov :

Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^    #####:"

Celles-ci doivent être identiques avant/après le dédoublonnage. Pour le comblement des lacunes, les lignes #####: pour le fichier cible doivent diminuer.

Règle 3 : Ne PAS dédupliquer les tests basés sur les propriétés

Les tests basés sur les propriétés / génératifs (par exemple, RapidCheck) sont intentionnellement redondants par conception. Ils vérifient des invariants avec des entrées randomisées. Ils ne doivent PAS être supprimés même s'ils chevauchent des tests basés sur des exemples.

Règle 4 : Ne PAS dédupliquer les tests avec des valeurs différentes

Deux tests pour le même opérateur avec des valeurs d'entrée différentes exercent des chemins de code différents. Ne supprimer que lorsque les valeurs d'entrée et les sorties attendues sont identiques.

Lors de l'écriture de nouveaux tests, utiliser des valeurs d'entrée DIFFÉRENTES des tests existants. Dupliquer les mêmes valeurs n'augmente pas la couverture.

Règle 5 : Toujours reconstruire à partir d'un état propre

Les fichiers .gcda sont consommés par gcov et deviennent obsolètes après tout changement de source ou de binaire. Toujours reconstruire et réexécuter avant de mesurer :

cmake --build build/cov --clean-first   # ou reconstruire à partir de zéro
./build/cov/<TestExecutable>

Règle 6 : Vérifier le comportement de GLOB de CMake

Si CMakeLists.txt utilise file(GLOB ...) avec CONFIGURE_DEPENDS, CMake détecte automatiquement les fichiers ajoutés/supprimés lors de la reconstruction (CMake 3.12+). Sur les versions plus anciennes de CMake, reconfigurer manuellement après avoir ajouté ou supprimé des fichiers de test.

Lors de l'écriture d'un nouveau fichier de test, vérifier que CMake le prend en compte :

  • Rechercher test_<module>.cpp dans la sortie de construction
  • S'il manque, reconfigurer : cmake -S test -B build/cov

Règle 7 : Coder en dur les valeurs attendues, ne pas recalculer

Lors de l'écriture de tests, ne JAMAIS calculer les valeurs attendues en utilisant le même code testé. Toujours les dériver manuellement et les coder en dur comme des littéraux. Cela permet de détecter les régressions où l'implémentation change mais où le test réussit toujours parce que les deux côtés utilisent le même code cassé.

Annoter les dérivations complexes :

// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 donc u=3, l=5-5=0
// _r = half_nonnegative(3-0) = 1.5, _xc = 0 + 1.5 = 1.5
CHECK_EQ(e.xc(), 1.5);

Règle 8 : Tester chaque branche

Pour chaque fonction avec logique conditionnelle, énumérer toutes les branches et écrire au moins un test par branche :

BrancheScénario de test
if (beta > tau)Chemin sans solution
if (beta < -tau)Chemin sans effet
else (succès)Chemin de succès
g > 0 dans le succèsGradient positif
g < 0 dans le succèsGradient négatif
Limite beta == tauSuccès toujours (≤)
Limite beta == -tauDépend de < vs ≤

Règle 9 : Prioriser les sources compilés par rapport aux en-têtes

Les fichiers .cpp compilés sont plus susceptibles d'avoir une couverture nulle car le code template exclusivement en en-tête est souvent exercé incidemment par d'autres tests. Prioriser le test :

  1. source/*.cpp sans test_*.cpp correspondant → priorité la plus élevée
  2. En-têtes avec moins de 90 % de couverture de lignes → priorité moyenne
  3. Branches non couvertes dans des fichiers par ailleurs couverts → priorité plus faible

Règle 10 : Respecter les conventions de test du projet

Avant d'écrire des tests, étudier 2-3 fichiers de test existants pour correspondre à :

  • Les conventions de dénomination (préfixes, suffixes)
  • Le style d'assertion (CHECK, REQUIRE, ASSERT, EXPECT)
  • L'utilisation de fixtures, de constructeurs de données
  • L'inclusion de l'en-tête (directe ou via un en-tête de test central)

Prêt à commencer ! Quels sont les répertoires de votre projet et les fichiers à analyser ?

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

Skills associés