• Nenhum resultado encontrado

Verificando a corretude de geradores automáticos de código

N/A
N/A
Protected

Academic year: 2021

Share "Verificando a corretude de geradores automáticos de código"

Copied!
71
0
0

Texto

(1)Pós-Graduação em Ciência da Computação. “Verificando a corretude de geradores automáticos de código” Por. Thiers Garretti Ramos Sousa Dissertação de Mestrado Profissional. Universidade Federal de Pernambuco posgraduacao@cin.ufpe.br www.cin.ufpe.br/~posgraduacao. RECIFE, ABRIL/2010.

(2) Universidade Federal de Pernambuco CENTRO DE INFORMÁTICA PÓS-GRADUAÇÃO EM CIÊNCIA DA COMPUTAÇÃO. Thiers Garretti Ramos Sousa. “Verificando a corretude de geradores automáticos de código". Este trabalho foi apresentado à Pós-Graduação em Ciência da Computação do Centro de Informática da Universidade Federal de Pernambuco como requisito parcial para obtenção do grau de Mestre Profissional em Ciência da Computação.. ORIENTADOR: Prof... Alexandre Cabral Mota. RECIFE,ABRIL/2010.

(3) Sousa, Thiers Garretti Ramos Verificando a corretude de geradores automáticos de código / Thiers Garretti Ramos Sousa. - Recife: O Autor, 2010. x, 58 folhas : il., fig., tab. Dissertação (mestrado profissional) Universidade Federal de Pernambuco. CIN. Ciência da Computação, 2010. Inclui bibliografia. 1. Engenharia de software. I. Título. 005.1. CDD (22. ed.). MEI2010 – 0124.

(4)

(5) A minha família. . ..

(6) Agradecimentos. Agradeço primeiramente a Deus, que com a sua sabedoria, nos guia pelos caminhos corretos da vida. Agradeço aos meus Pais, José Carlos e Marly, por todo incentivo durante o período deste estudo. Sem eles eu não conseguiria alcançar esse grande novo objetivo da minha vida. Agradeço também a minha irmã, Janaína, que sempre distante esteve perto, sempre me ajudando nos momentos que precisava. Amo vocês. Agradeço a minha namorada, Gabi, pela compreensão, incentivo, paciência e pela sua enorme calma. Nos momentos mais difíceis deste período me ajudou de diversas maneiras possíveis. Te amo! Agradeço também ao meu orientador, Alexandre Mota, primeiramente por ter acreditado que eu era capaz de realizar este trabalho, mesmo que a distância. Sua dedicação e paciência durante todo o período, sempre me dando conselhos e me mostrando o lado bom da academia. Sua ajuda foi essencial para a conclusão deste trabalho.Muito Obrigado. Queria agradecer também aos colegas que fiz durante essa jornada de estudos, principalmente o Cristiano Bertolini e o Henrique Rebêlo. Vocês me ajudaram bastante, principalmente nesta reta final do trabalho. Obrigado. Gostaria de agradecer ao Timothy Wahls pelos esclarecimentos acerca de jmle. Sendo sempre objetivo e pontual para tirar minhas dúvidas. Também queria agradecer ao meus amigos de Aracaju e os que eu fiz em Recife. Sempre me dando apoio. Obrigado a todos vocês!. iii.

(7) “No meio de toda dificuldade encontra-se a oportunidade”. Albert Einstein.

(8) Resumo. Os contratos (modelos que descrevem mais detalhadamente a arquitetura e os componentes) podem ser utilizados para a construção de softwares corretos. Esta construção pode ser realizada através do (1) cálculo de refinamento, (2) refinamento da estratégia e (3) fazendo a geração automática de código. Embora (1) e (2) são soluções corretamente comprovadas, eles requerem bastante esforço. Por outro lado, (3) é uma solução mais simples para derivação do código. No entanto, esta solução não pode fornecer um código confiável em relação aos seus contratos (ao menos que se comprove que o gerador de código é correto). Este trabalho propõe uma estratégia de testes baseados em modelos para verificar se determinado gerador de código é correto. A estratégia inicia-se com JML (linguagem baseada em contrato utilizada no Java) usando como estudo de caso o JavaCard, JMLe (um gerador de código baseado em JML) e o Jartege (gerador de testes baseados em modelos). Além disso, através deste trabalho é realizado um experimento onde é investigado o número de erros encontrados no jmle variando os valores do parâmetros do Jartege no nosso estudo de caso. Palavras-chave: jmle; Testes baseados em modelos; Especificações Formais;. v.

(9) Abstract. Contracts (models that describe more detail the architecture and components) can be used to build correct software. This can be accomplished by means of a (1) refinement calculus, a (2) refinement strategy, and by (3) automatic code generation. Although (1) and (2) are provencorrect solutions, they demand a lot of effort. On the other hand, (3) is a lightweight solution to derive code. However, this solution cannot provide a trustworthy code in relation to its contracts (unless the code generator was proven correct). In this paper we propose a model-based testing strategy to check whether a given code generator is correct. We instantiate this strategy with JML (the contract-based language for Java) using JavaCard as case study, jmle (a JML code generator), and Jartege (a model-based test generator). Furthermore, our instantiation becomes part of an experiment where we investigate if the number of bugs found in the application of jmle varies by changing the value of some parameters of Jartege. As result, we conclude that jmle has bugs and one of the causes is array handling. Furthermore, the parameters of Jartege influence the analysis. Keywords: jmle; Model-Based Testing; Formal Specification;. vi.

(10) Sumário. 1 Introdução 1.1 Motivação 1.2 Problema 1.3 Estratégia Proposta 1.4 Contribuições 1.5 Organização da dissertação. 1 1 2 3 3 3. 2 Estratégia Proposta 2.1 Estratégia 2.2 Projeto por Contrato 2.2.1 Pré e Pós-condições 2.2.2 Invariantes 2.3 Testes baseados em modelos 2.4 Desenvolvimento baseado em modelos 2.5 Geradores de código. 5 5 7 9 9 10 12 14. 3 Aplicando a Estratégia 3.1 A linguagem JML 3.1.1 Pré- e Pós- Condições 3.1.2 Invariantes 3.1.3 Constraints 3.1.4 Signals 3.1.5 Assignable 3.1.6 Pure 3.1.7 Also 3.1.8 Model 3.1.9 Quantificadores 3.1.10 Herança de especificações 3.1.11 Tipo de Especificações 3.1.12 Modificadores de Visibilidade 3.1.13 Especificações informais 3.1.14 Extensão de Expressões 3.1.15 Redundância 3.2 Ferramentas JML 3.3 O gerador de código jmle. 16 16 18 18 19 20 21 21 21 22 23 24 24 24 25 26 27 27 30. vii.

(11) SUMÁRIO. 3.4. Jartege. viii 32. 4 Experimentos 4.1 Setup 4.2 Análise quantitativa 4.3 Análise Qualitativa 4.4 Ameaças de validade 4.5 Discussão. 37 38 39 42 47 48. 5 Trabalhos relacionados. 49. 6 Conclusão 6.1 Trabalhos Futuros. 52 53.

