cactus-test-definitions 1.6.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.

Potentially problematic release.


This version of cactus-test-definitions might be problematic. Click here for more details.

Files changed (139) hide show
  1. cactus_test_definitions/__init__.py +39 -0
  2. cactus_test_definitions/__pycache__/__init__.cpython-312.pyc +0 -0
  3. cactus_test_definitions/__pycache__/actions.cpython-312.pyc +0 -0
  4. cactus_test_definitions/__pycache__/checks.cpython-312.pyc +0 -0
  5. cactus_test_definitions/__pycache__/csipaus.cpython-312.pyc +0 -0
  6. cactus_test_definitions/__pycache__/errors.cpython-312.pyc +0 -0
  7. cactus_test_definitions/__pycache__/events.cpython-312.pyc +0 -0
  8. cactus_test_definitions/__pycache__/parameters.cpython-312.pyc +0 -0
  9. cactus_test_definitions/__pycache__/schema.cpython-312.pyc +0 -0
  10. cactus_test_definitions/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
  11. cactus_test_definitions/__pycache__/test_procedures.cpython-312.pyc +0 -0
  12. cactus_test_definitions/__pycache__/variable_expressions.cpython-312.pyc +0 -0
  13. cactus_test_definitions/__pycache__/version.cpython-312.pyc +0 -0
  14. cactus_test_definitions/client/__init__.py +26 -0
  15. cactus_test_definitions/client/__pycache__/__init__.cpython-312.pyc +0 -0
  16. cactus_test_definitions/client/__pycache__/actions.cpython-312.pyc +0 -0
  17. cactus_test_definitions/client/__pycache__/checks.cpython-312.pyc +0 -0
  18. cactus_test_definitions/client/__pycache__/events.cpython-312.pyc +0 -0
  19. cactus_test_definitions/client/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
  20. cactus_test_definitions/client/__pycache__/validate.cpython-312.pyc +0 -0
  21. cactus_test_definitions/client/actions.py +98 -0
  22. cactus_test_definitions/client/checks.py +127 -0
  23. cactus_test_definitions/client/events.py +71 -0
  24. cactus_test_definitions/client/procedures/ALL-01.yaml +94 -0
  25. cactus_test_definitions/client/procedures/ALL-02.yaml +113 -0
  26. cactus_test_definitions/client/procedures/ALL-03-REJ.yaml +69 -0
  27. cactus_test_definitions/client/procedures/ALL-03.yaml +110 -0
  28. cactus_test_definitions/client/procedures/ALL-04.yaml +69 -0
  29. cactus_test_definitions/client/procedures/ALL-05.yaml +117 -0
  30. cactus_test_definitions/client/procedures/ALL-06.yaml +128 -0
  31. cactus_test_definitions/client/procedures/ALL-07.yaml +76 -0
  32. cactus_test_definitions/client/procedures/ALL-08.yaml +78 -0
  33. cactus_test_definitions/client/procedures/ALL-09.yaml +111 -0
  34. cactus_test_definitions/client/procedures/ALL-10.yaml +128 -0
  35. cactus_test_definitions/client/procedures/ALL-11.yaml +111 -0
  36. cactus_test_definitions/client/procedures/ALL-12.yaml +108 -0
  37. cactus_test_definitions/client/procedures/ALL-13.yaml +112 -0
  38. cactus_test_definitions/client/procedures/ALL-14.yaml +165 -0
  39. cactus_test_definitions/client/procedures/ALL-15.yaml +109 -0
  40. cactus_test_definitions/client/procedures/ALL-16.yaml +102 -0
  41. cactus_test_definitions/client/procedures/ALL-17.yaml +63 -0
  42. cactus_test_definitions/client/procedures/ALL-18.yaml +288 -0
  43. cactus_test_definitions/client/procedures/ALL-19.yaml +78 -0
  44. cactus_test_definitions/client/procedures/ALL-20.yaml +136 -0
  45. cactus_test_definitions/client/procedures/ALL-21.yaml +203 -0
  46. cactus_test_definitions/client/procedures/ALL-22.yaml +82 -0
  47. cactus_test_definitions/client/procedures/ALL-23.yaml +158 -0
  48. cactus_test_definitions/client/procedures/ALL-24.yaml +132 -0
  49. cactus_test_definitions/client/procedures/ALL-25-EXT.yaml +228 -0
  50. cactus_test_definitions/client/procedures/ALL-25.yaml +136 -0
  51. cactus_test_definitions/client/procedures/ALL-26.yaml +147 -0
  52. cactus_test_definitions/client/procedures/ALL-27.yaml +144 -0
  53. cactus_test_definitions/client/procedures/ALL-28.yaml +274 -0
  54. cactus_test_definitions/client/procedures/ALL-29.yaml +87 -0
  55. cactus_test_definitions/client/procedures/ALL-30.yaml +188 -0
  56. cactus_test_definitions/client/procedures/BES-01.yaml +136 -0
  57. cactus_test_definitions/client/procedures/BES-02.yaml +137 -0
  58. cactus_test_definitions/client/procedures/BES-03.yaml +135 -0
  59. cactus_test_definitions/client/procedures/BES-04.yaml +228 -0
  60. cactus_test_definitions/client/procedures/DRA-01.yaml +54 -0
  61. cactus_test_definitions/client/procedures/DRA-02.yaml +64 -0
  62. cactus_test_definitions/client/procedures/DRD-01.yaml +667 -0
  63. cactus_test_definitions/client/procedures/DRL-01.yaml +327 -0
  64. cactus_test_definitions/client/procedures/GEN-01.yaml +73 -0
  65. cactus_test_definitions/client/procedures/GEN-02.yaml +72 -0
  66. cactus_test_definitions/client/procedures/GEN-03.yaml +160 -0
  67. cactus_test_definitions/client/procedures/GEN-04.yaml +161 -0
  68. cactus_test_definitions/client/procedures/GEN-05.yaml +89 -0
  69. cactus_test_definitions/client/procedures/GEN-06.yaml +90 -0
  70. cactus_test_definitions/client/procedures/GEN-07.yaml +145 -0
  71. cactus_test_definitions/client/procedures/GEN-08.yaml +145 -0
  72. cactus_test_definitions/client/procedures/GEN-09.yaml +117 -0
  73. cactus_test_definitions/client/procedures/GEN-10.yaml +739 -0
  74. cactus_test_definitions/client/procedures/GEN-11.yaml +378 -0
  75. cactus_test_definitions/client/procedures/GEN-12.yaml +378 -0
  76. cactus_test_definitions/client/procedures/GEN-13.yaml +70 -0
  77. cactus_test_definitions/client/procedures/LOA-01.yaml +73 -0
  78. cactus_test_definitions/client/procedures/LOA-02.yaml +73 -0
  79. cactus_test_definitions/client/procedures/LOA-03.yaml +160 -0
  80. cactus_test_definitions/client/procedures/LOA-04.yaml +161 -0
  81. cactus_test_definitions/client/procedures/LOA-05.yaml +89 -0
  82. cactus_test_definitions/client/procedures/LOA-06.yaml +89 -0
  83. cactus_test_definitions/client/procedures/LOA-07.yaml +145 -0
  84. cactus_test_definitions/client/procedures/LOA-08.yaml +145 -0
  85. cactus_test_definitions/client/procedures/LOA-09.yaml +117 -0
  86. cactus_test_definitions/client/procedures/LOA-10.yaml +741 -0
  87. cactus_test_definitions/client/procedures/LOA-11.yaml +378 -0
  88. cactus_test_definitions/client/procedures/LOA-12.yaml +378 -0
  89. cactus_test_definitions/client/procedures/LOA-13.yaml +71 -0
  90. cactus_test_definitions/client/procedures/MUL-01.yaml +92 -0
  91. cactus_test_definitions/client/procedures/MUL-02.yaml +80 -0
  92. cactus_test_definitions/client/procedures/MUL-03.yaml +74 -0
  93. cactus_test_definitions/client/test_procedures.py +185 -0
  94. cactus_test_definitions/client/validate.py +98 -0
  95. cactus_test_definitions/csipaus.py +81 -0
  96. cactus_test_definitions/errors.py +15 -0
  97. cactus_test_definitions/parameters.py +153 -0
  98. cactus_test_definitions/py.typed +0 -0
  99. cactus_test_definitions/schema.py +22 -0
  100. cactus_test_definitions/server/README.md +170 -0
  101. cactus_test_definitions/server/__init__.py +23 -0
  102. cactus_test_definitions/server/__pycache__/__init__.cpython-312.pyc +0 -0
  103. cactus_test_definitions/server/__pycache__/actions.cpython-312.pyc +0 -0
  104. cactus_test_definitions/server/__pycache__/checks.cpython-312.pyc +0 -0
  105. cactus_test_definitions/server/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
  106. cactus_test_definitions/server/__pycache__/validate.cpython-312.pyc +0 -0
  107. cactus_test_definitions/server/actions.py +139 -0
  108. cactus_test_definitions/server/checks.py +117 -0
  109. cactus_test_definitions/server/procedures/S-ALL-01.yaml +42 -0
  110. cactus_test_definitions/server/procedures/S-ALL-02.yaml +65 -0
  111. cactus_test_definitions/server/procedures/S-ALL-03.yaml +65 -0
  112. cactus_test_definitions/server/procedures/S-ALL-04.yaml +137 -0
  113. cactus_test_definitions/server/procedures/S-ALL-05.yaml +111 -0
  114. cactus_test_definitions/server/procedures/S-OPT-01.yaml +42 -0
  115. cactus_test_definitions/server/procedures/S-OPT-02.yaml +40 -0
  116. cactus_test_definitions/server/procedures/S-OPT-03.yaml +44 -0
  117. cactus_test_definitions/server/procedures/S-OPT-04.yaml +32 -0
  118. cactus_test_definitions/server/test_procedures.py +122 -0
  119. cactus_test_definitions/server/validate.py +30 -0
  120. cactus_test_definitions/variable_expressions.py +419 -0
  121. cactus_test_definitions-1.6.0.dist-info/METADATA +289 -0
  122. cactus_test_definitions-1.6.0.dist-info/RECORD +139 -0
  123. cactus_test_definitions-1.6.0.dist-info/WHEEL +5 -0
  124. cactus_test_definitions-1.6.0.dist-info/licenses/LICENSE.txt +22 -0
  125. cactus_test_definitions-1.6.0.dist-info/top_level.txt +2 -0
  126. tests/__init__.py +0 -0
  127. tests/unit/__init__.py +0 -0
  128. tests/unit/client/__init__.py +0 -0
  129. tests/unit/client/test_actions.py +72 -0
  130. tests/unit/client/test_checks.py +71 -0
  131. tests/unit/client/test_events.py +36 -0
  132. tests/unit/client/test_test_procedures.py +103 -0
  133. tests/unit/client/test_validate.py +153 -0
  134. tests/unit/server/__init__.py +0 -0
  135. tests/unit/server/test_test_procedures.py +60 -0
  136. tests/unit/server/test_validate.py +62 -0
  137. tests/unit/test_csipaus.py +49 -0
  138. tests/unit/test_parameters.py +197 -0
  139. tests/unit/test_variable_expressions.py +402 -0
