cactus-test-definitions 1.0.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 (135) 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/actions.py +98 -0
  21. cactus_test_definitions/client/checks.py +117 -0
  22. cactus_test_definitions/client/events.py +71 -0
  23. cactus_test_definitions/client/procedures/ALL-01.yaml +94 -0
  24. cactus_test_definitions/client/procedures/ALL-02.yaml +108 -0
  25. cactus_test_definitions/client/procedures/ALL-03-REJ.yaml +69 -0
  26. cactus_test_definitions/client/procedures/ALL-03.yaml +110 -0
  27. cactus_test_definitions/client/procedures/ALL-04.yaml +69 -0
  28. cactus_test_definitions/client/procedures/ALL-05.yaml +117 -0
  29. cactus_test_definitions/client/procedures/ALL-06.yaml +128 -0
  30. cactus_test_definitions/client/procedures/ALL-07.yaml +76 -0
  31. cactus_test_definitions/client/procedures/ALL-08.yaml +78 -0
  32. cactus_test_definitions/client/procedures/ALL-09.yaml +103 -0
  33. cactus_test_definitions/client/procedures/ALL-10.yaml +128 -0
  34. cactus_test_definitions/client/procedures/ALL-11.yaml +111 -0
  35. cactus_test_definitions/client/procedures/ALL-12.yaml +108 -0
  36. cactus_test_definitions/client/procedures/ALL-13.yaml +112 -0
  37. cactus_test_definitions/client/procedures/ALL-14.yaml +165 -0
  38. cactus_test_definitions/client/procedures/ALL-15.yaml +109 -0
  39. cactus_test_definitions/client/procedures/ALL-16.yaml +102 -0
  40. cactus_test_definitions/client/procedures/ALL-17.yaml +63 -0
  41. cactus_test_definitions/client/procedures/ALL-18.yaml +288 -0
  42. cactus_test_definitions/client/procedures/ALL-19.yaml +78 -0
  43. cactus_test_definitions/client/procedures/ALL-20.yaml +136 -0
  44. cactus_test_definitions/client/procedures/ALL-21.yaml +203 -0
  45. cactus_test_definitions/client/procedures/ALL-22.yaml +82 -0
  46. cactus_test_definitions/client/procedures/ALL-23.yaml +158 -0
  47. cactus_test_definitions/client/procedures/ALL-24.yaml +132 -0
  48. cactus_test_definitions/client/procedures/ALL-25.yaml +136 -0
  49. cactus_test_definitions/client/procedures/ALL-26.yaml +147 -0
  50. cactus_test_definitions/client/procedures/ALL-27.yaml +144 -0
  51. cactus_test_definitions/client/procedures/ALL-28.yaml +274 -0
  52. cactus_test_definitions/client/procedures/ALL-29.yaml +87 -0
  53. cactus_test_definitions/client/procedures/ALL-30.yaml +188 -0
  54. cactus_test_definitions/client/procedures/BES-01.yaml +136 -0
  55. cactus_test_definitions/client/procedures/BES-02.yaml +137 -0
  56. cactus_test_definitions/client/procedures/BES-03.yaml +135 -0
  57. cactus_test_definitions/client/procedures/BES-04.yaml +228 -0
  58. cactus_test_definitions/client/procedures/DRA-01.yaml +54 -0
  59. cactus_test_definitions/client/procedures/DRA-02.yaml +64 -0
  60. cactus_test_definitions/client/procedures/DRD-01.yaml +667 -0
  61. cactus_test_definitions/client/procedures/DRL-01.yaml +327 -0
  62. cactus_test_definitions/client/procedures/GEN-01.yaml +73 -0
  63. cactus_test_definitions/client/procedures/GEN-02.yaml +72 -0
  64. cactus_test_definitions/client/procedures/GEN-03.yaml +160 -0
  65. cactus_test_definitions/client/procedures/GEN-04.yaml +161 -0
  66. cactus_test_definitions/client/procedures/GEN-05.yaml +89 -0
  67. cactus_test_definitions/client/procedures/GEN-06.yaml +90 -0
  68. cactus_test_definitions/client/procedures/GEN-07.yaml +145 -0
  69. cactus_test_definitions/client/procedures/GEN-08.yaml +145 -0
  70. cactus_test_definitions/client/procedures/GEN-09.yaml +117 -0
  71. cactus_test_definitions/client/procedures/GEN-10.yaml +737 -0
  72. cactus_test_definitions/client/procedures/GEN-11.yaml +376 -0
  73. cactus_test_definitions/client/procedures/GEN-12.yaml +376 -0
  74. cactus_test_definitions/client/procedures/GEN-13.yaml +70 -0
  75. cactus_test_definitions/client/procedures/LOA-01.yaml +73 -0
  76. cactus_test_definitions/client/procedures/LOA-02.yaml +73 -0
  77. cactus_test_definitions/client/procedures/LOA-03.yaml +160 -0
  78. cactus_test_definitions/client/procedures/LOA-04.yaml +161 -0
  79. cactus_test_definitions/client/procedures/LOA-05.yaml +85 -0
  80. cactus_test_definitions/client/procedures/LOA-06.yaml +85 -0
  81. cactus_test_definitions/client/procedures/LOA-07.yaml +145 -0
  82. cactus_test_definitions/client/procedures/LOA-08.yaml +145 -0
  83. cactus_test_definitions/client/procedures/LOA-09.yaml +117 -0
  84. cactus_test_definitions/client/procedures/LOA-10.yaml +739 -0
  85. cactus_test_definitions/client/procedures/LOA-11.yaml +376 -0
  86. cactus_test_definitions/client/procedures/LOA-12.yaml +376 -0
  87. cactus_test_definitions/client/procedures/LOA-13.yaml +71 -0
  88. cactus_test_definitions/client/procedures/MUL-01.yaml +92 -0
  89. cactus_test_definitions/client/procedures/MUL-02.yaml +80 -0
  90. cactus_test_definitions/client/procedures/MUL-03.yaml +78 -0
  91. cactus_test_definitions/client/procedures/OPT-1-IN-BAND.yaml +115 -0
  92. cactus_test_definitions/client/procedures/OPT-1-OUT-OF-BAND.yaml +101 -0
  93. cactus_test_definitions/client/procedures/test-procedures.yaml +75 -0
  94. cactus_test_definitions/client/test_procedures.py +296 -0
  95. cactus_test_definitions/csipaus.py +81 -0
  96. cactus_test_definitions/errors.py +15 -0
  97. cactus_test_definitions/parameters.py +149 -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/__pycache__/actions.cpython-312.pyc +0 -0
  102. cactus_test_definitions/server/__pycache__/checks.cpython-312.pyc +0 -0
  103. cactus_test_definitions/server/__pycache__/test_procedures.cpython-312-pytest-8.3.5.pyc +0 -0
  104. cactus_test_definitions/server/actions.py +139 -0
  105. cactus_test_definitions/server/checks.py +117 -0
  106. cactus_test_definitions/server/procedures/S-ALL-01.yaml +42 -0
  107. cactus_test_definitions/server/procedures/S-ALL-02.yaml +65 -0
  108. cactus_test_definitions/server/procedures/S-ALL-03.yaml +65 -0
  109. cactus_test_definitions/server/procedures/S-ALL-04.yaml +137 -0
  110. cactus_test_definitions/server/procedures/S-ALL-05.yaml +111 -0
  111. cactus_test_definitions/server/procedures/S-OPT-01.yaml +42 -0
  112. cactus_test_definitions/server/procedures/S-OPT-02.yaml +40 -0
  113. cactus_test_definitions/server/procedures/S-OPT-03.yaml +44 -0
  114. cactus_test_definitions/server/procedures/S-OPT-04.yaml +32 -0
  115. cactus_test_definitions/server/procedures/test-procedures.yaml +14 -0
  116. cactus_test_definitions/server/test_procedures.py +183 -0
  117. cactus_test_definitions/variable_expressions.py +419 -0
  118. cactus_test_definitions-1.0.0.dist-info/METADATA +288 -0
  119. cactus_test_definitions-1.0.0.dist-info/RECORD +135 -0
  120. cactus_test_definitions-1.0.0.dist-info/WHEEL +5 -0
  121. cactus_test_definitions-1.0.0.dist-info/licenses/LICENSE.txt +22 -0
  122. cactus_test_definitions-1.0.0.dist-info/top_level.txt +2 -0
  123. tests/__init__.py +0 -0
  124. tests/unit/__init__.py +0 -0
  125. tests/unit/client/__init__.py +0 -0
  126. tests/unit/client/test_actions.py +72 -0
  127. tests/unit/client/test_checks.py +47 -0
  128. tests/unit/client/test_config.py +61 -0
  129. tests/unit/client/test_events.py +36 -0
  130. tests/unit/client/test_test_procedures.py +150 -0
  131. tests/unit/server/__init__.py +0 -0
  132. tests/unit/server/test_test_procedures.py +86 -0
  133. tests/unit/test_csipaus.py +49 -0
  134. tests/unit/test_parameters.py +197 -0
  135. tests/unit/test_variable_expressions.py +402 -0
@@ -0,0 +1,149 @@
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
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ParameterSchema:
37
+ """What parameters can be passed to a given action/check. Describes a single optional/mandatory field"""
38
+
39
+ mandatory: bool # If this parameter required
40
+ expected_type: ParameterType
41
+
42
+
43
+ def is_valid_parameter_type(expected_type: ParameterType, value: Any) -> bool:
44
+ """Returns true if the specified value "passes" as the expected type. Only performs rudimentary checks to try
45
+ and catch obvious misconfigurations"""
46
+ if value is None:
47
+ return True # We currently allow None to pass to params. Make it a runtime concern
48
+
49
+ if is_resolvable_variable(value):
50
+ return True # Too hard to validate variable expressions. Make it a runtime concern
51
+
52
+ match expected_type:
53
+ case ParameterType.String:
54
+ return isinstance(value, str)
55
+ case ParameterType.Integer:
56
+ if isinstance(value, int):
57
+ return True
58
+ else:
59
+ # Floats/decimals can pass through so long as they have 0 decimal places
60
+ try:
61
+ return int(value) == value
62
+ except Exception:
63
+ return False
64
+ case ParameterType.Float:
65
+ return isinstance(value, float) or isinstance(value, Decimal) or isinstance(value, int)
66
+ case ParameterType.Boolean:
67
+ return isinstance(value, bool)
68
+ case ParameterType.DateTime:
69
+ return isinstance(value, datetime)
70
+ case ParameterType.ListString:
71
+ return isinstance(value, list) and all((isinstance(e, str) for e in value))
72
+ case ParameterType.HexBinary:
73
+ try:
74
+ int(value, 16)
75
+ return True
76
+ except Exception:
77
+ return False
78
+ case ParameterType.CSIPAusResource:
79
+ try:
80
+ return CSIPAusResource(value) == value
81
+ except Exception:
82
+ return False
83
+ case ParameterType.ListCSIPAusResource:
84
+ return isinstance(value, list) and all(
85
+ (is_valid_parameter_type(ParameterType.CSIPAusResource, e) for e in value)
86
+ )
87
+ case ParameterType.CSIPAusReadingType:
88
+ try:
89
+ return CSIPAusReadingType(value) == value
90
+ except Exception:
91
+ return False
92
+ case ParameterType.ListCSIPAusReadingType:
93
+ return isinstance(value, list) and all(
94
+ (is_valid_parameter_type(ParameterType.CSIPAusReadingType, e) for e in value)
95
+ )
96
+ case ParameterType.CSIPAusReadingLocation:
97
+ try:
98
+ return CSIPAusReadingLocation(value) == value
99
+ except Exception:
100
+ return False
101
+ case ParameterType.ReadingTypeValues:
102
+ if not value or not isinstance(value, dict):
103
+ return False
104
+
105
+ last_length: int | None = None
106
+ for reading_type, reading_vals in value.items():
107
+ if (
108
+ not is_valid_parameter_type(ParameterType.CSIPAusReadingType, reading_type)
109
+ or not isinstance(reading_vals, list)
110
+ or not all((is_valid_parameter_type(ParameterType.Float, rv) for rv in reading_vals))
111
+ ):
112
+ return False
113
+
114
+ if last_length is None:
115
+ last_length = len(reading_vals)
116
+ elif last_length != len(reading_vals):
117
+ return False
118
+ return True
119
+
120
+ raise TestProcedureDefinitionError(f"Unexpected ParameterType: {ParameterType}")
121
+
122
+
123
+ def validate_parameters(location: str, parameters: dict[str, Any], valid_schema: dict[str, ParameterSchema]) -> None:
124
+ """Validates parameters against valid_schema for the specified location label.
125
+
126
+ location: Label to decorate error messages (eg TestProcedureName.Step.Action)
127
+ parameters: The parameters dict to validate
128
+ valid_schema: The schema to validate parameters against. Keys will be the parameter names, value will be the schema
129
+
130
+ raises TestProcedureDefinitionError if parameters is invalid"""
131
+
132
+ # Check the supplied parameters match the schema definition
133
+ for param_name, param_value in parameters.items():
134
+ param_schema = valid_schema.get(param_name, None)
135
+ if param_schema is None:
136
+ raise TestProcedureDefinitionError(
137
+ f"{location} doesn't have a parameter {param_name}. Valid params are {set(valid_schema.keys())}"
138
+ )
139
+
140
+ # Check the type
141
+ if not is_valid_parameter_type(param_schema.expected_type, param_value):
142
+ raise TestProcedureDefinitionError(
143
+ f"{location} has parameter {param_name} expecting {param_schema.expected_type} but got {param_value}"
144
+ )
145
+
146
+ # Check all mandatory parameters are set
147
+ for param_name, param_schema in valid_schema.items():
148
+ if param_schema.mandatory and param_name not in parameters:
149
+ 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)
@@ -0,0 +1,170 @@
1
+
2
+ # Server Test Schema
3
+
4
+ This is for the **SERVER** test procedures.
5
+
6
+ At its most basic level, a server test is a series of actions by a virtual "client" that will probe the server and look for unexpected behaviour/responses.
7
+
8
+ ## Clients and Context
9
+
10
+ Each test will define one or more "virtual clients" as a precondition. These can be restricted to a specific client type (eg Aggregator or Device) or left unspecified.
11
+
12
+ At the beginning of each test, the client will only know it's LFDI, PEN, PIN and certificate details. Through running actions it will discover resources and populate a local "context" which represents the last seen CSIP-Aus resources. Each client's context is seperate, so if Client A performs discovery, those resources will be invisible to Client B.
13
+
14
+ ## Steps Schema
15
+
16
+ The most basic building block of a server `TestProcedure` is a `Step`. Each `Step` will always define a single `Action` which dictates the behaviour of a virtual client (eg sending a particular request) which is then followed by a series of `Check` objects to evaluate. In order for a step to pass it must:
17
+
18
+ 1) Execute with any errors (eg - able to successfully make a HTTP request to the utility server)
19
+ 2) Have all checks return passed
20
+
21
+
22
+ Steps:
23
+ - id: DISCOVERY
24
+ action:
25
+ type: discovery
26
+ parameters:
27
+ resources:
28
+ - DeviceCapability
29
+ - Time
30
+ - MirrorUsagePointList
31
+ - EndDevice
32
+ - DER
33
+ checks:
34
+ - type: discovered
35
+ parameters:
36
+ resources:
37
+ - DeviceCapability
38
+ - Time
39
+ - MirrorUsagePointList
40
+ - EndDevice
41
+ - DER
42
+ links:
43
+ - ConnectionPoint
44
+ - Registration
45
+ - DERCapability
46
+ - DERSettings
47
+ - DERStatus
48
+ - type: end-device
49
+ parameters:
50
+ matches_client: true
51
+ - type: time-synced
52
+
53
+
54
+ Step Schema:
55
+ ```
56
+ Steps:
57
+ - DESCRIPTIVE_TITLE_OF_STEP: # This is used for display
58
+ action: #
59
+ type: # string identifier of the action type - see table below
60
+ parameters: # Any parameters to modify the default behaviour of the action - see table below
61
+ checks: # A list of Check definitions that will need to be true for this event to trigger - see section on Checks below
62
+ - type: # string identifier of the check type - see table below
63
+ parameters: # Any parameters to modify the default behaviour of the check - see table below
64
+
65
+ client: # The string descriptor of the "Required Client" in the preconditions that will execute this step
66
+ # (Defaults to the first required client)
67
+
68
+ use_client_context: # The string descriptor of the "Required Client" in the preconditions whose context/memory
69
+ # of discovered resources will be used (for testing cross client authentication issues)
70
+ instructions: # List of text strings to render while this test is executing
71
+ repeat_until_pass: # Most steps if failed will abort the test, setting this to true will repeat this step regularly
72
+ # until a pass is recorded (eg - use it to prompt a server to inject a DERControl)
73
+ ```
74
+
75
+
76
+ ### Actions
77
+
78
+ These are the currently defined `Action` elements that can be included in a test.
79
+
80
+ This is an example of an `Action` elements that trigger a client to requests links from the device capability URI until all nominated resources are reached.
81
+
82
+ ```
83
+ action:
84
+ type: discovery
85
+ parameters:
86
+ resources:
87
+ - Time
88
+ - MirrorUsagePointList
89
+ - EndDevice
90
+ - DER
91
+ ```
92
+
93
+
94
+ | **name** | **params** | **description** |
95
+ | -------- | ---------- | --------------- |
96
+ | `discovery` | `resources: list[CSIPAusResource]` `next_polling_window: bool/None` | Performs a full discovery / refresh of the client's context from DeviceCapability downwards, looking to discover the specific resources. Can be delayed until the next polling window. |
97
+ | `notifications` | `collect: bool` `disable: bool` | If `collect`, consumes subscription notifications and inserts them into the current context, if `disable` causes the subscription notification webhook to simulate an outage (return HTTP 5XX) |
98
+ | `wait` | `duration_seconds: int` | Performs no action for the nominated period of time |
99
+ | `refresh-resource` | `resource: CSIPAusResource` `expect_rejection: bool/None` `expect_rejection_or_empty: bool/None` | Forces a particular resource to be refreshed (using existing hrefs in context). Can be set to expect a HTTP 4XX ErrorResponse and/or an empty list resource (if appropriate). |
100
+ | `insert-end-device` | `force_lfdi: str/None` `expect_rejection: bool/None` | Causes the client to submit a new EndDevice registration and resolves the returned Location header |
101
+ | `upsert-connection-point` | `connectionPointId: str` `expect_rejection: bool/None` | Causes the client to submit a new ConnectionPoint with ID for the client's EndDevice |
102
+ | `upsert-mup` | `mup_id: str` `location: CSIPAusReadingLocation` `reading_types: list[CSIPAusReadingType]` `mmr_mrids: list[str]/None` `pow10_multiplier: int/None` `expect_rejection: bool/None` | Submits a MirrorUsagePoint with MirrorMeterReading's. Will ensure stable MRID values for the same sets of parameters (unless overridden with mmr_mrids). `mup_id` will alias this MirrorUsagePoint for future action calls. |
103
+ | `insert-readings` | `mup_id: str` `values: ReadingTypeValues` `expect_rejection: bool/None` | Begins the submission of readings (at MUP post rate, to an earlier call to `upsert-mup` with the same `mup_id`). Will interleave transmission with subsequent steps (non blocking) if multiple sets of values are specified |
104
+ | `upsert-der-status` | `genConnectStatus: int/None` `operationalModeStatus: int/None` `alarmStatus: int/None` `expect_rejection: bool/None` | Sends DERStatus - validates that the server persisted the values correctly |
105
+ | `upsert-der-capability` | `type: int` `rtgMaxW: int` `modesSupported: int` `doeModesSupported: int` | Sends DERCapability - validates that the server persisted the values correctly |
106
+ | `upsert-der-settings` | `type: int` `setMaxW: int` `setGradW: int` `modesEnabled: int` `doeModesEnabled: int` | Sends DERSettings - validates that the server persisted the values correctly |
107
+ | `send-malformed-der-settings` | `updatedTime_missing: bool` `modesEnabled_int: bool` | Sends a malformed DERSettings - expects a failure and that the server will NOT change anything |
108
+ | `send-malformed-response` | `mrid_unknown: bool` `endDeviceLFDI_unknown: bool` `response_invalid: bool` | Sends a malformed Response (using the most recent DERControl replyTo) - expects a failure response |
109
+ | `create-subscription` | `sub_id: str` `resource: CSIPAusResource` | Sends a new Subscription - validates that the server persisted the values correctly via Location. `sub_id` will alias this subscription for future action calls. |
110
+ | `delete-subscription` | `sub_id: str` | Sends a deletion for a previously created Subscription. |
111
+ | `respond-der-controls` | None | Enumerates all known DERControls and sends a Response for any that require it. |
112
+
113
+
114
+ ### Checks
115
+
116
+ A `Check` is a boolean test of what the client has in its current context. They are typically defined as a success/failure condition to be run at the end of a `Step`.
117
+
118
+ | **name** | **params** | **description** |
119
+ | -------- | ---------- | --------------- |
120
+ | `discovered` | `resources: list[CSIPAusResource]` `links: list[CSIPAusResource]` | Does the client's context have the nominated resources (or the parent resource with an appropriate link). |
121
+ | `time-sync` | None | Does the client have a TimeResponse and does it closely map to the client's local time. |
122
+ | `end-device` | `matches_client: bool` `matches_pin` | Is there an EndDevice that matches the client's LFDI (can be negatively asserted) and does it have a specific Registration PIN |
123
+ | `der-program` | `minimum_count: int/None` `maximum_count: int/None` `primacy: int/None` `fsa_index: int/None` | Are there enough DERProgram(s) that satisfy the filter criteria? `fsa_index` matches DERPrograms that belong to the nth (0 based) FunctionSetAssignment |
124
+ | `der-control` | `minimum_count: int/None` `maximum_count: int/None` `latest: bool/None` `opModImpLimW: float/None` `opModExpLimW: float/None` `opModLoadLimW: float/None` `opModGenLimW: float/None` `opModEnergize: bool/None` `opModConnect: bool/None` `opModFixedW: float/None` `rampTms: int/None` `randomizeStart: int/None` `event_status: int/None` `responseRequired: int/None` `derp_primacy: int/None` | Are there enough DERProgram(s) that satisfy the filter criteria? `latest` will ONLY match the most recent DERControl. |
125
+ | `default-der-control` | `opModExpLimW: float/None` `opModLoadLimW: float/None` `opModGenLimW: float/None` `setGradW: int/None` | matches any DefaultDERControl with the specified values |
126
+ | `mirror-usage-point` | `matches: bool` `location: CSIPAusReadingLocation/None` `reading_types: list[CSIPAusReadingType]/None` `mmr_mrids: list[str]/None` `post_rate_seconds: int/None` | Does a MirrorUsagePoint exist with the specified values (or not exist if `matches` is false). Only asserts specified values. |
127
+ | `subscription` | `matches: bool` `resource: CSIPAusResource`| Does a Subscription exist for the specified resource (or not exist if `matches` is false). |
128
+ | `poll-rate` | `resource: CSIPAusResource` `poll_rate_seconds: int` | Does the nominated resource have the specified poll rate. |
129
+
130
+
131
+ ### Parameter Variable Resolution
132
+
133
+ Any `parameter` element expects a series of name/value pairs to pass to the "parent" `Action` or `Check` . For example:
134
+
135
+ ```
136
+ parameters:
137
+ number_param: 123
138
+ text_param: Text Content
139
+ date_param: 2020-01-02 03:04:05Z
140
+ csip_aus_resource: EndDeviceList
141
+
142
+ ```
143
+
144
+ But placeholder variables may also be used to reference things that aren't known until the test is underway. For example, the following would instead set `number_param` to the current setMaxW supplied by the client while `date_param` would be set to the moment in time that the `Action`, `Check` or `Event` is being evaluated.
145
+
146
+ ```
147
+ parameters:
148
+ number_param: $setMaxW
149
+ text_param: Text Content
150
+ date_param: $now
151
+
152
+ ```
153
+
154
+ The following are all the `NamedVariable` types currently implemented (these are distinct from the named variables
155
+ defined in the client test procedures)
156
+
157
+ | **name** | **description** |
158
+ | -------- | --------------- |
159
+ | `$now` | Resolves to the current moment in time (timezone aware). Returns a datetime |
160
+ | `$setMaxW` | Resolves to the current client configuration value for `DERSetting.setMaxW` as a number. |
161
+
162
+
163
+ Expressions are also supported.
164
+
165
+ ```
166
+ parameters:
167
+ number_param: $(setMaxW / 2)
168
+ text_param: Text Content
169
+ date_param: $(now - '5 mins')
170
+ ```
@@ -0,0 +1,139 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
5
+ from cactus_test_definitions.parameters import (
6
+ ParameterSchema,
7
+ ParameterType,
8
+ validate_parameters,
9
+ )
10
+ from cactus_test_definitions.variable_expressions import (
11
+ parse_variable_expression_body,
12
+ try_extract_variable_expression,
13
+ )
14
+
15
+
16
+ @dataclass
17
+ class Action:
18
+ type: str
19
+ client: str | None = None # use the client with this id to execute this action. If None, use the 0th client
20
+ parameters: dict[str, Any] = None # type: ignore # This will be forced in __post_init__
21
+
22
+ def __post_init__(self):
23
+ """Some parameter values might contain variable expressions (eg: a string "$now") that needs to be replaced
24
+ with an parsed Expression object instead."""
25
+ if self.parameters is None:
26
+ self.parameters = {}
27
+
28
+ for k, v in self.parameters.items():
29
+ variable_expr = try_extract_variable_expression(v)
30
+ if variable_expr:
31
+ self.parameters[k] = parse_variable_expression_body(variable_expr, k)
32
+
33
+
34
+ # The parameter schema for each action, keyed by the action name
35
+ ACTION_PARAMETER_SCHEMA: dict[str, dict[str, ParameterSchema]] = {
36
+ "discovery": {
37
+ "resources": ParameterSchema(True, ParameterType.ListCSIPAusResource), # What resources to try and resolve?
38
+ "next_polling_window": ParameterSchema(
39
+ False, ParameterType.Boolean
40
+ ), # If set - delay this until the upcoming polling window (eg- wait for the next whole minute)
41
+ }, # Performs a full discovery / refresh of the client's context from DeviceCapability downwards
42
+ "notifications": {
43
+ "collect": ParameterSchema(
44
+ False, ParameterType.Boolean
45
+ ), # Collects latest subscription notifications into context
46
+ "disable": ParameterSchema(False, ParameterType.Boolean), # Simulates HTTP 5XX outage at the endpoint
47
+ },
48
+ "wait": {
49
+ "duration_seconds": ParameterSchema(True, ParameterType.Integer)
50
+ }, # Waits (doing nothing - blocking other step actions) until the specified time period has passed
51
+ "comms-status": {
52
+ "notifications_enabled": ParameterSchema(True, ParameterType.Boolean) # Enable/Disble notification webhook
53
+ }, # Enables or disables certain communications
54
+ "refresh-resource": {
55
+ "resource": ParameterSchema(True, ParameterType.CSIPAusResource),
56
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # if set - expect 4XX and ErrorPayload
57
+ "expect_rejection_or_empty": ParameterSchema(
58
+ False, ParameterType.Boolean
59
+ ), # Similar to expect_rejection but also allow en empty list (if it's a list resource)
60
+ }, # Force an existing resource (in the client's context) to be re-fetched via href. Updates context on success
61
+ "insert-end-device": {
62
+ "force_lfdi": ParameterSchema(False, ParameterType.String), # Forces the use of this LFDI
63
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # If set - expect 4XX and ErrorPayload
64
+ }, # Inserts an EndDevice and then validates the returned Location header
65
+ "upsert-connection-point": {
66
+ "connectionPointId": ParameterSchema(True, ParameterType.String),
67
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # If set - expect ErrorPayload reasonCode 1
68
+ },
69
+ "upsert-mup": {
70
+ "mup_id": ParameterSchema(True, ParameterType.String), # Used to alias the returned MUP ID
71
+ "location": ParameterSchema(True, ParameterType.CSIPAusReadingLocation),
72
+ "reading_types": ParameterSchema(True, ParameterType.ListCSIPAusReadingType),
73
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # If set - expect 4XX and ErrorPayload
74
+ "mmr_mrids": ParameterSchema(
75
+ False, ParameterType.ListString
76
+ ), # Must correspond 1-1 with reading_types. Used for forcing specific mrid values
77
+ "pow10_multiplier": ParameterSchema(
78
+ False, ParameterType.Integer
79
+ ), # Force the use a particular pow10. Defaults to 0 otherwise
80
+ }, # Register a MUP with the specified values. MMR's based on hash of current client / reading types
81
+ "insert-readings": {
82
+ "mup_id": ParameterSchema(True, ParameterType.String), # Must be previously defined with register-mup
83
+ "values": ParameterSchema(
84
+ True, ParameterType.ReadingTypeValues
85
+ ), # The sequences of values to send at the MUP post rate
86
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # If set - expect 4XX and ErrorPayload
87
+ }, # Sends readings - validates that the telemetry is parsed correctly by the server
88
+ "upsert-der-status": {
89
+ "genConnectStatus": ParameterSchema(False, ParameterType.Integer),
90
+ "operationalModeStatus": ParameterSchema(False, ParameterType.Integer),
91
+ "alarmStatus": ParameterSchema(False, ParameterType.Integer),
92
+ "expect_rejection": ParameterSchema(False, ParameterType.Boolean), # If set - expect 4XX and ErrorPayload
93
+ }, # Sends DERStatus - validates that the server persisted the values correctly
94
+ "upsert-der-capability": {
95
+ "type": ParameterSchema(True, ParameterType.Integer),
96
+ "rtgMaxW": ParameterSchema(True, ParameterType.Integer),
97
+ "modesSupported": ParameterSchema(True, ParameterType.Integer),
98
+ "doeModesSupported": ParameterSchema(True, ParameterType.Integer),
99
+ }, # Sends DERCapability - validates that the server persisted the values correctly
100
+ "upsert-der-settings": {
101
+ "setMaxW": ParameterSchema(True, ParameterType.Integer),
102
+ "setGradW": ParameterSchema(True, ParameterType.Integer),
103
+ "modesEnabled": ParameterSchema(True, ParameterType.Integer),
104
+ "doeModesEnabled": ParameterSchema(True, ParameterType.Integer),
105
+ }, # Sends DERSettings - validates that the server persisted the values correctly
106
+ "send-malformed-der-settings": {
107
+ "updatedTime_missing": ParameterSchema(True, ParameterType.Boolean), # If true - updatedTime will be stripped
108
+ "modesEnabled_int": ParameterSchema(True, ParameterType.Boolean), # If true - modesEnabled will send an int
109
+ }, # Sends a malformed DERSettings - expects a failure and that the server will NOT change anything
110
+ "send-malformed-response": {
111
+ "mrid_unknown": ParameterSchema(True, ParameterType.Boolean), # If true - mrid will be random
112
+ "endDeviceLFDI_unknown": ParameterSchema(True, ParameterType.Boolean), # If true - endDeviceLfdi will be random
113
+ "response_invalid": ParameterSchema(True, ParameterType.Boolean), # If true - response will be a reserved value
114
+ }, # Sends a malformed Response (using the most recent DERControl replyTo) - expects a failure response
115
+ "create-subscription": {
116
+ "sub_id": ParameterSchema(True, ParameterType.String), # Used to alias the returned subscription ID
117
+ "resource": ParameterSchema(True, ParameterType.CSIPAusResource),
118
+ }, # Sends a new Subscription - validates that the server persisted the values correctly via Location
119
+ "delete-subscription": {
120
+ "sub_id": ParameterSchema(True, ParameterType.String), # Must match a previously
121
+ }, # Sends a Subscription deletion
122
+ "respond-der-controls": {}, # Enumerates all known DERControls and sends a Response for any that require it
123
+ }
124
+ VALID_ACTION_NAMES: set[str] = set(ACTION_PARAMETER_SCHEMA.keys())
125
+
126
+
127
+ def validate_action_parameters(procedure_name: str, step_name: int, action: Action) -> None:
128
+ """Validates the action parameters for the parent TestProcedure based on the ACTION_PARAMETER_SCHEMA
129
+
130
+ raises TestProcedureDefinitionError on failure"""
131
+ location = f"{procedure_name}.step[{step_name}]" # Descriptive location
132
+
133
+ parameter_schema = ACTION_PARAMETER_SCHEMA.get(action.type, None)
134
+ if parameter_schema is None:
135
+ raise TestProcedureDefinitionError(
136
+ f"{location} has an invalid action name '{action.type}'. Valid Names: {VALID_ACTION_NAMES}"
137
+ )
138
+
139
+ validate_parameters(location, action.parameters, parameter_schema)
@@ -0,0 +1,117 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
5
+ from cactus_test_definitions.parameters import (
6
+ ParameterSchema,
7
+ ParameterType,
8
+ validate_parameters,
9
+ )
10
+ from cactus_test_definitions.variable_expressions import (
11
+ parse_variable_expression_body,
12
+ try_extract_variable_expression,
13
+ )
14
+
15
+
16
+ @dataclass
17
+ class Check:
18
+ """A check represents some validation logic that runs during a Test Step and provides a pass/fail result with a
19
+ description. It will typically inspect the state of the client based on what it has seen from the server
20
+
21
+ eg: Ensuring that the client was able to see an EndDevice registration"""
22
+
23
+ type: str
24
+ parameters: dict[str, Any] = None # type: ignore # This will be forced in __post_init__
25
+
26
+ def __post_init__(self):
27
+ """Some parameter values might contain variable expressions (eg: a string "$now") that needs to be replaced
28
+ with an parsed Expression object instead."""
29
+ if self.parameters is None:
30
+ self.parameters = {}
31
+ for k, v in self.parameters.items():
32
+ variable_expr = try_extract_variable_expression(v)
33
+ if variable_expr:
34
+ self.parameters[k] = parse_variable_expression_body(variable_expr, k)
35
+
36
+
37
+ # The parameter schema for each action, keyed by the action name
38
+ CHECK_PARAMETER_SCHEMA: dict[str, dict[str, ParameterSchema]] = {
39
+ "discovered": {
40
+ "resources": ParameterSchema(False, ParameterType.ListCSIPAusResource),
41
+ "links": ParameterSchema(False, ParameterType.ListCSIPAusResource),
42
+ },
43
+ "time-synced": {}, # Passes if the current Time resource is synced with this client's date/time
44
+ "end-device": {
45
+ "matches_client": ParameterSchema(
46
+ True, ParameterType.Boolean
47
+ ), # assert the existence / non existence of an EndDevice for the current client
48
+ "matches_pin": ParameterSchema(
49
+ False, ParameterType.Boolean
50
+ ), # if set - The matches_client criteria will ALSO check the registration PIN for the EndDevice. Default False
51
+ },
52
+ "der-program": {
53
+ "minimum_count": ParameterSchema(False, ParameterType.Integer), # Needs at least this many derps to pass
54
+ "maximum_count": ParameterSchema(False, ParameterType.Integer), # Needs at most this many derps to pass
55
+ "primacy": ParameterSchema(False, ParameterType.Integer), # Filters derps based on this primacy value
56
+ "fsa_index": ParameterSchema(
57
+ False, ParameterType.Integer
58
+ ), # Filters derps that belong to the nth (0 based) FunctionSetAssignment index
59
+ },
60
+ "der-control": {
61
+ "minimum_count": ParameterSchema(False, ParameterType.Integer), # Needs at least this many controls to pass
62
+ "maximum_count": ParameterSchema(False, ParameterType.Integer), # Needs at most this many controls to pass
63
+ "latest": ParameterSchema(False, ParameterType.Boolean), # forces filter checks against the most recent control
64
+ "opModImpLimW": ParameterSchema(False, ParameterType.Float), # Filters controls based on this value
65
+ "opModExpLimW": ParameterSchema(False, ParameterType.Float), # Filters controls based on this value
66
+ "opModLoadLimW": ParameterSchema(False, ParameterType.Float), # Filters controls based on this value
67
+ "opModGenLimW": ParameterSchema(False, ParameterType.Float), # Filters controls based on this value
68
+ "opModEnergize": ParameterSchema(False, ParameterType.Boolean), # Filters controls based on this value
69
+ "opModConnect": ParameterSchema(False, ParameterType.Boolean), # Filters controls based on this value
70
+ "opModFixedW": ParameterSchema(False, ParameterType.Float), # Filters controls based on this value
71
+ "rampTms": ParameterSchema(False, ParameterType.Integer), # Filter on this val. 0 means negative assertion
72
+ "randomizeStart": ParameterSchema(False, ParameterType.Integer), # Filter on this val (in seconds)
73
+ "event_status": ParameterSchema(False, ParameterType.Integer), # Filter on Event.status value
74
+ "responseRequired": ParameterSchema(False, ParameterType.Integer), # Filter on responseRequired value
75
+ "derp_primacy": ParameterSchema(
76
+ False, ParameterType.Integer
77
+ ), # Filter to control's belonging to a DERProgram with this primacy value
78
+ }, # Matches many DERControls (specified by minimum_count) against additional other filter criteria
79
+ "default-der-control": {
80
+ "opModImpLimW": ParameterSchema(False, ParameterType.Float),
81
+ "opModExpLimW": ParameterSchema(False, ParameterType.Float),
82
+ "opModGenLimW": ParameterSchema(False, ParameterType.Float),
83
+ "opModLoadLimW": ParameterSchema(False, ParameterType.Float),
84
+ "setGradW": ParameterSchema(False, ParameterType.Integer), # Hundredths of a percent / second
85
+ }, # matches any DefaultDERControl with the specified values
86
+ "mirror-usage-point": {
87
+ "matches": ParameterSchema(True, ParameterType.Boolean), # True for positive assert, False for negative assert
88
+ "location": ParameterSchema(False, ParameterType.CSIPAusReadingLocation), # If not specified - match anything
89
+ "reading_types": ParameterSchema(False, ParameterType.ListCSIPAusReadingType), # If not specified - match all
90
+ "mmr_mrids": ParameterSchema(
91
+ False, ParameterType.ListString
92
+ ), # Must correspond 1-1 with reading_types. Used for forcing specific mrid values
93
+ "post_rate_seconds": ParameterSchema(False, ParameterType.Integer), # Only asserted if specified
94
+ }, # True if the matches assertion finds a MirrorUsagePoint with the specified parameters (requires exact match)
95
+ "subscription": {
96
+ "matches": ParameterSchema(True, ParameterType.Boolean), # True for positive assert, False for negative assert
97
+ "resource": ParameterSchema(True, ParameterType.CSIPAusResource),
98
+ }, # Matches the existence/nonexistence of a subscription for the specified resource
99
+ "poll-rate": {
100
+ "resource": ParameterSchema(True, ParameterType.CSIPAusResource),
101
+ "poll_rate_seconds": ParameterSchema(True, ParameterType.Integer),
102
+ }, # Asserts a specific poll rate value
103
+ }
104
+ VALID_CHECK_NAMES: set[str] = set(CHECK_PARAMETER_SCHEMA.keys())
105
+
106
+
107
+ def validate_check_parameters(procedure_name: str, check: Check) -> None:
108
+ """Validates the check parameters for the parent TestProcedure based on the CHECK_PARAMETER_SCHEMA
109
+
110
+ raises TestProcedureDefinitionError on failure"""
111
+ location = f"{procedure_name} Check: {check.type}" # Descriptive location of this action being validated
112
+
113
+ parameter_schema = CHECK_PARAMETER_SCHEMA.get(check.type, None)
114
+ if parameter_schema is None:
115
+ raise TestProcedureDefinitionError(f"{location} not a valid action name. {VALID_CHECK_NAMES}")
116
+
117
+ validate_parameters(location, check.parameters, parameter_schema)
@@ -0,0 +1,42 @@
1
+ Description: Discovery with Out-Of-Band registration
2
+ Category: Registration
3
+ Classes:
4
+ - A
5
+
6
+ TargetVersions:
7
+ - v1.2
8
+
9
+ Preconditions:
10
+ required_clients:
11
+ - id: client
12
+
13
+ Steps:
14
+ - id: DISCOVERY
15
+ action:
16
+ type: discovery
17
+ parameters:
18
+ resources:
19
+ - DeviceCapability
20
+ - Time
21
+ - MirrorUsagePointList
22
+ - EndDevice
23
+ - DER
24
+ checks:
25
+ - type: discovered
26
+ parameters:
27
+ resources:
28
+ - DeviceCapability
29
+ - Time
30
+ - MirrorUsagePointList
31
+ - EndDevice
32
+ - DER
33
+ links:
34
+ - ConnectionPoint
35
+ - Registration
36
+ - DERCapability
37
+ - DERSettings
38
+ - DERStatus
39
+ - type: end-device
40
+ parameters:
41
+ matches_client: true
42
+ - type: time-synced