(12) Lista de Figuras. 2.1 2.2 2.3 2.4 2.5 2.6 2.7. Estratégia Proposta Exemplo pré-condição e pós-condição Exemplo de invariante Exemplo de geração de testes baseado em modelos Fases do MBT Passos da especificação do MDA Exemplo do funcionamento do gerador de código. 6 9 10 10 11 13 15. 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16. Exemplo de uma anotação JML Pré- e Pós- Condições Exemplo do uso da constraint Exemplo do uso do signals Exemplo do uso do signals e ensures (retirado de [DFG05]) Exemplo do uso do also Exemplo do uso do model Exemplo de Quantificadores Exemplo dos Tipos de Especificação Exemplo de modificadores de visibilidade Exempo de uma especificação informal Exempo de uma especificação com redundância Estrutura do Compilador JML (JMLc) Exemplo de constraint programming Processos utilizados pelo Jartege Classe de geração de valores primitivos Classe de geração de valores primitivos. 17 20 20 21 22 23 23 25 26 26 27 29 30 34 35 36. 4.1 4.2 4.3 4.4 4.5. Box-Plot from classes AID, Applet and JCSystem Gráfico de Dispersão Especificação do método partialEquals Caso de teste do método partialEquals Método getAppletShareableInterfaceObject e a sua especificação JML Exemplo de violação das pré-condições pelo método AID. 41 41 43 43. 4.6. ix. 46 47.

(13) Lista de Tabelas. 3.1. Extensões Java. 27. 4.1 4.2 4.3 4.4. Configuração dos testes executados Configuração para geração das classes de teste Sumário dos Tratamentos Tabela de dispersão ANOVA. 38 39 40 42. x.

(14) C APÍTULO 1. Introdução. Neste capítulo descrevemos o que nos motivou a realizar este trabalho, identificando o problema que iremos nos dedicar em particular. Após apresentado o problema, discutimos sobre como lidaremos com o mesmo usando uma estratégia proposta. Para se ter uma ideia mais sucinta do que realizamos neste trabalho, apontamos nossas principais contribuições e finalmente apresentamos como os capítulos cobrirão todo o material envolvido na realização desta dissertação.. 1.1 Motivação Garantir a qualidade de software no processo de desenvolvimento é uma tarefa que demanda bastante esforço e disciplina. Ainda mais se considerarmos o fato de que erros são potencialmente introduzidos com grande frequência durante todo o desenvolvimento. Dentre as várias soluções encontradas para garantir a qualidade desejada para um produto, testes de software vêm sendo a mais amplamente empregada. Como em outras partes do processo de desenvolvimento, os testes de software não devem ser apenas executados no final do produto. É fato, já relatado na literatura [Boe02], que erros encontrados em fases finais do processo de desenvolvimento produzem um custo maior que aqueles identificados nas fases iniciais. Tal custo está associado a comum alteração de vários artefatos de desenvolvimento, desde requisitos até o código propriamente dito. Como boa parte dos erros está associada ao mau entendimento dos requisitos de um produto, o desenvolvimento baseado em modelos (ou Model-Driven Development – MDD) tomou força nestes últimos anos [PSM+ 09]. A grande vantagem do desenvolvimento baseado em modelos é o fato de que os modelos são usados para expressar os requisitos de um produto de forma única (não-ambígua). Além disso, tais modelos em geral possuem uma semântica bem definida que permite o desenvolvedor verificá-los e validá-los mais precisamente. O MDD está sendo utilizado em vários lugares, incluindo sistemas críticos, por exemplo, sistemas embarcados em aeronaves. A razão disto é quase que óbvia (na visão das empresas): MDD pode fornecer códigos executáveis assim como testes de conformidade com um baixo custo de produção. Outro fato importante sobre os modelos é que eles precisam ser suficientemente concretos (descrever detalhes sobre o produto) para que se possam derivar certos artefatos a partir dos mesmos. Nesta direção, o paradigma de projeto baseado em contratos (ou Design by Contracts—DbC) vem sendo bastante usado em empresas de software como Microsoft e IBM. Contratos nada mais são que modelos que descrevem mais detalhadamente a arquitetura e os. 1.

(15) 1.2 PROBLEMA. 2. componentes (consideramos classes como componentes) de um sistema. Tanto a indústria como a academia propuseram e usam geradores de código baseados em contrato, variando do SCADE (Gerador de código para modelos Simulink, encontrados na indústria aeronáutica) até jmle (gerador de código para Java que utiliza constraint programming baseado em especificações JML [LPC+ 04]). No entanto, instituições certificadoras exigem que os geradores de códigos produzam resultados corretos (os mais confiáveis dentro do possível). Existem várias maneiras de demonstrar que os códigos executáveis são corretos: 1. Criação de códigos utilizando um cálculo de refinamentos [Mor90]; 2. Derivar uma especificação de um código gerado e provar que esta especificação é um refinamento da especificação original [ZC09]; 3. Utilizar testes baseados em modelos para demonstrar que o código gerado está de acordo com o que foi especificado (teste de conformidade). Destas alternativas, apenas a (3) pode ser executada de forma automática em geral, embora nós apenas possamos afirmar a presença de erros através da execução dos testes. Quando utilizamos testes baseados em modelos, estamos utilizando uma técnica de teste de caixa-preta, onde a geração dos casos de testes e as análises dos resultados são baseadas em alguma especificação. Isto é, não usamos qualquer conhecimento sobre o código para originar os casos de testes. A partir das especificações formais é possível a geração automática de casos de testes. Ao se utilizar especificações no processo de desenvolvimento, podemos garantir, ao menos, uma padronização para ser utilizada durante todo o processo de desenvolvimento do software. Porém, a construção destas especificações é vista como um ponto crítico no desenvolvimento, devido à resistência dos responsáveis para sua criação. Neste trabalho assumimos que tais modelos (especificações) foram devidamente criados, mesmo sabendo da dificuldade de adoção na indústria. Como forma de amenizar a resistência da indústria, pode-se utilizar a estratégia descrita em [CS08] que descreve uma maneira de mapear os cenários de uma linguagem natural em uma linguagem formal. Outra solução seria a utilização de uma linguagem formal que não necessite de uma aprendizagem tão específica, o que diminuiria a curva de aprendizagem. A linguagem JML, utilizada neste trabalho, possui anotações próximas à linguagem Java, o que facilita na construção dos modelos.. 1.2 Problema Do exposto anteriormente, vemos claramente que investigar a confiabilidade de geradores automáticos de código é um problema frequente na indústria de sistemas críticos (tais como automóveis, aviões, etc.), devido à necessidade de certificar seus sistemas antes que seres humanos possam vir a utilizá-los. Este trabalho irá verificar exatamente a confiabilidade de um gerador automático de código..

(16) 1.3 ESTRATÉGIA PROPOSTA. 3. 1.3 Estratégia Proposta Como forma de investigar a confiabilidade (cumprimento do que está previsto no contrato da especificação) de geradores automáticos de código, observamos que do mesmo modo que o código é gerado automaticamente, também o podem ser casos de testes baseados nos contratos [GDG+ 08]. Do pressuposto de que o código gerado automaticamente é confiável, nenhuma quantidade de casos de testes, por maior que seja, pode resultar em uma única falha. Desta forma, propusemos uma estratégia onde usamos testes baseados em modelos para gerar uma quantidade ajustável de casos de teste e submetê-los em código derivado automaticamente. Caso detectemos uma falha podemos afirmar que o gerador já não é totalmente confiável [SMBR10]. E enquanto não detectarmos falhas, pode aumentar a quantidade de casos de teste em busca da mesma, inclusive guiados por experimentos estatísticos (controlados). Nossa estratégia necessita de um estudo de caso para poder ser aplicada. Quanto mais complexo e real for o mesmo, melhor. Neste trabalho usamos a API do JavaCard, que foi criada pela Sun Microsystems.. 1.4 Contribuições As principais contribuições deste trabalho são: 1. Uma estratégia simples para investigar a corretude de geradores de código, através de testes baseados em modelos, que utilizam contratos para a geração de códigos; 2. Aplicação da estratégia em sua situação real prática, baseada em JML (com as especificações JML da API JavaCard da Sun), jmle (um gerador de código baseado em JML) e Jartege (gerador de testes baseado em modelos); 3. Realização de um experimento para demonstrar que o jmle possui erros e um desses erros está associado à manipulação de arrays; 4. Demonstrar que o código da API JavaCard, disponibilizado pela Sun, está de acordo com as especificações JML. Isto é realizado com a reutilização da própria estratégia proposta por este trabalho.. 1.5 Organização da dissertação O Capítulo 2 apresenta a estratégia utilizada para verificar a corretude dos geradores de código baseados em contratos. Este capítulo também apresenta os conceitos básicos utilizados para formular a estratégia. Já o Capítulo 3 demonstra a estratégia proposta instanciando os elementos através de especificações JML da API JavaCard (Sun Microsystems), do gerador de código baseado em contratos jmle e da ferramenta de testes baseadas em modelo Jartege. No Capítulo 4 demonstramos a GQM (goal-question-metric) proposta e algumas análises sobre nosso experimento..

(17) 1.5 ORGANIZAÇÃO DA DISSERTAÇÃO. 4. O Capítulo 5 apresenta alguns trabalhos relacionados e nossas conclusões e trabalhos futuros são apresentados no Capítulo 6..

(18) C APÍTULO 2. Estratégia Proposta. Neste capítulo apresentamos a proposta de uma estratégia [SMBR10] a ser usada para investigar a confiabilidade de código produzido por gerador automático de código, a qual está fundamentada sobre o paradigma de projeto por contrato. Como esta estratégia é baseada em vários conceitos, decidimos por realizar uma apresentação top-down (de cima para baixo), descrevendo a estratégia de uma forma bem geral e depois introduzindo detalhes sobre os vários conceitos associados com sua aplicação e melhor entendimento.. 2.1 Estratégia Testes baseados em modelos ultimamente têm sido bastante utilizados tanto pela indústria quanto pela academia [BEG09]. Entretanto, a indústria só pode se beneficiar totalmente de testes baseados em modelos com a utilização de processos de desenvolvimento baseados em modelos, MDD [RB08]. A principal razão é que a aplicação de testes baseados em modelos com requisitos informais pode custar caro (o esforço da extração de modelos formais através de artefatos informais) e a obtenção de conclusões pouco confiável (devido à ambiguidade e à falta de precisão). O desenvolvimento baseado em contratos pode beneficiar diretamente os testes baseados em modelos porque os modelos formais já estarão prontos. Embora os testes baseados em modelo sejam geralmente utilizados para verificar a conformidade entre o código produzido por um ser humano no que diz respeito às características do modelo indicado, este trabalho propõe uma estratégia ligeiramente diferente: verificar a conformidade de um código produzido por uma ferramenta no que diz respeito à sua especificação (contratos). Com esta nova perspectiva, não está sendo verificado apenas se o código produzido é livre de erros (até o limite dos testes realizados), mas também a qualidade do próprio gerador de código, uma vez que sua corretude (confiabilidade) implica que o gerador de código deve passar em qualquer teste gerado através de testes baseados em modelos. A Figura 2.1 apresenta a estratégia proposta por este trabalho. A partir de um contrato, utilizamos um gerador de código para a obtenção de um código correspondente a esta especificação (contrato). A partir deste mesmo artefato (contratos), geramos certa quantidade de casos testes (suíte de testes) utilizando uma ferramenta de testes baseados em modelos (esta quantidade depende das características da ferramenta utilizada e do resultado obtido através dos experimentos [PSAK04]). Para a execução da estratégia proposta foi necessário assumir que os contratos utilizados estavam corretos e o gerador de casos de testes também estavam gerando os casos de testes de acordo com o contrato. Através da suíte de testes, executamos os testes sobre o código gerado para verificar se. 5.

(19) 2.1 ESTRATÉGIA. 6. Figura 2.1 Estratégia Proposta. realmente todos eles foram executados com sucesso. Se um simples caso de teste falhar, podese concluir que o gerador de código possui erros. Por outro lado, se todos os testes forem executados com sucesso, não se pode concluir que o gerador de código é correto. Podemos sim ter sua confiabilidade aumentada uma vez que nos cenários realizados, não conseguimos identificar qualquer discrepância. Seguindo a estratégia proposta, a quantidade de testes e parâmetros do gerador de testes pode ser aumentada (gerando diferentes seeds). Como podemos alterar os parâmetros do gerador de casos de testes baseados em modelos, podemos inclusive verificar se as alterações realizadas afetam no resultado da execução de uma nova suíte de testes. Aumentar a quantidade de testes com o objetivo de encontrar possíveis falhas no gerador de código é relevante porque testes bem sucedidos não garantem corretude do gerador de código. Além disso, em tal situação, pode-se utilizar o experimento controlado para orientar na busca de possíveis falhas, bem como para concluir sobre alguns fatos durante a campanha de testes. Por exemplo, a configuração X da ferramenta de teste baseado em modelo é a melhor para produzir erros relacionados a alguma característica de Y. Quanto aos testes gerados através de uma ferramenta de testes baseados em modelos, em geral elas não produzem testes com os dados para serem executados (por exemplo, jml-junit [CL01]). Em vez disso, estas ferramentas produzem apenas a estrutura dos casos de teste. A principal razão é que a obtenção dos dados que satisfazem os predicados que descrevem as entradas e saídas de um caso de teste não é uma tarefa tão simples. Portanto na aplicação da estratégia proposta deve-se decidir se as ferramentas gerarão dados de forma aleatória [CCLC08] ou soluções mais elaboradas usando, por exemplo, SMT Solvers [ZM03]. Tal decisão será vista mais detalhadamente no Capítulo 4, onde fazemos uso de (decidimos por) geração aleatória. As seções a seguir apresentam os conceitos utilizados neste trabalho relacionados com a estratégia proposta..

(20) 2.2 PROJETO POR CONTRATO. 7. 2.2 Projeto por Contrato A confiabilidade dos sistemas computacionais atuais é um dos pontos mais importantes em projetos de software. Esta qualidade pode ser bastante contestada, visto que códigos são escritos por seres humanos e estão suscetíveis a erros. Para que se possa garantir que um software é confiável, devem ser analisados vários aspectos. Não basta apenas analisar se o sistema está realizando todas as atividades previstas nos seus requisitos, o sistema deve também estar preparado para situações inesperadas. Para verificar se o sistema está preparado para todas estas situações, devem ser realizados testes utilizando técnicas bem definidas. Para [Mey00], um software com qualidade deve possuir algumas premissas básicas, com:o credibilidade através de códigos corretos e da robustez, modularidade através da extensibilidade e reutilização de códigos e a eficiência, portabilidade, facilidade de uso, economia, funcionalidade entre outros. Porém, os erros não necessariamente são encontrados apenas nos códigos fontes, eles também podem ser encontrados nas especificações. É importante que os erros sejam encontrados o quanto antes, pois quanto antes descoberto menos oneroso será para corrigi-los. Os sistemas orientados a objetos são exemplos de melhoria na qualidade do software. Isto porque os sistemas OO possuem algumas características que Meyer [Mey00] define como básicas. Porém, somente a utilização desse tipo de paradigma não garante a qualidade do software. Através do paradigma conhecido como Design by Contract (DbC), a qualidade de um software pode ser melhorada. Esta técnica consiste em realizar um contrato entre um fornecedor e um cliente em que ambas as partes possuem obrigações e benefícios. O cliente tem que garantir algumas propriedades antes da utilização do serviço do fornecedor; e este, por sua vez, tem que satisfazer algumas características que são esperadas pelo cliente. A utilização dos contratos ajuda na construção de melhores sistemas porque a comunicação entre os elementos do software é realizada de uma maneira precisa, pois são delegados as obrigações e os benefícios de ambas as partes [Mey00]. DbC é um paradigma de desenvolvimento de software que pode ser utilizado para verificar a consistência entre o software e sua especificação [DFG05]. Ou seja, através dos contratos é possível validar se o que está descrito na especificação foi corretamente transcrito no código final. O princípio deste método é que uma classe e seu cliente tenham um contrato entre si. As cláusulas dos contratos são chamadas de asserções pelo DbC. Uma asserção garante um relacionamento entre o cliente (quem invoca) e o fornecedor (método invocado). Em outras palavras, o cliente é o código invocador e o fornecedor é o objeto. As pré-condições e póscondições são tipos de asserções utilizadas pelo DbC e são aplicadas de maneira individual a cada rotina. Existem ainda os invariantes que são propriedades que serão válidas em todo o ciclo de vida do objeto. O uso de pré e pós-condições no desenvolvimento de software não são recentes. Tais conceitos foram originalmente introduzidos ainda no final da década de 60 por Hoare, Floyd e Dijkstra [Hoa72], [Dij76], [Flo67]. Porém, a técnica desenvolvida por Meyer, em 1986, apresentou como novidade a possibilidade de que os contratos fossem executados junto com os códigos. Os contratos são escritos na própria linguagem de programação e são transformados em códigos executáveis através dos compiladores [LC04]. Meyer propôs primeiramente a utilização desta teoria na linguagem Eiffel [Mey00]..

(21) 2.2 PROJETO POR CONTRATO. 8. Outra contribuição da utilização dos contratos é a documentação do próprio código. Através da análise dos contratos é possível compreender o que será executado pelo código. Diferentemente dos comentários (escritos dentro do código), as asserções do contrato são mais abstratas, pois não são utilizados algoritmos. Concentra-se apenas no que é assumido e o que deve ser alcançado [LC04]. Com a utilização do DbC, também é permitida a prevenção de verificações desnecessárias do código [LC04], ou seja, a utilização das pré-condições permite que sejam verificados, antes da execução, aspectos relativos a sua execução. Por exemplo, imagine um método que tem um parâmetro que não pode assumir um valor negativo. Na depuração do código, a pré-condição é validada, não sendo necessária a validação deste parâmetro dentro do método em tempo de execução, aumentando assim a eficiência do código. Como as responsabilidades são bem definidas, é mais fácil de identificar quem não cumpriu com sua parte do contrato. Caso uma pré-condição seja violada, a culpa será atribuída ao cliente. Ou seja, o código que faz a chamada a um método precisa ser revisto para verificar o que está ocasionando a não conformidade. Caso a pós-condição não seja a esperada, a culpa passará para a classe fornecedora, que deverá ser revista para verificação da não conformidade [LC04]. É importante que os contratos não possuam cláusulas que não estejam bem definidas e documentadas, pois isso pode acarretar na falta de credibilidade no cumprimento do contrato [Mey92]. Os contratos permitem ainda também um auxílio na modularização lógica dos códigos. Esta modularização contribui no entendimento do que está sendo feito e também na reutilização dos contratos através da herança. Quando clientes herdam contratos já definidos, os contratos definidos pela superclasse são garantidos. É obrigatório que a subclasse obedeça também as obrigações dos contratos da superclasse, porém como na herança OO, a subclasse pode redefinir algumas pré e pós condições e também os invariantes. Essa modularização lógica possui um custo, pois o cliente não será capaz de deduzir nada além do que está explícito no contrato [LC04]. O conceito de invariantes não foi criado pelo DbC, ele já existe antes da criação da Orientação a Objetos. Este conceito foi introduzido por Hoare [Hoa72] e estava diretamente ligada a invariante de dados. Posteriormente Meyer em sua publicação sobre a linguagem Eiffel introduziu um novo conceito sobre os invariantes, desta vez sobre as classes da orientação a objetos [Mey92]. As exceções também são tratadas pelo DbC. Esse tratamento faz com o que os contratos sejam robustos. O funcionamento normal de um contrato prevê que ele será invocado quando primeiramente sua pré-condição seja válida. Caso as pré-condições sejam válidas, o código pode retornar o que está previsto na sua pós-condição ou até mesmo lançar uma exceção. No segundo caso, é criado um conjunto de regras que são chamadas de pós-condições excepcionais [DFG05]. Estas pós-condições excepcionais possuem as mesmas características das póscondições normais, ou seja, para que seja utilizada, sua condição excepcional deve ser atendida. A utilização do DbC pode também facilitar a tarefa de testes de softwares. Com os contratos, é possível a adoção de abordagens de geração de testes de unidade automatizados. Outra possibilidade é realizar a verificação do código estaticamente. Nesse caso, tenta-se estabelecer.

(22) 2.2 PROJETO POR CONTRATO. 9. a conformidade com a especificação em todos os possíveis caminhos de execução, enquanto executados pela suíte de testes [DFG05]. Os estados dos objetos são representados através dos invariantes. Através deles é possível representar os estados desejáveis antes e após a execução de cada método. As asserções (pré e pós-condições) são escritas através de expressões lógicas, permitindo que sejam verificados os estados desejados para cada objeto. Como as expressões retornam expressões booleanas, fica possível a verificação da corretude do código. As asserções podem ser checadas durante a execução dos programas e suas violações podem ser tratadas e corrigidas [BGF+ 01]. 2.2.1 Pré e Pós-condições Pré e pós-condições são asserções básicas para o funcionamento dos contratos. É através destas asserções que se torna possível a verificação se o que foi acertado foi cumprido. As précondições indicam o que deve ser verdadeiro para que a execução de algum método, que possua um contrato, seja realizada. Já as pós-condições indicam o que o método deve retornar após ser executado. É importante delegar responsabilidades para o código do cliente e o método que será invocado. É através dessa delegação de responsabilidades que se torna possível a diminuição de testes redundantes de variáveis. Por exemplo, caso o código do cliente já esteja validando se uma variável é positiva, não será necessário que uma pré-condição do método invocado verifique se a variável que está sendo passada é positiva. Entretanto, literatura indica que a opção por fortalecer a pré-condição constitui uma boa estratégia. Nessa abordagem cada método pode se concentrar em desempenhar um papel bem definido, e o faz melhor por não precisar se preocupar com todos os casos imagináveis [DFG05]. A Figura 2.2 demonstra como é o funcionamento de uma pré e pós-condição. No método calcularIMC não será necessário que seja realizada uma validação do parâmetro peso. Isto porque a validação já estará sendo validada pela pré-condição. ... pré-condição peso > 0 && altura > 0; pós-condição calcularIMC(peso, altura) .... Figura 2.2 Exemplo pré-condição e pós-condição. 2.2.2 Invariantes São propriedades globais sobre a instância da classe, não somente a uma rotina como as pré e pós-condições. Ou seja, os valores dos invariantes devem ser atendidos em toda a instância da classe. Algumas regras devem ser atendidas nos invariantes [DFG05]: • O invariante deve ser satisfeito logo após a criação de toda instância da classe. Ou seja, seus construtores devem estabelecer o invariante; • Todo método não-estático deve garantir que o invariante seja preservado após sua execução, se esta o for imediatamente antes de sua invocação..

