luk036/dedup-cpp-tests

Encontre e remova testes unitários C++ redundantes, preencha lacunas de cobertura analisando relatórios gcov e adicione testes direcionados — tudo verificado com comparação de cobertura antes/depois.

O que é dedup-cpp-tests?

Esta skill automatiza a otimização de suites de testes C++ com dois modos complementares: **DEDUP** e **GAP-FILL**. No modo DEDUP, a skill identifica arquivos de teste redundantes que cobrem os mesmos caminhos de código com valores de entrada idênticos, removendo-os com segurança. A verificação rigorosa é feita através de relatórios gcov, garantindo que a cobertura de linha por arquivo e o conjunto exato de linhas não cobertas permaneçam inalterados após a remoção. No modo GAP-FILL, a skill analisa relatórios de cobertura para encontrar código não testado ou subtestado em arquivos fonte compilados e cabeçalhos, e então escreve testes unitários direcionados para preencher essas lacunas. A eficácia é validada comparando métricas de cobertura antes e depois. A skill é indicada para projetos C++ usando GCC/gcov, Clang/llvm-cov ou MSVC, e suporta frameworks como doctest, Catch2 e Google Test. Ela impõe regras críticas como não deduplicar testes baseados em propriedades, sempre recalcular a partir de um estado limpo e usar valores esperados hardcoded nos novos testes para evitar regressões. O processo é dividido em fases desde a análise inicial do cenário de testes até a geração de um relatório final de melhoria de cobertura.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que eu faço

Tenho dois modos de operação que funcionam juntos:

  1. DEDUP — Identificar e remover arquivos/cenários de teste redundantes onde outro arquivo já cobre os mesmos caminhos de código com os mesmos valores de entrada. Verificado através da comparação de cobertura gcov (antes vs. depois).

  2. GAP-FILL — Após a deduplicação, analisar o relatório de cobertura para encontrar código não testado ou subtestado (cabeçalhos e fontes compilados), e então escrever testes unitários direcionados para preencher essas lacunas. Verificado através da melhoria de cobertura gcov (antes vs. depois).

Ambos os modos compartilham um fluxo de trabalho comum de construção e medição de cobertura.

Quando me usar

Para DEDUP:

  • Sua suíte de testes C++ cresceu organicamente com vários arquivos cobrindo o mesmo terreno
  • Arquivos de teste de preenchimento de lacunas (*_gaps.cpp) foram adicionados junto com arquivos de teste abrangentes (*_comprehensive.cpp)
  • Você herdou uma base de código com cenários de teste duplicados
  • Você quer reduzir o tempo de teste no CI sem perder cobertura

Para GAP-FILL:

  • Você tem arquivos fonte .cpp compilados sem um arquivo de teste correspondente
  • O relatório de cobertura mostra cabeçalhos com cobertura de linha <90%
  • Você acabou de remover testes redundantes e quer reinvestir o tempo de teste economizado no preenchimento de lacunas
  • Você precisa aumentar a cobertura antes de um prazo de lançamento

NÃO me use quando:

  • Os testes são parametrizados/gerativos testando a mesma propriedade (essa é uma redundância intencional)
  • O projeto não possui ferramentas de cobertura configuradas
  • Você só precisa adicionar testes sem primeiro verificar redundância

Como eu trabalho

Fase 0: Entender o cenário de testes

Ler o diretório de teste e o CMakeLists.txt:

test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp              # fontes compilados

Verificar:

  • Pares de padrões: *_gaps.cpp + *_comprehensive.cpp, *_basic.cpp + *_detailed.cpp
  • Arquivos .cpp compilados em source/ sem test_*.cpp correspondente
  • Cabeçalhos em include/ sem arquivo de teste dedicado
  • Framework de teste (doctest, Catch2, Google Test, etc.) e globbing do CMake

Observar:

  • Framework de teste e macros usadas (TEST_CASE, CHECK_EQ, etc.)
  • Se o CMakeLists.txt usa file(GLOB ...) ou listas de fontes explícitas
  • O padrão de flags de cobertura (geralmente -DENABLE_TEST_COVERAGE=ON)

