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/solver.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
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.card import CardEnc
|
|
25
|
+
from pysat.formula import CNF
|
|
26
|
+
from pysat.solvers import Solver
|
|
27
|
+
|
|
28
|
+
from testomaton.constraint import Parser, Assignment
|
|
29
|
+
|
|
30
|
+
class TomatoSolver:
|
|
31
|
+
def __init__(self, function,
|
|
32
|
+
negate_constraints=False,
|
|
33
|
+
invert_constraints=False):
|
|
34
|
+
"""
|
|
35
|
+
TomatoSolver is a class that is used to determine if a test case (potentially incomplete) is
|
|
36
|
+
fulfilling defined constraints. It is also used to adapt the values of output parameters according
|
|
37
|
+
to defined assignment expressions. TomatoSolver class provides a set of functions that are used
|
|
38
|
+
throughout test generation that allow to dynamically update its state according to current step
|
|
39
|
+
in generation.
|
|
40
|
+
|
|
41
|
+
Public functions from the solver that are used in the generator:
|
|
42
|
+
restrict_test_case(restricted) : Adds a clause to the model semantics to restrict a given tuple (test case).
|
|
43
|
+
test(test_case) : Tests if a given test case satisfies the constraints.
|
|
44
|
+
choice_selected(parameter_index, choice) : Announces to the solver that a given choice is selected for a given parameter.
|
|
45
|
+
tuple_selected(test_tuple) : Updates the solver with the choices selected in a given test tuple.
|
|
46
|
+
new_test_case() : Resets the solver back to the initial model semantics, removing all the choices selected so far.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
function (Function): Function object that represents the function to be tested, containing parameters, choices, aliases and constraints.
|
|
51
|
+
negate_constraints (bool): If True, negates all constraints.
|
|
52
|
+
invert_constraints (bool): If True, inverts all constraints.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
none
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
self.function = function
|
|
59
|
+
self.parser = Parser()
|
|
60
|
+
self.parameters = self.function.get_parameter_names()
|
|
61
|
+
self.choices = self.function.get_leaf_choice_names()
|
|
62
|
+
|
|
63
|
+
# Prepeare choice mapping and model semantics
|
|
64
|
+
self.choice_mapping = self.__prepare_choice_mapping()
|
|
65
|
+
self.top_id = max([val for mapping in self.choice_mapping.values() for key, val in mapping.items()])
|
|
66
|
+
self.model_semantics = []
|
|
67
|
+
self.__prepare_model_semantics()
|
|
68
|
+
|
|
69
|
+
# Initializes and populates the 'aliases' included in the function, parsing the statement aliases.
|
|
70
|
+
aliases = {}
|
|
71
|
+
for name, formula in self.function.aliases.items():
|
|
72
|
+
aliases[name] = self.parser.parse_statement_alias(formula, aliases)
|
|
73
|
+
|
|
74
|
+
aggregated_constraint = None
|
|
75
|
+
constraints_to_process = []
|
|
76
|
+
|
|
77
|
+
# Go through each structure in the function and parse local aliases and constraints, then add them to the list of constraints to process.
|
|
78
|
+
for p in self.function.get_structures():
|
|
79
|
+
local_aliases = {}
|
|
80
|
+
for context, name, formula in p.get_aliases():
|
|
81
|
+
local_aliases[name] = self.parser.parse_statement_alias(formula, local_aliases, context)
|
|
82
|
+
for context, name, formula in p.get_constraints():
|
|
83
|
+
while isinstance(formula, tuple):
|
|
84
|
+
formula = formula[1]
|
|
85
|
+
constraints_to_process.append(self.parser.parse_constraint(formula, local_aliases, context).to_invariant())
|
|
86
|
+
|
|
87
|
+
# Goes through global constraints defined in 'self.function' and adds them to the list of constraints to process.
|
|
88
|
+
for name, formulas in self.function.constraints.items():
|
|
89
|
+
for context, formula in formulas:
|
|
90
|
+
while isinstance(formula, tuple):
|
|
91
|
+
formula = formula[1]
|
|
92
|
+
constraints_to_process.append(self.parser.parse_constraint(formula, aliases, context).to_invariant())
|
|
93
|
+
|
|
94
|
+
# Iterates over the constraints to process, aggregating them into a single constraint. And negates the constraint if 'negate_constraints' is True.
|
|
95
|
+
for c in constraints_to_process:
|
|
96
|
+
if invert_constraints:
|
|
97
|
+
c = c.negated()
|
|
98
|
+
if aggregated_constraint is None:
|
|
99
|
+
aggregated_constraint = c
|
|
100
|
+
else:
|
|
101
|
+
aggregated_constraint = aggregated_constraint & c
|
|
102
|
+
|
|
103
|
+
# If 'negate_constraints' is True, negates the aggregated constraint (inverts its truth value)
|
|
104
|
+
if aggregated_constraint is not None and negate_constraints:
|
|
105
|
+
aggregated_constraint = aggregated_constraint.negated()
|
|
106
|
+
|
|
107
|
+
# If the aggregated constraint is not None, converts it to CNF and adds it to the model semantics.
|
|
108
|
+
if aggregated_constraint is not None:
|
|
109
|
+
self.top_id, cnf = aggregated_constraint.to_cnf(self.choice_mapping, self.top_id)
|
|
110
|
+
self.model_semantics.extend(cnf.clauses)
|
|
111
|
+
|
|
112
|
+
# Initializes the solver with the prepared model semantics.
|
|
113
|
+
self.solver = Solver(bootstrap_with=self.model_semantics)
|
|
114
|
+
|
|
115
|
+
#Initializes and iterates over the 'assignments' dictionary and appends the parsed assignments into their respective name list.
|
|
116
|
+
self.assignments = {}
|
|
117
|
+
for name, formulas_list in self.function.assignments.items():
|
|
118
|
+
if name not in self.assignments:
|
|
119
|
+
self.assignments[name] = []
|
|
120
|
+
for formula in formulas_list:
|
|
121
|
+
assignment = self.parser.parse_assignment(formula, self.function, aliases)
|
|
122
|
+
# assignment_tokens = self.parser.parse_assignment(formula)
|
|
123
|
+
|
|
124
|
+
# assignment = Assignment(self.function, assignment_tokens[0], assignment_tokens[1])
|
|
125
|
+
self.assignments[name].append(assignment)
|
|
126
|
+
|
|
127
|
+
def restrict_test_case(self, restricted):
|
|
128
|
+
"""
|
|
129
|
+
Adds a clause to the model semantics to restrict a given tuple (test case).
|
|
130
|
+
This will be NOT (x1 AND x2 AND ... AND xn),
|
|
131
|
+
that is: NOT x1 OR NOT x2 OR ... OR NOT xn.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
restricted (list): List of choices to restrict in the tuple.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
none
|
|
138
|
+
"""
|
|
139
|
+
clause = []
|
|
140
|
+
for i, choice in enumerate(restricted):
|
|
141
|
+
if choice != None:
|
|
142
|
+
parameter_name = self.parameters[i]
|
|
143
|
+
# checks if the parameter is an output parameter, if so, it should not be restricted
|
|
144
|
+
is_output = parameter_name in [p.name for p in self.function.output_parameters]
|
|
145
|
+
mapping = self.choice_mapping[self.parameters[i]]
|
|
146
|
+
# if the parameter is not an output paramteter, append the negated choice to the clause list
|
|
147
|
+
if not is_output:
|
|
148
|
+
clause.append(-mapping[choice])
|
|
149
|
+
# append the clause to the model semantics
|
|
150
|
+
self.model_semantics.append(clause)
|
|
151
|
+
|
|
152
|
+
def test(self, test_case):
|
|
153
|
+
""" Tests if a given test case satisfies the constraints. Converting it to a clause and uses the solver to check it"""
|
|
154
|
+
return self.solver.solve(assumptions=self.__tuple_to_clause(test_case))
|
|
155
|
+
|
|
156
|
+
def choice_selected(self, parameter_index, choice):
|
|
157
|
+
"""
|
|
158
|
+
Announces to the solver that a given choice is selected for a given parameter.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
parameter_index (int): Index of the parameter in the tuple.
|
|
162
|
+
choice (int): Index of the choice selected for the parameter.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
none
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
The parameters are : ['colour', 'size']
|
|
169
|
+
The choices are : [['red', 'green'], ['small', 'big']]
|
|
170
|
+
the mapping is : {'colour': {'red': 1, 'green': 2}, 'size': {'small': 3, 'big': 4}}
|
|
171
|
+
If we call choice_selected(0, 'green'), it will add the clause [2] to the solver
|
|
172
|
+
indicating that the choice 'green' is selected for the parameter 'colour'.
|
|
173
|
+
"""
|
|
174
|
+
parameter_name = self.parameters[parameter_index]
|
|
175
|
+
mapping = self.choice_mapping[parameter_name]
|
|
176
|
+
self.solver.add_clause([mapping[choice]])
|
|
177
|
+
|
|
178
|
+
def tuple_selected(self, test_tuple):
|
|
179
|
+
"""
|
|
180
|
+
Updates the solver with the choices selected in a given test tuple.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
test_tuple (list): List of choices selected in the test tuple.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
none
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
The parameters are : ['colour', 'size']
|
|
190
|
+
The choices are : [['red', 'green'], ['small', 'big']]
|
|
191
|
+
the mapping is : {'colour': {'red': 1, 'green': 2}, 'size': {'small': 3, 'big': 4}}
|
|
192
|
+
If we call tuple_selected(['green', 'big']), it will add the clauses [2] and [4] to the solver
|
|
193
|
+
indicating that the choice 'green' is selected for the parameter 'colour' and the choice 'big'
|
|
194
|
+
is selected for the parameter 'size'.
|
|
195
|
+
"""
|
|
196
|
+
for i, choice in enumerate(test_tuple):
|
|
197
|
+
if choice != None:
|
|
198
|
+
mapping = self.choice_mapping[self.parameters[i]]
|
|
199
|
+
self.solver.add_clause([mapping[choice]])
|
|
200
|
+
|
|
201
|
+
def new_test_case(self):
|
|
202
|
+
""" Resets the solver back to the initial model semantics, removing all the choices selected so far.
|
|
203
|
+
Used when starting a new test case, ensures each test case is evaluated independently."""
|
|
204
|
+
self.solver = Solver(bootstrap_with=self.model_semantics)
|
|
205
|
+
|
|
206
|
+
def adapt(self, test_case):
|
|
207
|
+
"""
|
|
208
|
+
To modify a test case based on preconditions and assignments.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
test_case (list): List of choices selected in the test tuple.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
list: List of choices modified based on the preconditions and assignments.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
The parameters and choices are : ['colour', 'size'], [['red', 'green'], ['small', 'big']]
|
|
218
|
+
The assignments are : "'colour' IS 'red' => 'size' IS 'small'"
|
|
219
|
+
If we call adapt(['red', 'big']), it will check the assignments and modify the test case to ['red', 'small'].
|
|
220
|
+
"""
|
|
221
|
+
clause = self.__tuple_to_clause(test_case)
|
|
222
|
+
for assignment_group in self.assignments.values():
|
|
223
|
+
for assignment in assignment_group:
|
|
224
|
+
top_id, precondition_cnf = assignment.precondition.to_cnf(self.choice_mapping, self.top_id)
|
|
225
|
+
# Instantiate a new solver with the precondition and model semantics
|
|
226
|
+
solver = Solver(bootstrap_with=self.model_semantics)
|
|
227
|
+
solver.append_formula(precondition_cnf.clauses)
|
|
228
|
+
solver.add_clause([top_id])
|
|
229
|
+
# Check if the precondition is satisfied
|
|
230
|
+
if solver.solve(assumptions=clause):
|
|
231
|
+
assignment.apply(test_case)
|
|
232
|
+
return test_case
|
|
233
|
+
|
|
234
|
+
# Private methods below:
|
|
235
|
+
|
|
236
|
+
def __tuple_to_clause(self, tested_tuple):
|
|
237
|
+
"""
|
|
238
|
+
Converts a test case tuple into a logical clause that can be used by the solver.
|
|
239
|
+
This clause represents the choices selected for each parameter in the test case."""
|
|
240
|
+
clause = []
|
|
241
|
+
for n, choice in enumerate(tested_tuple):
|
|
242
|
+
if choice == None:
|
|
243
|
+
continue
|
|
244
|
+
# get the parameter name and the choice representation
|
|
245
|
+
parameter_name = self.parameters[n]
|
|
246
|
+
choice_representation = self.choice_mapping[parameter_name][choice]
|
|
247
|
+
# add the choice representation to the clause
|
|
248
|
+
clause.append(choice_representation)
|
|
249
|
+
return clause
|
|
250
|
+
|
|
251
|
+
def __prepare_choice_mapping(self):
|
|
252
|
+
"""
|
|
253
|
+
Creates a dictionary that maps each possible choice for every parameter to a unique number.
|
|
254
|
+
This is used in the conversion of human-readable choices to a format that can be used by the solver.
|
|
255
|
+
"""
|
|
256
|
+
choice_mapping = {}
|
|
257
|
+
i = 1
|
|
258
|
+
|
|
259
|
+
# iterates over the parameters
|
|
260
|
+
for n, parameter in enumerate(self.parameters):
|
|
261
|
+
mapping = {}
|
|
262
|
+
choice_mapping[parameter] = mapping
|
|
263
|
+
# iterates over the choices for the parameter and assigns a number to each choice
|
|
264
|
+
for choice in self.choices[n]:
|
|
265
|
+
mapping[choice] = i
|
|
266
|
+
i += 1
|
|
267
|
+
|
|
268
|
+
# returns the dictionary
|
|
269
|
+
return choice_mapping
|
|
270
|
+
|
|
271
|
+
def __get_parameter_choices_representation(self, parameter):
|
|
272
|
+
""" returns representation of all choices for a given parameter"""
|
|
273
|
+
choices = self.choice_mapping[parameter]
|
|
274
|
+
result = [value for key, value in choices.items()]
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
def __prepare_model_semantics(self):
|
|
278
|
+
"""
|
|
279
|
+
Define logical constraints/semantics that ensure only one choice is selected for each parameter.
|
|
280
|
+
These are added to the model semantics.
|
|
281
|
+
"""
|
|
282
|
+
self.model_semantics = CNF()
|
|
283
|
+
|
|
284
|
+
for parameter in self.parameters:
|
|
285
|
+
choices = self.__get_parameter_choices_representation(parameter)
|
|
286
|
+
|
|
287
|
+
# Define a cardinality constraint, ensuring only one choice is selected for the parameter
|
|
288
|
+
semantics = CardEnc.equals(lits=choices, bound=1, top_id=self.top_id)
|
|
289
|
+
|
|
290
|
+
# Add the clauses to the model semantics and update the top_id
|
|
291
|
+
self.model_semantics.extend(semantics.clauses)
|
|
292
|
+
self.top_id = semantics.nv
|
|
293
|
+
|