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/tomato.py ADDED
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ #
5
+ # Copyright Testify AS
6
+ #
7
+ # This file is part of testomaton suite
8
+ #
9
+ # testomaton is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Affero General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # For the commercial license, please contact Testify AS.
15
+ #
16
+ # testomaton is distributed in the hope that it will be useful,
17
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ # GNU Affero General Public License for more details.
20
+ #
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with testomaton. If not, see <http://www.gnu.org/licenses/>.
23
+ #
24
+ # See LICENSE file for the complete license text.
25
+ #
26
+
27
+ import sys
28
+ import fileinput
29
+ from io import StringIO
30
+ import argparse
31
+ import importlib.metadata as metadata
32
+ from pyaml import yaml
33
+ import itertools
34
+
35
+ import testomaton.generator as generator
36
+ import testomaton.model as model
37
+ import testomaton.validator as validator
38
+ from testomaton.exit_codes import ExitCodes as ec
39
+ from testomaton.solver import TomatoSolver
40
+
41
+ import csv
42
+
43
+ def version():
44
+ """Get the version of tomato"""
45
+ return metadata.version('testomaton')
46
+
47
+ def parse_args():
48
+ """Parse the command line arguments"""
49
+ parser = argparse.ArgumentParser()
50
+
51
+ parser.add_argument("--version", action='store_true',
52
+ default=False,
53
+ help="Print version and exit")
54
+ parser.add_argument("--demo-level", type=int, default=0,
55
+ help="Can be used to demostrate working of the algorithm. \
56
+ 1: Show progress, 2: Show progress and introduces delay at each step",
57
+ choices=[1, 2, 3])
58
+ parser.add_argument("-f", "--function", type=str, default=None,
59
+ help="Function name to generate tests for. If not provided\
60
+ tests for the first function defined in the model will be generated")
61
+
62
+ validation_group = parser.add_argument_group('Validation')
63
+ # validation_group.add_argument("-v", "--validate-model", action='store_true', default=False,
64
+ # help=f"Validate the model and exit. Returns {ec.SUCCESS} if the model is valid, \
65
+ # {ec.MODEL_VALIDATION_ERROR} if the model is not syntatically correct. \
66
+ # If this flag is used, the only effective arguments are -f/--function and the arguments from \
67
+ # the 'Model content filtering'. Validated elements can still be filtered using whitelists \
68
+ # and blacklists. Other arguments are silently ignored.")
69
+ validation_group.add_argument("-V", "--validate-tests", nargs='?', type=str, const=None,
70
+ default=argparse.SUPPRESS, metavar='TEST_FILE',
71
+ help=f"Validate the tests and exit. Prints on stdandard output the tests are valid, ie. \
72
+ they could be generated from this model using all the elements and constraints (blacklists, \
73
+ whitelists, negations and invertions still can be used). If a test or the headrow could not \
74
+ be generated using given model, it is printed in the stderr with an error message, \
75
+ The opional argument is a path to the file with tests (with headrow or not, configured by \
76
+ -H/--no-headrow flag). If no test file is provided, the tests are read from the standard input. \
77
+ In this case, if the model is also read from the standard input, the model is read first and \
78
+ must be separated from the tests by a line starting from '---' \
79
+ (three dashes). ")
80
+ validation_group.add_argument("--exit-on-error", action='store_true', default=False,
81
+ help="Exit on the first error. If this flag is not set, the program will continue \
82
+ to the next test or model element after an error.")
83
+ validation_group.add_argument("-F", "--no-error-formatting", action='store_true', default=False,
84
+ help="Do not format error messages. If this flag is not set, the error messages are formatted \
85
+ to be more readable and distinguishable from valid tests.")
86
+ validation_group.add_argument("-M", "--no-error-messages", action='store_true', default=False,
87
+ help="Do not print error messages. If this flag is set, only the tests that fail validation \
88
+ are printed on stderr, without any additional messages.")
89
+ validation_group.add_argument("--duplicate-headrow", action='store_true', default=False,
90
+ help="Print the headrow both on top of the valid tests and the tests that fail validation.")
91
+
92
+ model_content_group = parser.add_argument_group('Model content filtering')
93
+ model_content_group.add_argument("-w", "--whitelist", type=str, default=None,
94
+ help="Comma separated list of names and labels of elements\
95
+ that should be parsed. All function elements (parameters, choices, \
96
+ constraints etc.) without name or label on that list will be ignored.")
97
+ model_content_group.add_argument("-b", "--blacklist", type=str, default=None,
98
+ help="Comma separated list of names and labels of elements\
99
+ that should be ignored. All function elements (parameters, choices, \
100
+ constraints etc.) with name or label on that list will be ignored.")
101
+ model_content_group.add_argument("--input-whitelist", type=str, default=None,
102
+ help="Whitelist of input elements (parameters and choices) to parse.\
103
+ Labels for input elements are inherited from parent element of the same type.")
104
+ model_content_group.add_argument("--input-blacklist", type=str, default=None,
105
+ help="Blacklist of input elements (parameters and choices) to ignore.\
106
+ Labels for input elements are inherited from parent element of the same type.")
107
+ model_content_group.add_argument("--parameters-whitelist", type=str, default=None,
108
+ help="Whitelist of parameters to parse. Labels within structures are inherited\
109
+ from parent parameter.")
110
+ model_content_group.add_argument("--parameters-blacklist", type=str, default=None,
111
+ help="Blacklist of parameters to ignore. Labels within structures are inherited")
112
+ model_content_group.add_argument("--choices-whitelist", type=str, default=None,
113
+ help="Whitelist of choices to parse. Labels of nested choices are inherited from the parent choice.")
114
+ model_content_group.add_argument("--choices-blacklist", type=str, default=None,
115
+ help="Blacklist of choices to ignore. Labels of nested choices are inherited from the parent choice.")
116
+ model_content_group.add_argument("--logic-whitelist", type=str, default=None,
117
+ help="Whitelist of logic elements (constraints and assignments) to parse.")
118
+ model_content_group.add_argument("--logic-blacklist", type=str, default=None,
119
+ help="Blacklist of logic elements (constraints and assignments) to ignore.")
120
+ model_content_group.add_argument("--constraints-whitelist", type=str, default=None,
121
+ help="Whitelist of constraints to parse.")
122
+ model_content_group.add_argument("--constraints-blacklist", type=str, default=None,
123
+ help="Blacklist of constraints to ignore.")
124
+ model_content_group.add_argument("--assignments-whitelist", type=str, default=None,
125
+ help="Whitelist of assignments to parse.")
126
+ model_content_group.add_argument("--assignments-blacklist", type=str, default=None,
127
+ help="Blacklist of assignments to ignore.")
128
+
129
+
130
+ parser.add_argument("input_file", type=str, nargs='?', default=None,
131
+ help="Input file containing the model. If not provided, the model is read from the standard input.")
132
+
133
+ formatting_group = parser.add_argument_group('Formatting')
134
+ formatting_group.add_argument("-H", "--no-headrow", action='store_true',
135
+ default=False,
136
+ help="do not print the head row with the parameter names")
137
+ formatting_group.add_argument("--use-choice-names", action='store_true',
138
+ default=False,
139
+ help="When printing the output, do not print choice values but their full names.")
140
+ formatting_group.add_argument("-s", "--separator", type=str, default=',',
141
+ help="Separator to use in the output")
142
+
143
+ group = parser.add_argument_group('Generators', 'Define a generator to use')
144
+ generator_group = group.add_mutually_exclusive_group()
145
+ generator_group.add_argument("--cartesian", action='store_true',
146
+ help="generate cartesian product of input choices")
147
+ generator_group.add_argument("--random", action='store_true',
148
+ help="generate random test cases")
149
+ generator_group.add_argument("--nwise", action='store_true',
150
+ help="nwise generator (default)")
151
+
152
+ random_group = parser.add_argument_group('Random generator options:')
153
+ random_group.add_argument("-l", "--length", type=int, default=None,
154
+ help="length of the randomly generated suite. 0 for infinite")
155
+ random_group.add_argument("-D", "--duplicates", action='store_true', default=None,
156
+ help="""Allows test cases to repeat. If not set and the number of test cases is 0,
157
+ tests will be generated indifinitely""")
158
+ random_group.add_argument("--adaptive", action='store_true', default=None,
159
+ help="""Use adaptive techniques to generate tests that are far from
160
+ the ones already generated""")
161
+
162
+ nwise_group = parser.add_argument_group('Nwise generator options')
163
+ nwise_group.add_argument("-n", type=int, default=None, help="n in nwise. Default value: 2")
164
+ nwise_group.add_argument("-c", "--coverage", default=None,
165
+ help="""Required coverage. 100 means all test cases that are allowed
166
+ by constraints. Available for nwise and cartesian generators. Default vaule: 100""")
167
+
168
+ constraints_manipulation_group = parser.add_argument_group('Constraints manipulation:')
169
+ constraints_manipulation_group.add_argument("--ignore-constraints", action='store_true', default=False,
170
+ help="Ignore constraints when generating tests")
171
+ constraints_manipulation_group.add_argument("--ignore-assignments", action='store_true', default=False,
172
+ help="Ignore assignments when generating tests")
173
+ constraints_manipulation_group.add_argument("--negate-constraints", action='store_true', default=False,
174
+ help="Negate the aggregated constraint. This will mean that the generated tests will fail to satisfy at least one of the constraints.\
175
+ Negating implication constraint in fact means negating its invariant equivalence. So a constraint 'A => B' will be negated to 'A AND NOT B'.")
176
+ constraints_manipulation_group.add_argument("--invert-constraints", action='store_true', default=False,
177
+ help="Negate all individual constraints. This will mean that the generated tests will fail to satisfy all the defined constraints.")
178
+
179
+ args = parser.parse_args()
180
+
181
+ # Check if version is requested, then print and exit
182
+ if args.version:
183
+ print(f"tomato {version()}")
184
+ sys.exit(int(ec.SUCCESS))
185
+
186
+ # Set default generator
187
+ if not (args.random or args.nwise or args.cartesian):
188
+ args.nwise = True
189
+
190
+ # Check if random options are used correctly
191
+ if not args.random and (args.length is not None or args.duplicates is not None or args.adaptive is not None):
192
+ parser.error("--length, --duplicates and --no-adaptive can only be used with --random")
193
+
194
+ # Check if nwise options are used correctly
195
+ if not args.nwise and (args.n is not None or args.coverage is not None):
196
+ parser.error("-n and --coverage can only be used with --nwise")
197
+
198
+ # Check if input file is provided
199
+ if args.input_file is None:
200
+ sys.stderr.write("Reading model from stdin\n")
201
+ sys.stderr.flush()
202
+
203
+ # Check if whitelist and blacklist are used together
204
+ if args.whitelist is not None and args.blacklist is not None:
205
+ parser.error("--whitelist and --blacklist cannot be used together")
206
+
207
+ if args.input_whitelist is not None and (
208
+ args.input_blacklist is not None or args.parameters_blacklist is not None or args.choices_blacklist is not None):
209
+ parser.error("--input-whitelist and --input-blacklist/--parameters-blacklist/--choices-blacklist cannot be used together")
210
+
211
+ if args.input_blacklist is not None and (
212
+ args.input_whitelist is not None or args.parameters_whitelist is not None or args.choices_whitelist is not None):
213
+ parser.error("--input-whitelist and --input-blacklist/--parameters-blacklist/--choices-blacklist cannot be used together")
214
+
215
+ if args.parameters_whitelist is not None and (
216
+ args.parameters_blacklist is not None or args.input_blacklist is not None):
217
+ parser.error("--parameters-whitelist and --parameters-blacklist/--input-blacklist cannot be used together")
218
+
219
+ if args.parameters_blacklist is not None and (
220
+ args.parameters_whitelist is not None or args.input_whitelist is not None):
221
+ parser.error("--parameters-whitelist and --parameters-blacklist/--input-blacklist cannot be used together")
222
+
223
+ if args.choices_whitelist is not None and (
224
+ args.choices_blacklist is not None or args.input_blacklist is not None):
225
+ parser.error("--choices-whitelist and --choices-blacklist/--input-blacklist cannot be used together")
226
+
227
+ if args.choices_blacklist is not None and (
228
+ args.choices_whitelist is not None or args.input_whitelist is not None):
229
+ parser.error("--choices-whitelist and --choices-blacklist/--input-blacklist cannot be used together")
230
+
231
+ if args.logic_whitelist is not None and (args.logic_blacklist is not None or args.constraints_blacklist is not None or args.assignments_blacklist is not None):
232
+ parser.error("--logic-whitelist and --logic-blacklist/--constraints-blacklist/--assignments-blacklist cannot be used together")
233
+
234
+ if args.logic_blacklist is not None and (args.logic_whitelist is not None or args.constraints_whitelist is not None or args.assignments_whitelist is not None):
235
+ parser.error("--logic-whitelist and --logic-blacklist/--constraints-blacklist/--assignments-blacklist cannot be used together")
236
+
237
+ if args.constraints_whitelist is not None and (args.constraints_blacklist is not None or args.logic_blacklist is not None):
238
+ parser.error("--constraints-whitelist and --constraints-blacklist/--logic-blacklist cannot be used together")
239
+
240
+ if args.constraints_blacklist is not None and (args.constraints_whitelist is not None or args.logic_whitelist is not None):
241
+ parser.error("--constraints-whitelist and --constraints-blacklist/--logic-blacklist cannot be used together")
242
+
243
+ if args.assignments_whitelist is not None and (args.assignments_blacklist is not None or args.logic_blacklist is not None):
244
+ parser.error("--assignments-whitelist and --assignments-blacklist/--logic-blacklist cannot be used together")
245
+
246
+ if args.assignments_blacklist is not None and (args.assignments_whitelist is not None or args.logic_whitelist is not None):
247
+ parser.error("--assignments-whitelist and --assignments-blacklist/--logic-blacklist cannot be used together")
248
+
249
+ if args.ignore_constraints and (args.constraints_whitelist is not None or args.constraints_blacklist is not None):
250
+ parser.error("--ignore-constraints and --constraints-whitelist/--constraints-blacklist cannot be used together")
251
+
252
+ if args.ignore_assignments and (args.assignments_whitelist is not None or args.assignments_blacklist is not None):
253
+ parser.error("--ignore-assignments and --assignments-whitelist/--assignments-blacklist cannot be used together")
254
+
255
+ # Check if whitelist/blacklist is used with other whitelists/blacklists
256
+ if (args.whitelist is not None or args.blacklist is not None) and \
257
+ (args.input_whitelist is not None or
258
+ args.parameters_whitelist is not None or
259
+ args.choices_whitelist is not None or
260
+ args.logic_whitelist is not None or
261
+ args.constraints_whitelist is not None or
262
+ args.assignments_whitelist is not None or
263
+ args.ignore_constraints or
264
+ args.ignore_assignments or
265
+ args.input_blacklist is not None or
266
+ args.parameters_blacklist is not None or
267
+ args.choices_blacklist is not None or
268
+ args.logic_blacklist is not None or
269
+ args.constraints_blacklist is not None or
270
+ args.assignments_blacklist is not None):
271
+
272
+ parser.error("--whitelist/--blacklist cannot be used with other whitelists or blacklists")
273
+
274
+ if (args.exit_on_error or args.no_error_formatting or args.no_error_messages or args.duplicate_headrow) and ('validate_tests' not in args):
275
+ parser.error("--exit-on-error, --no-error-formatting, --no-error-messages, --duplicate-headrow can only be used with --validate-tests")
276
+ if args.no_headrow and args.duplicate_headrow:
277
+ parser.error("--no-headrow and --duplicate-headrow cannot be used together")
278
+
279
+ return args
280
+
281
+ def validate_tests(function, solver, args):
282
+ test_file = args.validate_tests
283
+
284
+ def validate(in_stream):
285
+ return validator.validate_test_stream(in_stream, function, solver,
286
+ separator=args.separator,
287
+ no_headrow=args.no_headrow,
288
+ use_choice_names=args.use_choice_names,
289
+ exit_on_error=args.exit_on_error,
290
+ no_error_formatting=args.no_error_formatting,
291
+ no_error_messages=args.no_error_messages,
292
+ duplicate_headrow=args.duplicate_headrow)
293
+
294
+ if test_file == None:
295
+ result = validate(sys.stdin)
296
+ else:
297
+ try:
298
+ with open(test_file, 'r', encoding='utf-8') as f:
299
+ result = validate(f)
300
+ except OSError as e:
301
+ print(f'Error: unable to open file {test_file}: {e}', file=sys.stderr)
302
+ sys.exit(int(ec.TEST_FILE_NOT_FOUND))
303
+ return result
304
+
305
+ def main():
306
+ args = parse_args()
307
+
308
+ function_name = args.function
309
+
310
+ whitelist = []
311
+ blacklist = []
312
+ parameters_blacklist = []
313
+ parameters_whitelist = []
314
+ choices_blacklist = []
315
+ choices_whitelist = []
316
+ constraints_blacklist = []
317
+ constraints_whitelist = []
318
+ assignments_blacklist = []
319
+ assignments_whitelist = []
320
+
321
+ # Parse filter lists
322
+ def parse_filter_list(list_str):
323
+ return [l.strip() for l in list_str.split(',')]
324
+
325
+ if args.whitelist is not None:
326
+ whitelist = parse_filter_list(args.whitelist)
327
+ if args.blacklist is not None:
328
+ blacklist = parse_filter_list(args.blacklist)
329
+ if args.input_whitelist is not None:
330
+ parameters_whitelist.extend(parse_filter_list(args.input_whitelist))
331
+ choices_whitelist.extend(parse_filter_list(args.input_whitelist))
332
+ if args.input_blacklist is not None:
333
+ parameters_blacklist.extend(parse_filter_list(args.input_blacklist))
334
+ choices_blacklist.extend(parse_filter_list(args.input_blacklist))
335
+ if args.parameters_whitelist is not None:
336
+ parameters_whitelist.extend(parse_filter_list(args.parameters_whitelist))
337
+ if args.parameters_blacklist is not None:
338
+ parameters_blacklist.extend(parse_filter_list(args.parameters_blacklist))
339
+ if args.choices_whitelist is not None:
340
+ choices_whitelist.extend(parse_filter_list(args.choices_whitelist))
341
+ if args.choices_blacklist is not None:
342
+ choices_blacklist.extend(parse_filter_list(args.choices_blacklist))
343
+ if args.logic_whitelist is not None:
344
+ constraints_whitelist.extend(parse_filter_list(args.logic_whitelist))
345
+ assignments_whitelist.extend(parse_filter_list(args.logic_whitelist))
346
+ if args.logic_blacklist is not None:
347
+ constraints_blacklist.extend(parse_filter_list(args.logic_blacklist))
348
+ assignments_blacklist.extend(parse_filter_list(args.logic_blacklist))
349
+ if args.constraints_whitelist is not None:
350
+ constraints_whitelist.extend(parse_filter_list(args.constraints_whitelist))
351
+ if args.constraints_blacklist is not None:
352
+ constraints_blacklist.extend(parse_filter_list(args.constraints_blacklist))
353
+ if args.assignments_whitelist is not None:
354
+ assignments_whitelist.extend(parse_filter_list(args.assignments_whitelist))
355
+ if args.assignments_blacklist is not None:
356
+ assignments_blacklist.extend(parse_filter_list(args.assignments_blacklist))
357
+
358
+ input_file = args.input_file
359
+
360
+ parsed_yaml_model = None
361
+ if input_file is not None:
362
+ try:
363
+ with open(input_file, 'r', encoding='utf-8') as f:
364
+ parsed_yaml_model = yaml.safe_load(f.read())
365
+ except OSError as e:
366
+ print(f'Error: unable to open file {input_file}: {e}', file=sys.stderr)
367
+ sys.exit(int(ec.MODEL_FILE_NOT_FOUND))
368
+ else:
369
+ try:
370
+ yaml_lines = []
371
+ #read from stdin until the end of input, or a line that starts with '----'
372
+ for line in sys.stdin:
373
+ if line.startswith('----'):
374
+ break
375
+ yaml_lines.append(line)
376
+ yaml_content = ''.join(yaml_lines)
377
+ parsed_yaml_model = yaml.safe_load(StringIO(yaml_content))
378
+ except yaml.YAMLError as e:
379
+ print(f"YAML parsing error: {e}", file=sys.stderr)
380
+ sys.exit(int(ec.YAML_PARSING_ERROR))
381
+
382
+ # Parse the model
383
+ function = model.parse_function(parsed_yaml_model, function_name,
384
+ ignore_constraints=args.ignore_constraints,
385
+ ignore_assignments=args.ignore_assignments,
386
+ whitelist=whitelist,
387
+ blacklist=blacklist,
388
+ parameters_whitelist=parameters_whitelist,
389
+ parameters_blacklist=parameters_blacklist,
390
+ choices_whitelist=choices_whitelist,
391
+ choices_blacklist=choices_blacklist,
392
+ constraints_whitelist=constraints_whitelist,
393
+ constraints_blacklist=constraints_blacklist,
394
+ assignments_whitelist=assignments_whitelist,
395
+ assignments_blacklist=assignments_blacklist)
396
+
397
+ # set the solver
398
+ solver = TomatoSolver(function,
399
+ negate_constraints=args.negate_constraints,
400
+ invert_constraints=args.invert_constraints)
401
+
402
+ if 'validate_tests' in args:
403
+ result = validate_tests(function, solver, args)
404
+ if not result:
405
+ sys.exit(int(ec.TEST_PARSING_ERROR))
406
+ else:
407
+ sys.exit(int(ec.SUCCESS))
408
+
409
+ # select the nwise generator
410
+ if args.nwise:
411
+ n = args.n
412
+ if n is None:
413
+ n = 2
414
+ if args.coverage is None:
415
+ args.coverage = 100
416
+ coverage = int(args.coverage)
417
+
418
+ gen = generator.nwise(function, solver, n, coverage=coverage)
419
+
420
+ # select the cartesian generator
421
+ if args.cartesian:
422
+ gen = generator.cartesian(function, solver)
423
+
424
+ # select the random generator
425
+ if args.random:
426
+ length = args.length
427
+ duplicates = args.duplicates
428
+ adaptive = args.adaptive
429
+ if length is None:
430
+ length = 0
431
+ if duplicates is None:
432
+ duplicates = False
433
+ if adaptive is None:
434
+ adaptive = False
435
+
436
+ gen = generator.random(function,
437
+ solver,
438
+ length=length,
439
+ duplicates=duplicates,
440
+ adaptive=adaptive)
441
+
442
+ sep = args.separator
443
+
444
+ # Set demo level
445
+ if args.demo_level > 0:
446
+ generator.show_progress = True
447
+ if args.demo_level == 2:
448
+ generator.step_delay = 0.05
449
+ if args.demo_level == 3:
450
+ generator.step_delay = 0.2
451
+
452
+ writer = csv.writer(sys.stdout, delimiter=sep)
453
+
454
+ if not args.no_headrow:
455
+ writer.writerow(function.get_parameter_names())
456
+ #generate test cases with the selected generator
457
+ for test in gen:
458
+ if not args.use_choice_names:
459
+ parameter_names = function.get_parameter_names()
460
+ for i, name in enumerate(parameter_names):
461
+ if name in [p.name for p in function.output_parameters]:
462
+ continue
463
+ parameter = function.get_parameter(name)
464
+ choice_name = test[i]
465
+ choice = parameter.get_choice(choice_name)
466
+ try:
467
+ test[i] = choice.value
468
+ except AttributeError:
469
+ raise ValueError(f"Choice '{choice_name}' not found in parameter {name}")
470
+ writer.writerow(test)
471
+
472
+ if __name__ == '__main__':
473
+ main()