cactus-test-definitions 1.8.0__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.
- cactus_test_definitions/__init__.py +39 -0
- cactus_test_definitions/__pycache__/__init__.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/actions.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/checks.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/csipaus.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/errors.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/events.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/parameters.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/schema.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
- cactus_test_definitions/__pycache__/test_procedures.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/variable_expressions.cpython-312.pyc +0 -0
- cactus_test_definitions/__pycache__/version.cpython-312.pyc +0 -0
- cactus_test_definitions/client/__init__.py +30 -0
- cactus_test_definitions/client/__pycache__/__init__.cpython-312.pyc +0 -0
- cactus_test_definitions/client/__pycache__/actions.cpython-312.pyc +0 -0
- cactus_test_definitions/client/__pycache__/checks.cpython-312.pyc +0 -0
- cactus_test_definitions/client/__pycache__/events.cpython-312.pyc +0 -0
- cactus_test_definitions/client/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
- cactus_test_definitions/client/__pycache__/validate.cpython-312.pyc +0 -0
- cactus_test_definitions/client/actions.py +99 -0
- cactus_test_definitions/client/checks.py +128 -0
- cactus_test_definitions/client/events.py +71 -0
- cactus_test_definitions/client/procedures/ALL-01.yaml +94 -0
- cactus_test_definitions/client/procedures/ALL-02.yaml +113 -0
- cactus_test_definitions/client/procedures/ALL-03-REJ.yaml +69 -0
- cactus_test_definitions/client/procedures/ALL-03.yaml +110 -0
- cactus_test_definitions/client/procedures/ALL-04.yaml +69 -0
- cactus_test_definitions/client/procedures/ALL-05.yaml +117 -0
- cactus_test_definitions/client/procedures/ALL-06.yaml +128 -0
- cactus_test_definitions/client/procedures/ALL-07.yaml +76 -0
- cactus_test_definitions/client/procedures/ALL-08.yaml +78 -0
- cactus_test_definitions/client/procedures/ALL-09.yaml +111 -0
- cactus_test_definitions/client/procedures/ALL-10.yaml +128 -0
- cactus_test_definitions/client/procedures/ALL-11.yaml +111 -0
- cactus_test_definitions/client/procedures/ALL-12.yaml +126 -0
- cactus_test_definitions/client/procedures/ALL-13.yaml +112 -0
- cactus_test_definitions/client/procedures/ALL-14.yaml +165 -0
- cactus_test_definitions/client/procedures/ALL-15.yaml +109 -0
- cactus_test_definitions/client/procedures/ALL-16.yaml +102 -0
- cactus_test_definitions/client/procedures/ALL-17.yaml +63 -0
- cactus_test_definitions/client/procedures/ALL-18.yaml +289 -0
- cactus_test_definitions/client/procedures/ALL-19.yaml +78 -0
- cactus_test_definitions/client/procedures/ALL-20.yaml +136 -0
- cactus_test_definitions/client/procedures/ALL-21.yaml +203 -0
- cactus_test_definitions/client/procedures/ALL-22.yaml +82 -0
- cactus_test_definitions/client/procedures/ALL-23.yaml +158 -0
- cactus_test_definitions/client/procedures/ALL-24.yaml +132 -0
- cactus_test_definitions/client/procedures/ALL-25-EXT.yaml +228 -0
- cactus_test_definitions/client/procedures/ALL-25.yaml +136 -0
- cactus_test_definitions/client/procedures/ALL-26.yaml +147 -0
- cactus_test_definitions/client/procedures/ALL-27.yaml +144 -0
- cactus_test_definitions/client/procedures/ALL-28.yaml +273 -0
- cactus_test_definitions/client/procedures/ALL-29.yaml +87 -0
- cactus_test_definitions/client/procedures/ALL-30.yaml +188 -0
- cactus_test_definitions/client/procedures/BES-01.yaml +136 -0
- cactus_test_definitions/client/procedures/BES-02.yaml +137 -0
- cactus_test_definitions/client/procedures/BES-03.yaml +135 -0
- cactus_test_definitions/client/procedures/BES-04.yaml +228 -0
- cactus_test_definitions/client/procedures/DRA-01.yaml +54 -0
- cactus_test_definitions/client/procedures/DRA-02.yaml +64 -0
- cactus_test_definitions/client/procedures/DRD-01.yaml +667 -0
- cactus_test_definitions/client/procedures/DRL-01.yaml +327 -0
- cactus_test_definitions/client/procedures/GEN-01.yaml +73 -0
- cactus_test_definitions/client/procedures/GEN-02.yaml +72 -0
- cactus_test_definitions/client/procedures/GEN-03.yaml +160 -0
- cactus_test_definitions/client/procedures/GEN-04.yaml +161 -0
- cactus_test_definitions/client/procedures/GEN-05.yaml +89 -0
- cactus_test_definitions/client/procedures/GEN-06.yaml +90 -0
- cactus_test_definitions/client/procedures/GEN-07.yaml +145 -0
- cactus_test_definitions/client/procedures/GEN-08.yaml +145 -0
- cactus_test_definitions/client/procedures/GEN-09.yaml +117 -0
- cactus_test_definitions/client/procedures/GEN-10.yaml +793 -0
- cactus_test_definitions/client/procedures/GEN-11.yaml +402 -0
- cactus_test_definitions/client/procedures/GEN-12.yaml +402 -0
- cactus_test_definitions/client/procedures/GEN-13.yaml +70 -0
- cactus_test_definitions/client/procedures/LOA-01.yaml +73 -0
- cactus_test_definitions/client/procedures/LOA-02.yaml +73 -0
- cactus_test_definitions/client/procedures/LOA-03.yaml +160 -0
- cactus_test_definitions/client/procedures/LOA-04.yaml +161 -0
- cactus_test_definitions/client/procedures/LOA-05.yaml +89 -0
- cactus_test_definitions/client/procedures/LOA-06.yaml +89 -0
- cactus_test_definitions/client/procedures/LOA-07.yaml +145 -0
- cactus_test_definitions/client/procedures/LOA-08.yaml +145 -0
- cactus_test_definitions/client/procedures/LOA-09.yaml +117 -0
- cactus_test_definitions/client/procedures/LOA-10.yaml +793 -0
- cactus_test_definitions/client/procedures/LOA-11.yaml +402 -0
- cactus_test_definitions/client/procedures/LOA-12.yaml +402 -0
- cactus_test_definitions/client/procedures/LOA-13.yaml +71 -0
- cactus_test_definitions/client/procedures/MUL-01.yaml +92 -0
- cactus_test_definitions/client/procedures/MUL-02.yaml +80 -0
- cactus_test_definitions/client/procedures/MUL-03.yaml +74 -0
- cactus_test_definitions/client/test_procedures.py +185 -0
- cactus_test_definitions/client/validate.py +98 -0
- cactus_test_definitions/csipaus.py +83 -0
- cactus_test_definitions/errors.py +15 -0
- cactus_test_definitions/parameters.py +153 -0
- cactus_test_definitions/py.typed +0 -0
- cactus_test_definitions/schema.py +22 -0
- cactus_test_definitions/server/README.md +172 -0
- cactus_test_definitions/server/__init__.py +27 -0
- cactus_test_definitions/server/__pycache__/__init__.cpython-312.pyc +0 -0
- cactus_test_definitions/server/__pycache__/actions.cpython-312.pyc +0 -0
- cactus_test_definitions/server/__pycache__/checks.cpython-312.pyc +0 -0
- cactus_test_definitions/server/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
- cactus_test_definitions/server/__pycache__/validate.cpython-312.pyc +0 -0
- cactus_test_definitions/server/actions.py +140 -0
- cactus_test_definitions/server/checks.py +152 -0
- cactus_test_definitions/server/procedures/S-ALL-01.yaml +45 -0
- cactus_test_definitions/server/procedures/S-ALL-02.yaml +65 -0
- cactus_test_definitions/server/procedures/S-ALL-03.yaml +65 -0
- cactus_test_definitions/server/procedures/S-ALL-04.yaml +137 -0
- cactus_test_definitions/server/procedures/S-ALL-05.yaml +111 -0
- cactus_test_definitions/server/procedures/S-ALL-06.yaml +111 -0
- cactus_test_definitions/server/procedures/S-ALL-07.yaml +100 -0
- cactus_test_definitions/server/procedures/S-ALL-08.yaml +77 -0
- cactus_test_definitions/server/procedures/S-ALL-09.yaml +43 -0
- cactus_test_definitions/server/procedures/S-ALL-10.yaml +68 -0
- cactus_test_definitions/server/procedures/S-ALL-11.yaml +36 -0
- cactus_test_definitions/server/procedures/S-ALL-12.yaml +36 -0
- cactus_test_definitions/server/procedures/S-ALL-13.yaml +36 -0
- cactus_test_definitions/server/procedures/S-ALL-14.yaml +36 -0
- cactus_test_definitions/server/procedures/S-ALL-15.yaml +79 -0
- cactus_test_definitions/server/procedures/S-ALL-16.yaml +79 -0
- cactus_test_definitions/server/procedures/S-ALL-17.yaml +39 -0
- cactus_test_definitions/server/procedures/S-ALL-18.yaml +35 -0
- cactus_test_definitions/server/procedures/S-ALL-19.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-20.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-21.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-22.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-23.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-24.yaml +37 -0
- cactus_test_definitions/server/procedures/S-ALL-25.yaml +151 -0
- cactus_test_definitions/server/procedures/S-ALL-41.yaml +343 -0
- cactus_test_definitions/server/procedures/S-ALL-42.yaml +81 -0
- cactus_test_definitions/server/procedures/S-ALL-43.yaml +146 -0
- cactus_test_definitions/server/procedures/S-ALL-44.yaml +51 -0
- cactus_test_definitions/server/procedures/S-ALL-45.yaml +34 -0
- cactus_test_definitions/server/procedures/S-ALL-48.yaml +29 -0
- cactus_test_definitions/server/procedures/S-ALL-49.yaml +56 -0
- cactus_test_definitions/server/procedures/S-ALL-51.yaml +35 -0
- cactus_test_definitions/server/procedures/S-ALL-52.yaml +35 -0
- cactus_test_definitions/server/procedures/S-ALL-53.yaml +42 -0
- cactus_test_definitions/server/procedures/S-ALL-55.yaml +45 -0
- cactus_test_definitions/server/procedures/S-ALL-56.yaml +36 -0
- cactus_test_definitions/server/procedures/S-ALL-57.yaml +62 -0
- cactus_test_definitions/server/procedures/S-OPT-01.yaml +42 -0
- cactus_test_definitions/server/procedures/S-OPT-02.yaml +40 -0
- cactus_test_definitions/server/procedures/S-OPT-03.yaml +47 -0
- cactus_test_definitions/server/procedures/S-OPT-04.yaml +32 -0
- cactus_test_definitions/server/procedures/S-OPT-05.yaml +33 -0
- cactus_test_definitions/server/test_procedures.py +156 -0
- cactus_test_definitions/server/validate.py +30 -0
- cactus_test_definitions/variable_expressions.py +419 -0
- cactus_test_definitions-1.8.0.dist-info/METADATA +289 -0
- cactus_test_definitions-1.8.0.dist-info/RECORD +173 -0
- cactus_test_definitions-1.8.0.dist-info/WHEEL +5 -0
- cactus_test_definitions-1.8.0.dist-info/licenses/LICENSE.txt +22 -0
- cactus_test_definitions-1.8.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/unit/__init__.py +0 -0
- tests/unit/client/__init__.py +0 -0
- tests/unit/client/test_actions.py +72 -0
- tests/unit/client/test_checks.py +71 -0
- tests/unit/client/test_events.py +36 -0
- tests/unit/client/test_test_procedures.py +103 -0
- tests/unit/client/test_validate.py +200 -0
- tests/unit/server/__init__.py +0 -0
- tests/unit/server/test_test_procedures.py +60 -0
- tests/unit/server/test_validate.py +75 -0
- tests/unit/test_csipaus.py +49 -0
- tests/unit/test_parameters.py +197 -0
- tests/unit/test_variable_expressions.py +402 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from importlib import resources
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from cactus_test_definitions.csipaus import CSIPAusVersion
|
|
7
|
+
from cactus_test_definitions.schema import UniqueKeyLoader
|
|
8
|
+
from cactus_test_definitions.server.actions import Action
|
|
9
|
+
from cactus_test_definitions.server.checks import Check
|
|
10
|
+
from dataclass_wizard import LoadMeta, YAMLWizard
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestProcedureId(StrEnum):
|
|
14
|
+
"""The set of all available test ID's
|
|
15
|
+
|
|
16
|
+
This should be kept in sync with the current set of test procedures loaded from the procedures directory"""
|
|
17
|
+
|
|
18
|
+
__test__ = False # Prevent pytest from picking up this class
|
|
19
|
+
S_ALL_01 = "S-ALL-01"
|
|
20
|
+
S_ALL_02 = "S-ALL-02"
|
|
21
|
+
S_ALL_03 = "S-ALL-03"
|
|
22
|
+
S_ALL_04 = "S-ALL-04"
|
|
23
|
+
S_ALL_05 = "S-ALL-05"
|
|
24
|
+
S_ALL_06 = "S-ALL-06"
|
|
25
|
+
S_ALL_07 = "S-ALL-07"
|
|
26
|
+
S_ALL_08 = "S-ALL-08"
|
|
27
|
+
S_ALL_09 = "S-ALL-09"
|
|
28
|
+
S_ALL_10 = "S-ALL-10"
|
|
29
|
+
S_ALL_11 = "S-ALL-11"
|
|
30
|
+
S_ALL_12 = "S-ALL-12"
|
|
31
|
+
S_ALL_13 = "S-ALL-13"
|
|
32
|
+
S_ALL_14 = "S-ALL-14"
|
|
33
|
+
S_ALL_15 = "S-ALL-15"
|
|
34
|
+
S_ALL_16 = "S-ALL-16"
|
|
35
|
+
S_ALL_17 = "S-ALL-17"
|
|
36
|
+
S_ALL_18 = "S-ALL-18"
|
|
37
|
+
S_ALL_19 = "S-ALL-19"
|
|
38
|
+
S_ALL_20 = "S-ALL-20"
|
|
39
|
+
S_ALL_21 = "S-ALL-21"
|
|
40
|
+
S_ALL_22 = "S-ALL-22"
|
|
41
|
+
S_ALL_23 = "S-ALL-23"
|
|
42
|
+
S_ALL_24 = "S-ALL-24"
|
|
43
|
+
S_ALL_25 = "S-ALL-25"
|
|
44
|
+
S_ALL_41 = "S-ALL-41"
|
|
45
|
+
S_ALL_42 = "S-ALL-42"
|
|
46
|
+
S_ALL_43 = "S-ALL-43"
|
|
47
|
+
S_ALL_44 = "S-ALL-44"
|
|
48
|
+
S_ALL_45 = "S-ALL-45"
|
|
49
|
+
S_ALL_48 = "S-ALL-48"
|
|
50
|
+
S_ALL_49 = "S-ALL-49"
|
|
51
|
+
S_ALL_51 = "S-ALL-51"
|
|
52
|
+
S_ALL_52 = "S-ALL-52"
|
|
53
|
+
S_ALL_53 = "S-ALL-53"
|
|
54
|
+
S_ALL_55 = "S-ALL-55"
|
|
55
|
+
S_ALL_56 = "S-ALL-56"
|
|
56
|
+
S_ALL_57 = "S-ALL-57"
|
|
57
|
+
S_OPT_01 = "S-OPT-01"
|
|
58
|
+
S_OPT_02 = "S-OPT-02"
|
|
59
|
+
S_OPT_03 = "S-OPT-03"
|
|
60
|
+
S_OPT_04 = "S-OPT-04"
|
|
61
|
+
S_OPT_05 = "S-OPT-05"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ClientType(StrEnum):
|
|
65
|
+
DEVICE = "device" # This is a direct device client - i.e. the cert will match a SPECIFIC EndDevice
|
|
66
|
+
AGGREGATOR = "aggregator" # This is an aggregator client - i.e. the cert can manage MANY EndDevices
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class RequiredClient:
|
|
71
|
+
"""A RequiredClient is a way for a test to assert that it needs a specific client type or set of clients. The id
|
|
72
|
+
will be used internally within a test to reference a specific client"""
|
|
73
|
+
|
|
74
|
+
id: str # How this client will be referred to within the step's of the test
|
|
75
|
+
client_type: ClientType | None = None # If set - the client type that is required
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Step:
|
|
80
|
+
"""A step is an action for a client to execute and then a series of checks to validate the results. If the action
|
|
81
|
+
raises an exception OR any of the checks fail, this step will marked as failed and the test will be aborted.
|
|
82
|
+
|
|
83
|
+
Actions might represent a single operation or they may represent a series of polls/checks over a period of time.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
id: str # Descriptive identifier for this step (must be unique)
|
|
87
|
+
action: Action # The action to execute when the step starts
|
|
88
|
+
client: str | None = None # The RequiredClient.id that will execute this step. If None - use the 0th client.
|
|
89
|
+
use_client_context: str | None = (
|
|
90
|
+
None # Specify to allow a request to execute with clientX using the context of clientY
|
|
91
|
+
)
|
|
92
|
+
checks: list[Check] | None = None # The checks (if any) to execute AFTER action completes to determine success
|
|
93
|
+
instructions: list[str] | None = None # Text to display while this step executes
|
|
94
|
+
|
|
95
|
+
repeat_until_pass: bool = False # If True - failing checks will cause this step to re-execute until successful
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Preconditions:
|
|
100
|
+
"""Preconditions are a way of setting up the test / server before the test begins.
|
|
101
|
+
|
|
102
|
+
Instructions are out-of-band information to show until the preconditions are all met.
|
|
103
|
+
|
|
104
|
+
If any checks are required - they will be polled regularly until ALL pass. For each check poll, a discovery
|
|
105
|
+
will be run to ensure data is available.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
required_clients: list[RequiredClient] # What client(s) need to be supplied to run this test procedure
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class TestProcedure(YAMLWizard):
|
|
113
|
+
"""Top level object for collecting everything relevant to a single TestProcedure"""
|
|
114
|
+
|
|
115
|
+
__test__ = False # Prevent pytest from picking up this class
|
|
116
|
+
description: str # Metadata from test definitions
|
|
117
|
+
category: str # Metadata from test definitions
|
|
118
|
+
classes: list[str] # Metadata from test definitions
|
|
119
|
+
target_versions: list[CSIPAusVersion] # What version(s) of csip-aus is this test targeting?
|
|
120
|
+
preconditions: Preconditions
|
|
121
|
+
steps: list[Step] # What behavior will the test procedure be evaluating?
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
LoadMeta(raise_on_unknown_json_key=True).bind_to(TestProcedure)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_test_procedure(yaml_contents: str) -> TestProcedure:
|
|
128
|
+
"""Given a YAML string - parse a TestProcedure.
|
|
129
|
+
|
|
130
|
+
This will ensure the YAML parser will use all the "strict" extensions to reduce the incidence of errors"""
|
|
131
|
+
|
|
132
|
+
return TestProcedure.from_yaml(
|
|
133
|
+
yaml_contents,
|
|
134
|
+
decoder=yaml.load, # type: ignore
|
|
135
|
+
Loader=UniqueKeyLoader,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_yaml_contents(test_procedure_id: TestProcedureId) -> str:
|
|
140
|
+
"""Finds the YAML contents for the TestProcedure with the specified TestProcedureId"""
|
|
141
|
+
yaml_resource = resources.files("cactus_test_definitions.server.procedures") / f"{test_procedure_id}.yaml"
|
|
142
|
+
with resources.as_file(yaml_resource) as yaml_file:
|
|
143
|
+
with open(yaml_file, "r") as f:
|
|
144
|
+
yaml_contents = f.read()
|
|
145
|
+
return yaml_contents
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_test_procedure(test_procedure_id: TestProcedureId) -> TestProcedure:
|
|
149
|
+
"""Gets the TestProcedure with the nominated ID by loading its definition from disk"""
|
|
150
|
+
yaml_contents = get_yaml_contents(test_procedure_id)
|
|
151
|
+
return parse_test_procedure(yaml_contents)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_all_test_procedures() -> dict[TestProcedureId, TestProcedure]:
|
|
155
|
+
"""Gets every TestProcedure, keyed by their TestProcedureId"""
|
|
156
|
+
return {tp_id: get_test_procedure(tp_id) for tp_id in TestProcedureId}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from cactus_test_definitions.errors import TestProcedureDefinitionError
|
|
2
|
+
from cactus_test_definitions.server.actions import validate_action_parameters
|
|
3
|
+
from cactus_test_definitions.server.checks import validate_check_parameters
|
|
4
|
+
from cactus_test_definitions.server.test_procedures import (
|
|
5
|
+
TestProcedure,
|
|
6
|
+
TestProcedureId,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_test_procedure(test_procedure: TestProcedure, test_procedure_id: TestProcedureId):
|
|
11
|
+
# Check preconditions
|
|
12
|
+
if not test_procedure.preconditions.required_clients:
|
|
13
|
+
raise TestProcedureDefinitionError(
|
|
14
|
+
f"{test_procedure_id} has no RequiredClients element. At least 1 entry required"
|
|
15
|
+
)
|
|
16
|
+
required_clients_by_id = dict(((rc.id, rc) for rc in test_procedure.preconditions.required_clients))
|
|
17
|
+
|
|
18
|
+
for step in test_procedure.steps:
|
|
19
|
+
validate_action_parameters(test_procedure_id, step.id, step.action)
|
|
20
|
+
|
|
21
|
+
# Validate step checks
|
|
22
|
+
if step.checks:
|
|
23
|
+
for check in step.checks:
|
|
24
|
+
validate_check_parameters(test_procedure_id, check)
|
|
25
|
+
|
|
26
|
+
# Ensure client exists
|
|
27
|
+
if step.client is not None and step.client not in required_clients_by_id:
|
|
28
|
+
raise TestProcedureDefinitionError(
|
|
29
|
+
f"{test_procedure_id} reference client {step.client} that isn't listed in RequiredClients."
|
|
30
|
+
)
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import tokenize
|
|
2
|
+
import abc
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from enum import IntEnum, auto
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from re import match, search
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from cactus_test_definitions.errors import UnparseableVariableExpressionError
|
|
11
|
+
|
|
12
|
+
ConstantType = timedelta | int | float
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Token:
|
|
17
|
+
"""Custom token implementaion
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
string: representation of original token from input
|
|
21
|
+
type: the kind of token found, an enum directly related from tokenize
|
|
22
|
+
line: the input line that the token belongs
|
|
23
|
+
start: coordinates of the token start wrt input
|
|
24
|
+
end: coordinates of the token end wrt input
|
|
25
|
+
param_key: optional to help with the special case of backfilling a self reference (i.e $this)
|
|
26
|
+
to its underlying named value
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
string: str
|
|
30
|
+
type: int
|
|
31
|
+
line: str
|
|
32
|
+
start: tuple[int, int]
|
|
33
|
+
end: tuple[int, int]
|
|
34
|
+
param_key: str | None = None
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def from_token_info(token_info: tokenize.TokenInfo, param_key: str | None = None) -> "Token":
|
|
38
|
+
"""Takes a tokenize.TokenInfo and returns an internal Token"""
|
|
39
|
+
return Token(
|
|
40
|
+
string=token_info.string,
|
|
41
|
+
type=token_info.type,
|
|
42
|
+
line=token_info.line,
|
|
43
|
+
start=token_info.start,
|
|
44
|
+
end=token_info.start,
|
|
45
|
+
param_key=param_key,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NamedVariableType(IntEnum):
|
|
50
|
+
# MUST resolve to a tz aware representation of the current datetime
|
|
51
|
+
# Referenced in a test definition as $(now)
|
|
52
|
+
NOW = auto()
|
|
53
|
+
|
|
54
|
+
# MUST resolve to the "DERSetting.setMaxW" of the current EndDevice under test. Value in Watts
|
|
55
|
+
# Referenced in a test definition as $(setMaxW)
|
|
56
|
+
DERSETTING_SET_MAX_W = auto()
|
|
57
|
+
DERSETTING_SET_MAX_VA = auto()
|
|
58
|
+
DERSETTING_SET_MAX_VAR = auto()
|
|
59
|
+
DERSETTING_SET_MAX_CHARGE_RATE_W = auto()
|
|
60
|
+
DERSETTING_SET_MAX_DISCHARGE_RATE_W = auto()
|
|
61
|
+
DERSETTING_SET_MAX_WH = auto()
|
|
62
|
+
|
|
63
|
+
# Must resolve to DERCapablity of the current EndDevice under test
|
|
64
|
+
DERCAPABILITY_RTG_MAX_VA = auto() # VA ( after multiplier applied), reference $rtgMaxVA
|
|
65
|
+
DERCAPABILITY_RTG_MAX_VAR = auto() # VAr ( atfer multiplier applied), reference $rtgMaxVar
|
|
66
|
+
DERCAPABILITY_RTG_MAX_W = auto() # W ( after multiplier applied), reference $rtgMaxW
|
|
67
|
+
DERCAPABILITY_RTG_MAX_CHARGE_RATE_W = auto() # W ( after multiiplier applied), reference $rtgMaxChargeRateW
|
|
68
|
+
DERCAPABILITY_RTG_MAX_DISCHARGE_RATE_W = auto() # W ( after multiplier applied), reference $rtgMaxDischargeRateW
|
|
69
|
+
DERCAPABILITY_RTG_MAX_WH = auto() # Wh ( after multiplier applied), reference $rtgMaxWh
|
|
70
|
+
|
|
71
|
+
# Storage extension
|
|
72
|
+
DERSETTING_SET_MIN_WH = auto()
|
|
73
|
+
DERCAPABILITY_NEG_RTG_MAX_CHARGE_RATE_W = auto() # -W (after multiplier applied), reference $negRtgMaxChargeRateW
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OperationType(IntEnum):
|
|
77
|
+
ADD = auto()
|
|
78
|
+
SUBTRACT = auto()
|
|
79
|
+
MULTIPLY = auto()
|
|
80
|
+
DIVIDE = auto()
|
|
81
|
+
EQ = auto()
|
|
82
|
+
NE = auto()
|
|
83
|
+
LT = auto()
|
|
84
|
+
LTE = auto()
|
|
85
|
+
GT = auto()
|
|
86
|
+
GTE = auto()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
OPERATION_MAPPINGS = {
|
|
90
|
+
"+": OperationType.ADD,
|
|
91
|
+
"-": OperationType.SUBTRACT,
|
|
92
|
+
"*": OperationType.MULTIPLY,
|
|
93
|
+
"/": OperationType.DIVIDE,
|
|
94
|
+
"==": OperationType.EQ,
|
|
95
|
+
"!=": OperationType.NE,
|
|
96
|
+
"<": OperationType.LT,
|
|
97
|
+
"<=": OperationType.LTE,
|
|
98
|
+
">": OperationType.GT,
|
|
99
|
+
">=": OperationType.GTE,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def snake_to_camel(snake: str) -> str:
|
|
104
|
+
"""Simple snake to camel case converter"""
|
|
105
|
+
temp = snake.replace("_", " ").title().replace(" ", "")
|
|
106
|
+
return temp[0].lower() + temp[1:]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def named_variable_repr(named_var: NamedVariableType) -> str:
|
|
110
|
+
"""Takes named variable enum and turns its name into its recognisable 2030.5 form"""
|
|
111
|
+
name = named_var.name
|
|
112
|
+
if len(name.split("_")) == 1:
|
|
113
|
+
return snake_to_camel(name)
|
|
114
|
+
match name.split("_", 1):
|
|
115
|
+
case ["DERCAPABILITY", "NEG_RTG_MAX_CHARGE_RATE_W"]:
|
|
116
|
+
return "(-DERCapability.rtgMaxChargeRateW)"
|
|
117
|
+
case ["DERCAPABILITY", "RTG_MAX_VA"]:
|
|
118
|
+
return "DERCapability.rtgMaxVA"
|
|
119
|
+
case ["DERSETTING", "SET_MAX_VA"]:
|
|
120
|
+
return "DERSetting.setMaxVA"
|
|
121
|
+
case ["DERCAPABILITY", param_name]:
|
|
122
|
+
return f"DERCapability.{snake_to_camel(param_name)}"
|
|
123
|
+
case ["DERSETTING", param_name]:
|
|
124
|
+
return f"DERSetting.{snake_to_camel(param_name)}"
|
|
125
|
+
|
|
126
|
+
return snake_to_camel(name)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def operation_repr(op: OperationType) -> str:
|
|
130
|
+
"""Takes an operation type and returns its string representation"""
|
|
131
|
+
operation_type_to_str_map: dict[OperationType, str] = {v: k for k, v in OPERATION_MAPPINGS.items()}
|
|
132
|
+
return operation_type_to_str_map[op]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class BaseExpression(abc.ABC):
|
|
136
|
+
"""A base class for all expressions to inherit from"""
|
|
137
|
+
|
|
138
|
+
@abc.abstractmethod
|
|
139
|
+
def expression_representation(self) -> str:
|
|
140
|
+
"""Method for representing an expression human readably without overriding magic methods"""
|
|
141
|
+
raise NotImplementedError
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class Constant(BaseExpression):
|
|
146
|
+
"""Represents a constant value that doesn't require any test execution time resolution"""
|
|
147
|
+
|
|
148
|
+
value: ConstantType # The parsed value
|
|
149
|
+
|
|
150
|
+
def expression_representation(self) -> str:
|
|
151
|
+
return f"{self.value}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class NamedVariable(BaseExpression):
|
|
156
|
+
"""A "NamedVariable" is value that can only be resolved at point during a test procedure execution (eg: as a
|
|
157
|
+
Step's action is being applied). There are a fixed set of known variable types defined by NamedVariableType.
|
|
158
|
+
|
|
159
|
+
Failures during resolving a variable (eg database doesn't have the data) MUST raise an exception
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
variable: NamedVariableType
|
|
163
|
+
|
|
164
|
+
def expression_representation(self) -> str:
|
|
165
|
+
return named_variable_repr(self.variable)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class Expression(BaseExpression):
|
|
170
|
+
"""An expression is a simple combination of two values that combine to make a single constant value. The operands
|
|
171
|
+
can be constants or NamedVariables."""
|
|
172
|
+
|
|
173
|
+
operation: OperationType
|
|
174
|
+
lhs_operand: NamedVariable | Constant # left hand side operand
|
|
175
|
+
rhs_operand: NamedVariable | Constant # right hand side operand
|
|
176
|
+
|
|
177
|
+
def expression_representation(self) -> str:
|
|
178
|
+
return " ".join(
|
|
179
|
+
[
|
|
180
|
+
f"{self.lhs_operand.expression_representation()}",
|
|
181
|
+
f"{operation_repr(self.operation)}",
|
|
182
|
+
f"{self.rhs_operand.expression_representation()}",
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_time_delta(var_body: str) -> timedelta:
|
|
188
|
+
"""Parses a string like '5 minutes' into a representative timedelta"""
|
|
189
|
+
|
|
190
|
+
m = match(r"(['\"])([0-9\-\.]*)\s*([^']*)(['\"])", var_body)
|
|
191
|
+
if m is None:
|
|
192
|
+
raise UnparseableVariableExpressionError(f"{var_body} can't be parsed into a timedelta")
|
|
193
|
+
|
|
194
|
+
open_quote = m.group(1)
|
|
195
|
+
number_string = m.group(2)
|
|
196
|
+
time_unit_string = m.group(3).lower()
|
|
197
|
+
close_quote = m.group(4)
|
|
198
|
+
|
|
199
|
+
if open_quote != close_quote:
|
|
200
|
+
raise UnparseableVariableExpressionError(f"{var_body} can't be parsed into a timedelta. Mismatching quotes")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
number = float(number_string)
|
|
204
|
+
except ValueError:
|
|
205
|
+
raise UnparseableVariableExpressionError(
|
|
206
|
+
f"{var_body} can't be parsed into a timedelta. Bad number {number_string}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if time_unit_string in {"day", "days"}:
|
|
210
|
+
return timedelta(days=number)
|
|
211
|
+
elif time_unit_string in {"hour", "hours", "hrs", "hr"}:
|
|
212
|
+
return timedelta(hours=number)
|
|
213
|
+
elif time_unit_string in {"minute", "minutes", "min", "mins"}:
|
|
214
|
+
return timedelta(minutes=number)
|
|
215
|
+
elif time_unit_string in {"second", "seconds", "sec", "secs"}:
|
|
216
|
+
return timedelta(seconds=number)
|
|
217
|
+
else:
|
|
218
|
+
raise UnparseableVariableExpressionError(
|
|
219
|
+
f"{var_body} can't be parsed into a timedelta. Unknown unit {time_unit_string}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def parse_unary_expression(token: Token) -> Constant | NamedVariable:
|
|
224
|
+
"""Parses a unary expression from a variable body"""
|
|
225
|
+
|
|
226
|
+
if token.type == tokenize.NAME:
|
|
227
|
+
# expect that a variable name is properly defined with correct case
|
|
228
|
+
match token.string:
|
|
229
|
+
case "now":
|
|
230
|
+
return NamedVariable(NamedVariableType.NOW)
|
|
231
|
+
case "this":
|
|
232
|
+
if token.param_key == "this" or token.param_key is None:
|
|
233
|
+
raise UnparseableVariableExpressionError(f"$this cannot resolve to parameter {token.param_key}")
|
|
234
|
+
# Modify token and maintain all other original data
|
|
235
|
+
token.string = token.param_key
|
|
236
|
+
token.param_key = None
|
|
237
|
+
return parse_unary_expression(token)
|
|
238
|
+
case "setMaxW":
|
|
239
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_W)
|
|
240
|
+
case "setMaxVA":
|
|
241
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_VA)
|
|
242
|
+
case "setMaxVar":
|
|
243
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_VAR)
|
|
244
|
+
case "setMaxChargeRateW":
|
|
245
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_CHARGE_RATE_W)
|
|
246
|
+
case "setMaxDischargeRateW":
|
|
247
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_DISCHARGE_RATE_W)
|
|
248
|
+
case "setMaxWh":
|
|
249
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MAX_WH)
|
|
250
|
+
case "rtgMaxVA":
|
|
251
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_VA)
|
|
252
|
+
case "rtgMaxVar":
|
|
253
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_VAR)
|
|
254
|
+
case "rtgMaxW":
|
|
255
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_W)
|
|
256
|
+
case "rtgMaxChargeRateW":
|
|
257
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_CHARGE_RATE_W)
|
|
258
|
+
case "rtgMaxDischargeRateW":
|
|
259
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_DISCHARGE_RATE_W)
|
|
260
|
+
case "rtgMaxWh":
|
|
261
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_RTG_MAX_WH)
|
|
262
|
+
# Storage extension
|
|
263
|
+
case "setMinWh":
|
|
264
|
+
return NamedVariable(NamedVariableType.DERSETTING_SET_MIN_WH)
|
|
265
|
+
case "negRtgMaxChargeRateW":
|
|
266
|
+
return NamedVariable(NamedVariableType.DERCAPABILITY_NEG_RTG_MAX_CHARGE_RATE_W)
|
|
267
|
+
|
|
268
|
+
raise UnparseableVariableExpressionError(f"'{token.string}' isn't recognized as a named variable")
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
if token.type == tokenize.NUMBER:
|
|
272
|
+
if "." in token.string:
|
|
273
|
+
return Constant(float(token.string))
|
|
274
|
+
else:
|
|
275
|
+
return Constant(int(token.string))
|
|
276
|
+
except ValueError:
|
|
277
|
+
raise UnparseableVariableExpressionError(f"'{token.string}' can't be converted to a number")
|
|
278
|
+
|
|
279
|
+
if token.type == tokenize.STRING:
|
|
280
|
+
return Constant(parse_time_delta(token.string))
|
|
281
|
+
|
|
282
|
+
raise UnparseableVariableExpressionError(f"Unable to parse token {token}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def parse_binary_expression(lhs_token: Token, operation: Token, rhs_token: Token) -> Expression:
|
|
286
|
+
|
|
287
|
+
if operation.type != tokenize.OP:
|
|
288
|
+
raise UnparseableVariableExpressionError(f"Expected an operation (eg + - / *) but found {operation}")
|
|
289
|
+
|
|
290
|
+
operation_type = OPERATION_MAPPINGS.get(operation.string, None)
|
|
291
|
+
if operation_type is None:
|
|
292
|
+
raise UnparseableVariableExpressionError(f"Unable to parse operator {operation.string} into a OperationType")
|
|
293
|
+
|
|
294
|
+
lhs = parse_unary_expression(lhs_token)
|
|
295
|
+
rhs = parse_unary_expression(rhs_token)
|
|
296
|
+
|
|
297
|
+
return Expression(operation=operation_type, lhs_operand=lhs, rhs_operand=rhs)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def parse_variable_expression_body(var_body: str, param_key: str | None) -> NamedVariable | Expression | Constant:
|
|
301
|
+
"""Given a variable definition: $(now - '5 seconds') - this function should be passed contents of that variable
|
|
302
|
+
definition (the string within the parentheses) eg: "now - '5 seconds'
|
|
303
|
+
|
|
304
|
+
Common variable patterns include,
|
|
305
|
+
$(now) - Will return a tz aware datetime corresponding to the current moment in time
|
|
306
|
+
$(now - '5 minute') - Same as above, but offset 5 minutes in the past
|
|
307
|
+
$(0.5 * setMaxW) - 50% of the currently configured setMaxW for the current EndDevice
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
var_body: parseable expression
|
|
311
|
+
param_key: the key that the expression body belongs e.g. setMaxW
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Parsed object
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
UnparseableVariableExpressionError: on failed parsing attempt
|
|
318
|
+
"""
|
|
319
|
+
if not var_body:
|
|
320
|
+
raise UnparseableVariableExpressionError("var_body is empty/None")
|
|
321
|
+
|
|
322
|
+
# Use the python parser to generate a simplified set of tokens representing the variable definition
|
|
323
|
+
# Convert these into the internal representation of a Token
|
|
324
|
+
try:
|
|
325
|
+
var_tokens = [
|
|
326
|
+
Token.from_token_info(t, param_key)
|
|
327
|
+
for t in tokenize.generate_tokens(StringIO(var_body).readline)
|
|
328
|
+
if t.type in {tokenize.NUMBER, tokenize.OP, tokenize.STRING, tokenize.NAME}
|
|
329
|
+
]
|
|
330
|
+
except tokenize.TokenError as exc:
|
|
331
|
+
raise UnparseableVariableExpressionError(f"Error tokenizing '{var_body}': {exc}")
|
|
332
|
+
|
|
333
|
+
if len(var_tokens) == 1:
|
|
334
|
+
return parse_unary_expression(var_tokens[0])
|
|
335
|
+
elif len(var_tokens) == 3:
|
|
336
|
+
return parse_binary_expression(var_tokens[0], var_tokens[1], var_tokens[2])
|
|
337
|
+
else:
|
|
338
|
+
raise UnparseableVariableExpressionError(f"Unable to parse {var_body} into a simple binary/unary expression")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def try_extract_variable_expression(body: Any) -> str | None:
|
|
342
|
+
"""Checks to see if a variable body (of any type) can be parsed by parse_variable_expression_body. If it can,
|
|
343
|
+
it will be returned as a string. Otherwise None will be returned
|
|
344
|
+
|
|
345
|
+
Can raise ValueError if body is a string appearing to contain a variable expression that is malformed (eg
|
|
346
|
+
mismatching parentheses)"""
|
|
347
|
+
if not isinstance(body, str):
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
begin_variable_defn = body.find("$")
|
|
351
|
+
if begin_variable_defn < 0:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
# The $ variable definition can be escaped with \$ so ensure it is checked
|
|
355
|
+
if begin_variable_defn > 0 and (body[begin_variable_defn - 1] == "\\"):
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# At this point we are definitely parsing a variable expression. Failures here will raise ValueError
|
|
359
|
+
if begin_variable_defn >= (len(body) - 1):
|
|
360
|
+
raise ValueError(f"'{body}' appears to be a malformed variable definition. Try escaping $ like '\\$'")
|
|
361
|
+
|
|
362
|
+
start_expr_body: int
|
|
363
|
+
end_expr_body: int
|
|
364
|
+
end_variable_defn: int # First character after the end of the full variable definition
|
|
365
|
+
if body[begin_variable_defn + 1] == "(":
|
|
366
|
+
start_expr_body = begin_variable_defn + 2
|
|
367
|
+
end_expr_body = body.index(")", start_expr_body)
|
|
368
|
+
end_variable_defn = end_expr_body + 2
|
|
369
|
+
else:
|
|
370
|
+
start_expr_body = begin_variable_defn + 1
|
|
371
|
+
|
|
372
|
+
remainder = body[start_expr_body:]
|
|
373
|
+
match_variable = match(r"[_a-zA-Z0-9]*", remainder)
|
|
374
|
+
if match_variable is None:
|
|
375
|
+
raise ValueError(f"'{body}' appears to be a malformed variable definition")
|
|
376
|
+
else:
|
|
377
|
+
end_expr_body = start_expr_body + match_variable.end()
|
|
378
|
+
end_variable_defn = end_expr_body + 1
|
|
379
|
+
|
|
380
|
+
if start_expr_body == end_expr_body:
|
|
381
|
+
raise ValueError(f"'{body}' appears to be a malformed variable definition.")
|
|
382
|
+
variable_expression = body[start_expr_body:end_expr_body]
|
|
383
|
+
|
|
384
|
+
# One last validation to look for leading/trailing text which would indicate
|
|
385
|
+
leading_text = search(r"[^\s]", body[0:begin_variable_defn])
|
|
386
|
+
if leading_text is not None:
|
|
387
|
+
raise ValueError("Variable expressions must ONLY be a single variable with no other text")
|
|
388
|
+
|
|
389
|
+
trailing_text = search(r"[^\s]", body[end_variable_defn:])
|
|
390
|
+
if trailing_text is not None:
|
|
391
|
+
raise ValueError("Variable expressions must ONLY be a single variable with no other text")
|
|
392
|
+
|
|
393
|
+
return variable_expression
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def is_resolvable_variable(v: Any) -> bool:
|
|
397
|
+
"""Returns True if the supplied value is a variable definition that requires resolving"""
|
|
398
|
+
return isinstance(v, NamedVariable) or isinstance(v, Expression) or isinstance(v, Constant)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def has_named_variable(
|
|
402
|
+
parameter_value: NamedVariable | Expression | Constant, named_variable: NamedVariableType
|
|
403
|
+
) -> bool:
|
|
404
|
+
"""Return True if the supplied named variable is used in the the parameter 'parameter_value'"""
|
|
405
|
+
|
|
406
|
+
if isinstance(parameter_value, Constant):
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
if isinstance(parameter_value, NamedVariable):
|
|
410
|
+
return parameter_value.variable == named_variable
|
|
411
|
+
|
|
412
|
+
if isinstance(parameter_value, Expression):
|
|
413
|
+
if isinstance(parameter_value.lhs_operand, NamedVariable):
|
|
414
|
+
if parameter_value.lhs_operand.variable == named_variable:
|
|
415
|
+
return True
|
|
416
|
+
if isinstance(parameter_value.rhs_operand, NamedVariable):
|
|
417
|
+
if parameter_value.rhs_operand.variable == named_variable:
|
|
418
|
+
return True
|
|
419
|
+
return False
|