(23) 2.3 TESTES BASEADOS EM MODELOS. 10. A Figura 2.3 demonstra um exemplo de invariante. No exemplo, a variável idade, durante o ciclo de vida do objeto, não pode receber um número menor ou igual a 0 e a variável nome não pode ser vazia. ... invariante idade > 0 && !nome.equals(""); .... Figura 2.3 Exemplo de invariante. 2.3 Testes baseados em modelos Testes baseados em modelos (ou apenas MBT) é uma abordagem de black-box (caixa preta) em que as atividades relativas ao processo de testes, como geração de casos de teste e análise dos resultados, são baseadas nas especificações da aplicação que está sendo testada [NSV+ 08]. Com o avanço da complexidade dos sistemas, testá-los tornou-se uma tarefa difícil e trabalhosa. Entretanto, para amenizar este problema, muitas empresas têm modificado a documentação dos seus requisitos. Elas estão substituindo a escrita dos requisitos em modelos informais por modelos formais (ou pelo menos mais precisos como UML) [PV05]. Com os modelos formais, os especificadores podem descrever de uma maneira mais coesa e íntegra as propriedades, o comportamento dos sistemas, identificar padrões e também identificar falhas na especificação [BH06]. Através das especificações é que poderemos indicar qual será o comportamento esperado pela aplicação que será construída. As especificações podem ser representadas de várias formas, dentre elas: modelos formais (máquinas de estado finitos ou gramáticas) e modelos semiformais (diagrama de classes e objetos) [PY07]. A Figura 2.4 demonstra como seria um teste baseado no modelo demonstrado na Figura 2.2. ... res = calculaIMC(50, 1.60); resultadoIMC = 50 / potencia(1.60,2); if res <> resultado then falha res = calculaIMC(-30, 1.80) resultadoIMC = -30 / potencia(1.80,2); Violação de pré-condição .... Figura 2.4 Exemplo de geração de testes baseado em modelos. A utilização dos modelos para a geração de casos de testes é importante para determinar a efetividade custo-benefício de se produzir especificações formais. É necessário que seja avaliado se o investimento realizado para a construção dos modelos teve como resultado final uma redução nos erros de especificação, redução do esforço no teste e o custo final do desenvolvimento..

(24) 2.3 TESTES BASEADOS EM MODELOS. 11. Como os modelos são fortemente baseados em lógicas formais e matemática, a validação dos métodos é realizada de uma maneira exata. Como os modelos formais são precisos, eles podem ser utilizados em várias etapas do desenvolvimento do sistema, desde a especificação até os testes. A Figura 2.5, adaptada de [Nas08], demonstra as fases para a construção de MBT. Requisitos. Modelo Mental. Construir. Construir. Modelo Formal Gerar. Casos de teste Executar. Resultados. Figura 2.5 Fases do MBT. O primeiro passo para a criação dos modelos é um entendimento prévio dos requisitos do sistema. Através deste conhecimento é que poderá ser produzido um modelo mental para ver quais são as necessidades para que se possa ser montado o modelo. Após a definição do modelo mental deve ser decidido qual será o modelo formal que melhor represente os requisitos do sistema. O segundo passo é a geração dos casos de testes a partir das especificações formais. Esta geração pode ser feita de maneira automatizada ou manualmente. Os casos de teste devem possuir seus passos bem definidos e os resultados esperados. Para automatizar esta etapa, basta definir o tipo de modelo escolhido e procurar uma ferramenta adequada para o modelo formal selecionado. A terceira etapa consiste na execução dos testes unitários desenvolvidos. Após a execução, é verificado se o que foi previsto na especificação está sendo cumprido. Caso exista algum resultado fora dos resultados previstos, conclui-se que o teste falhou. Ou seja, o contrato entre a implementação e as especificações está falho. A última etapa é a de coleta e análise dos resultados. Com esta análise, os testes podem sofrer melhorias para próximas execuções, deixando-os mais relevantes. Pode-se também verificar o tempo necessário para execução desses testes para que nos próximos testes possa ser mensurado o tempo de projeto para esta etapa. A utilização dos testes baseados em modelos pode trazer bastante vantagem para o processo de testes. A primeira grande vantagem é a possibilidade da criação automática dos casos de teste através de alguma ferramenta compatível com o modelo escolhido. Outra vantagem é a possível antecipação do processo de testes dentro do processo geral do desenvolvimento do sistema. Essa antecipação é possível porque o modelo já vai estar definido durante a definição dos requisitos..

(25) 2.4 DESENVOLVIMENTO BASEADO EM MODELOS. 12. Outra importante vantagem da utilização dos modelos é que, caso ocorram alterações no modelo, facilmente essa mudança pode ser refletida nos casos de teste, caso esteja utilizando ferramentas de automação de testes.. 2.4 Desenvolvimento baseado em modelos Desenvolvimento baseado em modelos (ou simplesmente MDD) é uma filosofia para desenvolvimento de software que possibilita que sejam construídos modelos de um sistema, que poderão ser utilizados para a construção do próprio sistema. O conceito principal dessa filosofia é que, ao invés de se utilizar linguagens de programação para criação de um sistema, devem ser criados modelos que indicam o comportamento que cada módulo deve possuir. Esse tipo de abordagem permite, que por serem modelos mais formais, sejam diminuídas as ambiguidades encontradas em módulos dos sistemas. Para [MCF03] a filosofia de MDD, apesar de possuir um grande potencial, devido a capacidade de construções de sistemas mais confiáveis e com um menor custo de forma automatizada, não está totalmente difundida. A OMG (Object Management Group) propôs uma arquitetura que define os padrões para utilização dos modelos. Esta arquitetura é conhecida como Model Driven Architecture. Na definição do MDA, os modelos significam a representação de parte de alguma funcionalidade, comportamento ou estrutura de um sistema. Para o MDA o processo de desenvolvimento deve ser direcionado à construção dos modelos, através do nível conceitual, independentemente da plataforma (o que permite a reutilização dos modelos para sistemas diferentes). Ou seja, uma vez que o modelo estiver construído, os códigos podem ser criados em qualquer plataforma. O MDA inicia em modelos abstratos (conceituais) para modelos cada vez mais ligados à implementação. Essas transformações são realizadas através das etapas: 1. É criado um PIM (Platform Independent Model), que possui um alto nível de abstração, pois é independente de plataforma. É no PIN que ficarão as perspectivas que melhor representam o sistema; 2. É criado um ou vários PSM (Platform Specific Models) que irá representar um PIM em alguma plataforma específica. Cada PIM pode possuir um ou mais PSMs; 3. Geração do código fonte a partir dos PSMs. Este código não necessariamente irá possuir apenas a estrutura básica. Dependendo do modelo que foi desenvolvido, o código gerado poderá possuir as regras de negócio do sistema. A Figura 2.6 demonstra os passos executados pelo MDA a partir da etapa do PIM até a geração do código. Para a estratégia que estamos utilizando neste trabalho, já possuímos os PSMs que são as especificações formais em Java que representam os contratos que serão utilizados. A partir destes PSMs é que iremos fazer a geração dos códigos, através de um gerador automático, para executarmos na máquina virtual Java tradicional. Os PSMs também serão utilizados para a.

(26) 2.4 DESENVOLVIMENTO BASEADO EM MODELOS. 13. PIM Primeira Transformação. PSM Segunda Transformação. Código. Primeira Transformação. PSM Segunda Transformação. Código. Figura 2.6 Passos da especificação do MDA. criação dos casos de testes, também através de um gerador automático compatível com o PSM que está sendo utilizado. Algumas vantagens podem ser percebidas através da utilização do MDA no processo de desenvolvimento de software: • Produtividade: devido à utilização do MDA, a maior parte do código pode ser gerada automaticamente; • Portabilidade: Já que o PIM é independente de plataforma, quando for necessário mudar de plataforma basta apenas utilizar o PSM específico. A preservação do conhecimento é mantida pelo PIM; • Interoperabilidade: Os PSMs possuem um canal de comunicação. Com isso, PSMs podem se comunicar com PSMs e códigos de outras plataformas; • Manutenção e Documentação: Todo o desenvolvimento deve ser focado na criação do PIM. Quando for necessária alguma manutenção no modelo, é o PIM que deve ser alterado. E com esta atualização do PIM, automaticamente toda a documentação do sistema também será atualizada. Apesar de todos os benefícios do MDA, o MDD em certos setores da indústria não é bem difundido. Isto porque existe uma grande resistência para a construção destes modelos e até mesmo sobre a qualidade do código que é gerado através destes modelos. O objetivo deste trabalho é exatamente validar um gerador de código que utiliza o MDD para geração dos códigos fontes..

