Boolean Expression

5.1 State-Based Specifications

State-based specifications describe software systems in terms of states and effects of operations on states. Dergelijke specificaties worden ondersteund door verschillende specificatietalen, die we in twee groepen hebben ingedeeld: klassieke specificatietalen, zoals Z , B , en VDM , en assertietalen, zoals Eiffel , JML (Java Modeling Language) , en Peters en Parnas’ tabellen . Terwijl klassieke specificatietalen toestanden en overgangen onafhankelijk van de implementatie definiëren, specificeren assertietalen toestanden en overgangen als verklaringen of annotaties van het bronprogramma.

Op toestanden gebaseerde specificaties worden gebruikt als testorakels door de overeenstemming van de testuitvoeringen met de specificaties te verifiëren. Het grootste probleem bij het genereren van test-oracles uit state-specificaties komt voort uit de verschillende abstractieniveaus tussen de specificaties en de operaties die tijdens de testuitvoering worden gecontroleerd; de specificaties van de operaties moeten dus worden vertaald in een controleerbare vorm. De specificatietalen zijn gewoonlijk expressief genoeg om verschillende gegevensstructuren en toestandsgebaseerde eigenschappen weer te geven, maar hun expressiviteit bemoeilijkt de vertaling naar een uitvoerbare vorm. Vaak worden bij de vertaling vereenvoudigende aannamen gedaan, zoals het beperken van oneindige verzamelingen, bijvoorbeeld de oneindige verzameling van natuurlijke getallen, en het slechts gedeeltelijk afhandelen van niet-uitvoerbare elementen, waarvoor soms menselijke tussenkomst nodig is.

Hier bespreken we de belangrijkste benaderingen om toestandsgebaseerde specificaties naar een uitvoerbare vorm te vertalen. De vertaling van Z, B, en VDM specificaties levert dezelfde problemen op met betrekking tot oneindige gegevensstructuren, gedeeltelijk gedefinieerde uitdrukkingen en object-georiënteerde structuren. De vertaling van Eiffel, JML en andere assertietalen naar een controleerbare vorm levert soortgelijke problemen op, maar op een ander abstractieniveau. Hieronder presenteren wij de belangrijkste benaderingen voor het vertalen van op toestanden gebaseerde specificaties in controleerbare vorm met verwijzing naar Z; de benaderingen voor andere talen zijn vergelijkbaar. Daarna bespreken we de problemen van het compileren van asserties in uitvoerbare controles met verwijzing naar Eiffel, JML, en Peters en Parnas’ tabellen.

De schema predicate compiler voorgesteld door Mikk compileert Z specificaties in C code. Voor elk Z schema, dat de basiseenheid in Z is, genereert de aanpak een C functie die de schema predicaten evalueert met behulp van de program state die als parameter aan de functie wordt doorgegeven. De gegenereerde code evalueert de predicaten van het schema met een “brute kracht” strategie: Gegeven een toestand, vervangt de code in wezen uitdrukkingen door hun waarden, evalueert propositionele calculus formules door gebruik te maken van waarheidstabellen, en evalueert kwantificaties en verzameling constructies door iteratie door de datastructuren. Om de complexiteit van de gegenereerde C-functies te verminderen, stelt de aanpak een optimalisatiestap voor om ervoor te zorgen dat de gegenereerde C-code altijd eindigt. Om de code te optimaliseren, definieert de techniek een “uitvoerbare” deelverzameling van de Z-taal. De deelverzameling bestaat uit eindige predikaten, zoals propositionele calculus formules, kwantificaties over eindige verzamelingen, en verzameling constructies gebaseerd op eindige iteratie. De techniek breidt de verzameling uitvoerbare predikaten uit met een verzameling regels om speciale niet-uitvoerbare predikaten die aan bepaalde voorwaarden voldoen in uitvoerbare predikaten om te zetten zonder hun semantiek te veranderen. De volgende twee regels elimineren bijvoorbeeld de kwantoren:

∀x:M-PM≠∅⇒PNotOccurxP∃x:M-PM≠∅∧PNotOccurxP