@@ -0,0 +1,185 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+ from importlib import resources
4
+
5
+ import yaml
6
+ from cactus_test_definitions.client.actions import Action
7
+ from cactus_test_definitions.client.checks import Check
8
+ from cactus_test_definitions.client.events import Event
9
+ from cactus_test_definitions.csipaus import CSIPAusVersion
10
+ from cactus_test_definitions.schema import UniqueKeyLoader
11
+ from dataclass_wizard import LoadMeta, YAMLWizard
12
+
13
+
14
+ class TestProcedureId(StrEnum):
15
+ """The set of all available test ID's
16
+
17
+ This should be kept in sync with the current set of client test procedures loaded from the procedures directory"""
18
+
19
+ __test__ = False # Prevent pytest from picking up this class
20
+ ALL_01 = "ALL-01"
21
+ ALL_02 = "ALL-02"
22
+ ALL_03 = "ALL-03"
23
+ ALL_03_REJ = "ALL-03-REJ"
24
+ ALL_04 = "ALL-04"
25
+ ALL_05 = "ALL-05"
26
+ ALL_06 = "ALL-06"
27
+ ALL_07 = "ALL-07"
28
+ ALL_08 = "ALL-08"
29
+ ALL_09 = "ALL-09"
30
+ ALL_10 = "ALL-10"
31
+ ALL_11 = "ALL-11"
32
+ ALL_12 = "ALL-12"
33
+ ALL_13 = "ALL-13"
34
+ ALL_14 = "ALL-14"
35
+ ALL_15 = "ALL-15"
36
+ ALL_16 = "ALL-16"
37
+ ALL_17 = "ALL-17"
38
+ ALL_18 = "ALL-18"
39
+ ALL_19 = "ALL-19"
40
+ ALL_20 = "ALL-20"
41
+ ALL_21 = "ALL-21"
42
+ ALL_22 = "ALL-22"
43
+ ALL_23 = "ALL-23"
44
+ ALL_24 = "ALL-24"
45
+ ALL_25 = "ALL-25"
46
+ ALL_25_EXT = "ALL-25-EXT"
47
+ ALL_26 = "ALL-26"
48
+ ALL_27 = "ALL-27"
49
+ ALL_28 = "ALL-28"
50
+ ALL_29 = "ALL-29"
51
+ ALL_30 = "ALL-30"
52
+ DRA_01 = "DRA-01"
53
+ DRA_02 = "DRA-02"
54
+ DRD_01 = "DRD-01"
55
+ DRL_01 = "DRL-01"
56
+ GEN_01 = "GEN-01"
57
+ GEN_02 = "GEN-02"
58
+ GEN_03 = "GEN-03"
59
+ GEN_04 = "GEN-04"
60
+ GEN_05 = "GEN-05"
61
+ GEN_06 = "GEN-06"
62
+ GEN_07 = "GEN-07"
63
+ GEN_08 = "GEN-08"
64
+ GEN_09 = "GEN-09"
65
+ GEN_10 = "GEN-10"
66
+ GEN_11 = "GEN-11"
67
+ GEN_12 = "GEN-12"
68
+ GEN_13 = "GEN-13"
69
+ LOA_01 = "LOA-01"
70
+ LOA_02 = "LOA-02"
71
+ LOA_03 = "LOA-03"
72
+ LOA_04 = "LOA-04"
73
+ LOA_05 = "LOA-05"
74
+ LOA_06 = "LOA-06"
75
+ LOA_07 = "LOA-07"
76
+ LOA_08 = "LOA-08"
77
+ LOA_09 = "LOA-09"
78
+ LOA_10 = "LOA-10"
79
+ LOA_11 = "LOA-11"
80
+ LOA_12 = "LOA-12"
81
+ LOA_13 = "LOA-13"
82
+ MUL_01 = "MUL-01"
83
+ MUL_02 = "MUL-02"
84
+ MUL_03 = "MUL-03"
85
+
86
+ # Storage extension
87
+ BES_01 = "BES-01"
88
+ BES_02 = "BES-02"
89
+ BES_03 = "BES-03"
90
+ BES_04 = "BES-04"
91
+
92
+
93
+ @dataclass
94
+ class Step:
95
+ """A step is a part of the test procedure that waits for some form of event before running a set of actions.
96
+
97
+ It's common for a step to activate other "steps" so that the state of the active test procedure can "evolve" in
98
+ response to client behaviour
99
+
100
+ Instructions are out-of-band operations that need performing during the step
101
+ e.g. disconnect DER from grid, disable power consumption etc.
102
+ """
103
+
104
+ event: Event # The event to act as a trigger
105
+ actions: list[Action] # The actions to execute when the trigger is met
106
+ instructions: list[str] | None = None
107
+
108
+
109
+ @dataclass
110
+ class Preconditions:
111
+ """Preconditions are run during the "initialization" state that precedes the start of a test. They typically
112
+ allow for the setup of the test.
113
+
114
+ Checks are also included to prevent a client from starting a test before they have correctly met preconditions
115
+
116
+ Instructions are out-of-band operations that need performing at the start of the test procedure
117
+ e.g. attach a load etc.
118
+
119
+ If immediate_start is set to True - the "initialization" step will be progressed through immediately so that the
120
+ client has no opportunity to interact with the server in this state. Any actions will still be executed. Do NOT
121
+ utilise immediate_start with precondition checks.
122
+ """
123
+
124
+ init_actions: list[Action] | None = None # To be executed as the runner starts (before anything can occur)
125
+ immediate_start: bool = False # If True - a test execution will have NO "pre-start" phase.
126
+ actions: list[Action] | None = None # To be executed as the test case "starts" (usually on request of client)
127
+ checks: list[Check] | None = None # Will prevent move from "init" state to "started" state of a test if any fail
128
+ instructions: list[str] | None = None
129
+
130
+
131
+ @dataclass
132
+ class Criteria:
133
+ """Criteria represent the final pass/fail analysis run after a TestProcedure completion. They can consider both
134
+ the final state of the test system as well as the interactions that happened while it was running"""
135
+
136
+ checks: list[Check] | None = None # These should be run at test procedure finalization to determine pass/fail
137
+
138
+
139
+ @dataclass
140
+ class TestProcedure(YAMLWizard):
141
+ """Top level object for collecting everything relevant to a single TestProcedure"""
142
+
143
+ __test__ = False # Prevent pytest from picking up this class
144
+ description: str # Metadata from test definitions
145
+ category: str # Metadata from test definitions
146
+ classes: list[str] # Metadata from test definitions
147
+ target_versions: list[CSIPAusVersion] # What version(s) of csip-aus is this test targeting?
148
+ steps: dict[str, Step]
149
+ preconditions: Preconditions | None = None # These execute during "init" and setup the test for a valid start state
150
+ criteria: Criteria | None = None # How will success/failure of this procedure be determined?
151
+
152
+
153
+ LoadMeta(raise_on_unknown_json_key=True).bind_to(TestProcedure)
154
+
155
+
156
+ def parse_test_procedure(yaml_contents: str) -> TestProcedure:
157
+ """Given a YAML string - parse a TestProcedure.
158
+
159
+ This will ensure the YAML parser will use all the "strict" extensions to reduce the incidence of errors"""
160
+
161
+ return TestProcedure.from_yaml(
162
+ yaml_contents,
163
+ decoder=yaml.load, # type: ignore
164
+ Loader=UniqueKeyLoader,
165
+ )
166
+
167
+
168
+ def get_yaml_contents(test_procedure_id: TestProcedureId) -> str:
169
+ """Finds the YAML contents for the TestProcedure with the specified TestProcedureId"""
170
+ yaml_resource = resources.files("cactus_test_definitions.client.procedures") / f"{test_procedure_id}.yaml"
171
+ with resources.as_file(yaml_resource) as yaml_file:
172
+ with open(yaml_file, "r") as f:
173
+ yaml_contents = f.read()
174
+ return yaml_contents
175
+
176
+
177
+ def get_test_procedure(test_procedure_id: TestProcedureId) -> TestProcedure:
178
+ """Gets the TestProcedure with the nominated ID by loading its definition from disk"""
179
+ yaml_contents = get_yaml_contents(test_procedure_id)
180
+ return parse_test_procedure(yaml_contents)
181
+
182
+
183
+ def get_all_test_procedures() -> dict[TestProcedureId, TestProcedure]:
184
+ """Gets every TestProcedure, keyed by their TestProcedureId"""
185
+ return {tp_id: get_test_procedure(tp_id) for tp_id in TestProcedureId}
@@ -0,0 +1,98 @@
1
+ from cactus_test_definitions.client.actions import Action, validate_action_parameters
2
+ from cactus_test_definitions.client.checks import validate_check_parameters
3
+ from cactus_test_definitions.client.events import validate_event_parameters
4
+ from cactus_test_definitions.client.test_procedures import (
5
+ TestProcedure,
6
+ TestProcedureId,
7
+ )
8
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
9
+
10
+
11
+ def validate_action(
12
+ procedure: TestProcedure, test_procedure_id: TestProcedureId, location: str, action: Action
13
+ ) -> None:
14
+ """Handles the full validation of an action's definition for a parent procedure.
15
+
16
+ procedure: The parent TestProcedure for action
17
+ test_procedure_id: The name of procedure (used for labelling errors)
18
+ location: Where in procedure can you find action? (used for labelling errors)
19
+ action: The action to validate
20
+
21
+ raises TestProcedureDefinitionError on failure
22
+ """
23
+ validate_action_parameters(test_procedure_id, location, action)
24
+
25
+ # Provide additional "action specific" validation
26
+ match action.type:
27
+ case "enable-steps" | "remove-steps":
28
+ for step_name in action.parameters["steps"]:
29
+ if step_name not in procedure.steps.keys():
30
+ raise TestProcedureDefinitionError(
31
+ f"{test_procedure_id}.{location}. Refers to unknown step '{step_name}'."
32
+ )
33
+
34
+
35
+ def validate_test_procedure_actions(test_procedure: TestProcedure, test_procedure_id: TestProcedureId) -> None:
36
+ """Validate actions of test procedure steps / preconditions
37
+
38
+ Ensure,
39
+ - action has the correct parameters
40
+ - if parameters refer to steps then those steps are defined for the test procedure
41
+ """
42
+
43
+ # Validate actions in the preconditions
44
+ if test_procedure.preconditions:
45
+ if test_procedure.preconditions.actions:
46
+ for action in test_procedure.preconditions.actions:
47
+ validate_action(test_procedure, test_procedure_id, "Precondition", action)
48
+
49
+ if test_procedure.preconditions.init_actions:
50
+ for action in test_procedure.preconditions.init_actions:
51
+ validate_action(test_procedure, test_procedure_id, "Precondition", action)
52
+
53
+ # Validate actions that exist on steps
54
+ for step_name, step in test_procedure.steps.items():
55
+ for action in step.actions:
56
+ validate_action(test_procedure, test_procedure_id, step_name, action)
57
+
58
+
59
+ def validate_test_procedure_checks(test_procedure: TestProcedure, test_procedure_id: TestProcedureId) -> None:
60
+ """Validate checks of a test procedure
61
+
62
+ Ensure,
63
+ - check has the correct parameters
64
+ """
65
+
66
+ if test_procedure.criteria and test_procedure.criteria.checks:
67
+ for check in test_procedure.criteria.checks:
68
+ validate_check_parameters(f"{test_procedure_id}: Criteria", check)
69
+
70
+ if test_procedure.preconditions and test_procedure.preconditions.checks:
71
+ for check in test_procedure.preconditions.checks:
72
+ validate_check_parameters(f"{test_procedure_id}: Preconditions", check)
73
+
74
+ for step_name, step in test_procedure.steps.items():
75
+ if step.event.checks:
76
+ for check in step.event.checks:
77
+ validate_check_parameters(f"{test_procedure_id}: Step {step_name}", check)
78
+
79
+
80
+ def validate_test_procedure_events(test_procedure: TestProcedure, test_procedure_id: TestProcedureId) -> None:
81
+ """Validate events of test procedure steps
82
+
83
+ Ensure,
84
+ - event has the correct parameters
85
+ """
86
+
87
+ if test_procedure.steps:
88
+ for step_name, step in test_procedure.steps.items():
89
+ validate_event_parameters(test_procedure_id, step_name, step.event)
90
+
91
+
92
+ def validate_test_procedure(test_procedure: TestProcedure, test_procedure_id: TestProcedureId) -> None:
93
+ """Performs additional "high level" validation of a test procedure. (eg: ensuring all action names are valid)
94
+
95
+ raises TestProcedureDefinitionError on error"""
96
+ validate_test_procedure_actions(test_procedure, test_procedure_id)
97
+ validate_test_procedure_checks(test_procedure, test_procedure_id)
98
+ validate_test_procedure_events(test_procedure, test_procedure_id)
@@ -0,0 +1,81 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class CSIPAusVersion(StrEnum):
5
+ """The various version identifiers for CSIP-Aus. Used for distinguishing what tests are compatible with what
6
+ released versions CSIP-Aus."""
7
+
8
+ RELEASE_1_2 = "v1.2"
9
+ BETA_1_3_STORAGE = "v1.3-beta/storage"
10
+
11
+
12
+ class CSIPAusResource(StrEnum):
13
+ """Labels for each resource type that the server/client tests might reference. This is not designed to be
14
+ an exhaustive list of all SEP2 / CSIP-Aus models - only the entities that might have tests applied to them."""
15
+
16
+ DeviceCapability = "DeviceCapability"
17
+
18
+ Time = "Time"
19
+
20
+ MirrorUsagePointList = "MirrorUsagePointList"
21
+ MirrorUsagePoint = "MirrorUsagePoint"
22
+
23
+ EndDeviceList = "EndDeviceList"
24
+ EndDevice = "EndDevice"
25
+ ConnectionPoint = "ConnectionPoint"
26
+ Registration = "Registration"
27
+
28
+ FunctionSetAssignmentsList = "FunctionSetAssignmentsList"
29
+ FunctionSetAssignments = "FunctionSetAssignments"
30
+
31
+ DERProgramList = "DERProgramList"
32
+ DERProgram = "DERProgram"
33
+
34
+ DERControlList = "DERControlList"
35
+ DERControl = "DERControl"
36
+
37
+ DefaultDERControl = "DefaultDERControl"
38
+
39
+ DERList = "DERList"
40
+ DER = "DER"
41
+ DERCapability = "DERCapability"
42
+ DERSettings = "DERSettings"
43
+ DERStatus = "DERStatus"
44
+
45
+ SubscriptionList = "SubscriptionList"
46
+ Subscription = "Subscription"
47
+
48
+
49
+ class CSIPAusReadingLocation(StrEnum):
50
+ Site = "Site" # The reading is measured at the site's connection point
51
+ Device = "Device" # The reading is measured at the actual device (behind the meter)
52
+
53
+
54
+ class CSIPAusReadingType(StrEnum):
55
+ """A non exhaustive set of CSIPAus reading types / role flags that can be specified in tests"""
56
+
57
+ ActivePowerAverage = "ActivePowerAverage"
58
+ ActivePowerInstantaneous = "ActivePowerInstantaneous"
59
+ ActivePowerMaximum = "ActivePowerMaximum"
60
+ ActivePowerMinimum = "ActivePowerMinimum"
61
+
62
+ ReactivePowerAverage = "ReactivePowerAverage"
63
+ ReactivePowerInstantaneous = "ReactivePowerInstantaneous"
64
+ ReactivePowerMaximum = "ReactivePowerMaximum"
65
+ ReactivePowerMinimum = "ReactivePowerMinimum"
66
+
67
+ FrequencyAverage = "FrequencyAverage"
68
+ FrequencyInstantaneous = "FrequencyInstantaneous"
69
+ FrequencyMaximum = "FrequencyMaximum"
70
+ FrequencyMinimum = "FrequencyMinimum"
71
+
72
+ VoltageSinglePhaseAverage = "VoltageSinglePhaseAverage"
73
+ VoltageSinglePhaseInstantaneous = "VoltageSinglePhaseInstantaneous"
74
+ VoltageSinglePhaseMaximum = "VoltageSinglePhaseMaximum"
75
+ VoltageSinglePhaseMinimum = "VoltageSinglePhaseMinimum"
76
+
77
+
78
+ def is_list_resource(resource: CSIPAusResource) -> bool:
79
+ """Returns true if the specified resource is classified as a list resource (i.e. it supports list query params) and
80
+ will return an entity with list attributes (eg 'all')"""
81
+ return resource.name.endswith("List") # This is a really simple method but it should work
@@ -0,0 +1,15 @@
1
+ class TestProcedureDefinitionError(Exception):
2
+ __test__ = False # Prevent pytest from picking up this class
3
+
4
+
5
+ class UnresolvableVariableError(Exception):
6
+ """Raised whenever a NamedVariable cannot be resolved at test execution time (eg: database doesn't have the
7
+ requisite information)"""
8
+
9
+ pass
10
+
11
+
12
+ class UnparseableVariableExpressionError(Exception):
13
+ """Raised whenever a raw string cannot parse to a NamedVariable/Expression"""
14
+
15
+ pass
@@ -0,0 +1,153 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from enum import IntEnum, auto
5
+ from typing import Any
6
+
7
+ from cactus_test_definitions.csipaus import (
8
+ CSIPAusReadingLocation,
9
+ CSIPAusReadingType,
10
+ CSIPAusResource,
11
+ )
12
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
13
+ from cactus_test_definitions.variable_expressions import is_resolvable_variable
14
+
15
+
16
+ class ParameterType(IntEnum):
17
+ """The various "basic" types that can be set in YAML for a parameter. Rudimentary type checking
18
+ will try to enforce these but they can't be guaranteed"""
19
+
20
+ String = auto()
21
+ Integer = auto()
22
+ Float = auto()
23
+ Boolean = auto()
24
+ DateTime = auto() # TZ aware datetime
25
+ ListString = auto() # List of strings
26
+ HexBinary = auto()
27
+ CSIPAusResource = auto() # Member of cactus_test_definitions.csipaus.CSIPAusResource
28
+ ListCSIPAusResource = auto() # List where each member is a cactus_test_definitions.csipaus.CSIPAusResource
29
+ CSIPAusReadingType = auto() # Member of cactus_test_definitions.csipaus.CSIPAusReadingType
30
+ ListCSIPAusReadingType = auto() # List where each member is a cactus_test_definitions.csipaus.CSIPAusReadingType
31
+ CSIPAusReadingLocation = auto() # Member of cactus_test_definitions.csipaus.CSIPAusReadingLocation
32
+ ReadingTypeValues = auto() # A dict of type dict[CSIPAusReadingType, list[float]], each list has the same length
33
+ UnsignedInteger = auto()
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class ParameterSchema:
38
+ """What parameters can be passed to a given action/check. Describes a single optional/mandatory field"""
39
+
40
+ mandatory: bool # If this parameter required
41
+ expected_type: ParameterType
42
+
43
+
44
+ def is_valid_parameter_type(expected_type: ParameterType, value: Any) -> bool:
45
+ """Returns true if the specified value "passes" as the expected type. Only performs rudimentary checks to try
46
+ and catch obvious misconfigurations"""
47
+ if value is None:
48
+ return True # We currently allow None to pass to params. Make it a runtime concern
49
+
50
+ if is_resolvable_variable(value):
51
+ return True # Too hard to validate variable expressions. Make it a runtime concern
52
+
53
+ match expected_type:
54
+ case ParameterType.String:
55
+ return isinstance(value, str)
56
+ case ParameterType.Integer:
57
+ if isinstance(value, int):
58
+ return True
59
+ else:
60
+ # Floats/decimals can pass through so long as they have 0 decimal places
61
+ try:
62
+ return int(value) == value
63
+ except Exception:
64
+ return False
65
+ case ParameterType.UnsignedInteger:
66
+ # Integer that is greater than or equal to 0
67
+ return is_valid_parameter_type(ParameterType.Integer, value) and value >= 0
68
+ case ParameterType.Float:
69
+ return isinstance(value, float) or isinstance(value, Decimal) or isinstance(value, int)
70
+ case ParameterType.Boolean:
71
+ return isinstance(value, bool)
72
+ case ParameterType.DateTime:
73
+ return isinstance(value, datetime)
74
+ case ParameterType.ListString:
75
+ return isinstance(value, list) and all((isinstance(e, str) for e in value))
76
+ case ParameterType.HexBinary:
77
+ try:
78
+ int(value, 16)
79
+ return True
80
+ except Exception:
81
+ return False
82
+ case ParameterType.CSIPAusResource:
83
+ try:
84
+ return CSIPAusResource(value) == value
85
+ except Exception:
86
+ return False
87
+ case ParameterType.ListCSIPAusResource:
88
+ return isinstance(value, list) and all(
89
+ (is_valid_parameter_type(ParameterType.CSIPAusResource, e) for e in value)
90
+ )
91
+ case ParameterType.CSIPAusReadingType:
92
+ try:
93
+ return CSIPAusReadingType(value) == value
94
+ except Exception:
95
+ return False
96
+ case ParameterType.ListCSIPAusReadingType:
97
+ return isinstance(value, list) and all(
98
+ (is_valid_parameter_type(ParameterType.CSIPAusReadingType, e) for e in value)
99
+ )
100
+ case ParameterType.CSIPAusReadingLocation:
101
+ try:
102
+ return CSIPAusReadingLocation(value) == value
103
+ except Exception:
104
+ return False
105
+ case ParameterType.ReadingTypeValues:
106
+ if not value or not isinstance(value, dict):
107
+ return False
108
+
109
+ last_length: int | None = None
110
+ for reading_type, reading_vals in value.items():
111
+ if (
112
+ not is_valid_parameter_type(ParameterType.CSIPAusReadingType, reading_type)
113
+ or not isinstance(reading_vals, list)
114
+ or not all((is_valid_parameter_type(ParameterType.Float, rv) for rv in reading_vals))
115
+ ):
116
+ return False
117
+
118
+ if last_length is None:
119
+ last_length = len(reading_vals)
120
+ elif last_length != len(reading_vals):
121
+ return False
122
+ return True
123
+
124
+ raise TestProcedureDefinitionError(f"Unexpected ParameterType: {ParameterType}")
125
+
126
+
127
+ def validate_parameters(location: str, parameters: dict[str, Any], valid_schema: dict[str, ParameterSchema]) -> None:
128
+ """Validates parameters against valid_schema for the specified location label.
129
+
130
+ location: Label to decorate error messages (eg TestProcedureName.Step.Action)
131
+ parameters: The parameters dict to validate
132
+ valid_schema: The schema to validate parameters against. Keys will be the parameter names, value will be the schema
133
+
134
+ raises TestProcedureDefinitionError if parameters is invalid"""
135
+
136
+ # Check the supplied parameters match the schema definition
137
+ for param_name, param_value in parameters.items():
138
+ param_schema = valid_schema.get(param_name, None)
139
+ if param_schema is None:
140
+ raise TestProcedureDefinitionError(
141
+ f"{location} doesn't have a parameter {param_name}. Valid params are {set(valid_schema.keys())}"
142
+ )
143
+
144
+ # Check the type
145
+ if not is_valid_parameter_type(param_schema.expected_type, param_value):
146
+ raise TestProcedureDefinitionError(
147
+ f"{location} has parameter {param_name} expecting {param_schema.expected_type} but got {param_value}"
148
+ )
149
+
150
+ # Check all mandatory parameters are set
151
+ for param_name, param_schema in valid_schema.items():
152
+ if param_schema.mandatory and param_name not in parameters:
153
+ raise TestProcedureDefinitionError(f"{location} is missing mandatory parameter {param_name}")
File without changes
@@ -0,0 +1,22 @@
1
+ import yaml
2
+
3
+
4
+ class UniqueKeyLoader(yaml.SafeLoader):
5
+ """Originally sourced from https://gist.github.com/pypt/94d747fe5180851196eb
6
+ Prevents duplicate keys from overwriting eachother instead of raising a ValueError.
7
+
8
+ eg - consider the following YAML, it will parse OK but should be treated as an error:
9
+
10
+ my_class:
11
+ key1: abc
12
+ key1: def
13
+ """
14
+
15
+ def construct_mapping(self, node, deep=False):
16
+ mapping = set()
17
+ for key_node, _ in node.value:
18
+ key = self.construct_object(key_node, deep=deep)
19
+ if key in mapping:
20
+ raise ValueError(f"Duplicate {key!r} key found in YAML.")
21
+ mapping.add(key)
22
+ return super().construct_mapping(node, deep)