Pular para o conteúdo
Development

Um teste que não provou nada, e o bug que ele deveria pegar

Por Victor Da Luz
rustsqlitetestingdev-loggreenhouse

A revisão de repositório que vem alimentando toda essa sequência de posts deixou um item sem abrir: uma mistura de cinco pequenas descobertas, todas de severidade LOW, empacotadas numa única issue de “lote de limpeza” em vez de cinco separadas. Um padrão repetido de mutex-unwrap, um comentário desatualizado, um campo de erro fixo no código, uma inconsistência de nomenclatura, e uma nota de texto de UI explicitamente marcada “corrigir depois.” Nada disso ia ganhar sua própria entrada de dev log pelos próprios méritos. Juntas, valiam uma tarde.

Três rápidas

Quatro call sites em dois arquivos faziam exatamente a mesma coisa para ler o estado compartilhado de vault-root do Greenhouse: state.vault_root.lock().expect("vault_root mutex poisoned"). Copiado e colado vezes suficientes para que corrigir o padrão uma vez significasse algo diferente de corrigi-lo em cada lugar onde aparecia. Como o valor protegido é um simples Option<PathBuf> que só é clonado para fora ou substituído por inteiro (nunca mutado em múltiplos passos enquanto está travado), recuperar de um lock poisoned é na verdade seguro aqui, não existe estado escrito pela metade que um holder em panic pudesse ter deixado para trás. Então o campo virou privado, e dois métodos assumiram: AppState::vault_root() para ler, AppState::set_vault_root() para escrever, os dois usando lock().unwrap_or_else(PoisonError::into_inner) em vez de um expect puro. Quatro call sites viraram dois métodos com uma única política de tratamento de poison, em vez de quatro cópias do mesmo julgamento.

Um comentário no router de nível superior ainda descrevia a etapa de seleção de pasta do onboarding no futuro, “assim que a issue do folder-picker configurar isso,” “vem com seu set_vault_root”, para uma feature que já foi lançada e está em uso diário há mais de uma semana. Comentários que descrevem trabalho como algo por vir não são revisitados depois que ele é lançado; nada força isso. Reescrevi no presente, descrevendo o que o código faz agora em vez do que ia fazer.

E uma função, load_initial, era o único nome em snake_case num arquivo Svelte que, fora isso, era consistentemente camelCase. Renomeei para loadInitial. O tipo de correção que leva dez segundos e teria custado a outra pessoa um momento de “espera, por que esse é diferente” toda vez que passasse os olhos por ali.

O que na verdade era um bug

O quarto item parecia igualmente pequeno no papel: uma função helper que analisa uma string de status do banco de dados reporta de qual coluna do result-set ela veio quando a análise falha, e esse número de coluna estava fixo em 2, correto para o único chamador que lê status como a terceira coluna selecionada, errado para um segundo chamador que seleciona status primeiro. Um problema cosmético numa mensagem de erro que ninguém veria a menos que outra coisa já tivesse dado errado. Escrevi um teste para provar a correção: inserir uma linha com um status inválido, chamar a query que seleciona status como coluna 0, verificar que o erro reporta coluna 0.

O teste falhou. Não porque a correção estava errada, mas porque o bug que eu estava corrigindo não podia de fato acontecer por aquele caminho de código. A query em questão filtra com WHERE status IN ('vaulted', 'released') antes de qualquer linha chegar ao parser, e esses são exatamente os dois valores que sempre são analisados com sucesso. Dê a ela um status que o parser não reconheceria, e a própria cláusula WHERE da query descarta a linha antes que a análise sequer seja tentada. O bug de coluna errada era real, o 2 fixo genuinamente estava errado para aquele chamador, mas ele era latente de um jeito que nenhum teste de integração jamais poderia observar, porque o SQL ao redor impede estruturalmente que a entrada que falha chegue até ali.

Isso é um formato diferente de “não dá para testar isso” comparado com as desculpas de sempre. Não é flaky, não é lento, não está travado atrás de infraestrutura que eu não tenho no CI. É uma correção de corretude para um caminho de código que hoje é comprovadamente inalcançável, e continua correto e corrigido se aquela cláusula WHERE algum dia afrouxar, ou se a query for reutilizada em algum lugar menos restritivo. Como eu não conseguia demonstrar a falha pelo caminho de integração, testei a unidade real em vez disso, chamei a função agora parametrizada diretamente com os dois valores de coluna e verifiquei que cada uma reporta de volta o que recebeu. Menos dramático do que uma reprodução de ponta a ponta, mas é o nível onde a correção de fato mora: a função recebe uma coluna agora, e usa a que foi dada.

Reflexão

A lição não é “sempre escreva um teste.” Eu escrevi um, e ele me contou algo verdadeiro, só que não a coisa que eu esperava. A lição é que um teste vermelho é informação independentemente de para qual lado da correção ele está apontando: pode significar que a correção está quebrada, ou pode significar que o modelo mental de como o bug acontece estava errado. Vale a pena descobrir os dois antes de mesclar. Um lote tão pequeno poderia ter entrado sem nenhum desse atrito, quatro ajustes de linha, lançar e pronto, mas o menor item do lote foi o que mudou o que eu entendia sobre o código.

Leitura relacionada

Development

A pasta que ficou parada

Uma revisão de código encontrou um invariante que a feature de adotar/importar do Greenhouse quebrava silenciosamente, e a correção que tornou tudo chato de novo.

Ler
Development

Ensinando um agente a testar cliques no meu app Tauri

O macOS não tem um WebDriver oficial do Tauri, um plugin da comunidade preenche essa lacuna, e a unificação de features do Cargo quase compilou o servidor de teste inteiro nos builds de release.

Ler
Development

Construindo o Greenhouse: o vault em disco

O layout de pastas que sobrevive ao app: zonas versus estágios, uma config que se recusa a sobrescrever, um arquivo retrato do banco de dados, e o parâmetro que não fazia nada.

Ler

Você também pode achar útil

Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais