Archive for outubro \12\+00:00 2011

Rodando testes antes do commit em Mercurial

outubro 12, 2011

Como todo mundo, entrei de cabeça na onda dos sistemas de versão distribuídos, como Git. Por várias razões, porém, meu DVCS “do coração” é Mercurial. (Razões as quais pretendo explicar em breve, por sinal).

De qualquer forma, vai aí minha primeira dica sobre o Mercurial. Frequentemente, estou consertando um bug em um projeto…

$ nano module1.c

…e, como uso TDD, rodo os testes:

$ make test
./run_all
................................................................................

OK (80 tests)

Quando os testes passam e o bug está corrigido, comito o código alterado:

$ hg commit -m "Bug #123 corrected"

Daí, passo a trabalhar em outra funcionalidade no mesmo projeto, escrevendo os testes primeiro:

$ nano test/module2.c

Novamente no espírito de TDD, vou rodar os testes, para quebrarem. Aperto então Control+P (ou ) para chegar ao comando que roda os testes novamente (make test). Infelizmente, porém, às vezes eu aperto Enter muito cedo, o que resulta em comitar minhas alterações recentes:

$ hg commit -m "Bug #123 corrected"

Isto é ruim porque cria uma versão com código quebrando no Mercurial. A solução paliativa é executar hg rollback (obrigado de novo, Stack Overflow!). Entretanto,  hg rollback é a pílula do dia seguinte do Mercurial: envolve vários riscos e deve ser usado com cuidado.

Então tive um estalo: por que não rodo os testes sempre antes do commit? A resposta é que eu comito por acidente, claro, mas posso fazer o Mercurial rodá-los antes de confirmar um commit: bastaria criar um hook. Para isto, alterei o programa que roda os testes para retornar um valor diferente de 0 (zero) quando os testes falhassem. Assim make test retorna um valor diferente de zero. Antes eu tinha algo assim:

void RunAllTests(void) {
    CuString *output = CuStringNew();
    CuSuite* suite = CuSuiteNew();
    CuSuiteAddSuite(suite, test_project_suite());
    // ... mais coisas aqui
    CuSuiteRun(suite);
    CuSuiteSummary(suite, output);
    CuSuiteDetails(suite, output);
    printf("%s\n", output->buffer);
    CuStringDelete(output);
    CuSuiteDelete(suite);
}
int main(void) {
    RunAllTests();
    return 0;
}

Agora eu tinha algo assim:

int RunAllTests(void) { // Retorna int ao invés de void
    CuString *output = CuStringNew();
    CuSuite* suite = CuSuiteNew();
    CuSuiteAddSuite(suite, test_project_suite());
    // ... mais coisas aqui
    CuSuiteRun(suite);
    CuSuiteSummary(suite, output);
    CuSuiteDetails(suite, output);
    printf("%s\n", output->buffer);
    CuStringDelete(output);
    CuSuiteDelete(suite);
    return suite->failCount; // Retorna contagem de erros
}
int main(void) {
    return RunAllTests(); // Retorna contagem
}

(Caso você esteja se perguntando, estou utiliando CuTest, o melhor e mais cacofônico framework de testes para C.)

Após fazer esta alteração, adicionei as linhas abaixo no arquivo .hg/hgrc do projeto:

[hooks]
pretxncommit.surefire = make test

O que acontece quando vou comitar erroneamente agora? Veja só:

$ hg commit -m "Bug #123 fixed"
cc -c  -Wall -std=c99 -Iinclude -Icutest src/test/util.c -o test_util.o
cc   run_all.o test_secretary.o  CuTest.o libsecretary.a   -o run_all
./run_all
...........................................................F....................

There was 1 failure:
1) test_util_beginning_of_day: src/test/util.c:34: expected 1 but was 0

!!!FAILURES!!!
Runs: 80 Passes: 79 Fails: 1

make: *** [test] Error 1
transaction abort!
rollback completed
abort: pretxncommit.surefire hook exited with status 2

Os testes falham, o que faz o programa que os roda retornar um valor diferente de zero. O programa, falhando, faz o make falhar, retornando também um valor diferente de zero. Como o make falhou, o hook falha também, impedindo que Mercurial siga em frente com o commit. Oras, o hook foi executado na fase pretxncommit (pre transaction commit), logo antes de Mercurial registrar o commit do código. Como o hook falhou, o commit não é efetivamente feito e meu histórico fica limpo.

Este exemplo utiliza testes escritos em C, mas serve para qualquer projeto. Eventualmente, não se pode alterar o programa que roda os testes, mas pode-se criar um script que lê a saída dos testes e retorna o valor correto.