(27) 2.5 GERADORES DE CÓDIGO. 14. 2.5 Geradores de código Como foi visto na seção anterior, através da utilização do MDD, é possível realizar a geração de código a partir de modelos. Porém, para isso é necessário um gerador de código que compreenda modelos MDD para a geração dos códigos. Os geradores de código utilizam os arquivos PSM de uma determinada linguagem para a transformação em código fonte. Esta tradução não é um processo simples. Portanto, todo código, logo após de ser gerado, deve ser validado. Se estivermos utilizando uma ferramenta que já foi certificada sua confiabilidade, este código gerado não precisará ser validado. O principal objetivo deste trabalho é exatamente esse: através da estratégia proposta, verificar se um gerador de código baseado em MDD é confiável. Apesar dos geradores de código gerarem, a partir de modelos, os códigos fontes de uma determinada linguagem, o usuário precisa estar ciente de alguns fatos: • Um conhecimento prévio do modelo é necessário. Com este conhecimento é possível verificar se o que foi gerado está correto; • Possuir um bom modelo físico apenas não necessariamente garante uma implementação ideal do modelo; • A eficiência e segurança do código dependem diretamente do gerador de código; Em algumas situações, a utilização de geradores de código é mais aconselhável do que a criação manual. Isto porque, sistemas críticos (por exemplo, sistemas de tempo real embarcados em aeronaves) necessitam ser livre de falhas. Para isso já existem geradores de códigos certificados (o que garante a qualidade final do código) como o SCADE [CCM+ 03]. Essas certificações proporcionam uma garantia na qualidade do serviço. Assim, o cliente poderá utilizar deste gerador de código. Com isso, o processo de desenvolvimento irá ser mais rápido e mais confiável. Atualmente a certificação mais conhecida para sistemas embarcados em automóveis é a IEC 61508 e para sistemas embarcados em aeronaves é a DO-178B. Porém, existem geradores de código na academia. Estes geradores não certificados podem não ter a confiabilidade necessária para serem usados pela indústria. Porém, podemos realizar uma estratégia de testes para indicar se estes geradores são confiáveis. Quando obtemos um resultado final errado, não podemos indicar que o erro está apenas no gerador de código. Os erros podem ser causados se possuímos modelos inadequados, em problemas de configuração do gerador de código, no próprio gerador ou no teste. O exemplo da Figura 2.7, demonstra como um gerador de código deve se comportar. No exemplo, temos como pré-condição um parâmetro x que deve ser maior do que 0. Temos também como pós-condição que indica que o resultado produzido pelo método multiplicado por ele mesmo tem que ser igual ao número passado por parâmetro. Ou seja, o método que irá ser gerado (Calculo) pelo gerador de código terá que ser a raiz quadrada de um número. A variável erro pode ser considerada como a precisão do cálculo realizado pelo computador. Para a realização dos nossos experimentos, foi utilizado um gerador de código disponível na academia que, a partir de modelos na linguagem Java, produz bytecodes para serem executados na máquina virtual tradicional do Java..

(28) 2.5 GERADORES DE CÓDIGO. ... pré-condição x > 0; pós-condição abs(resultado*resultado - x) <= erro ... funcao Calculo (inteiro x) { retorna RaizQuadrada(x); }. Figura 2.7 Exemplo do funcionamento do gerador de código. 15.

(29) C APÍTULO 3. Aplicando a Estratégia. Este capítulo irá demonstrar como a estratégia proposta no capítulo anterior pode ser utilizada na prática. Isto será possível considerando certo e interessantes elementos (gerador de código, ferramenta de testes baseados em modelo e um estudo de caso) que estão disponíveis para a academia e para a indústria. Para a linguagem baseada em contratos, decidiu-se que seria utilizado o JML [LPC+ 04] porque esta linguagem possui um conjunto de ferramentas que a utilizam [LRL+ 00]. Por exemplo, esta linguagem possui um gerador de código conhecido como jmle [KW07] e uma ferramenta de testes baseados em modelos conhecida como Jartege [Ori05]. Além disso, um estudo de caso ilustra a aplicação de todos estes elementos citados. Por exemplo, foi utilizada a API JavaCard que é fornecida pela Sun Microsystems [Mic10]. Também foram necessárias as especificações formais da API JavaCard em JML. Estas especificações foram encontradas na academia [NCW09].. 3.1 A linguagem JML Java Modeling Language (JML) [LC04] é uma linguagem baseada em contrato – Design by Contract (DbC) [Mey92]. Esta linguagem é utilizada para descrever os comportamentos esperados por módulos Java – os módulos Java são classes e interfaces. A utilização de contratos no processo de desenvolvimento de software traz alguns benefícios como: 1. Documentação implícita e detalhamento do que está sendo realizado no código; 2. Maior facilidade para identificação e causadores de possíveis erros; 3. Facilidade de entendimento da linguagem, ocasionada pela utilização da sintaxe Java. JML é uma linguagem que é utilizada para a especificação de interfaces (nomes e informações estáticas encontradas nas declarações Java) e comportamentos (indica como os módulos irão agir quando forem usados). JML é uma Behavioral Interface Specification Language (BISL) associada à linguagem de programação Java. JML é resultado da junção da sintaxe utilizada por Eiffel com a semântica utilizada nos modelos de Larch Shared Language (LSL) e VDM [LBR99]. Através de JML, é possível especificar (documentar) a interface que será utilizada pelo usuário ou até mesmo definir qual o comportamento que ela irá possuir. Por ser uma linguagem BSIL, JML pode elaborar especificações que detalham como devem ser os módulos da interface com os usuários e também o comportamento que é esperado por esse cliente. Diferentemente de outros modelos do BSIL, as especificações da interface e dos 16.

(30) 3.1 A LINGUAGEM JML. 17. métodos são escritos na linguagem Java, facilitando o desenvolvedor porque não será necessária a aprendizagem de outra linguagem. As especificações JML adotam o conceito do DbC, pois utilizam também asserções para verificações no código. As três asserções utilizadas são as pré-, pós-condições e os invariantes. Pela utilização deste conceito, as especificações podem ser utilizadas como um DbC para Java. JML possui um conjunto de ferramentas que podem ser utilizadas em vários aspectos. Estas ferramentas suportam DbC, checagem de código em tempo de execução, descoberta de invariantes, verificação formal e verificação estática (usando provadores de teoremas) [LC04]. As especificações são escritas através dos próprios construtores sintáticos de comentários da linguagem Java (Annotations). Isto é bem interessante porque um compilador tradicional de Java simplesmente descarta JML. Para que as ferramentas de JML entendam que um comentário é um contrato JML, o caractere @ é usado. Assim, usa-se //@ para contratos de apenas uma linha e /*@... @*/ para contratos em bloco (mais de uma linha). A Figura 3.1 apresenta uma especificação em JML da classe Person. package org.jmlspecs.samples.jmltutorial; //@ refine "Person.java"; public class Person { private /*@ spec_public non_null @*/ String name; private /*@ spec_public @*/ int weight; /*@ public invariant !name.equals("") @ && weight >= 0; @*/ //@ also //@ ensures \result != null; public String toString(); //@ also //@ ensures \result == weight; public /*@ pure @*/ int getWeight(); /*@ also @ requires kgs >= 0; @ requires weight + kgs >= 0; @ ensures weight == \old(weight + kgs); @*/ public void addKgs(int kgs); /*@ also @ assignable name; @ requires n != null && !n.equals(""); @ ensures n.equals(name) @ && weight == 0; @*/ public Person(String n); }. Figura 3.1 Exemplo de uma anotação JML Pré- e Pós- Condições. As principais asserções são escritas na própria linguagem Java, no entanto as expressões Java possuem carências na expressividade. Esta carência torna a linguagem mais especializada para a escrita de especificações de comportamento, ocasionando um problema na especificação de interfaces. Este problema foi resolvido através da extensão das expressões Java. Algumas destas extensões dão origem a operadores como \old, \result, \forall, \exists, \max e a algumas palavras reservadas como invariant, requires, ensure, signals, pure, assignable que serão detalhas posteriormente..

(31) 3.1 A LINGUAGEM JML. 18. Para realizar a verificação dos contratos em tempo de execução, o compilador JML (JMLc) traduz as anotações formais em códigos binários, cujo objetivo é testar se o predicado é válido no momento em que um método é chamado. Portanto, o uso de JMLc não garante que o contrato será respeitado. Apenas que, em caso de ser violado, o usuário receberá uma notificação da violação. Ou seja, usa-se o mesmo princípio de teste: pode-se mostrar presença de problemas (violação), mas não sua ausência. 3.1.1 Pré- e Pós- Condições Para a especificação de pré-condições de métodos ou construtores em JML deve ser utilizada a cláusula requires. Esta cláusula indica como devem estar as variáveis antes de ser executado o método corrente. As variáveis que são visíveis para ser utilizadas nas pré-condições são os parâmetros do método que está sendo especificado ou os atributos que estão visíveis na classe. Antes de iniciar os conceitos de pré e pós-condição é necessário explicar a classe demonstrada na Figura 3.1. A classe Person é utilizada para controlar o peso de uma pessoa. Através da classe, é possível consultar o peso através do método getWeigth, adicionar quilos a pessoa por meio do método addKgs e ainda construir objetos da classe recebendo como parâmetro o nome da pessoa. No exemplo da Figura 3.1 é possível identificar, no método addKgs, duas pré-condições. A primeira indica que o parâmetro kgs não pode possuir valor negativo e a segunda indica que o peso (weight) mais o kgs tem que ser maior do que zero. Um método pode possuir múltiplas pré-condições. Estas, por sua vez, têm o mesmo significado de uma única cláusula, cujo predicado seja a conjunção de todos os predicados anteriores [LPC+ 04]. Nas especificações JML podem ser utilizadas expressões ou chamadas de método do Java. Ou seja, classes que são implementadas no framework Java podem ser aproveitadas nas anotações. Por exemplo, no construtor do método da Figura 3.1 é utilizada a classe (equals no invariante para garantir que o objeto name seja diferente de vazio. Os métodos podem não possuir nenhuma pré-condição, neste caso será adotado o valor padrão da cláusula requires que é not_specified. Para o compilador de especificações JML, esta cláusula retorna um valor true, ou seja, a pré-condição está satisfeita. Já as pós-condições são especificadas através da cláusula ensures. O ensures é utilizado para indicar o que o método ou construtor deve retornar após sua execução. Nas pós-condições podem ser utilizadas duas outras expressões old (E) e result. A expressão old é utilizada para a verificação do valor da expressão E antes da execução. Já o result serve para a averiguação do valor resultante do método. A Figura 3.1 mostra a utilização destas expressões. O construtor Person, por exemplo, utiliza a pós-condição para verificar se o parâmetro n é igual à variável privada da classe name e também verifica se o weight da outra variável privada é igual à zero. 3.1.2 Invariantes São representados através da palavra reservada invariant. Esta anotação é utilizada para garantir o estado de um objeto durante todo o tempo de execução. Ou seja, o invariante deve ser válido antes e após da execução dos métodos e também após a execução do construtor. No caso dos.

(32) 3.1 A LINGUAGEM JML. 19. construtores, os invariantes podem ser visualizados como uma pós-condição, pois, como ela controla o estado do objeto quando ele for criado, obrigatoriamente sua condição terá que ser válida. Um invariante não deve expressar simplesmente um desejo e sim o que a classe realmente é capaz de garantir [DFG05]. Ou seja, não basta apenas utilizar os invariantes para demonstrar o que se está desejando na classe, devemos também utilizá-la para garantir o estado do objeto. Por exemplo, na Figura 3.1 caso o construtor não possuísse a pré-condição para impedir que o parâmetro n seja diferente de nulo, o invariante poderia facilmente ser violado. Para isso basta que o invocador do método passe como parâmetro um valor nulo. Assim, o invariante logo em seu construtor já seria violado. Na Figura 3.1, o invariante controla as variáveis privadas name e weight. Em nenhum momento da execução da classe Person a variável name pode ter valor vazio, enquanto que a variável weight sempre tem que ser maior ou igual a zero. Os invariantes podem ser declarados como static e instance. Assim como um método estático, um invariante estático não pode referenciar o objeto corrente com o this e também não tem acesso a propriedades e métodos não estáticos [LPC+ 04]. Os invariantes do tipo instance devem ser baseados nos construtores dos objetos, e são preservados por todos os métodos da classe. Já os invariantes do tipo static, como não possuem valores, assumem um invariante do tipo instance até que o bloco de inicialização estática estabeleça seu valor [LPC+ 04]. Se um invariante é declarado dentro da classe, por padrão ela será do tipo instance. 3.1.3 Constraints As constraints demonstram uma relação entre o pré e pós-estados do método, restringindo como a variável pode ser modificada. É possível definir que uma variável é constante ou que a mesma pode apenas receber valores maiores que o atual atribuído. As constraints restringem a forma como os valores dos atributos podem mudar durante a execução do programa, diferente dos invariantes, que assumem um caráter estático. As constraints podem ser vistas como sendo pós-condições para todos os métodos [Net07]. Similar ao conceito de invariantes, porém enquanto os predicados definidos nos invariantes controlam os estados visíveis que devem ser satisfeitos durante a execução do objeto, as constraints são relacionamentos que mantêm a combinação de cada estado e o próximo estado visível da combinação [LPC+ 04]. Ou seja, os invariantes assumem um caráter estático na medida em que estas são verificadas em determinados estados do objeto, enquanto que as constraints possuem um caráter dinâmico, já que elas devem ser satisfeitas ao longo da execução que se inicia e termina em dois estados visíveis subseqüentes [DFG05]. Em outras palavras as contraints, pelo seu caráter dinâmico, podem ao longo da execução verificar outros estados do objeto. Isto não ocorre com os invariantes, que tem um valor estático durante toda a execução do objeto. A Figura 3.2 exemplifica o uso das constraints. A Figura 3.2 mostra que a constraint pode ser utilizada para uma verificação do pré-estado da variável. Através deste controle, esta anotação pode ser considerada uma pós-condição implícita, pois ela impõe uma condição posterior à condição de um método. As constraints não se aplicam aos construtores porque os objetos não possuem estados antes da sua chamada..

(33) 3.1 A LINGUAGEM JML. 20. public class Person { //@constraint height >= \old(height) float height; public void addHeight(int newHeight) { this.height = newHeight; } }. Figura 3.2 Exemplo do uso da constraint. Quando é usada a herança de contratos, a classe que está herdando deve respeitar as constraints da superclasse. 3.1.4 Signals O signal é utilizado para representar as pós-condições excepcionais. Caso o método termine de forma inesperada, lançando uma exceção, a pós-condição excepcional verifica se algumas propriedades foram satisfeitas. Na Figura 3.4, foi alterado o método addKgs para a inserção da cláusula signals. O controle de exceção é tratado caso o parâmetro kgs possua um valor menor ou igual a zero. É importante verificar que a pós-condição excepcional não quer informar que, quando certa propriedade possuir algum valor, deve ocorrer uma exceção. A pós-condição excepcional indica que, quando ocorrer uma exceção, uma determinada propriedade deve ser satisfeita. /* @ @ @ @ @ @ */. requires weight + kgs >= 0 ensures kgs >= 0 && weight == \old (weight + kgs) signals_only IllegalArgumentException signals IllegalArgumentException kgs < 0. public void addKgs(int kgs) { if (kgs >= 0){ weight += kgs; } else { throw new IllegalArgumentException(); } }. Figura 3.3 Exemplo do uso do signals. O conceito de herança existe para este tipo de representação. Por exemplo, caso seja tratado uma exceção do tipo Exception, qualquer tipo de erro que o código venha lançar a anotação será capaz de tratar. Isto porque todas as exceções são subclasses da classe Exception na linguagem Java. Existe ainda uma pós-condição excepcional que é utilizada através da cláusula signals_only. Esta anotação indica quais exceções podem ser lançadas pelo método que está anotado. A.

(34) 3.1 A LINGUAGEM JML. 21. Figura 3.4 indica que somente as exceções do tipo IllegalArgumentException podem ser lançadas. Caso não seja indicada esta anotação, ela assume um valor padrão que é nothing, ou seja, este método não pode lançar nenhuma exceção. Existe uma diferença entre signals_only e signals. Signals_only apenas informa quais exceções podem ser lançadas, enquanto a cláusula signals indica o tratamento que deve ser realizado caso a exceção tenha sido lançada. É importante demonstrar que a cláusula signals não deve ser utilizada para garantir que, quando alguma propriedade for verdadeira, uma exceção deve ser lançada [LPC+ 04]. A cláusula que deve ser utilizada para esta situação deve ser a ensures através da negação de um predicado p, definido na pós-condição excepcional. Dessa forma, se p for válido o método fica obrigado a lançar a exceção, caso contrário a pós-condição normal será violada [DFG05]. Veja Figura 3.4. /*. @ @ @ @. requires x >= 0; ensures JMLDouble.approximatelyEqualTo(x, \result * \result, 0.1); signals (IllegalArgumentException e) e.getMessage() != null && x < 0;. @ */ public double raizQuadrada1(int x) throws IllegalArgumentException{...}. Figura 3.4 Exemplo do uso do signals e ensures (retirado de [DFG05]). 3.1.5 Assignable A cláusula assignable é utilizada para indicar quais serão as variáveis que podem ter seu valor alterado na execução do método. Caso não seja feito a anotação desta cláusula, o valor padrão que irá ser adotado será o everthing, que indica que todas as variáveis podem ser alteradas. Existe também o valor nothing que indica que nenhuma variável pode ser alterada. A Figura 3.1 demonstra a utilização da cláusula assignable no construtor da classe. No exemplo, apenas a variável name pode ser alterada pela execução do método. 3.1.6 Pure Métodos puros são aqueles em que a execução não pode alterar valores das variáveis. Este tipo de método é mais utilizado para consultas de valores de variáveis. Fazendo uma analogia ao uso da variável assignable, os métodos puros poderiam ser substituídos pela anotação //@ assignable nothing, ou seja, nenhuma variável pode ter seu estado alterado. No exemplo da Figura 3.1, é demonstrado o método getWeight que não pode alterar o valor de nenhuma variável do estado. Ele somente poderá chamar o método que irá retornar determinado valor, sem alteração do estado do objeto. 3.1.7 Also Utilizado para a herança de contratos de métodos, ou seja, quando utilizada esta cláusula o método corrente irá herdar todas as anotações de pré, pós-condições e invariantes da superclasse..

(35) 3.1 A LINGUAGEM JML. 22. Isto indica que todas as anotações que foram feitas pela superclasse também devem ser respeitadas. Nas anotações dos métodos herdados é permitido que seja realizado um refinamento das anotações da superclasse. Mas é necessário que estas novas anotações não violem as regras que foram definidas no método principal. Durante a execução de um método, é primeiro verificado as especificações da subclasse para, em seguida, ser verificado as especificações da superclasse. Esta sequência de processo deve-se ao fato que na subclasse o refinamento é maior, então ela é executada primeiramente. A Figura 3.5 demonstra um tipo de violação e um tipo de redefinição utilizando a cláusula also. /* @ requires weight + kgs >= 10 @ ensures kgs >= 0 @ */ public void addKgs(int kgs) //SuperClasse /* @ also @ requires weight + kgs >= 8 @ ensures kgs >= 0 @ */ public void addKgs(int kgs) //SubClasse Errada /* @ also @ requires weight + kgs >= 20 @ ensures kgs >= 0 @ */ public void addKgs(int kgs) //SubClasse Correta. Figura 3.5 Exemplo do uso do also. Na Figura 3.5, o primeiro método (superclasse) indica que o weight + kgs tem que ser maior ou igual a 10. Todos os métodos que herdarem desta classe obrigatoriamente terão que respeitar esta condição. No segundo método (método herdado) é utilizada a cláusula also de maneira incorreta. Isto porque está sendo violando a condição da anotação da superclasse, ou seja, pode ocorrer um refinamento do método como é feito na terceira assinatura, cumprindo as anotações do método da classe herdada. 3.1.8 Model A anotação model pode ser utilizada em propriedades, métodos, atributos e em classes. Esta anotação serve apenas para as anotações JML, ela não é utilizada no código Java em que está sendo realizada a anotação. A utilização do model é mais comum para a criação de atributos abstratos que encapsulam atributos concretos do modelo [LPC+ 04]. Os valores desta anotação são atribuídos através da cláusula represents. A Figura 3.6 demonstra a utilização desta anotação. Neste exemplo, a variável imc é usada para abstração do código que calcula seu valor. Com esta abstração as anotações ficam mais simples facilitando o entendimento..

Referências

Documentos relacionados

Figura A53 - Produção e consumo de resinas termoplásticas 2000 - 2009 Fonte: Perfil da Indústria de Transformação de Material Plástico - Edição de 2009.. A Figura A54 exibe

Para preparar a pimenta branca, as espigas são colhidas quando os frutos apresentam a coloração amarelada ou vermelha. As espigas são colocadas em sacos de plástico trançado sem

Com base nos resultados da pesquisa referente à questão sobre a internacionalização de processos de negócios habilitados pela TI com o apoio do BPM para a geração de ganhos para

Para se buscar mais subsídios sobre esse tema, em termos de direito constitucional alemão, ver as lições trazidas na doutrina de Konrad Hesse (1998). Para ele, a garantia

Informações tais como: percentual estatístico das especialidades médicas e doenças, taxas de ocupação dos principais recursos, tempos de permanência de internação, escores

Frondes fasciculadas, não adpressas ao substrato, levemente dimórficas; as estéreis com 16-27 cm de comprimento e 9,0-12 cm de largura; pecíolo com 6,0-10,0 cm de

Os dados referentes aos sentimentos dos acadêmicos de enfermagem durante a realização do banho de leito, a preparação destes para a realização, a atribuição

O score de Framingham que estima o risco absoluto de um indivíduo desenvolver em dez anos DAC primária, clinicamente manifesta, utiliza variáveis clínicas e laboratoriais