waarbij M een verzameling is, de variabele x een element in M voorstelt, en P een predikaat is. De operator -, ook bekend als de “Z-notatievlek”, betekent “zoals dat” wanneer hij gebruikt wordt met de kwantor ∃ en “dat,” wanneer hij gebruikt wordt met de kwantor ∀. Bijvoorbeeld, ∃ x : M – P betekent dat er ten minste een waarde x in M bestaat zodat predicaat P waar is. De voorwaarde NotOccur(x, P) geeft aan dat x geen vrije variabele is in het predikaat P. Hoewel de verzameling M oneindig kan zijn, kan de compiler toch specificaties compileren die quantificeerders over M gebruiken, zolang de quantificeerders kunnen worden geëlimineerd door toepassing van de bovenstaande regels. Voor door de gebruiker gedefinieerde functies die door axiomatische of generieke definities kunnen worden ingevoerd, vereist de compiler dat de gebruiker de overeenkomstige code verstrekt. Mikk stelt ook een oplossing voor het probleem van gedeeltelijk gedefinieerde specificaties voor. Hij gebruikt VDM-SL als tussenvorm en de door Schmidt en Hörcher ontwikkelde VDM-SL compiler om Z specificaties in C code te vertalen . Bij gedeeltelijk gedefinieerde expressies, d.w.z. functies of relaties die ongedefinieerd kunnen evalueren (aangeduid als ⊥) wanneer ze buiten hun definitiedomein worden geëvalueerd, genereert de aanpak exception handlers.

Object-georiënteerde uitbreidingen van Z, zoals Object-Z, verergeren de problemen nog. McDonald et al. vertalen Ojbect-Z specificaties van container klassen in C++ code in drie fasen: optimalisatie, structurele mapping, en predicaat vertaling . In de optimalisatiefase herschikken zij de specificatie om de daaropvolgende vertaling te vereenvoudigen door middel van verschillende stappen, waaronder het vervangen van constante variabelen door hun waarden en het verwijderen van secundaire variabelen en schemaverrijkingen. In de fase van de structurele toewijzing worden structuren in de Object-Z-specificatie toegewezen aan hun overeenkomstige code-elementen: Specificatie constanten en variabelen naar variabelen met de juiste initialisaties, en variabelen types naar primitieve C++ types of corresponderende klassen in de Z toolkit bibliotheek die door de techniek wordt gebruikt. Bijvoorbeeld, zij mappen de integer en boolean types naar int, en het type van integer sets naar z_set<int >. Ze zetten het initialisatieschema om in een constructor, en bewerkingsschema’s in lidfuncties, creëren een speciale lidfunctie, inv(), om de klasse-invariant te controleren, en genereren een uitzonderingsklasse voor elk uitzonderingsschema.

In de predicaat DeepL vertaling, richt de techniek zich op hoe code te genereren voor het controleren van de programmatoestand, in plaats van het evalueren van predicaten. De vertaling voorkomt bijvoorbeeld dat de functie inv() een andere memberfunctie aanroept, om oneindige recursie te voorkomen. In een constructor wordt de invariant van de klasse gecontroleerd bij het verlaten ervan.

Richardson et al. richten zich op tijdkritische systemen, behandelen een combinatie van Z en temporele intervallogica, en stellen een vertaling voor van specificaties in test-oracles door middel van symbolische interpretatie. Symbolische interpretatie kent de symbolische namen in de specificaties toe aan de stimulusinputs en de initiële toestandsvariabelen, door te verwijzen naar de data en control mappings. Vervolgens interpreteert de symbolische interpretatie elk controletraject in de specificaties, d.w.z. zij creëert een bewering die de voorwaarde voor de specificatiegegevens uitdrukt op basis van de symbolische toestand op elk specificatiecontrolepunt. Een specificatiecontrolepunt is een relevant element, zoals een geparametriseerde gebeurtenis, een overgang en een bewerking. Na de symbolische interpretatie van de Z-specificatie wordt de orakelinformatie weergegeven als een lijst van relevante Z-schema’s en de bijbehorende beweringen, die worden gecontroleerd als het schema wordt aangeroepen. De uitvoering van een testgeval verwijst naar een concrete testinput van waaruit concrete variabelen en gebeurtenissen worden gebonden aan hun tegenhangers in de testklasse (elke testklasse is een partitie van de testinput). De uitvoering van de testcase levert een uitvoeringsprofiel op, en de concrete gebeurtenissen en waarden van variabelen in het uitvoeringsprofiel worden gemapt naar het orakelniveau om hun consistentie met de orakelinformatie te controleren.

Assertietalen voegen specificaties samen met de broncode, waardoor het vertaalprobleem wordt vereenvoudigd maar niet geëlimineerd. Assertions worden door verschillende talen en methodologieën ondersteund. Design by contract is een software-ontwerpparadigma dat uitgaat van het gezamenlijk ontwerpen van code en asserties. Design by contract is voorgesteld door Meyer en wordt volledig ondersteund door de programmeertaal Eiffel. Anders dan in algemene specificatietalen, zoals Z, worden de specificaties in Eiffel, contracten genoemd, samengevoegd in de broncode en definiëren zij verplichtingen en voordelen van de softwarecomponenten die communiceren via goed ontworpen interfaces. In talen die contracten ondersteunen, worden de verplichtingen en voordelen van de klant uitgedrukt als pre- en postvoorwaarden van de dienstverleners. De broncode wordt verrijkt met invarianten die toestandskenmerken weergeven die tijdens elke interactie moeten gelden.

Contracten zijn uitgebreid naar vele talen. iContract is het eerste programma dat contracten in Java ondersteunt. In iContract worden contracten geschreven als code commentaar met speciale tags. De contracten die door iContract worden ondersteund zijn booleaanse uitdrukkingen die voldoen aan de Java syntaxis, met uitzondering van enkele syntactische uitbreidingen waaronder de universele kwantificatie (∀), de existentiële kwantificatie (∃), en de implicatie (→). Deze uitbreidingen zijn bedoeld om de vertaling naar Java-code te vereenvoudigen. Bijvoorbeeld, the syntax of ∀ is forall < Class >< var > in < Enum >< Exp_var >, where < Class > is the type of the variable < var >, which represents an element of < Enum >. A reasonable translation of the ∀ expression is to use a Java loop to iterate over the enumeration < Enum > and evaluate the boolean expression < Exp_var > in each iteration. The existential quantification is translated similarly. An implication expression C implies I can be simply translated into an if statement: “als C dan I controleert.”

Naast de eenvoudige vertaling van booleaanse uitdrukkingen, behandelt iContract zaken die te maken hebben met de object levenscyclus, de reikwijdte van variabelen, en overerving, waarvan de meeste specifiek zijn voor object-georiënteerd programmeren. De belangrijkste regels met betrekking tot de levenscyclus van objecten zijn als volgt:

In een objectconstructie moeten de randvoorwaarden aan het begin van de constructie worden gecontroleerd, terwijl de invarianten en de postvoorwaarden alleen worden gecontroleerd als de constructie normaal wordt beëindigd (zonder uitzonderingen).

Voor normale methode-aanroepen op objecten worden de voor- en postvoorwaarden gecontroleerd bij het binnenkomen en verlaten van de aangeroepen methoden, en worden invarianten niet gecontroleerd voor privémethoden.

In objectvernietiging is geen controle vereist. Er wordt dus geen controlecode ingevoegd in de finalize() methoden.

De predicaten die in de postvoorwaarden worden gebruikt, verwijzen vaak zowel naar de terugkeerwaarde van de methode als naar de “oude” waarde (of “entry-waarde”) van sommige variabelen. iContract synthetiseert een pseudo-variabele, genaamd return, om de retourwaarde weer te geven, slaat de waarden van de variabelen op bij de entry, en maakt ze toegankelijk als de variabelennamen met de postfix @pre. expressies, hun object referenties worden direct opgeslagen. iContract staat aanroepen naar member methods in contracten toe. Als gevolg daarvan kunnen er potentiële oneindige recursies optreden bij het controleren van invarianten. Om dit probleem op te lossen, houdt iContract de recursiediepte voor elk object binnen elke thread bij en controleert het alleen invarianten met diepte 0. De type-extensiemechanismen in Java hebben verschillende implicaties voor de manier waarop contracten van de typen moeten worden afgehandeld. iContract propageert contracten door de volgende regels te volgen:

Invarianten worden geconjugeerd, dat wil zeggen dat de invariant van een subtype de conjunctie is van de invarianten van al zijn supertypen.

Postcondities zijn conjuncted, dat wil zeggen dat de postconditie van een subtype de conjunctie is van de postcondities van al zijn supertypen.

Precondities zijn disjuncted, dat wil zeggen dat de preconditie van een subtype de disjunctie is van de precondities van al zijn supertypen.

JML is een populaire gedragsspecificatietaal die Design by Contract in Java ondersteunt. JML contracten zijn in de vorm van speciale opmerkingen in Java. JML drukt preconditions, postconditions, en invariants uit als boolean expressies voorafgegaan door de keywords requires, ensures, en invariant, respectievelijk, en gebruikt ingebouwde methode old(x) om de waarde van x voor een operatie weer te geven. Vergelijkbaar met de ▵ notatie in Z, biedt JML het trefwoord modifies om de variabelen aan te geven die in een methodetekst worden gewijzigd. Bovendien behandelt JML diverse objectgeoriënteerde kenmerken, zoals zichtbaarheid en overerving.

JML vormt de basis van veel benaderingen voor het genereren van testoraken. Bhorkar heeft een techniek voorgesteld om JML-contracten te vertalen in Java-asserties. Hoewel deze techniek alleen precondities ondersteunt, worden problemen aangepakt met betrekking tot specificatie-only variabelen, verfijning, specificatie-erfelijkheid en privacy. Vergelijkbaar met iContract behandelt de techniek de overerving van contracten door de contracten te conjuncteren en disjuncteren op basis van hun types. De techniek maakt ook gebruik van een hash-tabel (per thread) om recursies bij runtime-controles te voorkomen. Expressies die quantifiers gebruiken, worden door de techniek als niet-uitvoerbaar beschouwd, en voor dergelijke expressies wordt geen Java-code gegenereerd.

Korat heeft een techniek voorgesteld om zowel testgevallen als orakels te genereren uit JML-specificaties. Korat’s techniek genereert testgevallen uit precondities en invarianten en orakels uit postcondities en invarianten. Araujo et al. breidden JML uit met concurrent contracts en stelden een runtime-ondersteuning voor om runtime-controle mogelijk te maken van assertions die concurrent properties specificeren.

Peters en Parnas introduceerden een aanpak om test-oracles te genereren uit formele documentatie. De documentatie wordt gegeven in een tabelvorm die bestaat uit relaties die de interfaces van procedures specificeren en wordt vertaald in orakels in C en C++ code. Het kernelement van de formele documentatie bestaat uit predicaten expressies geschreven in een variant van predicatenlogica met speciale constructen om met partiële functies om te gaan. Vergelijkbaar met andere specificatie-gebaseerde orakel technieken, behandelt deze techniek quantifiers door ze te beperken tot eindige verzamelingen.

Om iteraties over verzamelingen van elementen met quantifiers mogelijk te maken, introduceren Peters en Parnas inductief gedefinieerde predicaten. Voor een verzameling S (te gebruiken in kwantificaties) wordt een inductief gedefinieerd predikaat P gedefinieerd met drie elementen I, G en Q. De initiële verzameling I bevat een eindig aantal elementen, G is een functie, de zogenaamde “generatorfunctie”, die de elementen in de verzameling genereert, en Q is een predikaat dat de eigenschappen van de verzameling karakteriseert. De verzameling S wordt gedefinieerd als de kleinste verzameling die in de onderstaande inductieve stappen wordt geconstrueerd:

S0 = I

Sn + 1 = Sn ∪ {G(x)|x ∈ Sn ∧ Q(x)}

I, G en Q moeten zodanig worden gedefinieerd dat zij voldoen aan een speciale voorwaarde die garandeert dat de geconstrueerde verzameling S eindig is.

Peters en Parnas stellen een aanpak voor om de documentaties in tabelvorm te vertalen in C-code . Aangezien de vorm van de in de documentaties gebruikte expressies goed gedefinieerd is, vindt de vertaling systematisch plaats.

Ook andere op toestanden gebaseerde specificaties worden gebruikt voor het afleiden van test oracles. UML-modellen zijn over het algemeen informeel; desondanks hebben Bouquet et al. een subset van UML 2.1 gedefinieerd, genaamd UML-MBT, die nauwkeurig genoeg is voor het automatisch genereren van testgevallen. UML-MBT omvat klassendiagrammen, toestandsmachines, en een subset van OCL 2.0. Bouquet et al. gebruiken specificaties geschreven in VDM-SL om test oracles te genereren.