15
15
16
16
import copy
17
17
import logging
18
+ import re
18
19
from dataclasses import dataclass , field
19
20
from enum import Enum , auto
20
21
from typing import Optional
21
22
22
23
from . import fixes
23
24
from .constraints import get_constraints , is_typed_constraint
24
25
from .definitions import SpecDefinitions
25
- from .errors import TestStepError , TestStepKeyError , TestStepValueNameError
26
+ from .errors import (TestStepEnumError , TestStepEnumSpecifierNotUnknownError , TestStepEnumSpecifierWrongError , TestStepError ,
27
+ TestStepKeyError , TestStepValueNameError )
26
28
from .pics_checker import PICSChecker
27
29
from .yaml_loader import YamlLoader
28
30
38
40
'SubscribeAll' ,
39
41
]
40
42
43
+ # If True, enum values should use a valid name instead of a raw value
44
+ STRICT_ENUM_VALUE_CHECK = False
45
+
41
46
42
47
class UnknownPathQualifierError (TestStepError ):
43
48
"""Raise when an attribute/command/event name is not found in the definitions."""
@@ -169,6 +174,99 @@ def _value_or_config(data, key, config):
169
174
return data [key ] if key in data else config .get (key )
170
175
171
176
177
+ class EnumType :
178
+ def __init__ (self , enum : Enum ):
179
+ self .type = enum .name
180
+ self .base_type = enum .base_type
181
+
182
+ self ._codes = {}
183
+ self .entries_by_name = {}
184
+ self .entries_by_code = {}
185
+ self ._compute_entries (enum )
186
+
187
+ def translate (self , key : str , value ) -> int :
188
+ if self ._codes .get (key ) is not None and self ._codes .get (key ) == value :
189
+ return self ._codes .get (key )
190
+
191
+ if type (value ) is str :
192
+ code = self ._get_code_by_name (value )
193
+ else :
194
+ code = self ._get_code_by_value (value )
195
+
196
+ if code is None :
197
+ raise TestStepEnumError (value , self .entries_by_name )
198
+
199
+ self ._codes [key ] = code
200
+ return code
201
+
202
+ def _get_code_by_name (self , value ):
203
+ # For readability the name could sometimes be written as "enum_name(enum_code)" instead of "enum_name"
204
+ # In this case the enum_code should be checked to ensure that it is correct, unless enum_name is UnknownEnumValue
205
+ # in which case only invalid enum_code are allowed.
206
+ specified_name , specified_code = self ._extract_name_and_code (value )
207
+ if specified_name not in self .entries_by_name :
208
+ return None
209
+
210
+ enum_code = self .entries_by_name .get (specified_name )
211
+ if specified_code is None or specified_code == enum_code :
212
+ return enum_code
213
+
214
+ if specified_name != f'{ self .type } .UnknownEnumValue' :
215
+ raise TestStepEnumSpecifierWrongError (
216
+ specified_code , specified_name , enum_code )
217
+
218
+ enum_name = self .entries_by_code .get (specified_code )
219
+ if enum_name :
220
+ raise TestStepEnumSpecifierNotUnknownError (value , enum_name )
221
+
222
+ return specified_code
223
+
224
+ def _get_code_by_value (self , value ):
225
+ enum_name = self .entries_by_code .get (value )
226
+ if not enum_name :
227
+ return None
228
+
229
+ if STRICT_ENUM_VALUE_CHECK :
230
+ raise TestStepEnumError (value , self .entries_by_name )
231
+
232
+ return value
233
+
234
+ def _compute_entries (self , enum : Enum ):
235
+ enum_codes = []
236
+ for enum_entry in enum .entries :
237
+ name = f'{ self .type } .{ enum_entry .name } '
238
+ code = enum_entry .code
239
+
240
+ self .entries_by_name [name ] = code
241
+ self .entries_by_code [code ] = name
242
+ enum_codes .append (code )
243
+
244
+ # search for the first invalid entry if any
245
+ max_code = 0xFF + 1
246
+ if self .base_type == 'enum16' :
247
+ max_code = 0xFFFF + 1
248
+
249
+ for code in range (0 , max_code ):
250
+ if code not in enum_codes :
251
+ name = f'{ self .type } .UnknownEnumValue'
252
+ self .entries_by_name [name ] = code
253
+ self .entries_by_code [code ] = name
254
+ break
255
+
256
+ def _extract_name_and_code (self , enum_name : str ):
257
+ match = re .match (r"([\w.]+)(?:\((\w+)\))?" , enum_name )
258
+ if match :
259
+ name = match .group (1 )
260
+ code = int (match .group (2 )) if match .group (2 ) else None
261
+ return name , code
262
+
263
+ return None , None
264
+
265
+ @staticmethod
266
+ def is_valid_type (target_type : str ):
267
+ return target_type == 'enum8' or target_type == 'enum16'
268
+
269
+
172
270
class _TestStepWithPlaceholders :
173
271
'''A single YAML test parsed, as is, from YAML.
174
272
@@ -441,7 +539,11 @@ def _as_mapping(self, definitions, cluster_name, target_name):
441
539
element = definitions .get_type_by_name (cluster_name , target_name )
442
540
443
541
if hasattr (element , 'base_type' ):
444
- target_name = element .base_type .lower ()
542
+ if EnumType .is_valid_type (element .base_type ):
543
+ target_name = EnumType (element )
544
+ else :
545
+ target_name = element .base_type
546
+
445
547
elif hasattr (element , 'fields' ):
446
548
target_name = {f .name : self ._as_mapping (
447
549
definitions , cluster_name , f .data_type .name ) for f in element .fields }
@@ -480,7 +582,11 @@ def _update_with_definition(self, container: dict, mapping_type):
480
582
481
583
if key == 'value' :
482
584
value [key ] = self ._update_value_with_definition (
483
- item_value , mapping )
585
+ value ,
586
+ key ,
587
+ item_value ,
588
+ mapping
589
+ )
484
590
elif key == 'saveAs' and type (item_value ) is str and item_value not in self ._parsing_config_variable_storage :
485
591
self ._parsing_config_variable_storage [item_value ] = None
486
592
elif key == 'saveDataVersionAs' and type (item_value ) is str and item_value not in self ._parsing_config_variable_storage :
@@ -491,37 +597,80 @@ def _update_with_definition(self, container: dict, mapping_type):
491
597
# the the value type for the target field.
492
598
if is_typed_constraint (constraint ):
493
599
value [key ][constraint ] = self ._update_value_with_definition (
494
- constraint_value , mapping )
600
+ item_value ,
601
+ constraint ,
602
+ constraint_value ,
603
+ mapping
604
+ )
495
605
else :
496
606
# This key, value pair does not rely on cluster specifications.
497
607
pass
498
608
499
- def _update_value_with_definition (self , value , mapping_type ):
609
+ def _update_value_with_definition (self , container : dict , key : str , value , mapping_type ):
610
+ """
611
+ Processes a given value based on a specified mapping type and returns the updated value.
612
+ This method does not modify the container in place; rather, it returns a new value that should be
613
+ used to update or process further as necessary.
614
+
615
+ The 'container' and 'key' parameters are primarily used for error tagging. If an error occurs
616
+ during the value processing, these parameters allow for the error to be precisely located and
617
+ reported, facilitating easier debugging and error tracking.
618
+
619
+ Parameters:
620
+ - container (dict): A dictionary that serves as a context for the operation. It is used for error
621
+ tagging if processing fails, by associating errors with specific locations within the data structure.
622
+ - key (str): The key related to the value being processed. It is used alongside 'container' to tag
623
+ errors, enabling precise identification of the error source.
624
+ - value: The value to be processed according to the mapping type.
625
+ - mapping_type: Dictates the processing or mapping logic to be applied to 'value'.
626
+
627
+ Returns:
628
+ The processed value, which is the result of applying the specified mapping type to the original 'value'.
629
+ This method does not update the 'container'; any necessary updates based on the processed value must
630
+ be handled outside this method.
631
+
632
+ Raises:
633
+ - TestStepError: If an error occurs during the processing of the value. The error includes details
634
+ from the 'container' and 'key' to facilitate error tracing and debugging.
635
+ """
636
+
500
637
if not mapping_type :
501
638
return value
502
639
503
640
if type (value ) is dict :
504
641
rv = {}
505
- for key in value :
642
+ for item_key in value :
506
643
# FabricIndex is a special case where the framework requires it to be passed even
507
644
# if it is not part of the requested arguments per spec and not part of the XML
508
645
# definition.
509
- if key == 'FabricIndex' or key == 'fabricIndex' :
510
- rv [key ] = value [key ] # int64u
646
+ if item_key == 'FabricIndex' or item_key == 'fabricIndex' :
647
+ rv [item_key ] = value [item_key ] # int64u
511
648
else :
512
- if not mapping_type .get (key ):
513
- raise TestStepKeyError (value , key )
514
- mapping = mapping_type [key ]
515
- rv [key ] = self ._update_value_with_definition (
516
- value [key ], mapping )
649
+ if not mapping_type .get (item_key ):
650
+ raise TestStepKeyError (value , item_key )
651
+ mapping = mapping_type [item_key ]
652
+ rv [item_key ] = self ._update_value_with_definition (
653
+ value ,
654
+ item_key ,
655
+ value [item_key ],
656
+ mapping
657
+ )
517
658
return rv
659
+
518
660
if type (value ) is list :
519
- return [self ._update_value_with_definition (entry , mapping_type ) for entry in value ]
661
+ return [self ._update_value_with_definition (container , key , entry , mapping_type ) for entry in value ]
662
+
520
663
# TODO currently unsure if the check of `value not in config` is sufficant. For
521
664
# example let's say value = 'foo + 1' and map type is 'int64u', we would arguably do
522
665
# the wrong thing below.
523
666
if value is not None and value not in self ._parsing_config_variable_storage :
524
- if mapping_type == 'int64u' or mapping_type == 'int64s' or mapping_type == 'bitmap64' or mapping_type == 'epoch_us' :
667
+ if type (mapping_type ) is EnumType :
668
+ try :
669
+ value = mapping_type .translate (key , value )
670
+ except (TestStepEnumError , TestStepEnumSpecifierNotUnknownError , TestStepEnumSpecifierWrongError ) as e :
671
+ e .tag_key_with_error (container , key )
672
+ raise e
673
+ elif mapping_type == 'int64u' or mapping_type == 'int64s' or mapping_type == 'bitmap64' or mapping_type == 'epoch_us' :
525
674
value = fixes .try_apply_float_to_integer_fix (value )
526
675
value = fixes .try_apply_yaml_cpp_longlong_limitation_fix (value )
527
676
value = fixes .try_apply_yaml_unrepresentable_integer_for_javascript_fixes (
0 commit comments