Fase 1: Detectar redundância no nível de ARQUIVO

Para cada arquivo candidato, comparar seus cenários de teste contra todos os outros arquivos de teste na suíte.

Método de comparação concreto:

  1. Ler ambos os arquivos lado a lado. Agrupar cenários de teste pelo código que eles exercitam:

    • Funções livres (gcd, lcm, etc.)
    • Métodos de classe (construtores, operadores, acessores, modificadores)
    • Instanciações e especializações de template
    • Cobertura de casos de borda (zero, infinito, NaN, valores limite)
  2. Verificar duplicação no nível de valor. Se ambos os arquivos testam a mesma função com valores idênticos (ex.: gcd_recur(12, 8) = 4 em ambos), esse cenário é redundante. Variações menores (números de Fibonacci em um, mas não no outro) NÃO são redundantes — essas são entradas únicas.

  3. Marcar um arquivo como "redundante" se >90% de seus cenários de teste aparecem em outro arquivo com a mesma cobertura ou mais abrangente. Os <10% restantes de cenários únicos (variações triviais) podem ser descartados com segurança.

Fase 2: Construir linha de base (ANTES da remoção)

Construir a suíte de testes com instrumentação de cobertura:

FerramentaConfiguração
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"
MSVCUsar OpenCppCoverage ou a cobertura integrada do VS Enterprise

Depois construir e executar:

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

Gerar cobertura para cabeçalhos de bibliotecas

cd build/cov

# Para cada cabeçalho da biblioteca, executar gcov contra um arquivo objeto de teste que o #inclui
gcov -o CMakeFiles/<Tests>.dir/source/<test_file>.cpp.o <path>/include/<project>/<header>.hpp

# Para cada fonte compilado, executar gcov contra o arquivo objeto da biblioteca
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp

Registrar métricas da linha de base:

Cabeçalhos:
  array_like.hpp:    100%  (28/28)
  bpqueue.hpp:       100%  (73/73)
  ...
Fontes compilados:
  ell1d.cpp:           0%   (0/23)   ← NÃO TESTADO!
  ldlt_mgr.cpp:      100%  (10/10)
  ...

Casos de teste: N  |  Asserções: M  |  Status: SUCESSO

