Data de Depósito: 05.03.2004
A
Assinatura: jf^ç- - '
Estudo do teste de mutação em programas
funcionais SML
Thaise Yano
Orientador: Prof. Dr. José Carlos Maldonado
Dissertação apresentada ao Instituto de Ciências Matemáticas e de Computação - ICMC-USP, como parte dos requisitos para obtenção do título de Mestre em Ciências - Área: Ciências de Computação e Matemática Computacional.
USP - São Carlos Março/2004
A Comissão Julgadora:
Prof. Dr. José Carlos Maldonado
Prof. Dr. Mareio Eduardo Delamaro
Taqueshi e Tamiko.
Agradecimentos
A Deus pela vida e pela fé que me deu coragem durante os momentos difíceis.
Aos meus pais, Taqueshi e Tamiko, pela formação que me deram, pela confiança e amor depositados em mim, pela força e pela compreensão de minha ausência em virtude do trabalho.
Aos meus irmãos, Michel e Neise, pelo carinho e incentivo.
Ao Luciano, por todo amor e compreensão, sempre ao meu lado em todos os momentos. Ao meu orientador, Maldonado, pelo apoio e confiança durante o trabalho.
Ao Ades, pelo apoio incondicional, pela troca de idéias e pelas valiosas e essenciais ajudas ao meu trabalho.
A Simone, minha amiga, pela amizade, por todo apoio, sempre pronta a ajudar e por me receber em sua casa de portas abertas.
Ao pessoal da academia, Simone, Glaucia, Osnete, Helena e Erika pelos momentos de descontração e alegria.
Aos amigos Ades, André, Auri e Tati pela paciência e pelo auxílio no meu trabalho. Ao pessoal do Labes e aos amigos do Labes, Ades, Simone, André, Tati, Eilen, Andrea, Auri, Sandro, Erika, Naninha, Sarita, Alê, Maris, Elisa, Lu, Ju, Osnete, Tânia, Mateus, Gelza, Débora, Marco, Rosana, André Rocha, Bira, Valter, Fabiano, Otávio, Camila e Edilson pelo companheirismo, alegrias e apoio.
Ao CNPq, pelo apoio financeiro.
A todas as pessoas que participaram e colaboraram de alguma maneira na realização deste trabalho.
1 Introdução 1 1.1 Contexto e Motivação 1 1.2 Objetivos 3 1.3 Organização do Trabalho 3 2 Teste de Software 5 2.1 Considerações Iniciais 5 2.2 Conceitos Gerais 6 2.3 Técnica Funcional 7 2.4 Técnica Estrutural 8 2.5 Técnica Baseada em Erros 9
2.5.1 Teste de Mutação 10 2.6 Ferramentas de Teste 15 2.7 Considerações Finais 16 3 Programação Funcional 19 3.1 Considerações Iniciais 19 3.2 Standard ML 20 3.2.1 Implementações de Standard ML 22 3.2.2 Aplicações em SML 24 3.3 Standard ML vs Linguagens Procedimentais 26
3.4 Programas Funcionais: VV&T 27
3.5 Considerações Finais 28
4 Proteum/SML 31
4.1 Considerações Iniciais 31 4.2 Operadores de Mutação Definidos na Proteum/SML 32
4.3 Arquitetura 44 4.3.1 Proteum/SML vs Proteum/CPN 45 4.4 Modelagem 46 4.4.1 Esquema Conceituai 47 4.4.2 Esquema Navegacional 48 v
4.4.3 Modelo de Interface Abstrata 50 4.5 Aspectos Relevantes da Implementação 62
4.5.1 Operadores de Mutação: Implementação 64
4.6 Considerações Finais 67
5 Proteum/SML: Um Exemplo Completo 69
5.1 Considerações Iniciais 69 5.2 Exemplo 69 5.3 Aspectos Operacionais 72 5.3.1 Acesso 7 3 5.3.2 Criação de Projeto 75 5.3.3 Criação de Sessão 78 5.4 Considerações Finais 92 6 Conclusões 95 6.1 Contribuições 96 6.2 Trabalhos Futuros 97 Referências Bibliográficas 99 A Gramática de SML H l
A.l Analisador Léxico H l A.2 Gramática Livre de Contexto 112
2.1 Mutantes Gerados pela Aplicação dos Operadores de Mutação 13
2.2 Exemplos de Mutantes 13
4.1 Arquitetura da P R O T E U M / S M L 45
4.2 Esquema Conceituai da P R O T E U M / S M L 4 7
4.3 Esquema Navegacional da P R O T E U M / S M L - visão do administrador. . . . 49 4.4 Esquema Navegacional da P R O T E U M / S M L - visão do proprietário de
projeto 49
4.5 Esquema Navegacional da P R O T E U M / S M L - visão do usuário de sessão. . 50
4.6 Esquema de Contextos Navegacionais da P R O T E U M / S M L 51
4 . 7 A D V d a P R O T E U M / S M L 52
4.8 ADVs da Área de Conteúdo Variado da Figura 4.7 55
4.9 ADVs de mais baixo nível da ferramenta P R O T E U M / S M L 56
4.10 ADV Charts Login e Logout 56 4.11 ADV Charts Formulário de Registro 57
4.12 ADV Charts Autorização 57 4.13 ADV Charts Projetos 58 4.14 ADV Charts Operadores 58 4.15 ADV Charts Código Fonte 59 4.16 ADV Charts Sessões 60 4.17 ADV Charts Casos de Teste 60
4.18 ADV Charts Inserir Caso de Teste 61
4.19 ADV Charts Mutantes 61 4.20 ADV Charts Inserir/Remover Mutantes 62
4.21 ADV Charts Relatórios 62
4.22 Modelo Entidade-Relacionamento da P R O T E U M / S M L 63
4.23 Operador de Mutação 0LR 66 4.24 Tela Principal da MuDeC Animator 67
5.1 Programa identifier.sml (contém ao menos um erro). 70
5.2 Página de Acesso à P R O T E U M / S M L 73
5.3 Formulário de Registro de Usuário 74
5.4 Página de Autorização de Conta de Usuário 75 5.5 Página de Ativação de Conta de Usuário 75
5.6 Página de Criação de Projeto 76 5.7 Página de Seleção de Operadores de Mutação 77
5.8 Página de Inclusão do Código Fonte 78 5.9 Janela de Diálogo para Importar Arquivo 78
5.10 Status de Geração de Mutantes 79 5.11 Requisição de Autorização 80 5.12 Propósito da Requisição de Autorização 80
5.13 Página de Autorização para Criação de Sessão 81
5.14 Página de Criação de Sessão 81 5.15 Inclusão de Mutantes por Operador 82
5.16 Inclusão de Mutantes por Linha de Código 82 5.17 Inclusão de Mutantes por Número de Mutante 83
5.18 Página de Inclusão de Caso de Teste 84 5.19 Casos de Teste de uma Sessão 84
5.20 Inclusão de T0 85
5.21 Inclusão de Ti 86 5.22 Relatório por Operador 87
5.23 Relatório por Caso de Teste 87 5.24 Mutantes de uma Sessão 88 5.25 Visualização Lado a Lado do Programa Original e do Mutante 88
5.26 Inclusão de T2 89
5.27 Mutantes Equivalentes no Projeto Identifier 89
5.28 Inclusão de T3 90
5.29 Mutante Error-revealing 90 5.30 Programa identifier.sml Corrigido 91
5.31 Página de Configuração de skins 92 5.32 Outro Modo de Exibição Possível da P R O T E U M / S M L 93
4.1 Operadores de Mutação definidos na P R O T E U M / S M L 34
4.2 Conjunto de Constantes Requeridas 41
4.3 Construtores da MuVeC 66 5.1 Informações do Programa identifier.sml 71
5.2 Operadores de Mutação: identifier.sml 71 5.3 Evolução do Escore de Mutação do Programa identifier.sml 72
Resumo
Ao contrário das linguagens procedimentais, em que os programas são escritos como uma sequência de instruções, as linguagens de programação funcional, tais como SML (Standard Meta Language), Haskell e Lisp, enfatizam regras e casamento de padrões. Os pro-gramas em linguagens funcionais podem conter erros pela falta de entendimento de suas propriedades. Assim, a atividade de teste é de grande importância na identificação desses erros, podendo for-necer evidências da qualidade do produto em teste. No entanto, existem poucas iniciativas para o teste de programas funcionais bem como no desenvolvimento de ferramentas de apoio a essa atividade. Além disso, os trabalhos existentes não possuem uma medida de cobertura da atividade de teste. Assim, motiva-se a investigação de novas formas de se realizarem testes em programas funcionais. Um critério de teste que fornece uma maneira de auxiliar na geração e na avaliação de um conjunto de casos de teste é o Teste de Mutação. Neste trabalho, estabelecem-se subsídios para a investigação da apli-cabilidade do Teste de Mutação para o teste programas funcionais, escritos em SML. Devido ao grande volume de informações que estão envolvidas na aplicação do Teste de Mutação, é essencial a existência de ferramentas de apoio para o uso desse critério. A fim de viabi-lizar a aplicação do Teste de Mutação para SML, foi desenvolvida a ferramenta web PROTEUM/SML, que implementa os operadores de mutação definidos neste trabalho. Um exemplo é fornecido para ilustrar os conceitos e a ferramenta P R O T E U M / S M L .
Functional programming languages, such as SML (Standard Meta Language), Haskell and Lisp, focus on rules and matching of patterns, in contrast to procedural languages in which programs are written as a sequence of instructions. Programs in functional languages may have errors due to the misunderstanding of their properties. Testing is one of the essential activities to identify these errors and to guarantee the quality of the product under development. However, there are few initiatives and tools to support the testing of functional programs. Moreover, an important issue that is often not taken into consideration in this context is to provide a means to quantify the test activity. In this work, we establish mechanisms to investigate the aplicability of Mutation Testing for testing functional programs, written in SML. Mutation Testing is a test criterion that allows to evaluate the quality of a test set and to guide the generation of test sets. The existence of a tool to support this criterion is essential due to the large amount of information related to its application. The web tool PROTEUM/SML, developed with the aim of applying the Mutation Testing to SML, implements the mutation operators defi-ned in this work. An example is provided to illustrate the concepts and P R O T E U M / S M L tool.
CAPÍTULO
i
Introdução
Neste capítulo, são apresentados o contexto no qual este trabalho está inserido, os fatores que motivam a sua realização e os objetivos atingidos durante o seu desenvolvimento.
1.1 Contexto e Motivação
Um grande desafio no desenvolvimento de software é a busca por produtos de software de alta qualidade. Isso está associado ao fato do uso de sistemas computacionais ser cada vez maior, abrangendo praticamente todas as atividades da sociedade contemporânea, bem como a busca por diferenciais no ambiente económico, caracterizado por uma grande competitividade. Com esse propósito, as atividades de garantia de qualidade de software têm sido aplicadas a fim de aumentar a qualidade do produto de software a cada passo do processo de desenvolvimento (Pressman, 2000). Dentre essas atividades, estão as de VV&T — Verificação, Validação e Teste. A atividade de teste é uma das técnicas de VV&T mais utilizadas (Sommerville, 1995; Harrold, 2000), consistindo de uma análise dinâmica do produto que deve ser conduzida de maneira sistemática e criteriosa, sendo de grande importância para a identificação e eliminação de erros no produto. Tal atividade visa a fornecer evidências de confiabilidade de um produto de software.
A atividade de teste, como toda atividade humana, está propensa a erros e, em geral, sua condução manual é improdutiva e limitada a produtos muito simples (Horgan & Mathur, 1992). Assim, é de fundamental importância a disponibilidade de ferramentas de apoio, possibilitando também a condução de estudos empíricos que visem a avaliar e comparar os diversos critérios de teste e, consequentemente, contribuindo para um desenvolvimento de software de maior qualidade e produtividade. Nesse contexto, vários trabalhos podem ser encontrados (Beizer, 1990; Delamaro, 1997; Souza, 1996; DeMillo, 1980; DeMillo et ai, 1988; Luts, 1990; Frankl & Weyuker, 1985; Horgan k Mathur, 1992; Korel & Laski, 1985; Chaim, 1991; Delamaro, 1993; Fabbri et al., 1995a; Sugeta et ai., 1999; Simão, 2000; Vincenzi et al., 2002, 2003; Wong et al., 2003).
Existem diversas técnicas e critérios de teste que estabelecem os requisitos que devem ser testados e satisfeitos. O Teste de Mutação, critério de teste alvo deste trabalho, procura revelar erros típicos cometidos no desenvolvimento de um produto. Um aspecto importante desse critério é fornecer uma maneira de auxiliar na geração e na avaliação de um conjunto de casos de teste. Devido ao grande volume de informações que estão envolvidas na aplicação do Teste de Mutação, em que, geralmente, um grande número de produtos deve ser gerado, executado e comparado, é essencial a existência de ferramentas de apoio para o uso desse critério.
Linguagens funcionais realizam computações por meio de definições e aplicações de funções (Ullman, 1998). Essas linguagens são livres de side-effects, não possuindo, por-tanto, comandos de atribuições. Dessa forma, uma expressão sempre será avaliada com o mesmo valor. Segundo Claessen et al. (2002), apesar de linguagens funcionais apre-sentarem um estilo de programação que favorece o desenvolvimento de programas com menores taxas de erros, programas funcionais podem conter erros decorrentes da falta de entendimento de suas propriedades. Nesse contexto, é importante a investigação de critérios de teste para apoiar o teste de programas funcionais. Entretanto, existem poucas iniciativas para o teste de programas funcionais bem como no desenvolvimento de ferramentas que apoiam essa atividade (Claessen & Hughes, 2000; Oliveira et al., 2003). Além disso, tais iniciativas não possuem uma medida de cobertura da atividade de teste. Assim, neste trabalho, com a motivação de buscar novas formas de realizar teste em programas funcionais que permitam avaliar a qualidade e adequação da atividade de teste, investigou-se a aplicação do Teste de Mutação para tais programas.
1.2. Objetivos 3
1.2 Objetivos
A linha deste trabalho é a investigação da adequabilidade da aplicação de critérios de teste tradicionalmente utilizados em programas imperativos no teste de programas funcionais. Em particular, busca-se estabelecer subsídios para a investigação a aplicação do Teste de Mutação para o teste de programas em SML (Standard Meta Language) (Milner et al.,
1997). A fim de viabilizar tal aplicação, foi desenvolvida a ferramenta P R O T E U M / S M L ,
que implementa os operadores de mutação definidos neste trabalho para SML. Os opera-dores de mutação caracterizam o Teste de Mutação para a linguagem/especificação alvo, estabelecendo os requisitos de teste a serem satisfeitos.
1.3 Organização do Trabalho
Esta dissertação está organizada como apresentado a seguir.
Neste capítulo foram apresentados o contexto em que se insere este trabalho, as motivações e os objetivos para o seu desenvolvimento.
No Capítulo 2 são apresentados os conceitos e técnicas de teste de software. E descrito com maiores detalhes o Teste de Mutação, critério de teste alvo deste trabalho.
No Capítulo 3 são apresentados os principais aspectos da programação funcional. Em particular, é descrita a linguagem SML. São citadas algumas aplicações e ambientes de apoio a SML. É apresentada uma breve comparação entre SML e linguagens convencio-nais. Também são relacionados alguns trabalhos sobre Verificação, Validação e Teste na linguagem SML.
No Capítulo 4 é apresentada a ferramenta desenvolvida neste trabalho, a PRO-TEUM/SML. São descritos os operadores de mutação definidos neste trabalho e imple-mentados na ferramenta. Além disso, são apresentados os aspectos de implementação mais relevantes, a arquitetura e a modelagem da P R O T E U M / S M L .
No Capítulo 5 são apresentadas as principais características operacionais da ferramenta
P R O T E U M / S M L e um exemplo completo de sua utilização.
No Capítulo 6 são apresentadas as conclusões desta dissertação, as principais contri-buições e as sugestões para trabalhos futuros.
No Apêndice A é apresentada a gramática livre de contexto da linguagem SML, utilizada na definição dos operadores de mutação implementados na ferramenta
CAPÍTULO
2
Teste de Software
2.1 Considerações Iniciais
Embora durante todo o processo de desenvolvimento de software sejam utilizadas técnicas, métodos e ferramentas, a fim de se evitar que erros sejam introduzidos no produto, a atividade de teste continua sendo de fundamental importância para a identificação dos erros que persistem. A atividade de teste de software visa a fornecer evidências de confiabilidade e qualidade de um produto de software, em complemento a outras atividades, como por exemplo, o uso de revisões e de técnicas formais rigorosas de especificação e de verificação. Assim, essa atividade constitui um elemento crítico para a garantia de qualidade de software (Pressman, 2000; Harrold, 2000).
Neste capítulo é apresentada uma síntese dos aspectos teóricos e práticos relacionados à atividade de teste abordados por Maldonado et al. (2003). Na Seção 2.2 são apresentados os conceitos gerais de teste de software. Nas Seções 2.3, 2.4 e 2.5 são descritas as principais técnicas e critérios de teste. Na Seção 2.5.1 é descrito com mais detalhes o Teste de Mutação, critério de teste alvo deste trabalho. Na Seção 2.6 são apresentadas algumas ferramentas de apoio ao Teste de Mutação existentes.
2.2 Conceitos Gerais
A atividade de teste envolve quatro etapas: planejamento de testes, projetos de casos de teste, execução e avaliação dos resultados dos testes (Myers, 1979; Pressman, 2000). Tais atividades devem ser desenvolvidas ao longo do processo de desenvolvimento de software e, em geral, são conduzidas em três fases: teste de unidade, teste de integração e teste de sistema. O teste de unidade concentra-se na validação da menor unidade de projeto de software, que pode ser tanto um módulo quanto um procedimento do programa, dependendo da granularidade desejada. O teste de integração é aplicado na validação das interfaces entre os módulos. Já o teste de sistema, concentra-se na validação do software integrado em seu ambiente, avaliando, por exemplo, seu desempenho, segurança e robustez.
Segundo Myers (1979), o principal objetivo do teste de software é revelar a presença de erros no produto. Portanto, um teste bem sucedido é aquele que consegue determinar casos de teste para os quais o programa em teste falhe. Assim, um ponto crucial que deve ser considerado na atividade de teste de software é o projeto e/ou avaliação da qualidade de um determinado conjunto de casos de teste a fim de que esse tenha alta probabilidade de encontrar a maioria dos erros com um mínimo de tempo e esforço, visto que, em geral, é impraticável utilizar todo o domínio de dados de entrada para avaliar os aspectos funcionais e operacionais de um produto em teste.
Critérios de teste estabelecem os requistos que devem ser testados e satisfeitos, po-dendo ser utilizados tanto para auxiliar na geração de conjuntos de casos de teste como para auxiliar na avaliação da adequação desses conjuntos. Em geral, os critérios de teste de software são classificados em três técnicas: funcional, estrutural e baseada em erros. Tais técnicas se diferenciam basicamente pela origem da informação que é utilizada para avaliar ou construir conjuntos de casos de teste. Na técnica funcional, utiliza-se a especificação do software para obter os requisitos de testes. Na técnica estrutural, os critérios e requisitos de teste são derivados a partir dos aspectos de implementação do software. Já na técnica
baseada em erros, os requisitos para caracterizar a atividade de teste são baseados em erros típicos que podem ser cometidos durante o desenvolvimento do software.
Vale ressaltar que as técnicas de teste possuem características complementares e devem ser aplicadas em conjunto, buscando-se utilizar as vantagens de cada uma a fim de obter uma atividade de teste de melhor qualidade. Para tanto, o estabelecimento de estratégias de teste eficazes e de baixo custo é de grande importância, constituindo uma das principais direções para a pesquisa na área de teste de software (Harrold, 2000). Deve-se destacar
2.3. Técnica Funcional 7 também que para apoiar essa tarefa, é relevante a condução de estudos teóricos e empíricos entre os critérios de teste.
A seguir, apresentam-se os principais aspectos das técnicas funcional, estrutural e baseada em erros. Ênfase é dada ao Teste de Mutação, um dos critérios da técnica baseada em erros, alvo deste trabalho.
2.3 Técnica Funcional
A técnica funcional é conhecida como "teste de caixa preta" pelo fato de tratar o software como urna caixa fechada, cujo conteúdo é desconhecido e apenas as entradas fornecidas e os resultados obtidos podem ser visualizados. Nessa técnica, utiliza-se essencialmente a especificação funcional do software para obter os requisitos de testes, sem considerar deta-lhes de implementação. Assim, uma especificação correta e de acordo com os requisitos do usuário é de fundamental importância para apoiar a aplicação dos critérios relacionados a essa técnica. Pode-se citar como exemplos de critérios do teste funcional (Pressman,
2000):
• Particionamento de Equivalência: O domínio de entrada é dividido em classes de equivalência, que são classes de dados válidas e inválidas, a partir das quais os casos de teste são derivados. As classes são definidas de modo que idealmente os erros revelados por um caso de teste sejam os mesmos revelados pelos demais elementos da classe a que pertence. Dessa maneira, basta selecionar um caso de teste de cada classe para cobrir o domínio de entrada.
• Análise do Valor Limite: Ao invés de selecionar qualquer elemento de uma classe de equivalência, como no particionamento de classes de equivalência, são es-colhidos casos de teste associados às extremidades das classes, pois, empiricamente, observa-se que erros são encontrados com maior frequência nesses pontos. Além disso, o domínio de saída também pode ser particionado e são exigidos casos de teste que produzam resultados nos limites dessas classes de saída (Myers, 1979).
• Grafo de Causa-Efeito: As condições de entrada (causas) e as ações (efeitos) do programa são identificadas e relacionadas em um grafo com uma notação adequada. Em seguida, esse grafo é convertido em uma tabela de decisão, a partir da qual são derivados os casos de teste.
Um problema referente à técnica funcional é a dificuldade de quantificar a atividade de teste, pois não se pode garantir que partes essenciais ou críticas do programa sejam
executadas. Além disso, como essa técnica deriva os casos de teste a partir da especi-ficação, que em geral é feita de modo descritivo e não formal, torna-se sujeita a diversas inconsistências. Isso dificulta a automatização dos critérios funcionais. Entretanto, os critérios funcionais podem ser utilizados em qualquer contexto e em qualquer fase de teste, visto que é apenas necessário identificar as entradas, a função a ser computada e a saída do programa.
2.4 Técnica Estrutural
A técnica estrutural é conhecida como "teste de caixa branca", pois, ao contrário do teste de caixa preta, os critérios e requisitos de teste são baseados nos aspectos de imple-mentação do software. A maioria dos critérios dessa técnica utiliza uma representação de programa conhecida como grafo de fluxo de controle ou grafo de programa, a partir do qual são escolhidos os componentes que devem ser executados. Tal grafo é um grafo orientado, com um único nó de entrada e um único nó de saída, no qual cada vértice representa um bloco indivisível de comandos e cada aresta representa um desvio de um bloco a outro e indica um possível fluxo de controle (Pressman, 2000). Os blocos são caracterizados por não existirem desvios para o interior do bloco ou partir do interior do bloco, e uma vez que o primeiro comando do bloco é executado, todos os demais comandos desse são executados sequencialmente. Os critérios dessa técnica baseiam-se em tipos de estruturas diferentes para determinar quais componentes são requeridos na execução. Exemplos de critérios da técnica estrutural são:
• Critérios Baseados na Complexidade: Derivam os requisitos de teste a partir da complexidade do programa. O critério mais conhecido dessa classe é o critério de McCabe, que utiliza a complexidade ciclomática para derivar os requisitos de teste. Essencialmente, esse critério requer que um conjunto de caminhos linearmente independentes do grafo de programa seja executado (Pressman, 2000).
• Critérios Baseados em Fluxo de Controle: Derivam os requisitos de teste utilizando apenas as características do controle de execução do programa. En-tre os critérios mais conhecidos estão (Pressman, 2000): todos-nós, todos-arcos e todos-caminhos. A exigência do critério todos-nós e todos-arcos é que a execução do programa passe pelo menos uma vez em cada vértice e em cada aresta do grafo de programa, respectivamente. Já o critério todos-caminhos requer que todos os cami-nhos possíveis do grafo de programa sejam executados e, em geral, é impraticável.
2.5. Técnica Baseada em Erros 9
• Critérios Baseados em Fluxo de Dados: Derivam os requisitos de teste a partir das informações sobre o fluxo de dados do programa. Esses critérios utilizam dois conceitos básicos: definição da variável (ponto do programa no qual um valor é atribuído a uma variável) e uso da variável (ponto no qual esse valor é utilizado). Os caminhos a serem exercitados são definidos em função das associações definição-uso das variáveis do programa. Quando uma variável é usada em uma computação, diz-se que seu uso é computacional (c-uso); quando usada em uma condição, seu uso é predicativo (p-uso). Pode-se citar como exemplos dessa classe os critérios definidos por Rapps & Weyuker (1985), tais como todas-definições (todas-def), todos-usos, todos-du-caminhos e todos-p-usos, e também pode-se citar os critérios potenciais-usos (Maldonado, 1991). 0 critério todos-usos, por exemplo, requer que cada associação definição-uso de uma variável seja exercitada por ao menos um caminho.
Uma limitação relacionada à técnica estrutural é decorrente da impossibilidade, em geral, de se determinar caminhos e associações não executáveis (Rapps & Weyuker, 1985). Isso introduz problemas na automatização do processo de validação de software (Maldonado, 1991). Mesmo com essas desvantagens, essa técnica é vista como comple-mentar à técnica funcional (Pressman, 2000) e informações obtidas pela aplicação desses critérios têm sido consideradas relevantes para as atividades de manutenção, depuração e confiabilidade de software (Hartmann & Robson, 1990; Ostrand & Weyuker, 1988; Pressman, 2000; Varadan, 1995; Veevers & Marshall, 1994).
2.5 Técnica Baseada em Erros
Na técnica baseada em erros, os critérios e requisitos de teste são oriundos do conhecimento sobre os erros mais frequentemente cometidos durante o desenvolvimento do software. Exemplos de critérios dessa técnica são os critérios Semeadura de Erros e Teste de Mutação.
No critério Semeadura de Erros (Budd, 1981), erros são artificialmente inseridos no programa. Durante os testes, verifica-se a razão entre os erros artificiais e os erros naturais encontrados. Essa razão indica, teoricamente, a quantidade de erros naturais que ainda não foram revelados. No entanto, a precisão da estimativa obtida depende diretamente da uniformidade da distribuição dos erros no programa, o que, em geral, não acontece na prática. Segundo Pressman (2000), uma região que possui um erro tem grande probabilidade de possuir outros erros. Deve-se ainda considerar que alguns erros
artificiais podem interagir com erros naturais, omitindo, assim, os erros naturais com os artificiais.
O Teste de Mutação, por ser o critério alvo deste trabalho, é apresentado com mais detalhes na seção a seguir.
2.5.1 Teste de Mutação
O Teste de Mutação (DeMillo et al., 1978) surgiu na década de 70 na Yale University c Geórgia Institute of Technology. Esse critério utiliza programas criados a partir do programa a ser testado com pequenas alterações sintáticas (mutantes) a fim de testar a corretitude de um programa através da geração de casos de teste capazes de diferenciar o comportamento do programa original e de seus mutantes. Pode ser utilizado para avaliar a adequação de um conjunto de casos de teste em revelar determinados erros típicos cometidos ao desenvolver um sistema e para guiar a geração de um conjunto de casos de teste adequados a fim de revelar tais erros. Os mutantes são gerados por operadores de mutação que estão associados a um tipo ou classe de erros que se pretende revelar no programa.
Duas hipóteses são utilizadas para escolher o conjunto de erros a serem modelados: a hipótese do programador competente e a hipótese do efeito de acoplamento (DeMillo et al., 1978). A hipótese do programador competente assume que os programadores escrevem programas corretos ou próximos do correto. Dessa forma, o conjunto de erros que deve ser modelado pode ser reduzido apenas aos erros típicos cometidos. Já a hipótese do efeito
de acoplamento afirma que casos de teste que revelam erros simples são eficientes para revelar erros mais complexos. Um erro simples está associado a apenas uma alteração sintática simples. Um erro é complexo quando várias alterações sintáticas são necessárias para criar uma versão correta do programa (um erro complexo pode ser considerado como uma composição de erros simples). Apesar de não formalmente provada para o caso mais geral, a hipótese do efeito de acoplamento é normalmente aceita como empiricamente válida. Com base nessas duas hipóteses, os erros modelados podem ser restringidos a erros simples.
Apesar de ter sido inicialmente proposto para o teste de unidade, o Teste de Mutação vem sendo utilizado em diversos contextos, tais como no teste de integração, teste de programas orientados a objeto e teste de especificação de sistemas reativos. Para a adequação do uso do conceito de mutação para o teste de integração foi definido o critério Mutação de Interface (Delamaro, 1997; Delamare et al., 2001a), que tem como propósito principal viabilizar o teste de interface entre as unidades que compõem o software, ao invés
2.5. Técnica Baseada em Erros 11
de somente explorar as características das unidades separadamente, como faz o Teste de Mutação.
Vários trabalhos podem ser destacados na definição de operadores de mutação para programas orientados a objeto (Bieman et al., 2001; Kim et al., 2000, 2001; Ma et al., 2002). Vincenzi (2000) identificou para o teste de métodos quais operadores de mutação de unidade da linguagem C definido por Agrawal (1989) poderiam ser aplicados no contexto de programas C + + e Java. Além disso, tem-se explorado o Teste de Mutação no contexto de sistemas distribuídos que se comunicam via CORBA (Ghosh & Mathur, 2001; Sridhanan et al., 2000) e no trabalho de Delamaro et al. (2001b) foi definido um conjunto de operadores de mutação que modelam erros típicos cometidos em programas concorrentes em Java.
No contexto de especificações de sistemas reativos, foi explorada a adequação do uso do Teste de Mutação no teste de especificações baseadas em Máquinas de Estados Finitos (Fabbri et al., 1994, 1999a), Statecharts (Fabbri et al., 1999b; Sugeta, 1999; Sugeta et al., 2001), Redes de Petri (Fabbri et al., 1995b; Simão, 2000; Simão et al, 2000), Redes de Petri Colorida (Simão, 2002), Estelle (Probert & Guo, 1991; Souza et al., 2000) e SDL (Sugeta et al., 2004).
Neste trabalho busca-se fornecer subsídios para a investigação da aplicabilidade do Teste de Mutação para o teste de programas funcionais, escritos na linguagem SML (.Standard ML) (Milner et al, 1997).
Por meio de estudos empíricos, têm-se obtido evidências de que o Teste de Mutação pode constituir na prática um critério atrativo para o teste de programas (Mathur & Wong,
1993). Esses experimentos, além de mostrarem como o Teste de Mutação se relaciona com outros critérios de teste, buscam novas estratégias a fim de reduzir os custos associados a sua aplicação, visto que no Teste de Mutação é gerado um grande número de mutantes mesmo para programas simples.
Alguns estudos empíricos foram conduzidos visando a estabelecer alternativas viáveis para a aplicação do Teste de Mutação, as quais reduzem seu custo de aplicação através da diminuição do número de mutantes a ser analisado. Dentre essas alternativas estão: a investigação de critérios de mutação alternativos, tais como Mutação Aleatória (Randomly Selected X% Mutation) e Mutação Restrita (Constrained Mutation) (Mathur & Wong, 1993); o estabelecimento de uma ordem incremental entre classes de mutação restrita (sub-conjuntos de operadores de mutação) (Wong et al., 1994a; Souza, 1996); a determinação de um conjunto essencial de operadores de mutação para programas Fortran (Offutt et al., 1996a) e para programas C (Barbosa, 1998; Barbosa et al., 2001); e o estabelecimento
de uma estratégia de teste incremental com o Teste de Mutação e Mutação de Interface (Vincenzi, 1998; Vincenzi et al., 2000).
Uma outra direção dos estudos empíricos em relação ao Teste de Mutação é a sua comparação com outros critérios de teste. Foram desenvolvidos estudos comparativos com os critérios de fluxo de dados todos-usos (Mathur & Wong, 1994; Wong et al., 1994b; Offutt, et al., 1996b) e potenciais-usos (Souza, 1996). Em relação ao critério todos-usos, os resultados obtidos indicam que o Teste de Mutação possui um custo de aplicação maior mas apresenta uma eficácia maior para revelar erros. O custo de aplicação é estimado pelo número de casos de teste necessário para satisfazer o critério. Em relação aos critérios potenciais-usos, os resultados demonstram que o custo do Teste de Mutação é maior. Além disso, observou-se que, em relação à dificuldade de satisfação, o Teste de Mutação e o critério todos-potenciais-usos são incomparáveis, de maneira geral, mesmo do ponto de vista empírico.
A seguir são descritos os passos para a aplicação do Teste de Mutação no teste de programa, que consistem em:
1. Geração dos mutantes;
2. Execução do programa original; 3. Exeaição dos mutantes; e 4. Análise dos mutantes vivos.
Passo 1: Geração dos Mutantes
Os mutantes são gerados por meio da aplicação dos operadores de mutação no programa P sendo testado. Entende-se por operadores de mutação as regras que definem as alterações que devem ser aplicadas no programa original P para dar origem aos programas mutantes. De um ponto de vista abstrato, um operador de mutação é uma função que toma um programa P e resulta em um conjunto de programas semelhantes a P, porém que possuem o erro modelado. Como o mesmo tipo de erro pode ocorrer em diferentes posições do programa, um operador gera um conjunto de programas mutantes, sendo um mutante para cada ponto onde a alteração pode ser aplicada. Na Figura 2.1 é ilustrada a aplicação dos operadores de mutação em um programa P, gerando os mutantes correspondentes.
Além dos tipos de erros que se desejam revelar e da cobertura que se quer garantir, a escolha de um conjunto de operadores de mutação depende também da linguagem em que estão escritos. Por exemplo, em Budd (1981), encontra-se a relação de 22 operadores
2.5. Técnica Baseada em Erros 13
Programa op 21 • ..•
°P M
Figura 2.1: Mutantes Gerados pela Aplicação dos Operadores de Mutação.
de mutação utilizados por um sistema de mutação para programas em Fortran. Para a linguagem C, Agrawal (1989) define um conjunto de 71 operadores de mutação, os quais foram implementados na ferramenta Proteum/C (Delamaro, 1993). Por exemplo, dois possíveis mutantes gerados pelo operador ORRN implementado na Proteum/C, que troca um operador relacional por outro relacional, são ilustrados pela Figura 2.2.
Se o conjunto M de mutantes modelasse todos os erros possíveis em um programa P, um conjunto T de casos de teste adequado para M poderia ser utilizado para provar que o programa não possui erros e, consequentemente, provar que P está correto. No entanto, o conjunto de mutantes seria infinito e a adequação de T não poderia ser avaliada. Portanto, para tornar factível a tarefa de avaliação da adequação, M deve ser finito, escolhendo-se apenas alguns tipos de erros. A escolha de quais erros modelar é um dos passos mais importantes para a eficácia e eficiência da aplicação do Teste de Mutação, visto que a quantidade de mutantes gerados está relacionada à escolha dos operadores de mutação. Os mutantes devem ser projetados de forma a encontrarem o maior número de erros com o menor custo de aplicação, ou seja, com o menor número de mutantes. Pois, um dos maiores problemas para a utilização do Teste de Mutação diz respeito ao grande número de mutantes gerados, mesmo em programas simples, que devem ser compilados e executados exigindo um alto custo computacional.
P: Programa Original Mutante 1: Mutante 2:
if a < = b then if a < b then if a > b then
Passo 2: Execução do Programa Original
Neste passo, executa-se o programa original P usando os casos de teste de T e verifica-se se o resultado obtido é o esperado. A tarefa de oráculo, que consiste em decidir se o resultado está correto ou não, geralmente, é desempenhada pelo testador (Delamaro & Maldonado, 1993). Caso o programa apresente um comportamento incorreto para algum caso de teste, então um erro foi revelado e o processo termina. Por outro lado, se nenhum dos casos de teste revelar a presença de erros, realiza-se o passo seguinte, que consiste em executar o conjunto de mutantes com os casos de teste.
Passo 3: Execução dos Mutantes
Cada mutante m G M gerado é executado usando os casos de teste Te os resultados obtidos são comparados com o de P. Se um mutante apresenta um resultado diferente do resultado obtido pelo programa original, então ele é dito "morto" pelo caso de teste, ou que o caso de teste "matou" o mutante. Uma vez que o programa mutante está morto, pode-se concluir que o programa original não possui o erro modelado por esse mutante no ponto em que a mutação ocorreu. Se um caso de teste não matar um mutante, duas situações podem estar ocorrendo. Ou o mutante é equivalente ao programa original (e nenhum caso de teste poderá matá-lo) ou o caso de teste não foi adequado para revelar o erro no mutante. Para essa segunda situação, um novo caso de teste deve ser criado para matar o mutante em questão.
Um conjunto T de casos de teste é dito adequado a um programa P e a um conjunto M de mutantes se para cada mutante m G M que não é equivalente a P existir um caso de teste t E T tal que t mata m com relação a P.
A adequação de um conjunto de casos de teste em relação a um programa em teste é obtida através do escore de mutação, que fornece a percentagem de mutantes não-equiva-lentes mortos pelo conjunto de casos de teste. Dessa forma, o escore de mutação fornece uma medida da confiabilidade do programa testado e é definido da seguinte maneira:
DM(P,T)
mS[- ' } M(P) — EM(P) sendo:
DM(P,T): número de mutantes mortos por T; M(P): número total de mutantes gerados; e
2.6. Ferramentas de Teste 15
Passo 4: Análise dos Mutantes Vivos
Este é o passo que requer mais intervenção do testador. Primeiramente, deve-se decidir em continuar ou não o teste. Se o escore de mutação já estiver bem próximo de 1 — o testador deve julgar o que é "bem próximo" — então pode-se encerrar o teste e considerar T como um bom conjunto de casos de teste para P. Decidindo-se por continuar o teste, deve-se analisar os mutantes que sobreviveram à execução dos casos de teste disponíveis e decidir se são ou não equivalentes ao programa original.
Em geral, o problema de decidir se dois programas são equivalentes é indecidível. No entanto, alguns métodos e heurísticas têm sido propostos para determinar a equivalência de programas em grande porcentagem dos casos de interesse (Budd, 1981; Simão & Maldonado, 2000).
A seção a seguir apresenta algumas ferramentas que apoiam o Teste de Mutação.
2.6 Ferramentas de Teste
O desenvolvimento de ferramentas de apoio à atividade de teste de programa é de grande importância, uma vez que essa atividade, se aplicada manualmente, é muito propensa a erros, improdutiva e limitada a programas muito simples (Horgan & Mathur, 1992). As ferramentas de teste podem apoiar estudos empíricos que visem a avaliar e a comparar os diversos critérios de teste existentes, a fim de auxiliar no estabelecimento de estratégias de teste. Outra vantagem do desenvolvimento de ferramentas é a possibilidade de se usar critérios de teste em ambientes industriais de produção de software, visto que uma das limitações para que os critérios deixem o estado da arte é a inexistência de ferramentas que apoiam sua aplicação. Dessa maneira, a disponibilidade de ferramentas propicia maior qualidade e produtividade para a atividade de teste.
Diversas ferramentas de apoio ao teste de programas podem ser encontradas. Para a aplicação dos critérios baseados em análise de fluxo de dados e/ou de controle, pode-se citar as ferramentas: Asset (Frankl & Weyuker, 1985), ATAC (Horgan & Mathur, 1992), ATACOBOL (Sze & Lyu, 2000), JaBUTI (Vincenzi et al., 2003), Proteste (Price & Zorzo, 1990), Poke-Tool (Maldonado et al., 1989; Chaim, 1991) e xSuds (Agrawal et al., 1998). Em Domingues (2002) são discutidos diversos critérios e ferramentas de teste de programas orientados a objeto.
Devido ao grande volume de informações que estão envolvidas na aplicação do Teste de Mutação, critério alvo deste trabalho, é inviável a sua realização manual. Geralmente, um grande número de mutantes deve ser gerado, executado e comparado. Dessa forma,
é essencial a existência de ferramentas de apoio para o uso desse critério na atividade de teste. Uma ferramenta que pode-se destacar em relação ao Teste de Mutação de programas, é a Mothra (DeMillo et al., 1988). Essa ferramenta apóia o Teste de Mutação para o teste de programas em Fortran-77, possuindo um total de 22 operadores de mutação (DeMillo et al., 1988). Foi desenvolvida na Purdue University e na Geórgia Institute of Technology. A ferramenta apresenta interface baseada em janelas, facilitando a visualização das informações, e permite a incorporação de outras ferramentas (gerador de casos de teste, verificador de equivalência e oráculo).
O grupo de pesquisa cm Engenharia de Software do Instituto de Ciências Matemáticas e de Computação - ICMC/USP, a fim de apoiar a validação da aplicação do Teste de Mutação, desenvolveu a família de ferramentas Proteum. Essa é composta por um conjunto de ferramentas que dão apoio ao Teste de Mutação tanto em nível de especificação quanto em nível de implementação. Fazem parte dessa família as ferramentas Proteum, Proteum/IM, Proteum-RS/FSM, Proteum-RS/ST, Proteum-RS/PN, P R O T E U M / C P N e
PROTEUM/SML, sendo a última desenvolvida neste trabalho.
A primeira ferramenta desenvolvida pelo grupo, denominada Proteum, apoia a aplica-ção do Teste de Mutaaplica-ção para programas C em nível de unidade. A fim de se evitar con-fusão, neste trabalho, a ferramenta Proteum é referida como Proteum/C. A Proteum/IM implementa o critério Mutação de Interface (Delamaro, 1997; Delamaro et al., 2001a) para o teste de integração de programas C. Dessa maneira, o testador pode usar o mesmo conceito durante as fases de teste de unidade e de integração. Atualmente, as ferramentas Proteum/C e Proteum/IM estão integradas em único ambiente de teste, Proteum/IM 2.0 (Delamaro et al., 2000).
Além disso, para o teste de especificações foram desenvolvidas as ferramentas Proteum-RS/FSM (Fabbri, 1996), Proteum-RS/PN (Simão, 2 0 0 0 ) , Proteum-RS/ST (Sugeta, 1999)
que implementam o Teste de Mutação para a validação de especificações baseadas em Máquinas de Estados Finitos, Redes de Petri e Statecharts, respectivamente. Há também as ferramentas que são disponíveis em ambiente web: P R O T E U M / C P N (Simão, 2002),
que oferece apoio ao Teste de Mutação para Redes de Petri Coloridas, e P R O T E U M / S M L
(Yano et al., 2 0 0 3 ) , que oferece apoio ao Teste de Mutação para programas funcionais em
Standard ML.
2.7 Considerações Finais
Neste capítulo foi apresentada uma descrição de alguns critérios de teste das técnicas funcional, estrutural e baseada em erros, enfatizando-se o Teste de Mutação, alvo deste
2.7. Considerações Finais 17 trabalho. É importante ressaltar que essas diferentes técnicas de teste são complementares e devem ser utilizadas em conjunto, de forma que as vantagens de cada uma sejam melhor exploradas em uma estratégia de teste que leve a uma atividade de teste de boa qualidade e, portanto, eficaz e de baixo custo. Além disso, foram citadas algumas ferramentas de apoio ao Teste de Mutação, visto sua importância na aplicação desse critério. Em particular, neste trabalho foi desenvolvida a ferramenta P R O T E U M / S M L que oferece
apoio ao Teste de Mutação para programas na linguagem SML. Assim, no próximo capítulo são apresentadas as principais características, aplicações e ambientes de apoio da linguagem SML, como também, são citados alguns trabalhos relacionados à VV&T de programas funcionais.
CAPÍTULO
3
Programação Funcional
3.1 Considerações Iniciais
As linguagens de programação funcional, tais como Standard ML, Lisp, Scheme e Haskell, enfatizam regras e casamento de padrões. Ao contrário de um programa procedimen-tal, por exemplo um programa em C ou Pascal, que é escrito como uma sequência de instruções, informando ao computador os passos para a resolução do problema. Neste capítulo são apresentadas algumas características da programação funcional com base na linguagem Standard ML (SML — Standard Meta Language) (Milner et al., 1997), linguagem alvo da definição do Teste de Mutação deste trabalho. SML é uma linguagem relativamente nova que possui características interessantes de uma linguagem de pro-gramação moderna. Além disso, SML distingue-se das linguagens funcionais puras por possuir construtores imperativos, mecanismo de exceção e recursos para modularidade, encapsulamento de conceitos e reuso de software.
Na seção a seguir são apresentadas as principais características de SML, bem como alguns ambientes de apoio e aplicações de SML. Na Seção 3.3 é descrita uma breve com-paração entre SML e linguagens convencionais. Finalmente, na Seção 3.4 são relacionados alguns trabalhos direcionados à Verificação, Validação e Teste de programas funcionais.
3.2 Standard ML
Nesta seção são apresentados os aspectos mais relevantes da linguagem de programação SML. Para uma descrição mais detalhada, podem ser consultados diversos materiais sobre
S M L (Cumming, 1998; Gilmore, 2000; Harper, 1998, 2002; Milner et al., 1997; Paulson, 1996; Tofte, 1989, 1993, 1996; Ullman, 1998). Em Milner et al. ( 1 9 9 7 ) é apresentada a
gramática completa e a definição formal da semântica de SML. Há vários livros sobre programação em S M L , entre os quais o de Ullman ( 1 9 9 8 ) e Paulson ( 1 9 9 6 ) . Além disso,
pode-se encontrar alguns tutoriais disponíveis na WWW que auxiliam na aprendizagem teórica e prática da linguagem (Cumming, 1998; Gilmore, 2000; Harper, 1998, 2002; Tofte,
1989, 1993, 1996). No Apêndice A deste trabalho é apresentada a gramática livre de
contexto de SML utilizada na definição dos operadores de mutação implementados na ferramenta P R O T E U M / S M L .
A linguagem SML originou-se como uma meta-linguagem para encontrar e realizar provas em uma linguagem lógica. A linguagem evoluiu incorporando várias idéias de uma linguagem de programação moderna, tanto para pequena quanto para grande escala.
Essa linguagem foi proposta em 1983 e, posteriormente, definida formalmente em notações matemáticas por Milner et al. (1990), sendo chamada de SML'90. Uma revisão da linguagem, conhecida como SML'97, foi feita para torná-la mais simples, sendo descrita por Milner et al. (1997). A maioria das implementações de SML segue esse padrão ou está em processo de conversão para o mesmo. Seguindo essa direção, o SML'97 também é adotado neste trabalho. Algumas das implementações que seguem o SML'97 são descritas na Seção 3.2.1.
SML caracteriza-se, principalmente, por ser uma linguagem de programação funcional, realizando computações por meio de definições e aplicações de funções. As funções são consideradas como valores tais como inteiro e string e novas funções podem ser obtidas por composição de funções. Assim, SML apoia a programação higher-order, na qual uma função pode retornar outra função e utilizar funções como parâmetros (Ullman, 1998).
Uma consequência da programação funcional é que a computação ocorre através das avaliações de expressões e não por atribuições a variáveis, sendo livre de side-effects e independente do estado atual de execução do programa. Porém, SML difere das linguagens funcionais puras no sentido de permitir o uso de construtores imperativos tais como variáveis, atribuições e sequências de operações side-effects (Pucella, 2001)
Um aspecto importante de SML é ser uma linguagem fortemente tipada, na qual todos os valores e variáveis possuem um tipo que pode ser determinado em tempo de compilação, sem a necessidade de executar o programa. Isso garante que apenas operações compatíveis
3.2. Standard ML 21
sejam realizadas (Pucella, 2001). Por exemplo, no modo interativo, se a expressão em seguida é digitada,
1+2*3;
vai it — 7 : int
SML responde que o valor da variável i t é 7 e que seu tipo é inteiro. A variável i t é utilizada em SML para receber o valor de qualquer expressão no modo interativo.
Esse processo de checagem de tipo permite que muitos erros sejam encontrados pelo compilador, não sendo necessária a execução do programa. Embora a maioria das lingua-gens fortemente tipadas requerem uma declaração de tipo de todas variáveis, em SML isso não é necessário e apenas se requer uma declaração de tipo quando é impossível de se deduzir o tipo (Ullman, 1998).
SML possui tipos básicos tais como inteiro, real, caracter e string, e também tipos compostos tais como tupla, registro, lista e vetor a fim de criar objetos de dados complexos. A lista é um tipo de dado muito utilizado em SML, que constitue em uma sequência de itens de mesmo tipo. Por exemplo, uma lista de três inteiros 1, 2, 3 é representada em SML por [1,2,3]. A resposta a essa expressão é
[ 1 , 2 , 3 ] ;
vai it = [1,2,3] : int list
na qual atribui-se à lista o tipo int list, que pode ser lido como "lista de inteiros". Uma lista vazia é representa pela constante nil ou por um par de conchetes, [].
Novos tipos de dados podem ser definidos assim como podem ser tipos abstratos de dados, que restriguem o acesso a objetos de um determinado tipo, permitindo apenas que o acesso seja feito através de um conjunto de operações definidas para tal tipo (Ullman, 1998). Um exemplo é um tipo "pilha", para o qual definem-se operações de empilha e desempilha, entre outras, que são o único modo de ler ou modificar uma pilha.
Os tipos abstratos de dados são definidos através de structures (conjunto de tipos de dados, funções, exceções e demais elementos), signatures (tipo para structure) e functors (função que possui como argumento structures e outros elementos, e retorna um struc-ture). Esses permitem a SML fornecer facilidades como modularidade, encapsulamento de conceitos e reuso de software, visando ao desenvolvimento de programas de larga escala
(Ullman, 1998).
Além disso, SML suporta polimorfismo que permite uma função ter argumentos de vários tipos. Por exemplo, pode-se citar a função pré-definida length. que retorna o tamanho de uma lista independentemente do seu tipo. O tipo dessa função é definido como
length : 'a list — > int
em que o identificador ' a é denominado de variável tipo que representa qualquer tipo erri SML.
Ern SML, o uso de recursão é estimulado fortemente embora seja disponível construto-res iterativos. Funções recursivas permitem expconstruto-ressar as idéias computacionais de forma mais concisa. SML também permite a programação baseada em regras, semelhantemente a Prolog, quando ações são baseadas em regras se-então. A idéia principal é elaborar um conjunto de padrões-ações, no qual um valor é comparado com vários padrões. O primeiro padrão unificado provoca a execução da ação correspondente (Ullman, 1998).
SML fornece um mecanismo de tratamento de exceções, sendo que as exceções podem incluir valores arbitrários, inclusive funções. Em qualquer ponto do código, uma exceção pode ocorrer, abortando a operação atual e retornando o controle para o último tratador de exceção definido para tal exceção (Pucella, 2001).
A seguir são apresentadas algumas implementações e aplicações de SML.
3.2.1 Implementações de Standard ML
Existem diversas implementações disponíveis de SML'97, pode-se citar: SML/NJ, Mos-cow ML, Poly/ML, ML Kit, MLj e MLton. Uma breve descrição de cada uma dessas implementações é feita em seguida.
A maioria das implementações implementam em parte ou completamente a biblioteca base de SML (Standard ML Basis Library). A biblioteca base é um conjunto de estruturas padrões que contêm diversas facilidades tais como operações matemáticas, de entrada e saída e de processamento de lista e string. A biblioteca base de SML é o resultado do esforço conjunto de desenvolvedores de SML/NJ, Moscow ML e MLWorks a fim de melhorar a portabilidade de programas SML.
O presente trabalho adotou uma implementação particular de SML, chamada de SML/NJ1 (Standard ML of New Jersey), desenvolvida pelos Laboratórios Bell e pela Universidade de Princeton. Possui versões disponíveis para Unix, Linux e Windows e sua distribuição é livre com permissão de modificação no código fonte. SML/NJ implementa a linguagem SML'97 por completo e a maioria da biblioteca base de SML. Além disso, estende a linguagem SML'97 com higher-order functors e padrões-OU.
Moscow ML2 (Romanenko et al., 2000) implementa a linguagem SML'97 na sua tota-lidade, incluindo grande parte da biblioteca base de SML. Entretanto, não há operações
1 Disponível em <http://www. smlnj .org>. Ultimo acesso em 05/03/04.
3.2. Standard ML 23
de entrada e saída na biblioteca de Moscow ML. Existem versões disponíveis para Li-nux RedHat, Windows e MacOS. Moscow ML é um software livre, que pode ser redis-tribuído/modificado sobre os termos de GNU General Public License. Pode-se chamar funções C de Moscow ML, sendo de responsabilidade dessas funções fazerem o acesso e construção de valores SML apropriadamente. Provê interfaces com os servidores de banco de dados relacionais MySQL e PostgreSQL, interface à Internet e file sockets. Além disso, possui uma estrutura para produzir imagens PNG.
Poly/ML3 (Matthews, 1985, 2001) é uma implementação completa de SML'97 dis-ponível como código aberto. Implementa a maioria da biblioteca base de SML e também possui extensões tais como tratamento de processos e um modo de operação para manter a consistência de programas compostos por vários módulos. Possui um depurador simbólico o qual permite inserir breakpoints e visualizar variáveis locais como valores SML. A versão Windows inclui um ambiente para executar Poly/ML e editar arquivos com código SML. Também possui suporte para MacOS.
ML Kit4 (Elsman & Hallenberg, 2002a) possui código aberto e é hospedado pelo IT da Universidade de Copenhagen. ML Kit implementa toda a definição de SML'97, como também, implementa a maioria da biblioteca base de SML. Em particular, ML Kit compila módulos SML usando-se um esquema de compilação denominado de interpretação estática, no qual os módulos são interpretados durante a compilação e não em tempo de execução como nas outras implementações de SML. Além disso, ML Kit é a única entre as implementações de ML em que todas as diretivas de alocação de memória são inferidas pelo compilador, que usa análises do programa a respeito do tempo de vida e layout de armazenagem. O sistema possui um gráfico sobre o uso de memória por regiões, permitindo maior controle sobre seu uso. As aplicações ML Kit podem fazer chamadas a funções C usando-se as convenções padrões de chamadas C.
MLj5 (Benton et al., 1999) é um compilador para SML que produz Java bytecodes, permitindo que o código compilado seja executado em qualquer computador com uma máquina virtual Java. Implementa a maioria de SML'97, exceto o subconjunto de functor. Entretanto, adiciona extensões da linguagem para simplificar a interface com Java. Desse modo, compila apenas aplicações modulares cujo programa consiste em uma coleção de structures e signatures de nível alto. Também implementa a maioria da biblioteca base de SML. MLj é um software livre, que pode ser redistribuído/modificado sobre os termos de GNU General Public License.
3Disponível em <http://www.polyml.org/>. Ultimo acesso em 05/03/04.
4Disponível em <http://www.itu.dk/research/mlkit/>. Ultimo acesso em 05/03/04.
MLton6 (Fluet & Weeks, 2001; Weeks, 1999) implementa por completo a linguagem SML'97, bem como, a biblioteca base. Suporta as plataformas Linux, FreeBSD, Windows e SunOS. Pode fazer chamadas de SML para C ou vice-versa. Possui estratégias para gerenciamento de memória. Suporta grandes quantidades de memória e vetores grandes. Possui bibliotecas, como por exemplo, para continuações, finalizações, salvar e recuperar pilha, números aleatórios.
3.2.2 Aplicações em SML
A linguagem SML tem sido utilizada em diversas aplicações, tais como em compiladores, provadores de teoremas, desenvolvimento de sistemas, parser e pesquisas em linguagens de programação. A seguir são descritas algumas dessas aplicações.
Sistemas de Software e Redes
ML Server Pages (MSP) (Sestoft et al., 2000) é uma linguagem de script web, uma integração de SML e HTML no estilo da Surís Java Server Pages, Microsoft's Active Server Pages ou PHP.
SMLserver (Elsman & Hallenberg, 2002b) é um servidor web que compila programas SML em arquivos bytecodes, que são carregados uma única vez e podem ser executados quantas vezes o cliente requerer. SMLserver é baseado na implementação ML Kit. SML-server é conectado ao AOLSML-server, sendo um servidor web multi-threaded de código aberto provido pela America Online e usado para web sites dinâmicos e de larga escala Além disso, herda diversas característica do AOLserver, tal como ter a possibilidade de acessar uma variedade de sistemas de gerenciamento de banco de dados relacional, como Oracle e PostgreSQL.
O projeto Express (Ford et al., 1997) tem como finalidade desenvolver um kernel de sistema operacional e um ambiente de programação que suportarão e permitirão explorar implementações de linguagens avançadas. Atualmente, está em desenvolvimento um kernel experimental construído para permitir que a implementação SML/NJ execute sobre um PC.
O projeto Fox (Biagioni et al., 1997, 1995) da Universidade Carnegie Mellon inclui estudos teóricos de linguagens de programação e suas propriedades, desenvolvimento de novos compiladores e tecnologia run-time, e estudos empíricos da aplicação de conceitos e idéias de linguagens avançadas para problemas de programação do mundo real, espe-cialmente na área de redes e sistemas operacionais. SML foi a linguagem escolhida para
3.2. Standard ML 25
iniciar o trabalho de projeto e implementação de linguagens. A fim de avaliar a viabilidade de SML como um veículo para programação de sistemas, o projeto Fox tem implementado um conjunto de módulos de software que implementam protocolo de comunicação de rede. Esse conjunto de módulos é chamado de FoxNet e é implementado inteiramente em uma extensão de SML.
CPN Tools (Beaudouin-Lafon et al., 2001; Ratzer et al., 2001) é uma ferramenta para edição, simulação e análise de Redes de Petri Coloridas hierárquicas temporais e não temporais, desenvolvida na Universidade de Aarhus. CPN Tools tem como objetivo substituir a Design/CPN que é ferramenta muito utilizada nesse contexto, a fim de aperfeiçoar a interface gráfica com o usuário através do uso de técnicas de interação avançadas. CPN Tools, assim como a Design/CPN, utiliza a linguagem CPN ML para declarações e inscrições da rede. Essa linguagem é uma extensão de SML, no sentido de adicionar facilidades sintáticas para declarações de conjuntos de cores, de permitir ao usuário declarar o tipo da variável e de permitir a definição do escopo de variáveis de referência.
Parser
fxp7 é um parser para XML escrito completamente na linguagem SML. Analisa um documento XML e encontra erros de sintaxe, de validade e entre outros problemas.
Provadores de Teoremas
Isabelle (Nipkow et al., 2003) é um ambiente genérico de provador de teorema, implemen-tado em SML e desenvolvido pela Universidade de Cambridge e Universidade de Munich, sendo um sistema genérico para implementação de formalismos lógicos.
A ferramenta STeP (Stanford Temporal Prover) (Bjorner et al., 1998) foi implementada em SML. STeP é uma ferramenta para a verificação formal de propriedades temporais de sistemas reativos, de tempo-real e híbridos. Ao contrário da maioria de sistema de verificação temporal, STeP não é restrita a sistema de estado finito e combina model ehecking com métodos dedutivos para permitir a verificação de uma grande classe de sistemas.
O sistema ADATE (Automatic Design of Algorithms Through Evolution) (Olsson, 1995) é desenvolvido em SML/NJ. ADATE é um sistema para programação automática através de inferências indutivas de algoritmos. Pode gerar automaticamente algoritmos
7Disponível em <http://atseidl2.informatik.tu-muenchen.de/~berlea/Fxp/>. Ultimo acesso
novos e não triviais. Os algoritmos sao gerados através de buscas combinatórias de larga escala que empregam transformações sofisticadas de programa.
Pesquisa em Linguagem de Programação
SML/NJ é utilizado na implementação do interpretador Terzo8 de Lambda Prolog, de-senvolvido e mantido pelos Laboratórios Bell e pelas universidades de Carnegie Mellon, Duke e Pensilvânia.
A implementação de Elf (Pfenning, 1991), uma linguagem de programação lógica res-trita desenvolvida na Universidade Carnegie Mellon, é um interpretador escrito em SML, em particular, SML/NJ. Elf é uma meta-linguagem para especificação, implementação e comprovação de propriedades de linguagens de programação e lógicas.
Um dos objetivos do projeto Venari (Wing et al., 1992) da Universidade Carnegie Mel-lon é prover apoio a software que armazenam, acessam e recuperam objetos baseando-se na sua semântica. Para tanto, têm sido projetado e implementado extensões da linguagem SML a fim de se obter dados e transações persistentes.
Moby (Moby, 2003) é um experimento em projeto e implementação de linguagem e tem como objetivo combinar o suporte para programação orientada a objetos baseada em classe e concorrência higher-order com as características de linguagens similares a SML: sistema de módulos avançados, polimorfismo, tipos de dados indutivos, casamento de padrão e funções first-class.
O projeto FLINT (Shao, 1997) da Universidade de Yale tem como propósito desen-volver sistemas de software industriais para linguagens de programação modernas tais como ML, Java e Safe C. Através de uma linguagem intermediária comum fortemente tipada, espera-se conseguir, por exemplo, código de baixo-nível seguro independente da linguagem e plataforma, interoperabilidade fina entre linguagens type-safe modernas e código eficiente através da compilação type-directed e análise de fluxo.
3.3 Standard ML vs Linguagens Procedimentais
SML, por ser urna linguagem funcional, apresenta as diferenças inerentes desse tipo de linguagem em relação às linguagens procedimentais. Linguagens funcionais enfatizam a avaliação de expressões, ao contrário das linguaguens procedimentais que executam sequências de comandos. Não há side-effects em programas funcionais, assim o valor da avaliação de uma expressão sempre será o mesmo, diferentemente de linguaguens como
3.4. Programas Funcionais: VV&T 27
Pascal ou C, que usam comandos com side-effects como a maneira mais usual de se realizar computações. Por exemplo, em Pascal, a atribuição a := b+c tem um side-effect, visto que o valor da variável a é modificado após a execução da atribuição. Em SML, ao contrário, ao avaliar a expressão b+c, é criado um elemento totalmente novo com o qual o resultado é associado (Ullman, 1998).
Em SML, a capacidade de tratar as funções como valores de primeira-classe permite a programação higher-order, com significante generalização. Enquanto que linguagens como Pascal ou C suportam funções como argumentos apenas de maneira limitada (Ullman, 1998).
Funções recursivas em SML substituem iterações tais como for-loops ou while-loops, que são normalmente utilizadas em Pascal ou C. Comandos iterativos são deselegantes e geralmente desencorajados na programação funcional, embora sejam encontrados em SML.
Uma função em SML pode ter um tipo polimórfico operando sobre valores de vários tipos. Considere, por exemplo, uma função busca que retorna um determinado elemento de uma lista. Em Pascal ou C, é necessário criar busca para cada tipo de lista, tal como para lista de inteiros ou de string. Em SML, é necessário apenas criar uma única função busca para qualquer tipo de elemento da lista.
Os structures de SML provêem o poder das "classes" usadas nas linguagens de pro-gramação orientada a objetos como C + + , Java ou Smalltalk. No entanto, a noção de estrutura em SML também inclui e generaliza idéias como a de biblioteca de funções encontrada em muitas linguagens e a de classes "friends" em C + + (Ullman, 1998).
SML é uma linguagem fortemente tipada, em que um valor de um determinado tipo não pode ser atribuído a uma variável de outro tipo. Já linguagens, como C, permitem que um valor mude seu tipo arbitrariamente através do mecanismo de cast.
O gerenciamento de memória, em SML, é automático, no qual um garbage collector é encarregado de recuperar memória em que dados não estão sendo mais utilizados. Isso elimina vários problemas relacionados a ponteiros perdidos em linguagens com gerencia-mento de memória explícito, tais como C e C + + (Pucella, 2001).
3.4 Programas Funcionais: VV&T
Segundo Claessen et al. (2002), apesar de linguagens funcionais apresentarem um estilo de programação que favorece o desenvolvimento de programas com menores taxas de erros, programas funcionais podem conter erros decorrentes da falta de entendimento de suas propriedades. Nesse contexto, é importante a investigação de critérios de teste para apoiar
o teste de programas funcionais. Entretanto, existem poucas iniciativas para o teste de programas funcionais bem como no desenvolvimento de ferramentas que dão apoio a essa atividade.
Uma ferramenta que pode-se citar é a QuickCheck (Claessen & Hughes, 2000, 2002) que apoia o teste de programas Haskell, uma linguagem de programação funcional pura. Tal ferramenta testa propriedades dos programas, as quais são descritas como funções Haskell. O teste pode ser feito automaticamente por entradas aleatórias ou pelo testador. Porém, com essa abordagem de teste aleatório, QuickCheck torna-se limitado a pequenos programas e não possui uma medida de cobertura que contribui para avaliar a qualidade e a adequação da atividade de teste.
HUnit (Herington, 2002) é um framework de teste de unidade para Haskell, similar à ferramenta JUnit para Java. Permite que casos de teste sejam estruturados hierarquica-mente, podendo ser executados automaticamente.
Auburn (Moss & Runciman, 1999) é uma ferramenta para benchmarking de estruturas de dados funcionais. Produz uma árvore que classifica as melhores estruturas de dados de acordo como é utilizado. Possui a capacidade de extrair um perfil de como uma aplicação usa uma estrutura de dados.
C A S L T E S T (Oliveira et al., 2003) é uma ferramenta que auxilia no teste de
espe-cificações C A S L utilizando a linguagem S M L . C A S L é uma linguagem unificada para
descrever especificações algébricas. O objetivo da ferramenta é projetar oráculos com dados de teste de especificações C A S L , gerando oráculos executáveis e dados de teste em
SML para cada axioma da especificação. Dessa maneira, verificam-se se os casos de teste derivados dos axiomas da especificação são satisfeitos pelos programas escritos em SML. Entretanto, C A S L T E S T também não estabelece nenhuma medida de cobertura do teste
realizado.
3.5 Considerações Finais
SML é fundamentalmente uma linguagem funcional, no sentido de que apresenta toda a capacidade das funções matemáticas. Entretanto, diferencia-se das linguagens funcionais puras por possuir construtores imperativos e mecanismo de exceção. Além disso, possui facilidades para o gerenciamento de programas de grande escala através de um sistema de módulos. E, finalmente, é uma linguagem fortemente tipada e oferece apoio ao polimorfismo (Milner et al., 1997).
Outras linguagens também possuem algumas características de SML. Por exemplo, Lisp é principalmente funcional, suporta funções higher-order e promove o uso de recursão.