1818import re
1919from dataclasses import dataclass , field
2020from enum import Enum , auto
21- from typing import Optional
21+ from typing import Any , Optional
2222
2323from . import fixes
2424from .constraints import get_constraints , is_typed_constraint , is_variable_aware_constraint
4646STRICT_ENUM_VALUE_CHECK = False
4747
4848
49+ def build_revision_var_name (endpoint , cluster ) -> str :
50+ """Helper to create a unique variable name for the cluster revision read-out."""
51+ return f'__cluster_revision_read_EP_{ endpoint } _CL_{ cluster } '
52+
53+
4954class UnknownPathQualifierError (TestStepError ):
5055 """Raise when an attribute/command/event name is not found in the definitions."""
5156
@@ -301,6 +306,8 @@ def __init__(self, test: dict, config: dict, definitions: SpecDefinitions, pics_
301306
302307 self .identity = _value_or_none (test , 'identity' )
303308 self .fabric_filtered = _value_or_none (test , 'fabricFiltered' )
309+ self .min_revision = _value_or_none (test , 'minRevision' )
310+ self .max_revision = _value_or_none (test , 'maxRevision' )
304311 self .min_interval = _value_or_none (test , 'minInterval' )
305312 self .max_interval = _value_or_none (test , 'maxInterval' )
306313 self .keep_subscriptions = _value_or_none (test , 'keepSubscriptions' )
@@ -748,6 +755,10 @@ def __init__(self, test: _TestStepWithPlaceholders, step_index: int, runtime_con
748755 self ._test .group_id )
749756 self ._test .node_id = self ._config_variable_substitution (
750757 self ._test .node_id )
758+ self ._test .min_revision = self ._config_variable_substitution (
759+ self ._test .min_revision )
760+ self ._test .max_revision = self ._config_variable_substitution (
761+ self ._test .max_revision )
751762 test .update_arguments (self .arguments )
752763 test .update_responses (self .responses )
753764
@@ -855,6 +866,60 @@ def event_number(self, value):
855866 def pics (self ):
856867 return self ._test .pics
857868
869+ @property
870+ def min_revision (self ):
871+ return self ._test .min_revision
872+
873+ @property
874+ def max_revision (self ):
875+ return self ._test .max_revision
876+
877+ @property
878+ def is_revision_condition_passed (self ) -> bool :
879+ """Checks if the revision conditions passed properly based on min/maxRevision.
880+
881+ This is evaluated at runtime by the runner.
882+
883+ Returns True if step can be run, False if it cannot be run, or raises an exception
884+ if there was an error processing revision (i.e. internal error).
885+ """
886+ # If no revision checks are defined, the step is OK to run.
887+ if self .min_revision is None and self .max_revision is None :
888+ return True
889+
890+ endpoint = self .endpoint
891+ cluster = self .cluster
892+
893+ # The runner will have executed a prior step to read the revision, based
894+ # on the parser having injected that step (search for minRevision in
895+ # `YamlTests` class).
896+ var_name = build_revision_var_name (endpoint , cluster )
897+
898+ cluster_revision = self .get_runtime_variable (var_name )
899+
900+ if cluster_revision is None :
901+ raise KeyError (
902+ f"Step '{ self .label } ': Cannot check min/maxRevision. "
903+ f"ClusterRevision variable '{ var_name } ' is not set (read step may have been skipped or failed)."
904+ )
905+
906+ # Perform the checks
907+ try :
908+ # Ensure values are integers for comparison
909+ cluster_revision = int (cluster_revision )
910+
911+ return all ([
912+ (self .min_revision is None ) or (cluster_revision >= int (self .min_revision )),
913+ (self .max_revision is None ) or (cluster_revision <= int (self .max_revision )),
914+ ])
915+ except (ValueError , TypeError ) as e :
916+ # Failed to convert revision to int. This can happen with malformed YAML.
917+ raise ValueError (
918+ f"Step '{ self .label } ': Error checking min/maxRevision. "
919+ f"cluster_revision='{ cluster_revision } ', min='{ self .min_revision } ', max='{ self .max_revision } '. "
920+ f"Error: { e } ."
921+ )
922+
858923 def _get_last_event_number (self , responses ) -> Optional [int ]:
859924 if not self .is_event :
860925 return None
@@ -877,6 +942,10 @@ def _get_last_event_number(self, responses) -> Optional[int]:
877942
878943 return event_number
879944
945+ def get_runtime_variable (self , name : str ) -> Any :
946+ """Gets a runtime variable from the test context, or None if missing."""
947+ return self ._runtime_config_variable_storage .get (name )
948+
880949 def post_process_response (self , received_responses ):
881950 result = PostProcessResponseResult ()
882951
@@ -1159,7 +1228,8 @@ def _response_constraints_validation(self, expected_response, received_response,
11591228
11601229 for constraint in constraints :
11611230 try :
1162- constraint .validate (received_value , response_type_name , self ._runtime_config_variable_storage )
1231+ constraint .validate (
1232+ received_value , response_type_name , self ._runtime_config_variable_storage )
11631233 result .success (check_type , error_success )
11641234 except TestStepError as e :
11651235 e .update_context (expected_response , self .step_index )
@@ -1265,9 +1335,19 @@ class YamlTests:
12651335
12661336 def __init__ (self , parsing_config_variable_storage : dict , definitions : SpecDefinitions , pics_checker : PICSChecker , tests : dict ):
12671337 self ._parsing_config_variable_storage = parsing_config_variable_storage
1338+ self ._runtime_config_variable_storage = copy .deepcopy (
1339+ parsing_config_variable_storage )
1340+ self ._definitions = definitions
1341+ self ._pics_checker = pics_checker
1342+
1343+ tests_with_cluster_revision_injections = self ._tests_with_cluster_revision_checks (
1344+ tests )
1345+
1346+ # Build list of enabled tests from the starting point where pseudo steps are added/injected for
1347+ # things like min/maxRevision tests.
12681348 enabled_tests = []
12691349 try :
1270- for step_index , step in enumerate (tests ):
1350+ for step_index , step in enumerate (tests_with_cluster_revision_injections ):
12711351 test_with_placeholders = _TestStepWithPlaceholders (
12721352 step , self ._parsing_config_variable_storage , definitions , pics_checker )
12731353 if test_with_placeholders .is_enabled :
@@ -1278,12 +1358,93 @@ def __init__(self, parsing_config_variable_storage: dict, definitions: SpecDefin
12781358
12791359 fixes .try_update_yaml_node_id_test_runner_state (
12801360 enabled_tests , self ._parsing_config_variable_storage )
1281- self ._runtime_config_variable_storage = copy .deepcopy (
1282- parsing_config_variable_storage )
1361+
12831362 self ._tests = enabled_tests
12841363 self ._index = 0
12851364 self .count = len (self ._tests )
12861365
1366+ def _tests_with_cluster_revision_checks (self , tests : dict ) -> list [TestStep ]:
1367+ """Injection Logic for synthetic steps needed to process cluster revision checks."""
1368+
1369+ # Pre-pass: Find all (endpoint, cluster) pairs that *need* a revision check.
1370+ default_endpoint = self ._get_config_value (
1371+ self ._parsing_config_variable_storage , 'endpoint' )
1372+ default_cluster = self ._get_config_value (
1373+ self ._parsing_config_variable_storage , 'cluster' )
1374+
1375+ needed_revisions = set () # Set of (endpoint, cluster) tuples
1376+ for step in tests :
1377+ # A step needs a revision check if it has min/maxRevision and is not disabled.
1378+ if 'disabled' in step and step ['disabled' ]:
1379+ continue
1380+
1381+ if 'minRevision' in step or 'maxRevision' in step :
1382+ endpoint = step .get ('endpoint' , default_endpoint )
1383+ cluster = step .get ('cluster' , default_cluster )
1384+
1385+ if endpoint is not None and cluster is not None :
1386+ needed_revisions .add ((endpoint , cluster ))
1387+
1388+ # Main pass: Inject steps
1389+ injected_tests = []
1390+ seen_injections = set () # Set of (endpoint, cluster) tuples
1391+
1392+ for step in tests :
1393+ endpoint = step .get ('endpoint' , default_endpoint )
1394+ cluster = step .get ('cluster' , default_cluster )
1395+ key = (endpoint , cluster )
1396+ is_disabled = 'disabled' in step and step ['disabled' ]
1397+ step_has_revision_condition = 'minRevision' in step or 'maxRevision' in step
1398+
1399+ # Determine if an enabled step is the first that would need a cluster revision not yet read.
1400+ # Prepend the read to such steps.
1401+ if step_has_revision_condition and (key in needed_revisions ) and (key not in seen_injections ) and (not is_disabled ):
1402+ # Inject the step *before* the current step.
1403+ revision_var_name = build_revision_var_name (endpoint , cluster )
1404+
1405+ # Add to config storage so it's a known variable
1406+ if revision_var_name not in self ._parsing_config_variable_storage :
1407+ self ._parsing_config_variable_storage [revision_var_name ] = None
1408+
1409+ # Also update the runtime storage, since it was already copied.
1410+ if revision_var_name not in self ._runtime_config_variable_storage :
1411+ self ._runtime_config_variable_storage [revision_var_name ] = None
1412+
1413+ injected_step = {
1414+ 'label' : f'Read ClusterRevision for conditions (EP: { endpoint } , CL: { cluster } )' ,
1415+ 'cluster' : cluster ,
1416+ 'endpoint' : endpoint ,
1417+ 'command' : 'readAttribute' ,
1418+ 'attribute' : 'ClusterRevision' ,
1419+ 'response' : {
1420+ 'saveAs' : revision_var_name ,
1421+ 'constraints' : {
1422+ 'type' : 'int16u'
1423+ }
1424+ }
1425+ }
1426+
1427+ injected_tests .append (injected_step )
1428+ seen_injections .add (key )
1429+
1430+ injected_tests .append (step )
1431+
1432+ return injected_tests
1433+
1434+ def _get_config_value (self , config_storage , key ):
1435+ value = config_storage .get (key )
1436+ if isinstance (value , dict ) and 'defaultValue' in value :
1437+ return value ['defaultValue' ]
1438+ return value
1439+
1440+ def set_runtime_variable (self , name : str , value : Any ) -> None :
1441+ """Sets a runtime variable in the test context."""
1442+ self ._runtime_config_variable_storage [name ] = value
1443+
1444+ def get_runtime_variable (self , name : str ) -> Any :
1445+ """Gets a runtime variable from the test context, or None if missing."""
1446+ return self ._runtime_config_variable_storage .get (name )
1447+
12871448 def __iter__ (self ):
12881449 return self
12891450
0 commit comments