Também registrar as linhas exatas não cobertas para cada arquivo (as linhas #####: na saída .gcov).

Fase 3: Remover os arquivos redundantes (modo DEDUP)

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

Fase 4: Reconstruir e coletar métricas APÓS DEDUP

# Reconfigurar (o GLOB do CMake pode detectar arquivos faltando)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

Comparar cobertura com a linha de base. Se a cobertura de qualquer cabeçalho cair ou as linhas não cobertas mudarem, reverter imediatamente — o arquivo NÃO era redundante.

Critérios de veredito para DEDUP:

VerificaçãoRequisito
Todos os testes passamDeve passar (antes e depois)
Cobertura de linha por cabeçalhoExatamente igual antes e depois
Linhas não cobertasConjunto idêntico de linhas não cobertas
Contagem de testes diminuiuEsperado — casos de teste removidos
Contagem de asserções diminuiuEsperado — asserções removidas

Fase 5: Analisar lacunas de cobertura (modo GAP-FILL)

Se após a deduplicação (ou começando do zero) o relatório de cobertura mostrar lacunas, identificar código não testado:

Tipo de lacuna A: Fonte compilado com cobertura zero

Verificar source/*.cpp contra test/source/*.cpp. Qualquer .cpp em source/ sem um test_*.cpp correspondente provavelmente não é testado.

# Encontrar arquivos fonte sem cobertura de teste
Get-ChildItem source/*.cpp | ForEach-Object {
    $base = $_.Name -replace '\.cpp$'
    $test = "test/source/test_$base.cpp"
    if (-not (Test-Path $test)) { Write-Host "NÃO TESTADO: $($_.Name)" }
}

Tipo de lacuna B: Cabeçalhos de bibliotecas com baixa cobertura

Olhar o relatório gcov para cabeçalhos com cobertura de linha <90% ou "sem linhas executáveis". Cabeçalhos com muitos templates podem atribuir cobertura ao arquivo .cpp — verificar ambos.

Tipo de lacuna C: Funções/ramos dentro de arquivos cobertos

As linhas não cobertas (#####: no gcov) dentro de arquivos parcialmente cobertos revelam ramos específicos que não são exercitados. Esses são os alvos de maior valor.

Fase 6: Escrever testes direcionados

6a: Ler o código fonte não testado

Ler o arquivo fonte completamente para entender:

  • A API da classe/função (construtores, métodos, parâmetros)
  • Todos os ramos condicionais (if/else, casos switch, retornos antecipados)
  • Casos de borda (entradas nulas/zero, valores limite, condições de erro)
  • O comportamento matemático ou algorítmico

6b: Projetar casos de teste que cubram cada ramo

Para cada método, enumerar os cenários de teste necessários:

MétodoCondiçãoEntradaComportamento esperado
update(cut)beta > taug=1, beta=10 com _r=5Retorna NoSoln, estado inalterado
update(cut)beta < -taug=1, beta=-10 com _r=5Retorna NoEffect, estado inalterado
update(cut)`beta<= tau, g>0`
update(cut)`beta<= tau, g<0`
update_central_cut(cut)g > 0g=1, beta=0Divide _r pela metade, desloca centro para a esquerda
update_central_cut(cut)g < 0g=-1, beta=0Divide _r pela metade, desloca centro para a direita

Incluir casos de borda:

  • Intervalos de largura zero
  • Valores limite (beta == tau, beta == -tau)
  • Gradiente zero (se aplicável)
  • Operações repetidas/encadeadas para verificar transições de estado

6c: Escrever o arquivo de teste

Escrever um novo test_<module>.cpp em test/source/ seguindo os padrões de teste existentes do projeto:

#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>

#include <ellalgo/<module>.hpp>   // cabeçalho sob teste
#include <ellalgo/ell_config.hpp>  // se necessário para definições de tipo
#include <utility>                 // se necessário para std::pair

TEST_CASE("<Módulo>: construção") {
    auto x = <Módulo>(...);
    CHECK_EQ(x.some_method(), valor_esperado);
}

TEST_CASE("<Módulo>: método com caminho de sucesso") {
    auto x = <Módulo>(...);
    auto status = x.update(...);
    CHECK_EQ(status, CutStatus::Success);
    CHECK_EQ(x.state(), estado_esperado);
}

TEST_CASE("<Módulo>: método com caminho de falha") {
    auto x = <Módulo>(...);
    auto status = x.update(...);  // dispara o ramo beta > tau
    CHECK_EQ(status, CutStatus::NoSoln);
}

TEST_CASE("<Módulo>: casos de borda") {
    // Intervalo de largura zero, condições limite, etc.
}

TEST_CASE("<Módulo>: operações encadeadas") {
    // Múltiplas atualizações em sequência
}

Regras para valores de teste:

  • Usar valores esperados hardcoded (não computados a partir do mesmo código sob teste)
  • Anotar cada valor esperado com a derivação matemática em um comentário (ex.: // tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5)
  • Testar cada ramo if pelo menos uma vez
  • Testar cada caso switch pelo menos uma vez
  • Testar pelo menos uma operação encadeada/sequencial para verificar a persistência de estado

Fase 7: Reconstruir e coletar métricas APÓ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>

# Regenerar relatórios de cobertura
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

Fase 8: Relatar melhoria de cobertura

| Métrica | Antes | Depois | Δ |
|--------|--------|-------|---|
| Casos de teste | N | N' | +Δ |
| Asserções | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (compilado) | 0% | 100% | +100% |
| Todos os outros arquivos | ≥90% | ≥90% | 0% |

Lacunas de cobertura preenchidas: ✅

REGRAS CRÍTICAS

Regra 1: Cobertura é a verdade, não a contagem de testes

Um arquivo de teste é redundante apenas se removê-lo deixar a cobertura de linha de cada cabeçalho da biblioteca inalterada. Se a cobertura cair, o arquivo estava cobrindo caminhos de código únicos. Reverter imediatamente.

Ao adicionar testes, um novo teste só é bem-sucedido se aumentar a cobertura do arquivo alvo. Se a cobertura do arquivo alvo não aumentar, o teste não está exercitando o código — revisitar a lógica do teste.

Regra 2: Verificar linhas não cobertas, não apenas a porcentagem

Duas execuções podem mostrar "98.06%" mas ter linhas não cobertas diferentes. Sempre comparar o conjunto exato de linhas #####: na saída do arquivo .gcov:

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

Elas devem ser idênticas antes/depois da deduplicação. Para preenchimento de lacunas, as linhas #####: para o arquivo alvo devem diminuir.

Regra 3: NÃO deduplicar testes baseados em propriedades

Testes baseados em propriedades/gerativos (ex.: RapidCheck) são intencionalmente redundantes por design. Eles verificam invariantes com entradas aleatórias. Eles NÃO devem ser removidos, mesmo que se sobreponham a testes baseados em exemplos.

Regra 4: NÃO deduplicar testes com valores diferentes

Dois testes para o mesmo operador com valores de entrada diferentes exercitam caminhos de código diferentes. Remover apenas quando os valores de entrada e as saídas esperadas são idênticos.

Ao escrever novos testes, usar valores de entrada DIFERENTES dos testes existentes. Duplicar os mesmos valores não aumenta a cobertura.

Regra 5: Sempre reconstruir a partir de um estado limpo

Os arquivos .gcda são consumidos pelo gcov e ficam obsoletos após qualquer mudança no código fonte ou binário. Sempre reconstruir e reexecutar antes de medir:

cmake --build build/cov --clean-first   # ou reconstruir do zero
./build/cov/<TestExecutable>

Regra 6: Verificar o comportamento GLOB do CMake

Se o CMakeLists.txt usa file(GLOB ...) com CONFIGURE_DEPENDS, o CMake detecta automaticamente arquivos adicionados/removidos na reconstrução (CMake 3.12+). Em CMake mais antigo, reconfigurar manualmente após adicionar ou remover arquivos de teste.

Ao escrever um novo arquivo de teste, verificar se o CMake o detecta:

  • Procurar por test_<module>.cpp na saída da construção
  • Se estiver faltando, reconfigurar: cmake -S test -B build/cov

Regra 7: Hardcodar valores esperados, não recalcular

Ao escrever testes, NUNCA calcular valores esperados usando o mesmo código sob teste. Sempre derivá-los manualmente e hardcodá-los como literais. Isso detecta regressões onde a implementação muda, mas o teste ainda passa porque ambos os lados usam o mesmo código quebrado.

Anotar derivações complexas:

// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 so 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);

Regra 8: Testar cada ramo

Para cada função com lógica condicional, enumerar todos os ramos e escrever pelo menos um teste por ramo:

RamoCenário de teste
if (beta > tau)Caminho sem solução
if (beta < -tau)Caminho sem efeito
else (sucesso)Caminho de sucesso
g > 0 dentro do sucessoGradiente positivo
g < 0 dentro do sucessoGradiente negativo
Limite beta == tauAinda sucesso (≤)
Limite beta == -tauDepende de < vs ≤

Regra 9: Priorizar fontes compilados sobre cabeçalhos

Arquivos .cpp compilados são mais propensos a ter cobertura zero porque o código de template somente cabeçalho é frequentemente exercitado incidentalmente por outros testes. Priorizar o teste de:

  1. source/*.cpp sem test_*.cpp correspondente → maior prioridade
  2. Cabeçalhos com cobertura de linha <90% → prioridade média
  3. Ramos não cobertos dentro de arquivos já cobertos → menor prioridade

Regra 10: Corresponder às convenções de teste do projeto

Antes de escrever testes, estudar 2-3 arquivos de teste existentes para corresponder:

  • …

[truncado para tradução]

Individual skills in this repo

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

Habilidades Relacionadas