A suíte de testes do monólito Python da Ramp estava se aproximando de 20 minutos à medida que a equipe ultrapassava 5.000 testes, em sua maioria no estilo funcional/integração. Em vez de reescrever a suíte, a equipe de infraestrutura atacou o tempo de execução diretamente: aumentar a concorrência da CI corrigindo um gargalo na configuração do banco de dados que limitava os workers paralelos a quatro, parar de destruir e reconstruir o banco de dados de teste do zero entre execuções locais, e construir um executor de testes incremental que só executa os testes relevantes para uma determinada mudança de código.
A primeira versão do executor incremental usava o coverage.py para mapear, para cada teste, quais linhas de código ele executava, e então, em um novo commit, comparava os arquivos alterados com esse mapa. Funcionava, mas tinha uma lacuna específica: a base de código da Ramp usa classes de modelo do Flask-SQLAlchemy, e a medição de cobertura atribui quase toda a execução às entranhas do SQLAlchemy, e não à própria declaração da classe de modelo. Adicionar uma coluna a um modelo não era registrado como 'esta classe mudou' sob cobertura puramente por linhas, então o executor incremental podia pular testes que deveriam ter capturado um bug que altera o esquema.
Para corrigir isso, a equipe substituiu o coverage.py pelo sys.settrace nativo do Python, inspecionando eventos `call` em busca de um argumento `self` ou `cls`, resolvendo esse objeto para sua classe e fazendo uma busca em largura por `__bases__` para mapear cada classe ancestral ao arquivo onde foi definida. Isso permitiu que o snapshot registrasse não apenas quais linhas um teste executava, mas também de quais classes em toda a sua cadeia de herança ele dependia — assim, uma mudança na declaração de uma classe base invalidava corretamente os testes de todas as subclasses, mesmo quando nenhum caminho de código da subclasse era diretamente exercitado.
Combinado com a correção de concorrência e a eliminação das reconstruções completas do banco de dados, o executor incremental cobria de 30 a 40% da suíte em um commit típico e capturava cerca de 90% das falhas que uma execução completa capturaria, dando aos engenheiros um sinal acionável em poucos minutos em vez dos 10 a 20 completos.
Rastrear a herança de classes em tempo de execução para que uma edição em classe base não exercitada ainda dispare seus testes
O coverage.py registra as linhas executadas, não as classes às quais elas pertencem, então a execução dentro das entranhas do ORM do SQLAlchemy não é atribuída à classe de modelo do usuário.
O sys.settrace expõe os frames de chamada diretamente, então inspecionar `self`/`cls` recupera 'qual é esta classe' independentemente de onde ocorreu a execução em nível de linha.
Percorrer `__bases__` a partir dessa classe transforma um evento de chamada em uma lista completa de ancestrais, então uma mudança em classe base é atribuída a cada arquivo da cadeia.
Executar o executor incremental junto com a suíte completa, e não em vez dela, fez com que a lacuna de detecção de ~10% nunca se tornasse um risco em produção.
Um atalho de dependência baseado em cobertura pode deixar passar dependências reais, como herança, que as ferramentas de cobertura não atribuem ao código alterado — verifique esse ponto cego antes de confiar no caminho rápido.
A Ramp julgou que o executor incremental não valia mais investimento além desse ponto — fechar a lacuna de detecção restante de ~10% exigiria uma reestruturação em torno de algo como o Bazel, o que não era a maior prioridade naquele momento. A correção de concorrência (dobrar o tamanho da máquina após remover uma reinicialização redundante do banco de dados por worker) sozinha reduziu o tempo de CI pela metade, e pular as reconstruções completas do banco de dados localmente economizou de 10 a 30 segundos por execução. A equipe publicou a travessia de herança baseada em sys.settrace como uma alternativa a opções de código aberto como o pytest-testmon, que não tinha o suporte de paralelização da Ramp.
CONFIRA AS EVIDÊNCIAS
As fontes
- Faster Python Tests Ramp Engineering (Builders Blog) · builders.ramp.com ·