www.digitalmars.com
Last update Wed May 31 17:11:58 2006

Análise de Cobertura de Código

A parte principal da engenharia do projeto de um software proficional é criar uma suíte de teste para ele. Sem algum tipo de suíte de teste, é impossível saber se o software trabalha ao todo. A linguagem D tem muitas características para ajudar na criação de suítes de teste, tais como testes de unidade e programação de contrato. Mas há o assunto d como testar o código de suíte de teste completamente. O profiler pode dar informação valiosa de que funções foram chamadas, e por quem. Mas olhar dentro de uma função, e determinar que instruções foram executadas e que não foram, requer um analisador de cobertura de código.

Um análisador de cobertura de código irá te ajudar destes modos:

  1. Expor código que não é exercisado pela suíte de teste. Adicionar casos de teste que irão exercitá-lo.
  2. Identificar código que é inalcançável. Código inalcançável é freqüêntemente resultado de sobra de mudanças no design do programa. Código inalcaçável deveria ser removido, como pode ser muito confuso para o programador de manutenção.
  3. Pode ser usado para encalçar por que uma seção particular de código existe, como o caso de teste que causa a execução será iluminado por que.
  4. Já que contas de execução são dadas a cada linha, é possível usar a análise de cobertura para reordenar os blocos básicos em uma função para minimizar pulos no caminho mais usado, assim otimizando-a.

Experiência com analizadores de cobertura de código mostra que eles reduzem dramaticamente o número de bugs em código navegando. Mas não é uma panacéia, um analizador de cobertura não ajudará com:

  1. Identificar condições de raça.
  2. Problemas de consumo de memória.
  3. Bugs com ponteiros.
  4. Verificar que o programa obteve o resultado correto.

Analizadores de cobertura de código estão disponíveis para muitas linguagem populares tais como C++, mas são freqüêntemente produtos de terceiros que se integram pobremente com o compilador, e são freqüêntemente muito caros. Um grande problema com produtos de terceiros é, em vez de instrumentar o código fonte, eles devem incluir o que é essencialmente um front end de compilador sobrado para a mesma linguagem. Isso não é só uma proposição cara, freqüêntemente  arejam fora de passo com os vários vendedores de compiladores como suas implementações mudam e como elas evoluem várias extensões. (gcov, o analizador de cobertura da Gnu, é uma exceção como ele é livre e integrado na gcc.)

O analizador de cobertura de código D é nativo como parte do compilador D. Então, está sempre em perfeito sincronismo com a implementação da linguagem. Ele é implementado estabelecendo um contador para cada linha em cada módulo compilado com o interruptor -cov. Código é inserido no começo de cada instrução para implementar o contador correspondente. Quando o programa termina, um destrutor estático para std.cover coleta todos os contadores, funde-os com os arquios fonte, e escreve os relatórios para arquivos de listagem (.lst).

Por exemplo, considere o programa Sieve:

/* Eratosthenes Sieve prime number calculation. */
bit flags[8191];
int main()
{ int i, prime, k, count, iter;

printf("10 iterations\n");
for (iter = 1; iter <= 10; iter++)
{ count = 0;
flags[] = true;
for (i = 0; i < flags.length; i++)
{ if (flags[i])
{ prime = i + i + 3;
k = i + prime;
while (k < flags.length)
{
flags[k] = false;
k += prime;
}
count += 1;
}
}
}
printf ("\n%d primes\n", count);
return 0;
}

Compile e execute-o com:

dmd sieve -cov
sieve

O arquivo de saída será criado chamado sieve.lst, o conteúdo dele será:

 |/* Eratosthenes Sieve prime number calculation. */
|
|bit flags[8191];
|
|int main()
5|{ int i, prime, k, count, iter;
|
1| printf("10 iterations\n");
22| for (iter = 1; iter <= 10; iter++)
10| { count = 0;
10| flags[] = true;
163840| for (i = 0; i < flags.length; i++)
81910| { if (flags[i])
18990| { prime = i + i + 3;
18990| k = i + prime;
168980| while (k < flags.length)
| {
149990| flags[k] = false;
149990| k += prime;
| }
18990| count += 1;
| }
| }
| }
1| printf ("\n%d primes\n", count);
1| return 0;
|}
sieve.d is 100% covered

Os números à esquerda do | são os contadores de execução para aquela linha. Linhas que não tem código executável são deixadas em branco. Linhas que tem código executável, mas não foi executado, tem um "0000000" como contador de execução. No final do arquivo .lst, o percentual de cobertura é dado.

Há 3 linhas com um contador de execução de 1, estes foram executados uma vez cada um. A linha de declaração para i, prime, etc., tem 5 pois há 5 declarações, e a inicialização de cada declaração conta como uma instrução.

O primeiro laço for mostra 22. Essa é a soma das 3 do cabeçalho do for. Se o cabeçalho do for for quebrado em 3 linhas, os dados são divididos iguamente:

 1| for (iter = 1;
11| iter <= 10;
10| iter++)

que somam 22.

Expressões e1&&e2 e e1||e2 condicionalmente executam o rvalue e2. Então, o rvalue é tratado como uma instrução separada com seu próprio contador:

 |void foo(int a, int b)
|{
5| bar(a);
8| if (a && b)
1| bar(b);
|}

Colocar o rvalue em uma linha separada iluminará as coisas:

 |void foo(int a, int b)
|{
5| bar(a);
5| if (a &&
3| b)
1| bar(b);
|}

Semelhantemente, para as expressões e?e1:e2, e1 e e2 são tratadas como instruções separadas.

Controlando o Analizador de Cobertura

O comportamente do analizador de cobertura pode ser controlado através do módulo std.cover.

Quando o interruptor -cov é lançado, o identificador de versão D_Coverage é definido.

Referências

Wikipedia