Posts Tagged ‘C’

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.

Anúncios

Tratamento de erros em C com goto

setembro 13, 2008

Esses dias, começou-se a discutir na lista de discussão da Python Brasil razões para se utilizar exceções. Em um certo momento, um participante reconhecidamente competente comentou o quanto é difícil tratar erros através do retorno de funções, como em C.

Quando se tem um algoritmo complexo, cada operação passível de erro implica em uma série de ifs para verificar se a operação ocorreu corretamente. Se a operação tiver falhado, será necessário reverter todas as operações anteriores para sair do algoritmo sem alterar o estado do programa.

Vejamos um exemplo. Suponha que eu tenha a segunte struct para representar arrays:

typedef struct {
        int size;
        int *array;
} array_t;

Agora, eu vou fazer uma função que lê, de um arquivo texto, o número de elementos a ser posto em um desses arrays e, logo em seguida, os elementos. Essa função também vai alocar a struct do array e o array de fato. O problema é que essa função é bastante propensa a erros, pois podemos não conseguir

  • abrir o arquvo dado;
  • alocar a struct;
  • ler o número de elementos do arquvo dado, seja por erro de entrada/saída, seja por fim do arquivo;
  • alocar memória para guardar os elementos a serem lidos;
  • ler um dos elementos, seja por erro de entrada/saída, seja por fim do arquivo.

Complicado, né? Note que, se conseguirmos abrir o arquivo mas não conseguirmos alocar a struct, temos de fechar o arquivo; se conseguirmos abrir o arquivo e alocar a struct mas não conseguirmos ler o número de elementos do arquivo, temos de dealocar a struct e fechar o arquivo; e assim por diante. Assim sendo, se verificarmos todos os erros e adotarmos a tradição de, em caso de erro, retornar NULL, nossa função seria mais ou menos assim:

array_t *readarray(const char *filename) {
        FILE *file;
        array_t *array;
        int i;

        file = fopen(filename, "r");
        if (file == NULL) return NULL;

        array = malloc(sizeof(array_t));
        if (array == NULL) {
		fclose(file);
		return NULL;
	}

        if (fscanf(file, "%d", &(array->size)) == EOF) {
		free(array);
		fclose(file);
		return NULL;
	}

        array->array = malloc(sizeof(int)*array->size);
        if (array->array == NULL)  {
		free(array);
		fclose(file);
		return NULL;
	}

        for (i = 0; i < array->size; i++) {
                if (fscanf(file, "%d", array->array+i) == EOF) {
			free(array->array);
			free(array);
			fclose(file);
			return NULL;
		}
        }
        return array;
}

De fato, bastante trabalhoso, e com muito código repetido…

Note, porém, como há duas situações no código acima. Em uma, quando tenho duas operações para reverter, preciso reverter primeiro a última executada, e depois a anterior. Por exemplo, quando vou dealocar tanto a struct quanto o array de inteiros, preciso dealocar primeiro o array de inteiros e depois a struct. Se dealoco a struct primeiro. posso não conseguir dealocar o array posteriormente.

Na outra situação, a ordem não importa. Por exemplo, se vou dealocar a struct e fechar o arquivo, não importa em que ordem eu o faça. Isso implica que eu posso, também, reverter primeiro a última operação executada e depois a primeira operação.

Qual o sentido disso? Bem, na prática, nunca vi uma situação onde eu tenha de reverter primeiro a primeira operação executada, depois a segunda e assim por diante. Isso significa que, quando faço as operações a(), b(), c() etc. a maneira “natural” de revertê-las é chamando os reversores de trás para frente, mais ou menos como:

a();
b();
c();
/* ... */
revert_c();
revert_b();
revert_a();

Agora, vem o pulo do gato. No código acima, após cada operação, vamos colocar um if para verificar se ela falhou ou não. Se falhou, executar-se-á um goto para o reversor da última operação bem sucedida:

a();
if (failed_a()) goto FAILED_A;
b();
if (failed_b()) goto FAILED_B;
c();
if (failed_c()) goto FAILED_C;
/* ... */
revert_c();
FAILED_C:
revert_b();
FAILED_B:
revert_a();
FAILED_A:
return;

Se  a() falhar, o algoritmo retorna; se  b() falhar, o algoritmo vai para FAILED_B:, reverte  a() e retorna; se c() falhar, o algoritmo vai para FAILED_C, reverte b(), reverte  a() e retorna. Consegue ver o padrão?

Pois bem, se aplicarmos esse padrão à nossa função readarray() o resultado será algo como:

array_t *readarray(const char *filename) {
        FILE *file;
        array_t *array;
        int i;

        file = fopen(filename, "r");
        if (file == NULL) goto FILE_ERROR;

        array = malloc(sizeof(array_t));
        if (array == NULL) goto ARRAY_ALLOC_ERROR;

        if (fscanf(file, "%d", &(array->size)) == EOF)
                goto SIZE_READ_ERROR;

        array->array = malloc(sizeof(int)*array->size);
        if (array->array == NULL) goto ARRAY_ARRAY_ALLOC_ERROR;

        for (i = 0; i < array->size; i++) {
                if (fscanf(file, "%d", array->array+i) == EOF)
                        goto ARRAY_CONTENT_READ_ERROR;
        }
        return array;

        ARRAY_CONTENT_READ_ERROR:
        free(array->array);
        ARRAY_ARRAY_ALLOC_ERROR:
        SIZE_READ_ERROR:
        free(array);
        ARRAY_ALLOC_ERROR:
        fclose(file);
        FILE_ERROR:
        return NULL;
}

Quais as vantagens desse padrão? Bem, ele reduz a repetição de código de reversão de operações e separa o código de tratamento de erro da lógica da função. Na verdade, apesar de eu achar exceções o melhor método de tratamento de erros moderno, para tratamento de erros in loco (dentro da própria função) eu acho esse método muito mais prático.


%d blogueiros gostam disto: