testomaton 0.2.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testomaton/__init__.py +0 -0
- testomaton/beaver.py +180 -0
- testomaton/constraint.py +669 -0
- testomaton/exit_codes.py +20 -0
- testomaton/generator.py +300 -0
- testomaton/jigsaw.py +347 -0
- testomaton/model.py +605 -0
- testomaton/solver.py +293 -0
- testomaton/tomato.py +473 -0
- testomaton/validator.py +238 -0
- testomaton-0.2.2.dist-info/LICENSE +661 -0
- testomaton-0.2.2.dist-info/METADATA +1193 -0
- testomaton-0.2.2.dist-info/RECORD +16 -0
- testomaton-0.2.2.dist-info/WHEEL +6 -0
- testomaton-0.2.2.dist-info/entry_points.txt +4 -0
- testomaton-0.2.2.dist-info/top_level.txt +7 -0
testomaton/constraint.py
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright Testify AS
|
|
3
|
+
#
|
|
4
|
+
# This file is part of testomaton suite
|
|
5
|
+
#
|
|
6
|
+
# testomaton is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# For the commercial license, please contact Testify AS.
|
|
12
|
+
#
|
|
13
|
+
# testomaton is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU Affero General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License
|
|
19
|
+
# along with testomaton. If not, see <http://www.gnu.org/licenses/>.
|
|
20
|
+
#
|
|
21
|
+
# See LICENSE file for the complete license text.
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
from pysat.formula import CNF
|
|
25
|
+
from pyparsing import *
|
|
26
|
+
from copy import deepcopy
|
|
27
|
+
|
|
28
|
+
def parse_expression(expression_tokens, aliases, context):
|
|
29
|
+
"""
|
|
30
|
+
A function to parse a logical expression into correct components based on the given tokens,
|
|
31
|
+
such as 'AND', 'OR', 'NOT', 'IS', 'IN', 'IS NOT', 'NOT IN'. Returning instances of classes,
|
|
32
|
+
the classes being : NegatedExpression, CompositeExpression, PrimitiveStatement.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
expression_tokens (list): A list of tokens representing a logical expression.
|
|
36
|
+
aliases (dict): A dictionary of aliases.
|
|
37
|
+
context (str): A context to be used for the expression. The context is either None, meaning that
|
|
38
|
+
the expression was defined for a function. Or it is the path to a structure (parameter)
|
|
39
|
+
where the expression is defined.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
OBJECT: An object representing the parsed expression. The object that is returned, must have a toCNF function
|
|
43
|
+
so it can be converted to CNF form.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
Exception: If the expression is made up of a single token which is not an alias.
|
|
47
|
+
Normally there are at least two tokens on the high level(NOT expr), usually
|
|
48
|
+
three(eg. expr AND/OR expr, or par IS choice). If there is only one token,
|
|
49
|
+
it means that we are useing a previously defined alias.
|
|
50
|
+
"""
|
|
51
|
+
# checks if the expression is a single string and if it is an alias, returns the corresponding statement
|
|
52
|
+
if len(expression_tokens) == 1 and isinstance(expression_tokens[0], str):
|
|
53
|
+
if expression_tokens[0] in aliases:
|
|
54
|
+
return aliases[expression_tokens[0]].statement
|
|
55
|
+
else:
|
|
56
|
+
raise Exception(f"'{expression_tokens}' is not a defined alias")
|
|
57
|
+
|
|
58
|
+
# if 'expression_tokens' is a single statement, it is believed to be a nested list and unwraps it, loops as long as the 'expression_tokens' is 1
|
|
59
|
+
while len(expression_tokens) == 1:
|
|
60
|
+
expression_tokens = expression_tokens[0]
|
|
61
|
+
|
|
62
|
+
# handles expressions starting with 'NOT' and returns a negatedExpression object, removing the 'NOT' token
|
|
63
|
+
if expression_tokens[0] == 'NOT':
|
|
64
|
+
return NegatedExpression(tokens=expression_tokens[1:], aliases=aliases, context=context)
|
|
65
|
+
|
|
66
|
+
elif any(element in expression_tokens.asList() for element in ['AND', 'OR']) and len(expression_tokens):
|
|
67
|
+
# conjunction or disjunction
|
|
68
|
+
rval_tokens = expression_tokens.pop()
|
|
69
|
+
operator = expression_tokens.pop()
|
|
70
|
+
lval_tokens = expression_tokens
|
|
71
|
+
|
|
72
|
+
return CompositeExpression(operator, tokens=[lval_tokens, rval_tokens], aliases=aliases, context=context)
|
|
73
|
+
|
|
74
|
+
elif any(element in expression_tokens.asList() for element in ['IS', 'IS NOT', 'IN', 'NOT IN']) and len(expression_tokens) == 3:
|
|
75
|
+
# primitive statement
|
|
76
|
+
rval_tokens = expression_tokens.pop()
|
|
77
|
+
operator = expression_tokens.pop()
|
|
78
|
+
lval_tokens = expression_tokens
|
|
79
|
+
|
|
80
|
+
return PrimitiveStatement(lval_tokens, operator, rval_tokens, context=context)
|
|
81
|
+
|
|
82
|
+
class CompositeExpression:
|
|
83
|
+
"""
|
|
84
|
+
A composite expression represents the union(AND) or disjunction(OR) of two expressions.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
operator (str): The operator of the composite expression.
|
|
88
|
+
lval (object): The left value of the composite expression.
|
|
89
|
+
rval (object): The right value of the composite expression.
|
|
90
|
+
|
|
91
|
+
Methods:
|
|
92
|
+
to_cnf: A function to convert a composite expression into CNF.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, operator,
|
|
96
|
+
aliases,
|
|
97
|
+
tokens=None,
|
|
98
|
+
expr=None,
|
|
99
|
+
context=None):
|
|
100
|
+
"""
|
|
101
|
+
Initializes the CompositeExpression class and
|
|
102
|
+
parses the left and right expressions based on the given tokens.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
operator (str): The operator of the composite expression.
|
|
106
|
+
aliases (dict): A dictionary of aliases.
|
|
107
|
+
tokens (list): A list of tokens representing the composite expression.
|
|
108
|
+
expr (list): A list of expressions representing the composite expression.
|
|
109
|
+
context (str): A context to be used for the expression.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
None
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
self.operator = operator
|
|
116
|
+
if tokens is not None:
|
|
117
|
+
self.lval = parse_expression(tokens[0], aliases, context)
|
|
118
|
+
self.rval = parse_expression(tokens[1], aliases, context)
|
|
119
|
+
elif expr is not None:
|
|
120
|
+
self.lval = expr[0]
|
|
121
|
+
self.rval = expr[1]
|
|
122
|
+
|
|
123
|
+
def __str__(self):
|
|
124
|
+
return '(' + str(self.lval) + ' ' + self.operator + ' ' + str(self.rval) + ')'
|
|
125
|
+
|
|
126
|
+
def to_cnf(self, choice_mapping, top_id):
|
|
127
|
+
"""
|
|
128
|
+
A function to convert a composite expression into CNF.
|
|
129
|
+
Handles the 'AND' and 'OR' operators, generating appropriate
|
|
130
|
+
CNF clauses.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
choice_mapping (dict): A dictionary of choices.
|
|
134
|
+
top_id (int): The top id of the composite expression.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
int: The top id of the composite expression.
|
|
138
|
+
CNF: A CNF representation of the composite expression.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
Exception: If the operator is unknown.
|
|
142
|
+
"""
|
|
143
|
+
# recursively convert the left and right expressions to CNF
|
|
144
|
+
lval_id, lval = self.lval.to_cnf(choice_mapping, top_id)
|
|
145
|
+
rval_id, rval = self.rval.to_cnf(choice_mapping, lval_id)
|
|
146
|
+
clauses = lval.clauses + rval.clauses
|
|
147
|
+
my_id = rval_id + 1
|
|
148
|
+
|
|
149
|
+
# if the operator is 'AND', the CNF clauses are extended, y = A AND B compiles to [A, !y] AND [B, !y] AND [!A, !B, y]
|
|
150
|
+
if self.operator == 'AND':
|
|
151
|
+
clauses.extend([[lval_id, -my_id], [rval_id, -my_id], [-lval_id, -rval_id, my_id]])
|
|
152
|
+
|
|
153
|
+
# if the operator is 'OR', the CNF clauses are extended, y = A OR B compiles to [!A, y] AND [!B, y] AND [A, B, !y]
|
|
154
|
+
elif self.operator == 'OR':
|
|
155
|
+
clauses.extend([[-lval_id, my_id], [-rval_id, my_id], [lval_id, rval_id, -my_id]])
|
|
156
|
+
|
|
157
|
+
# if the operator is not 'AND' or 'OR', raise an exception
|
|
158
|
+
else:
|
|
159
|
+
raise Exception(f'Parsing expression {str(self)}: unknown operator {self.operator}')
|
|
160
|
+
|
|
161
|
+
# return the top id and the CNF representation of the composite expression
|
|
162
|
+
cnf = CNF(from_clauses=clauses)
|
|
163
|
+
return my_id, cnf
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class NegatedExpression:
|
|
167
|
+
"""
|
|
168
|
+
A class to represent negated expressions.
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
expression (object): The expression to be negated.
|
|
172
|
+
|
|
173
|
+
Methods:
|
|
174
|
+
to_cnf: A function to convert a negated expression into CNF.
|
|
175
|
+
"""
|
|
176
|
+
def __init__(self,
|
|
177
|
+
tokens = None,
|
|
178
|
+
expr = None,
|
|
179
|
+
aliases = None,
|
|
180
|
+
context = None):
|
|
181
|
+
"""
|
|
182
|
+
Initializes the NegatedExpression class and parses the expression based on the given tokens.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tokens (list): A list of tokens representing the negated expression.
|
|
186
|
+
expr (list): A list of expressions representing the negated expression.
|
|
187
|
+
aliases (dict): A dictionary of aliases.
|
|
188
|
+
context (str): A context to be used for the expression.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
None
|
|
192
|
+
"""
|
|
193
|
+
# if the tokens are not None, parse the expression based on the given tokens,
|
|
194
|
+
# else directly assign the expr value to self.expression
|
|
195
|
+
if tokens is not None:
|
|
196
|
+
self.expression = parse_expression(tokens, aliases, context=context)
|
|
197
|
+
elif expr is not None:
|
|
198
|
+
self.expression = expr
|
|
199
|
+
|
|
200
|
+
def __str__(self):
|
|
201
|
+
return '(NOT ' + str(self.expression) + ')'
|
|
202
|
+
|
|
203
|
+
def to_cnf(self, choice_mapping, top_id):
|
|
204
|
+
"""
|
|
205
|
+
A function to convert a negated expression into CNF.
|
|
206
|
+
Negates the expression and returns the CNF representation.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
choice_mapping (dict): A dictionary of choices.
|
|
210
|
+
top_id (int): The top id of the negated expression.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
int: The top id of the negated expression.
|
|
214
|
+
CNF: A CNF representation of the negated expression.
|
|
215
|
+
"""
|
|
216
|
+
expr_id, expr = self.expression.to_cnf(choice_mapping, top_id)
|
|
217
|
+
my_id = expr_id + 1
|
|
218
|
+
#negate expr
|
|
219
|
+
expr.extend([[my_id, expr_id], [-my_id, -expr_id]])
|
|
220
|
+
|
|
221
|
+
# return the id and the CNF representation of the negated expression (expr)
|
|
222
|
+
return my_id, expr
|
|
223
|
+
|
|
224
|
+
class PrimitiveStatement:
|
|
225
|
+
"""
|
|
226
|
+
Primitive statments are the basic construction bricks of a constraint.
|
|
227
|
+
They are expressions in the form "parameters IS/IS NOT choice" or
|
|
228
|
+
"parameters IN/NOT IN [choice1, choice2, ...]".
|
|
229
|
+
|
|
230
|
+
Attributes:
|
|
231
|
+
lval (str): The left value of the primitive statement.
|
|
232
|
+
operator (str): The operator of the primitive statement.
|
|
233
|
+
values (list): The values of the primitive statement.
|
|
234
|
+
|
|
235
|
+
Methods:
|
|
236
|
+
to_cnf: A function to convert a primitive statement into CNF.
|
|
237
|
+
"""
|
|
238
|
+
def __init__(self, lval, operator, rval, context):
|
|
239
|
+
"""
|
|
240
|
+
Initializes the PrimitiveStatement class and assigns the left value, operator, and values based on the given tokens.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
lval (str): The left value of the primitive statement.
|
|
244
|
+
operator (str): The operator of the primitive statement.
|
|
245
|
+
rval (list): The values of the primitive statement.
|
|
246
|
+
context (str): A context to be used for the expression.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
None
|
|
250
|
+
"""
|
|
251
|
+
self.negated = False
|
|
252
|
+
if 'NOT' in operator:
|
|
253
|
+
self.negated = True
|
|
254
|
+
|
|
255
|
+
self.lval = lval[0]
|
|
256
|
+
if context is not None:
|
|
257
|
+
self.lval = context + '::' + self.lval
|
|
258
|
+
|
|
259
|
+
# if the operator is 'IS' set the values to the right value,
|
|
260
|
+
# else if the operator is 'IN' set the values to the list of values
|
|
261
|
+
if 'IS' in operator:
|
|
262
|
+
self.values = [rval]
|
|
263
|
+
elif 'IN' in operator:
|
|
264
|
+
self.values = rval.asList()
|
|
265
|
+
|
|
266
|
+
def __str__(self):
|
|
267
|
+
return '(' + str(self.lval) + (' NOT IN ' if self.negated else ' IN ') + str(self.values) + ')'
|
|
268
|
+
|
|
269
|
+
def to_cnf(self, choice_mapping, top_id):
|
|
270
|
+
"""
|
|
271
|
+
A function to convert a primitive statement into CNF.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
choice_mapping (dict): A dictionary of choices.
|
|
275
|
+
top_id (int): The top id of the primitive statement.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
int: The top id of the primitive statement.
|
|
279
|
+
CNF: A CNF representation of the primitive statement.
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
parameter_name = self.lval
|
|
283
|
+
parameter_mapping = choice_mapping[parameter_name]
|
|
284
|
+
|
|
285
|
+
def filter_choices(choices, values):
|
|
286
|
+
""" filters the choices based on the values, gotten from the operators."""
|
|
287
|
+
result = []
|
|
288
|
+
for choice in choices:
|
|
289
|
+
for value in values:
|
|
290
|
+
if choice == value or choice.startswith(value + '::'):
|
|
291
|
+
result.append(choice)
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
selected_choices = filter_choices(parameter_mapping.keys(), self.values)
|
|
295
|
+
|
|
296
|
+
# if the primitive statement is negated, filter the choices and select those that are not in the 'selected_choices'
|
|
297
|
+
if self.negated:
|
|
298
|
+
selected_choices = [choice for choice in parameter_mapping.keys() if choice not in selected_choices]
|
|
299
|
+
|
|
300
|
+
# maps the selected choices to the corresponding values in the parameter_mapping
|
|
301
|
+
allowed = [parameter_mapping[choice] for choice in selected_choices]
|
|
302
|
+
clauses = []
|
|
303
|
+
|
|
304
|
+
my_id = top_id + 1
|
|
305
|
+
last_clause = []
|
|
306
|
+
# loops through and for each element in 'allowed', appends the clause
|
|
307
|
+
# to the 'clauses' list and appends the element to the 'last_clause' list
|
|
308
|
+
for x in allowed:
|
|
309
|
+
clauses.append([-x, my_id])
|
|
310
|
+
last_clause.append(x)
|
|
311
|
+
last_clause.append(-my_id)
|
|
312
|
+
clauses.append(last_clause)
|
|
313
|
+
|
|
314
|
+
# returns the top id and the CNF representation of the primitive statement
|
|
315
|
+
return my_id, CNF(from_clauses=clauses)
|
|
316
|
+
|
|
317
|
+
class Invariant:
|
|
318
|
+
"""
|
|
319
|
+
A inveriant expression is a logical statement that must be fulfilled for all test cases
|
|
320
|
+
for them to be valid.
|
|
321
|
+
|
|
322
|
+
Attributes:
|
|
323
|
+
expression (object): The expression of the invariant.
|
|
324
|
+
context (str): The context of the invariant.
|
|
325
|
+
|
|
326
|
+
Methods:
|
|
327
|
+
__init__: Initializes the Invariant class and parses the expression based on the given tokens.
|
|
328
|
+
__str__: Returns a string representation of the invariant.
|
|
329
|
+
negated: A function to negate the invariant.
|
|
330
|
+
to_cnf: A function to convert an invariant into CNF.
|
|
331
|
+
to_invariant: A function to return the invariant.
|
|
332
|
+
expression: A function to return the expression of the invariant.
|
|
333
|
+
__and__: A function to logically AND two invariants.
|
|
334
|
+
__or__: A function to logically OR two invariants.
|
|
335
|
+
"""
|
|
336
|
+
def __init__(self, expression_tokens, aliases, context, expression=None):
|
|
337
|
+
"""
|
|
338
|
+
Initializes the Invariant class.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
expression_tokens (list): A list of tokens representing the expression.
|
|
342
|
+
aliases (dict): A dictionary of aliases.
|
|
343
|
+
context (str): The context of the invariant.
|
|
344
|
+
expression (object): The expression of the invariant. (optional)
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
None
|
|
348
|
+
"""
|
|
349
|
+
self.context = context
|
|
350
|
+
# if the expression is not None, assign the expression to self.expression, else parse the expression based on the given tokens and arguments
|
|
351
|
+
if expression is not None:
|
|
352
|
+
self.expression = expression
|
|
353
|
+
else:
|
|
354
|
+
self.expression = parse_expression(expression_tokens, aliases, context=context)
|
|
355
|
+
|
|
356
|
+
def __str__(self):
|
|
357
|
+
return str(self.expression)
|
|
358
|
+
|
|
359
|
+
def negated(self):
|
|
360
|
+
expression = NegatedExpression(expr=self.expression)
|
|
361
|
+
return Invariant(None, None, expression=expression, context=self.context)
|
|
362
|
+
|
|
363
|
+
#returns top_id and CNF
|
|
364
|
+
def to_cnf(self, choice_mapping, top_id):
|
|
365
|
+
"""
|
|
366
|
+
A function to convert an invariant into CNF.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
choice_mapping (dict): A dictionary of choices.
|
|
370
|
+
top_id (int): The top id of the invariant.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
int: The top id of the invariant.
|
|
374
|
+
CNF: A CNF representation of the invariant.
|
|
375
|
+
"""
|
|
376
|
+
invariant_id, result = self.expression.to_cnf(choice_mapping, top_id)
|
|
377
|
+
result.append([invariant_id])
|
|
378
|
+
|
|
379
|
+
return invariant_id, result
|
|
380
|
+
|
|
381
|
+
def to_invariant(self):
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
""" Returns the underlying expression of the invariant."""
|
|
385
|
+
def expression(self):
|
|
386
|
+
return self.expression
|
|
387
|
+
|
|
388
|
+
""" Creates a new 'invariant' object representing the logical AND of two invariants, (current invariant and another)."""
|
|
389
|
+
def __and__(self, other):
|
|
390
|
+
expression = CompositeExpression('AND', None, expr=[self.expression, other.expression], context=self.context)
|
|
391
|
+
result = Invariant(None, None, context=self.context, expression=expression)
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
""" Creates a new 'invariant' object representing the logical OR of two invariants, (current invariant and another)."""
|
|
395
|
+
def __or__(self, other):
|
|
396
|
+
self.expression = CompositeExpression('OR', None, expr=[self.expression, other.expression], context=self.context)
|
|
397
|
+
return self
|
|
398
|
+
|
|
399
|
+
class Implication:
|
|
400
|
+
"""
|
|
401
|
+
Implication is a expression in the form of 'IF precondition THEN postcondition'.
|
|
402
|
+
Which indicates that if the precondition is satisfied in a test case, then the
|
|
403
|
+
postcondition must also be satisfied. If not, the test case is not valid.
|
|
404
|
+
An implication can be converted into an invariant expression, in the form
|
|
405
|
+
of 'NOT precondition OR postcondition'.
|
|
406
|
+
|
|
407
|
+
Attributes:
|
|
408
|
+
precondition (object): The precondition of the implication.
|
|
409
|
+
postcondition (object): The postcondition of the implication.
|
|
410
|
+
context (str): The context of the implication.
|
|
411
|
+
|
|
412
|
+
Methods:
|
|
413
|
+
to_invariant: A function to convert the implication into an invariant.
|
|
414
|
+
negated: A function to negate the implication.
|
|
415
|
+
to_cnf: A function to convert the implication into CNF.
|
|
416
|
+
expression: A function to return the expression of the implication.
|
|
417
|
+
"""
|
|
418
|
+
def __init__(self, precondition_tokens, postcondition_tokens, aliases, context):
|
|
419
|
+
"""
|
|
420
|
+
Initializes the Implication class and parses the precondition and postcondition based on the given tokens.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
precondition_tokens (list): A list of tokens representing the precondition.
|
|
424
|
+
postcondition_tokens (list): A list of tokens representing the postcondition.
|
|
425
|
+
aliases (dict): A dictionary of aliases.
|
|
426
|
+
context (str): The context of the implication.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
None
|
|
430
|
+
"""
|
|
431
|
+
self.precondition = parse_expression(precondition_tokens, aliases, context=context)
|
|
432
|
+
self.postcondition = parse_expression(postcondition_tokens, aliases, context=context)
|
|
433
|
+
self.context = context
|
|
434
|
+
|
|
435
|
+
def __str__(self):
|
|
436
|
+
return 'IF ' + str(self.precondition) + ' THEN ' + str(self.postcondition)
|
|
437
|
+
|
|
438
|
+
def to_invariant(self):
|
|
439
|
+
""" Converts the implication into an invariant."""
|
|
440
|
+
negated_precondition = NegatedExpression(expr=self.precondition)
|
|
441
|
+
equivalent_expr = CompositeExpression('OR', None, expr=[negated_precondition, self.postcondition])
|
|
442
|
+
return Invariant(None, None, context=self.context, expression=equivalent_expr)
|
|
443
|
+
|
|
444
|
+
def negated(self):
|
|
445
|
+
return self.to_invariant().negated()
|
|
446
|
+
|
|
447
|
+
#returns top_id and CNF
|
|
448
|
+
def to_cnf(self, choice_mapping, top_id):
|
|
449
|
+
"""
|
|
450
|
+
A function to convert the implication into CNF.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
choice_mapping (dict): A dictionary of choices.
|
|
454
|
+
top_id (int): The top id of the implication.
|
|
455
|
+
"""
|
|
456
|
+
precondition_id, precondition_cnf = self.precondition.to_cnf(choice_mapping, top_id)
|
|
457
|
+
postcondition_id, postcondition_cnf = self.postcondition.to_cnf(choice_mapping, precondition_id)
|
|
458
|
+
|
|
459
|
+
return postcondition_id, CNF(from_clauses=[[-precondition_id, postcondition_id]] + precondition_cnf.clauses + postcondition_cnf.clauses)
|
|
460
|
+
|
|
461
|
+
""" Returns the expression of the implication."""
|
|
462
|
+
def expression(self):
|
|
463
|
+
return self.to_invariant().expression
|
|
464
|
+
|
|
465
|
+
class Assignment:
|
|
466
|
+
"""
|
|
467
|
+
Assignments represent expressions that modify output parameters of a function,
|
|
468
|
+
if precondition is satisfied.
|
|
469
|
+
|
|
470
|
+
Attributes:
|
|
471
|
+
function (object): The function of the assignment.
|
|
472
|
+
precondition (object): The precondition of the assignment.
|
|
473
|
+
assignments (dict): The assignments of the assignment.
|
|
474
|
+
|
|
475
|
+
Methods:
|
|
476
|
+
apply: A function to apply the assignment to a test case.
|
|
477
|
+
"""
|
|
478
|
+
def __init__(self, function, precondition_tokens, assignments_tokens, aliases):
|
|
479
|
+
"""
|
|
480
|
+
Initializes the Assignment class and parses the precondition and assignments based on the given tokens.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
function (object): The function of the assignment.
|
|
484
|
+
precondition_tokens (list): A list of tokens representing the precondition.
|
|
485
|
+
assignments_tokens (list): A list of tokens representing the assignments.
|
|
486
|
+
aliases (dict): A dictionary of aliases.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
None
|
|
490
|
+
"""
|
|
491
|
+
self.function = function
|
|
492
|
+
self.precondition = parse_expression(precondition_tokens, aliases, context=None)
|
|
493
|
+
self.assignments = {}
|
|
494
|
+
# get the output parameters of the function
|
|
495
|
+
output_parameters = [p.name for p in function.output_parameters]
|
|
496
|
+
# loop through the assignments and assign the values to the corresponding output parameters
|
|
497
|
+
for assignment in assignments_tokens:
|
|
498
|
+
name = assignment[0]
|
|
499
|
+
value = assignment[1]
|
|
500
|
+
if name not in output_parameters:
|
|
501
|
+
raise Exception(f'Unknown output parameter: {name}')
|
|
502
|
+
self.assignments[name] = value
|
|
503
|
+
|
|
504
|
+
def __str__(self):
|
|
505
|
+
return 'IF ' + str(self.precondition) + ' THEN ' + str(self.assignments)
|
|
506
|
+
|
|
507
|
+
def apply(self, test_case):
|
|
508
|
+
""" Applies the assignment to a test case."""
|
|
509
|
+
for name, value in self.assignments.items():
|
|
510
|
+
test_case[self.function.get_parameter_index(name)] = value
|
|
511
|
+
|
|
512
|
+
class StatementAlias:
|
|
513
|
+
"""
|
|
514
|
+
A class to represent statement aliases, allowing complex statements
|
|
515
|
+
to be referenced with simpler names.
|
|
516
|
+
|
|
517
|
+
Attributes:
|
|
518
|
+
statement (object): The statement of the alias.
|
|
519
|
+
"""
|
|
520
|
+
def __init__(self, statement_tokens, aliases, context=None):
|
|
521
|
+
"""
|
|
522
|
+
Initializes the StatementAlias class and parses the statement based on the given tokens.
|
|
523
|
+
Creates the corresponding logical expressions through parsing the statement.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
statement_tokens (list): A list of tokens representing the statement.
|
|
527
|
+
aliases (dict): A dictionary of aliases.
|
|
528
|
+
context (str): A context to be used for the statement, provides additional information for parsing.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
None
|
|
532
|
+
"""
|
|
533
|
+
self.statement = parse_expression(statement_tokens, aliases, context=context)
|
|
534
|
+
|
|
535
|
+
def __str__(self):
|
|
536
|
+
return "Alias: " + str(self.statement)
|
|
537
|
+
|
|
538
|
+
class Parser:
|
|
539
|
+
"""
|
|
540
|
+
A class to represent a parser, which provides methods for parsing constraints,
|
|
541
|
+
logical statements and assignments.
|
|
542
|
+
|
|
543
|
+
Methods:
|
|
544
|
+
parse_statement_alias: A function to parse a statement alias.
|
|
545
|
+
parse_constraint: A function to parse a constraint.
|
|
546
|
+
parse_assignment: A function to parse an assignment.
|
|
547
|
+
"""
|
|
548
|
+
def __init__(self):
|
|
549
|
+
# create the necessary literals
|
|
550
|
+
self.is_literal = CaselessLiteral("IS")
|
|
551
|
+
self.not_literal = CaselessLiteral("NOT")
|
|
552
|
+
self.in_literal = CaselessLiteral("IN")
|
|
553
|
+
|
|
554
|
+
# create the necessary relations
|
|
555
|
+
self.is_relation = self.is_literal
|
|
556
|
+
self.is_not_relation = Combine(self.is_literal + self.not_literal, adjacent=False, joinString=' ')
|
|
557
|
+
self.in_relation = self.in_literal
|
|
558
|
+
self.not_in_relation = Combine(self.not_literal + self.in_literal, adjacent=False, joinString=' ')
|
|
559
|
+
|
|
560
|
+
self.name = QuotedString('\'', escChar='\\') | QuotedString('"', escChar='\\')
|
|
561
|
+
# self.name = QuotedString('\'', escChar='\\') | QuotedString('"', escChar='\\') | Word(printables, excludeChars=',')
|
|
562
|
+
# self.name = QuotedString('\'', escChar='\\') | QuotedString('"', escChar='\\') | Word(pyparsing_unicode.printables, excludeChars=',')
|
|
563
|
+
self.aggregated_name = Group(Suppress('[') + self.name + OneOrMore(Suppress(',') + self.name) + Suppress(']'))
|
|
564
|
+
|
|
565
|
+
# create the necessary statements
|
|
566
|
+
self.simple_statement = self.name + self.is_relation + self.name | self.name + self.is_not_relation + self.name
|
|
567
|
+
self.aggregate_statement = self.name + self.in_relation + self.aggregated_name | self.name + self.not_in_relation + self.aggregated_name
|
|
568
|
+
self.primitive_statement = Group(self.simple_statement | self.aggregate_statement)
|
|
569
|
+
self.alias = self.name
|
|
570
|
+
|
|
571
|
+
# create the necessary logical operators
|
|
572
|
+
self.and_literal = CaselessLiteral("AND")
|
|
573
|
+
self.or_literal = CaselessLiteral("OR")
|
|
574
|
+
self.logical_operator = self.and_literal | self.or_literal
|
|
575
|
+
self.logical_operator.setResultsName('logical_operator')
|
|
576
|
+
# Uses 'infixNotation' to define grammar for logical expressions that can use 'NOT', 'AND', 'OR' operators
|
|
577
|
+
self.expression = Group(infixNotation(self.primitive_statement | self.alias,
|
|
578
|
+
[
|
|
579
|
+
(self.not_literal, 1, opAssoc.RIGHT),
|
|
580
|
+
(self.logical_operator, 2, opAssoc.LEFT),
|
|
581
|
+
(self.or_literal, 2, opAssoc.LEFT)
|
|
582
|
+
]))
|
|
583
|
+
|
|
584
|
+
# creates conditional literals
|
|
585
|
+
self.if_literal = CaselessLiteral("IF")
|
|
586
|
+
self.then_literal = CaselessLiteral("THEN")
|
|
587
|
+
self.implies_literal = CaselessLiteral("=>")
|
|
588
|
+
|
|
589
|
+
def parse_statement_alias(self, statement_alias_string, aliases, context=None):
|
|
590
|
+
"""
|
|
591
|
+
Parses statement alias string into a StatementAlias object.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
statement_alias_string (str): The statement alias string.
|
|
595
|
+
aliases (dict): A dictionary of aliases.
|
|
596
|
+
context (str): The context to be used for the statement alias.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
StatementAlias: A StatementAlias object.
|
|
600
|
+
|
|
601
|
+
Raises:
|
|
602
|
+
Exception: If the statement alias string cannot be parsed.
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
tokens = self.expression.parseString(statement_alias_string, parseAll=True)
|
|
606
|
+
except ParseException as e:
|
|
607
|
+
raise Exception(f'Parsing statement alias {statement_alias_string}: {e.msg}, {e.explain(e)}')
|
|
608
|
+
return StatementAlias(tokens, aliases, context)
|
|
609
|
+
|
|
610
|
+
def parse_constraint(self, constraint_string, aliases, context):
|
|
611
|
+
"""
|
|
612
|
+
Parses constraint string into an Invariant or Implication object.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
constraint_string (str): The constraint string.
|
|
616
|
+
aliases (dict): A dictionary of aliases.
|
|
617
|
+
context (str): The context to be used for the constraint.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Invariant or Implication: An Invariant or Implication object.
|
|
621
|
+
|
|
622
|
+
Raises:
|
|
623
|
+
Exception: If the constraint string cannot be parsed.
|
|
624
|
+
"""
|
|
625
|
+
implication = self.expression + Suppress(self.implies_literal) + self.expression | Suppress(self.if_literal) + self.expression + Suppress(self.then_literal) + self.expression
|
|
626
|
+
invariant = self.expression
|
|
627
|
+
constraint = implication | invariant
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
tokens = constraint.parseString(constraint_string, parseAll=True)
|
|
631
|
+
except ParseException as e:
|
|
632
|
+
raise Exception(f'Parsing constraint {constraint_string}: {e.msg}, {e.explain(e)}')
|
|
633
|
+
# if the length of the tokens is 1, return an Invariant object, else return an Implication object
|
|
634
|
+
if len(tokens) == 1:
|
|
635
|
+
# invariant
|
|
636
|
+
return Invariant(tokens[0], aliases, context)
|
|
637
|
+
if len(tokens) == 2:
|
|
638
|
+
# implication
|
|
639
|
+
return Implication(tokens[0], tokens[1], aliases, context)
|
|
640
|
+
|
|
641
|
+
def parse_assignment(self, assignment_string, function, aliases):
|
|
642
|
+
"""
|
|
643
|
+
Parses assignment string into an Assignment object.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
assignment_string (str): The assignment string.
|
|
647
|
+
function (object): The function of the assignment.
|
|
648
|
+
aliases (dict): A dictionary of aliases.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Assignment: An Assignment object.
|
|
652
|
+
|
|
653
|
+
Raises:
|
|
654
|
+
Exception: If the assignment string cannot be parsed.
|
|
655
|
+
"""
|
|
656
|
+
equals_literal = CaselessLiteral("=")
|
|
657
|
+
assignment_value = QuotedString('\'', escChar='\\') | Word(printables, excludeChars=',=')
|
|
658
|
+
# assignment_value = QuotedString('\'', escChar='\\') | Word(pyparsing_unicode.printables, excludeChars=',=')
|
|
659
|
+
assignent_statement = Group(self.name + Suppress(equals_literal) + assignment_value)
|
|
660
|
+
assignents_list = Group(assignent_statement + ZeroOrMore(Suppress(',') + assignent_statement))
|
|
661
|
+
|
|
662
|
+
# assignment = self.expression + Suppress(self.implies_literal) + assignents_list
|
|
663
|
+
assignment = self.expression + Suppress(self.implies_literal) + assignents_list | Suppress(self.if_literal) + self.expression + Suppress(self.then_literal) + assignents_list
|
|
664
|
+
try:
|
|
665
|
+
tokens = assignment.parseString(assignment_string, parseAll=True)
|
|
666
|
+
except ParseException as e:
|
|
667
|
+
raise Exception(f'Parsing assignment {assignment_string}: {e.msg}, {e.explain(e)}')
|
|
668
|
+
return Assignment(function, tokens[0], tokens[1], aliases)
|
|
669
|
+
|
testomaton/exit_codes.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class ExitCodes(Enum):
|
|
4
|
+
SUCCESS = 0
|
|
5
|
+
|
|
6
|
+
CONFLICTING_PROGRAM_ARGS = 1
|
|
7
|
+
MODEL_FILE_NOT_FOUND = 2
|
|
8
|
+
YAML_PARSING_ERROR = 3
|
|
9
|
+
MODEL_PARSING_ERROR = 4
|
|
10
|
+
MODEL_VALIDATION_ERROR = 5
|
|
11
|
+
TEST_FILE_NOT_FOUND = 6
|
|
12
|
+
TEST_PARSING_ERROR = 7
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return str(self.value)
|
|
16
|
+
|
|
17
|
+
def __int__(self):
|
|
18
|
+
return self.value
|
|
19
|
+
|
|
20
|
+
|