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/validator.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from testomaton.model import Function, OutputParameter
|
|
2
|
+
from testomaton.solver import TomatoSolver
|
|
3
|
+
|
|
4
|
+
from itertools import combinations, product
|
|
5
|
+
import sys
|
|
6
|
+
import copy
|
|
7
|
+
import csv
|
|
8
|
+
import itertools
|
|
9
|
+
|
|
10
|
+
def validate_headrow(headrow: list, function: Function):
|
|
11
|
+
messages = []
|
|
12
|
+
parameter_names = function.get_parameter_names()
|
|
13
|
+
valid = (headrow == parameter_names)
|
|
14
|
+
if not valid:
|
|
15
|
+
messages.append(f'Expected:\n{parameter_names}')
|
|
16
|
+
return messages, valid
|
|
17
|
+
|
|
18
|
+
def validate_test_by_choice_names(test: list, function: Function, solver: TomatoSolver):
|
|
19
|
+
messages = []
|
|
20
|
+
result = True
|
|
21
|
+
|
|
22
|
+
geninput = function.get_generator_input()
|
|
23
|
+
parameter_names = geninput[0]
|
|
24
|
+
parameters = [function.get_parameter(name) for name in parameter_names]
|
|
25
|
+
leaf_choices = geninput[1]
|
|
26
|
+
|
|
27
|
+
#first check if the test has the same number of values as the function has parameters
|
|
28
|
+
if len(test) != len(parameter_names):
|
|
29
|
+
result = False
|
|
30
|
+
messages.append(f'Number of values in the test ({len(test)}) does not match the number of parameters in the function ({len(parameter_names)})')
|
|
31
|
+
# print_exit_messages(messages, result)
|
|
32
|
+
return messages, result
|
|
33
|
+
|
|
34
|
+
#check if the values in the test are in the list of allowed values for the corresponding parameter
|
|
35
|
+
for i in range(len(test)):
|
|
36
|
+
parameter = parameters[i]
|
|
37
|
+
if isinstance(parameter, OutputParameter):
|
|
38
|
+
continue
|
|
39
|
+
if parameter.get_choice(test[i]) is None:
|
|
40
|
+
result = False
|
|
41
|
+
messages.append(f"Choice '{test[i]}' is not defined for parameter '{parameter_names[i]}'")
|
|
42
|
+
|
|
43
|
+
if not result:
|
|
44
|
+
# print_exit_messages(messages, result)
|
|
45
|
+
return messages, result
|
|
46
|
+
|
|
47
|
+
#test if the test case satisfies the constraints
|
|
48
|
+
test_copy = [test[i] if not isinstance(parameters[i], OutputParameter) else parameters[i].default_value for i in range(len(test))]
|
|
49
|
+
if not solver.test(test_copy):
|
|
50
|
+
result = False
|
|
51
|
+
messages.append(f'Test case does not satisfy the constraints')
|
|
52
|
+
# print_exit_messages(messages, result)
|
|
53
|
+
return messages, result
|
|
54
|
+
|
|
55
|
+
#test if the test case satisfies the assignments
|
|
56
|
+
test_copy = solver.adapt(test_copy)
|
|
57
|
+
for i in range(len(test)):
|
|
58
|
+
if test_copy[i] != test[i]:
|
|
59
|
+
result = False
|
|
60
|
+
messages.append(f"Value of parameter {parameter_names[i]} should be {test_copy[i]}")
|
|
61
|
+
|
|
62
|
+
# print_exit_messages(messages, result)
|
|
63
|
+
return messages, result
|
|
64
|
+
|
|
65
|
+
def validate_test_by_values(test: list, function: Function, solver: TomatoSolver):
|
|
66
|
+
def print_exit_messages(messages, success):
|
|
67
|
+
messages[0] = messages[0] + f'{VALID if success else INVALID}'
|
|
68
|
+
[print(msg) for msg in messages]
|
|
69
|
+
|
|
70
|
+
messages = []
|
|
71
|
+
result = True
|
|
72
|
+
|
|
73
|
+
geninput = function.get_generator_input()
|
|
74
|
+
parameter_names = geninput[0]
|
|
75
|
+
parameters = [function.get_parameter(name) for name in parameter_names]
|
|
76
|
+
leaf_choices = geninput[1]
|
|
77
|
+
|
|
78
|
+
#first check if the test has the same number of values as the function has parameters
|
|
79
|
+
if len(test) != len(parameter_names):
|
|
80
|
+
result = False
|
|
81
|
+
messages.append(f'Number of values in the test ({len(test)}) does not match the number of parameters in the function ({len(parameter_names)})')
|
|
82
|
+
# print_exit_messages(messages, result)
|
|
83
|
+
return messages, result
|
|
84
|
+
|
|
85
|
+
#check if the values in the test are in the list of allowed values for the corresponding parameter
|
|
86
|
+
for i in range(len(test)):
|
|
87
|
+
parameter = parameters[i]
|
|
88
|
+
full_name = parameter_names[i]
|
|
89
|
+
if isinstance(parameter, OutputParameter):
|
|
90
|
+
continue
|
|
91
|
+
leaf_choices[i] = choices = [choice for choice in leaf_choices[i] if parameter.get_choice(choice).value == test[i]]
|
|
92
|
+
if len(choices) == 0:
|
|
93
|
+
result = False
|
|
94
|
+
messages.append(f"Value '{test[i]}' not allowed for parameter '{full_name}'")
|
|
95
|
+
if not result:
|
|
96
|
+
# print_exit_messages(messages, result)
|
|
97
|
+
return messages, result
|
|
98
|
+
|
|
99
|
+
#in theory it is possible that many choices have the same value,
|
|
100
|
+
#so we need to check each potential combination of choices
|
|
101
|
+
test_cases = [list(test_case) for test_case in product(*leaf_choices)]
|
|
102
|
+
#remove test cases that do not satisfy the constraints
|
|
103
|
+
constrained_test_cases = [test_case for test_case in test_cases if solver.test(test_case)]
|
|
104
|
+
if len(constrained_test_cases) == 0:
|
|
105
|
+
result = False
|
|
106
|
+
messages.append(f'Test case does not satisfy the constraints')
|
|
107
|
+
# print_exit_messages(messages, result)
|
|
108
|
+
return messages, result
|
|
109
|
+
|
|
110
|
+
#remove test cases that do not satisfy the assignments
|
|
111
|
+
def check_output_values(test_case):
|
|
112
|
+
output_values = [test_case[i] if not isinstance(parameters[i], OutputParameter) else parameter.default_value for i in range(len(test_case))]
|
|
113
|
+
output_values = solver.adapt(output_values)
|
|
114
|
+
test_case_copy = [test_case[i] if not isinstance(parameters[i], OutputParameter) else test[i] for i in range(len(test_case))]
|
|
115
|
+
the_same = True
|
|
116
|
+
return (test_case_copy == output_values)
|
|
117
|
+
|
|
118
|
+
assigned_test_cases = [test_case for test_case in constrained_test_cases if check_output_values(test_case)]
|
|
119
|
+
if len(assigned_test_cases) == 0:
|
|
120
|
+
messages.append(f'Output values not correct ')
|
|
121
|
+
#in most cases, the values of choices are unique so the length of
|
|
122
|
+
#constraints_test_cases should be 1. In this case, we can compare the
|
|
123
|
+
#result of assignment and indicate the exact differences
|
|
124
|
+
if len(constrained_test_cases) == 1:
|
|
125
|
+
test_case = copy.deepcopy(constrained_test_cases[0])
|
|
126
|
+
test_case = solver.adapt(test_case)
|
|
127
|
+
for i in range(len(test_case)):
|
|
128
|
+
if test_case[i] != constrained_test_cases[0][i]:
|
|
129
|
+
messages.append(f"Value of parameter {parameter_names[i]} should be {test_case[i]}")
|
|
130
|
+
# print_exit_messages(messages, False)
|
|
131
|
+
return messages, False
|
|
132
|
+
|
|
133
|
+
# print_exit_messages(messages, result)
|
|
134
|
+
|
|
135
|
+
return messages, result
|
|
136
|
+
|
|
137
|
+
def validate_test_stream(in_stream, function, solver, **kwargs):
|
|
138
|
+
"""
|
|
139
|
+
Validates a stream of test cases. The tests that are valid are written to stdout, the invalid ones are written to stderr with error message.
|
|
140
|
+
The function returns True if all tests are parsed without problems. If there is a parsing error, the function returns False.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def print_error_messages(row, messages):
|
|
144
|
+
print(f'{RED}', file=sys.stderr, end='')
|
|
145
|
+
error_writer.writerow(row)
|
|
146
|
+
if not no_error_messages:
|
|
147
|
+
[print(f'{msg}', file=sys.stderr) for msg in messages]
|
|
148
|
+
print(f'{END}', file=sys.stderr, end='', flush=True)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
no_error_formatting = False
|
|
152
|
+
if 'no_error_formatting' in kwargs:
|
|
153
|
+
no_error_formatting = kwargs['no_error_formatting']
|
|
154
|
+
|
|
155
|
+
GREEN = '\033[92m' if not no_error_formatting else ''
|
|
156
|
+
RED = '\033[91m' if not no_error_formatting else ''
|
|
157
|
+
END = '\033[0m' if not no_error_formatting else ''
|
|
158
|
+
VALID = f'{GREEN}✓{END}' if not no_error_formatting else ''
|
|
159
|
+
INVALID = f'{RED}✗{END}' if not no_error_formatting else ''
|
|
160
|
+
|
|
161
|
+
separator = ','
|
|
162
|
+
if 'separator' in kwargs:
|
|
163
|
+
separator = kwargs['separator']
|
|
164
|
+
no_headrow = False
|
|
165
|
+
if 'no_headrow' in kwargs:
|
|
166
|
+
no_headrow = kwargs['no_headrow']
|
|
167
|
+
|
|
168
|
+
use_choice_names = False
|
|
169
|
+
if 'use_choice_names' in kwargs:
|
|
170
|
+
use_choice_names = kwargs['use_choice_names']
|
|
171
|
+
|
|
172
|
+
exit_on_error = False
|
|
173
|
+
if 'exit_on_error' in kwargs:
|
|
174
|
+
exit_on_error = kwargs['exit_on_error']
|
|
175
|
+
|
|
176
|
+
no_error_messages = False
|
|
177
|
+
if 'no_error_messages' in kwargs:
|
|
178
|
+
no_error_messages = kwargs['no_error_messages']
|
|
179
|
+
|
|
180
|
+
duplicate_headrow = False
|
|
181
|
+
if 'duplicate_headrow' in kwargs:
|
|
182
|
+
duplicate_headrow = kwargs['duplicate_headrow']
|
|
183
|
+
|
|
184
|
+
#skip empty lines
|
|
185
|
+
in_stream = filter(lambda x: x.strip(), in_stream)
|
|
186
|
+
#check if there is any data to read
|
|
187
|
+
try:
|
|
188
|
+
first_line = next(in_stream)
|
|
189
|
+
except StopIteration:
|
|
190
|
+
print(f"Empty test stream", file=sys.stderr)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
#peek the first line to read it again
|
|
194
|
+
reader = csv.reader(itertools.chain([first_line], in_stream), delimiter=separator)
|
|
195
|
+
writer = csv.writer(sys.stdout, delimiter=separator)
|
|
196
|
+
error_writer = csv.writer(sys.stderr, delimiter=separator)
|
|
197
|
+
parsing_ok = True
|
|
198
|
+
if not no_headrow:
|
|
199
|
+
try:
|
|
200
|
+
headrow = next(reader)
|
|
201
|
+
# print(f'Validating headrow', file=sys.stderr)
|
|
202
|
+
messages, result = validate_headrow(headrow, function)
|
|
203
|
+
except csv.Error as e:
|
|
204
|
+
messages, result = [f"CSV parsing error: {e}"], False
|
|
205
|
+
parsing_ok = False
|
|
206
|
+
|
|
207
|
+
if result:
|
|
208
|
+
writer.writerow(headrow)
|
|
209
|
+
if duplicate_headrow:
|
|
210
|
+
error_writer.writerow(headrow)
|
|
211
|
+
else:
|
|
212
|
+
print_error_messages(headrow, messages)
|
|
213
|
+
if duplicate_headrow:
|
|
214
|
+
writer.writerow(headrow)
|
|
215
|
+
if exit_on_error:
|
|
216
|
+
return parsing_ok
|
|
217
|
+
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
row = next(reader)
|
|
221
|
+
if not use_choice_names:
|
|
222
|
+
messages, result = validate_test_by_values(row, function, solver)
|
|
223
|
+
else:
|
|
224
|
+
messages, result = validate_test_by_choice_names(row, function, solver)
|
|
225
|
+
if result:
|
|
226
|
+
writer.writerow(row)
|
|
227
|
+
else:
|
|
228
|
+
print_error_messages(row, messages)
|
|
229
|
+
if exit_on_error:
|
|
230
|
+
return parsing_ok
|
|
231
|
+
|
|
232
|
+
except StopIteration:
|
|
233
|
+
break
|
|
234
|
+
except csv.Error as e:
|
|
235
|
+
print(f"CSV parsing error while fetching next test: {e}", file=sys.stderr)
|
|
236
|
+
parsing_ok = False
|
|
237
|
+
continue
|
|
238
|
+
return parsing_ok
|