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/model.py ADDED
@@ -0,0 +1,605 @@
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 copy import deepcopy
25
+ import sys
26
+
27
+ ignore_constraints = False
28
+ ignore_assignments = False
29
+ global_whitelist = []
30
+ global_blacklist = []
31
+ parameters_blacklist = []
32
+ parameters_whitelist = []
33
+ choices_blacklist = []
34
+ choices_whitelist = []
35
+ constraints_blacklist = []
36
+ constraints_whitelist = []
37
+ assignments_blacklist = []
38
+ assignments_whitelist = []
39
+
40
+ def should_parse_element(description, specific_whitelist=[], specific_blacklist=[], extra_labels=[]):
41
+ """
42
+ Check if an element described by 'description' should be parsed based on global and local whitelists and blacklists,
43
+ as well as other conditions.
44
+
45
+ Args:
46
+ description (dict): The description of the element
47
+ specific_whitelist (list): The list of labels that are allowed
48
+ specific_blacklist (list): The list of labels that are not allowed
49
+ extra_labels (list): The list of labels that are inherited from the parent
50
+
51
+ Returns:
52
+ bool: True if the element should be parsed, False otherwise.
53
+
54
+ Examples:
55
+ >>> should_parse_element('choice')
56
+ True
57
+ >>> should_parse_element('constraint', ignore_constraints=True)
58
+ False
59
+ """
60
+ #if description is a primitive type, return true and allow parsing
61
+ if not isinstance(description, dict):
62
+ return True
63
+
64
+ #get the keys of the dictionary and set the element type to the first key
65
+ element_type = list(description.keys())[0]
66
+ if element_type == 'constraint' and ignore_constraints:
67
+ return False
68
+ if element_type == 'assignment' and ignore_assignments:
69
+ return False
70
+
71
+ name = description[element_type]
72
+ labels = [] + extra_labels
73
+ #if labels are in the description, set the labels list to the labels in the description
74
+ if 'labels' in description:
75
+ labels = description['labels']
76
+ labels.append(name)
77
+
78
+ #set the blacklist to the union of the global_blacklist and the specific_blacklist
79
+ blacklist = list(set(global_blacklist) | set(specific_blacklist))
80
+ #set the whitelist to the union of the global_whitelist and the specific_whitelis
81
+ whitelist = list(set(global_whitelist) | set(specific_whitelist))
82
+
83
+ #if the whitelist is not empty, check if any of the labels in 'labels' are in the whitelist, if not return false
84
+ if len(whitelist) > 0:
85
+ if len([label for label in labels if label in whitelist]) == 0:
86
+ return False
87
+ #if the blacklist is not empty, check if any of the labels in 'labels' are in the blacklist, if so return false
88
+ if len(blacklist) > 0:
89
+ if len([label for label in labels if label in blacklist]) > 0:
90
+ return False
91
+
92
+ return True
93
+
94
+ class ParameterParent:
95
+ """ Parent class for parameters, which includes all methods for handling parameters in the model"""
96
+ def __init__(self, description, global_params, inherited_labels=[]) -> None:
97
+ """
98
+ Initialize a Parameter object.
99
+
100
+ Args:
101
+ description (dict): The description of the parameter.
102
+ global_params (dict): The global parameters.
103
+ inherited_labels (list): The list of labels that are inherited from the parent. Defaults to an empty list.
104
+
105
+ Raises:
106
+ Exception: If the parameter has both 'parameters' and 'choices' keys. This is not allowed.
107
+
108
+ Attributes:
109
+ name (str): The name of the parameter.
110
+ choices (list): A list of sub-choices, each being represented as a choice object.
111
+ parameters (list): A list of sub-parameters, each being represented as a parameter object.
112
+ aliases (dict): A dictionary of aliases.
113
+ constraints (dict): A dictionary of constraints.
114
+
115
+ Returns:
116
+ None
117
+ """
118
+ if 'parameter' in description:
119
+ self.name = str(description['parameter'])
120
+ elif 'output parameter' in description:
121
+ self.name = str(description['output parameter'])
122
+
123
+ self.parameters = []
124
+ self.output_parameters = []
125
+ #if description is a yaml node, set the name to the value of the 'parameter' key
126
+ if isinstance(description, dict):
127
+ #if description has 'parameters' key, set labels to the inherited labels and the labels in the description
128
+ if 'parameters' in description:
129
+ labels = [] + inherited_labels
130
+ if 'labels' in description:
131
+ labels += description['labels']
132
+ #iterate through the parameters in the description and create parameter objects for each
133
+ for parameter in description['parameters']:
134
+ if not should_parse_element(parameter, parameters_whitelist, parameters_blacklist, labels):
135
+ continue
136
+
137
+ #if description has 'output parameter'
138
+ if 'output parameter' in parameter:
139
+ param = OutputParameter(parameter)
140
+ self.parameters.append(param)
141
+ self.output_parameters.append(param)
142
+
143
+ #if description has 'linked parameter' key, copies and modifies parameter before appending to the parameters list
144
+ if 'linked parameter' in parameter:
145
+ linked_to = parameter['linked to']
146
+ name = parameter['linked parameter']
147
+ whitelist = parameter['constraints whitelist'] if 'constraints whitelist' in parameter else None
148
+ blacklist = parameter['constraints blacklist'] if 'constraints blacklist' in parameter else None
149
+ if whitelist is not None and blacklist is not None:
150
+ raise Exception(f'Both constraints whitelist and blacklist are defined for linked parameter {parameter.name}')
151
+ copy = deepcopy(global_params[linked_to])
152
+ copy.name = name
153
+ copy.filter_constraints(whitelist, blacklist)
154
+ self.parameters.append(copy)
155
+ else:
156
+ self.parameters.append(Parameter(parameter, global_params, labels))
157
+
158
+ def get_leaf_choice_names(self):
159
+ # Get all leaf parameters with full path
160
+ parameter_names = self.get_parameter_names()
161
+ result = []
162
+
163
+ for name in parameter_names:
164
+ if name is None:
165
+ continue
166
+
167
+ p = self.get_parameter(name)
168
+ if not p.parameters:
169
+ # If the parameter has no sub-parameters, return its own choices
170
+ result.append(p.get_choice_names()) # Append the list of choices as a group
171
+ else:
172
+ # If the parameter has sub-parameters, recursively get their choices
173
+ result.append(p.get_leaf_choice_names())
174
+
175
+ result = [r for r in result if r]
176
+
177
+ return result
178
+
179
+ def get_parameter_names(self, current_path=''):
180
+ result = []
181
+ for parameter in self.parameters:
182
+ if isinstance(parameter, OutputParameter):
183
+ continue
184
+ if parameter.parameters:
185
+ if current_path == '':
186
+ result.extend(parameter.get_parameter_names(parameter.name))
187
+ else:
188
+ result.extend(parameter.get_parameter_names(current_path + '::' + parameter.name))
189
+ else:
190
+ if(current_path == ''):
191
+ result.append(parameter.name)
192
+ else:
193
+ result.append(current_path + '::' + parameter.name)
194
+
195
+ return result
196
+
197
+
198
+ def get_parameter(self, name):
199
+ #print("get_parameter is being called in the parent class")
200
+ #if the name contains '::', split the name into tokens
201
+ if name is None:
202
+ return None
203
+
204
+ if '::' in name:
205
+ tokens = name.split('::')
206
+ child_name = tokens[0]
207
+ remains = '::'.join(tokens[1:])
208
+ #iterate through the parameters and recursively call get_parameter to get each parameter
209
+ for parameter in self.parameters:
210
+ if parameter.name == child_name:
211
+ return parameter.get_parameter(remains)
212
+ else:
213
+ #iterate through the parameters and return the parameter with the given name
214
+ for parameter in self.parameters:
215
+ if parameter.name == name:
216
+ return parameter
217
+ return None
218
+
219
+
220
+ def get_parameter_index(self, name):
221
+ """ Get the index of a parameter by name."""
222
+ for i, parameter in enumerate(self.get_parameter_names()):
223
+ if parameter == name:
224
+ return i
225
+ return None
226
+
227
+ class ConstraintParent(ParameterParent):
228
+ def __init__(self, description) -> None:
229
+ self.aliases = {}
230
+ self.constraints = {}
231
+ if 'logic' in description:
232
+ logic = description['logic']
233
+
234
+ for element in logic:
235
+ #if element is an alias, set the name and expression of the alias
236
+ if 'alias' in element:
237
+ name = str(element['alias'])
238
+ expression = str(element['expression'])
239
+ if name in self.aliases:
240
+ raise Exception(f"Alias '{name}' already defined")
241
+ self.aliases[name] = expression
242
+
243
+ #if the element is a constraint, check if the element should be parsed,
244
+ # then set the name and expression of the constraint
245
+ if 'constraint' in element:
246
+ if not should_parse_element(element, constraints_whitelist, constraints_blacklist):
247
+ continue
248
+ name = str(element['constraint'])
249
+ expression = str(element['expression'])
250
+ if name not in self.constraints:
251
+ self.constraints[name] = []
252
+ self.constraints[name].append((None, expression))
253
+
254
+ #add potential constraints from structures
255
+ # for parameter in [p for p in self.parameters if p.is_structure()]:
256
+ # context = parameter.name
257
+ # for context, name, expression in parameter.get_constraints():
258
+ # if name not in self.constraints:
259
+ # self.constraints[name] = []
260
+ # self.constraints[name].append((context, expression))
261
+
262
+ def filter_constraints(self, whitelist, blacklist):
263
+ if whitelist is not None and blacklist is not None:
264
+ raise Exception(f'Both constraints whitelist and blacklist are defined for parameter {self.name}')
265
+ if whitelist is not None:
266
+ self.constraints = {k: v for k, v in self.constraints.items() if k in whitelist}
267
+ if blacklist is not None:
268
+ self.constraints = {k: v for k, v in self.constraints.items() if k not in blacklist}
269
+
270
+ for parameter in self.parameters:
271
+ parameter.filter_constraints(whitelist, blacklist)
272
+
273
+ class ChoiceParent:
274
+ """ Parent class for choices, which includes all methods for handling choices in the model"""
275
+ def __init__(self, description, inherited_labels=[]) -> None:
276
+ self.choices = []
277
+
278
+ #description is a directory created from parsing the yaml file
279
+ if isinstance(description, dict):
280
+ if 'choices' in description:
281
+ labels = inherited_labels
282
+ if 'labels' in description:
283
+ labels += description['labels']
284
+ self.choices = [Choice(choice, labels) for choice in description['choices'] if should_parse_element(choice, choices_whitelist, choices_blacklist, labels)]
285
+
286
+
287
+ def get_choice_names(self, current_path=''):
288
+ result = []
289
+ for choice in self.choices:
290
+ if choice.choices:
291
+ if current_path == '':
292
+ result.extend(choice.get_choice_names(choice.name))
293
+ else:
294
+ result.extend(choice.get_choice_names(current_path + '::' + choice.name))
295
+ else:
296
+ if(current_path == ''):
297
+ result.append(choice.name)
298
+ else:
299
+ result.append(current_path + '::' + choice.name)
300
+
301
+ return result
302
+
303
+ def get_choice(self, name):
304
+ """
305
+ Get a choice by name.
306
+
307
+ Args:
308
+ name (str): The name of the choice to get.
309
+
310
+ Returns:
311
+ choice: The choice object with the given name.
312
+ or None if the choice is not found.
313
+ """
314
+ #if the name contains '::', split the name into tokens
315
+ if '::' in name:
316
+ tokens = name.split('::')
317
+ child_name = tokens[0]
318
+ #set the remains to the concatenation of the tokens from index 1, separated by '::'
319
+ remains = '::'.join(tokens[1:])
320
+
321
+ #for each choice in the choices list, if the name of the choice is equal to the child_name,
322
+ #recursively call get_choice with the remains
323
+ for choice in self.choices:
324
+ if choice.name == child_name:
325
+ return choice.get_choice(remains)
326
+ else:
327
+ #for each choice in the choices list, if the name of the choice
328
+ #is equal to the name, return the choice
329
+ for choice in self.choices:
330
+ if choice.name == name:
331
+ return choice
332
+ return None
333
+
334
+ class Choice(ChoiceParent):
335
+ def __init__(self, description, global_params, inherited_labels=[]):
336
+ self.name = ''
337
+ self.value = ''
338
+
339
+ #description is a yaml node
340
+ if isinstance(description, dict):
341
+ children = list(description.keys())
342
+ if 'choice' in children:
343
+ self.name = str(description['choice'])
344
+ if 'value' in description:
345
+ self.value = str(description['value'])
346
+ if 'choices' in description:
347
+ raise Exception(f"Choice '{self.name}' has both value and choices")
348
+ else:
349
+ self.value = self.name
350
+ else:
351
+ self.value = self.name
352
+ else:
353
+ self.name = str(description)
354
+ self.value = str(description)
355
+
356
+
357
+ super().__init__(description, inherited_labels)
358
+
359
+
360
+ def __str__(self):
361
+ return f'{self.name}: {self.value}'
362
+
363
+ def __repr__(self):
364
+ return str(self)
365
+
366
+
367
+ class OutputParameter(ParameterParent):
368
+ """
369
+ A class used to represent an output parameter in a model.
370
+
371
+ Output parameters do not have choices which are used in combinatoric test generation.
372
+ However, their value can be defined based on a precondition defined in assignments
373
+
374
+ Attributes:
375
+ name (str): The name of the output parameter.
376
+ default_value (str): The default value linked to the output parameter.
377
+ parameters (list): The list of parameters that are children of the current output parameter.
378
+
379
+ Methods:
380
+ get_choice_names: returns a default_value which has been predefined.
381
+ """
382
+ def __init__(self, description):
383
+ """
384
+ Initialize an OutputParameter object.
385
+
386
+ Args:
387
+ description (dict): The description of the output parameter.
388
+
389
+ Attributes:
390
+ name (str): The name of the output parameter.
391
+ default_value (str): The default value linked to the output parameter.
392
+ parameters (list): A list of sub-parameters, each being represented as a parameter object.
393
+
394
+ Returns:
395
+ None
396
+ """
397
+ self.name = str(description['output parameter'])
398
+ self.default_value = str(description['default value'])
399
+ self.parameters = []
400
+
401
+ def __str__(self):
402
+ return f'{self.name}: {self.default_value}'
403
+
404
+ def __repr__(self):
405
+ return str(self)
406
+
407
+ def get_choice_names(self):
408
+ return [str(self.default_value)]
409
+
410
+ def is_structure(self):
411
+ return False
412
+
413
+ class Parameter(ConstraintParent, ChoiceParent):
414
+ def __init__(self, description, global_params, inherited_labels=[]) -> None:
415
+ """
416
+ Initialize a Parameter object.
417
+
418
+ Args:
419
+ description (dict): The description of the parameter.
420
+ global_params (dict): The global parameters.
421
+ inherited_labels (list): The list of labels that are inherited from the parent. Defaults to an empty list.
422
+
423
+ Raises:
424
+ Exception: If the parameter has both 'parameters' and 'choices' keys. This is not allowed.
425
+
426
+ Attributes:
427
+ name (str): The name of the parameter.
428
+ choices (list): A list of sub-choices, each being represented as a choice object.
429
+ parameters (list): A list of sub-parameters, each being represented as a parameter object.
430
+ aliases (dict): A dictionary of aliases.
431
+ constraints (dict): A dictionary of constraints.
432
+
433
+ Returns:
434
+ None
435
+ """
436
+ ChoiceParent.__init__(self, description, inherited_labels)
437
+ ParameterParent.__init__(self, description, global_params, inherited_labels)
438
+ ConstraintParent.__init__(self, description)
439
+
440
+ if isinstance(description, dict):
441
+ #if the description has both 'parameters and 'choices' keys, raise an exception and exit
442
+ if 'parameters' in description and 'choices' in description:
443
+ raise Exception(f'Error: {self.name1} has both parameters and choices')
444
+
445
+ def __str__(self):
446
+ result = f'{self.name}'
447
+ if self.parameters:
448
+ result += '('
449
+ for parameter in self.parameters:
450
+ result += f'{parameter}, '
451
+ result = result[:-2] + ')'
452
+ return result
453
+
454
+ def __repr__(self):
455
+ return str(self)
456
+
457
+
458
+ def get_structures(self):
459
+ return [p for p in self.parameters if p.is_structure()]
460
+
461
+
462
+ def is_structure(self):
463
+ return len(self.parameters) > 0
464
+
465
+
466
+ def get_constraints(self):
467
+ result = []
468
+ for name, expressions in self.constraints.items():
469
+ for expression in expressions:
470
+ result.append((self.name, name, expression))
471
+
472
+ if self.parameters:
473
+ for parameter in self.parameters:
474
+ for context, name, expression in parameter.get_constraints():
475
+ result.append((self.name + '::' + context, name, expression))
476
+
477
+ return result
478
+
479
+ def get_aliases(self):
480
+ result = []
481
+ for name, expression in self.aliases.items():
482
+ result.append((self.name, name, expression))
483
+
484
+ if self.parameters:
485
+ for parameter in self.parameters:
486
+ for context, name, expression in parameter.get_aliases():
487
+ result.append((self.name + '::' + context, name, expression))
488
+ return result
489
+
490
+ class Function(ConstraintParent, ParameterParent):
491
+ def __init__(self, global_params, description):
492
+ """
493
+ Initialize a Function object.
494
+
495
+ Args:
496
+ global_params (dict): The global parameters.
497
+ description (dict): The description of the function.
498
+
499
+ Attributes:
500
+ name (str): The name of the function.
501
+ parameters (list): A list of sub-parameters, each being represented as a parameter object.
502
+ output_parameters (list): A list of sub-output parameters, each being represented as an output parameter object.
503
+ aliases (dict): A dictionary of aliases.
504
+ constraints (dict): A dictionary of constraints.
505
+ assignments (dict): A dictionary of assignments.
506
+
507
+ Returns:
508
+ None
509
+ """
510
+ if 'function' not in description:
511
+ raise ValueError("The 'function' key is missing in the description.")
512
+
513
+ self.name = str(description['function'])
514
+ ParameterParent.__init__(self, description, global_params)
515
+ ConstraintParent.__init__(self, description)
516
+
517
+ self.assignments = {}
518
+ if 'logic' in description:
519
+ logic = description['logic']
520
+ for element in logic:
521
+ #if it is an assignment, check if it should be parsed and set name and expression
522
+ if 'assignment' in element:
523
+ if not should_parse_element(element, assignments_whitelist, assignments_blacklist):
524
+ continue
525
+ name = str(element['assignment'])
526
+ expression = str(element['expression'])
527
+ if name not in self.assignments:
528
+ self.assignments[name] = []
529
+ self.assignments[name].append(expression)
530
+
531
+ def __str__(self):
532
+ return f'{self.name}({[parameter.name for parameter in self.parameters]})'
533
+
534
+ def get_structures(self):
535
+ return [p for p in self.parameters if p.is_structure()]
536
+
537
+ def get_generator_input(self):
538
+ return self.get_parameter_names(), self.get_leaf_choice_names()
539
+
540
+ def parse_function(model, function_name=None, **kwargs):
541
+ """
542
+ Parse a function from a model file.
543
+
544
+ Args:
545
+ file (str): The path to the model file.
546
+ function_name (str): The name of the function to parse. Defaults to None.
547
+ **kwargs: Additional keyword arguments.
548
+
549
+ Returns:
550
+ function: The function object.
551
+ """
552
+ global ignore_constraints, ignore_assignments
553
+ global global_whitelist, global_blacklist
554
+ global parameters_blacklist, parameters_whitelist
555
+ global choices_blacklist, choices_whitelist
556
+ global constraints_blacklist, constraints_whitelist
557
+ global assignments_blacklist, assignments_whitelist
558
+
559
+ if 'ignore_constraints' in kwargs:
560
+ ignore_constraints = kwargs['ignore_constraints']
561
+ if 'ignore_assignments' in kwargs:
562
+ ignore_assignments = kwargs['ignore_assignments']
563
+ if 'whitelist' in kwargs:
564
+ global_whitelist = kwargs['whitelist']
565
+ if 'blacklist' in kwargs:
566
+ global_blacklist = kwargs['blacklist']
567
+ if 'parameters_blacklist' in kwargs:
568
+ parameters_blacklist = kwargs['parameters_blacklist']
569
+ if 'parameters_whitelist' in kwargs:
570
+ parameters_whitelist = kwargs['parameters_whitelist']
571
+ if 'choices_blacklist' in kwargs:
572
+ choices_blacklist = kwargs['choices_blacklist']
573
+ if 'choices_whitelist' in kwargs:
574
+ choices_whitelist = kwargs['choices_whitelist']
575
+ if 'constraints_blacklist' in kwargs:
576
+ constraints_blacklist = kwargs['constraints_blacklist']
577
+ if 'constraints_whitelist' in kwargs:
578
+ constraints_whitelist = kwargs['constraints_whitelist']
579
+ if 'assignments_blacklist' in kwargs:
580
+ assignments_blacklist = kwargs['assignments_blacklist']
581
+ if 'assignments_whitelist' in kwargs:
582
+ assignments_whitelist = kwargs['assignments_whitelist']
583
+
584
+ global_params = {}
585
+ #Makes parameter objects for global parameters if they exist
586
+ if 'global parameters' in model:
587
+ for parameter in model['global parameters']:
588
+ global_params[parameter['parameter']] = Parameter(parameter, global_params)
589
+
590
+ if 'functions' not in model:
591
+ raise Exception('No functions defined in model')
592
+ if function_name is None:
593
+ function_def = model['functions'][0]
594
+ #Get the function if the function name is provided
595
+ else:
596
+ function_defs = [function for function in model['functions'] if function['function'] == function_name]
597
+ if len(function_defs) > 1:
598
+ raise Exception(f'Multiple functions with name {function_name}')
599
+ if len(function_defs) == 0:
600
+ raise Exception(f'Function {function_name} not found in the model file')
601
+ function_def = function_defs[0]
602
+ function = Function(global_params, function_def)
603
+
604
+ return function
605
+