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,61 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from cactus_test_definitions.client import TestProcedureConfig
6
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
7
+ from cactus_test_definitions.variable_expressions import (
8
+ Constant,
9
+ Expression,
10
+ NamedVariable,
11
+ NamedVariableType,
12
+ OperationType,
13
+ )
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "filename",
18
+ [
19
+ # No 'steps' parameters defined for enable-steps action (NOT-A-VALID-PARAMETER-NAME supplied instead)
20
+ "tests/data/config_with_errors1.yaml",
21
+ # Action refers to step "NOT-A-VALID-STEP"
22
+ "tests/data/config_with_errors2.yaml",
23
+ ],
24
+ )
25
+ def test_TestProcedures_validate_raises_exception(filename: str):
26
+
27
+ with pytest.raises(TestProcedureDefinitionError):
28
+ _ = TestProcedureConfig.from_yamlfile(path=Path(filename))
29
+
30
+
31
+ def test_TestProcedures_action_parameter_types():
32
+ """Tests that lists/dicts/constants/datetimes can all be encoded/decoded via the yaml action definition"""
33
+ cfg = TestProcedureConfig.from_yamlfile(path=Path("tests/data/config_action_parameters.yaml"), skip_validation=True)
34
+ test_proc = cfg.test_procedures["CUSTOM-01"]
35
+ step = test_proc.steps["Step-1"]
36
+ action = step.actions[0]
37
+
38
+ assert action.parameters == {
39
+ "param_list_str": ["List Item 1", "List Item 2", "List Item 3"],
40
+ "param_list_int": [11, 22, 33],
41
+ "param_list_mixed": [
42
+ 11,
43
+ 2.2,
44
+ "List Item 3",
45
+ datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
46
+ ],
47
+ "param_dict_str": {"key1": "value1", "key2": "value2"},
48
+ "param_dict_int": {"key11": 11, "key22": 22},
49
+ "param_int": 11,
50
+ "param_str": "Value 11",
51
+ "param_datetime_utc": datetime(2025, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
52
+ "param_datetime_naive": datetime(2025, 1, 2, 3, 4, 5, tzinfo=None),
53
+ "param_with_variable_date": NamedVariable(NamedVariableType.NOW),
54
+ "param_with_variable_db_lookup": NamedVariable(NamedVariableType.DERSETTING_SET_MAX_W),
55
+ "param_with_variable_relative_db_lookup": Expression(
56
+ OperationType.MULTIPLY, Constant(0.25), NamedVariable(NamedVariableType.DERSETTING_SET_MAX_W)
57
+ ),
58
+ "param_with_variable_relative_date": Expression(
59
+ OperationType.SUBTRACT, NamedVariable(NamedVariableType.NOW), Constant(timedelta(minutes=5))
60
+ ),
61
+ }
@@ -0,0 +1,36 @@
1
+ import pytest
2
+ from cactus_test_definitions import variable_expressions as varexps
3
+ from cactus_test_definitions.client.events import Event, validate_event_parameters
4
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
5
+
6
+
7
+ @pytest.mark.parametrize(
8
+ "event, is_valid",
9
+ [
10
+ (Event("foo", {}), False), # Unknown check
11
+ (Event("wait", {}), False),
12
+ (Event("wait", {"duration_seconds": "3"}), False),
13
+ (Event("wait", {"duration_seconds": 3}), True),
14
+ (Event("wait", {"duration_seconds": 3, "other": 4}), False),
15
+ (Event("GET-request-received", {"endpoint": "/dcap"}), True),
16
+ ],
17
+ )
18
+ def test_validate_event_parameters(event: Event, is_valid: bool):
19
+ if is_valid:
20
+ validate_event_parameters("foo", "bar", event)
21
+ else:
22
+ with pytest.raises(TestProcedureDefinitionError):
23
+ validate_event_parameters("foo", "bar", event)
24
+
25
+
26
+ def test_event_expression() -> None:
27
+ """Tests the creation of an Event that has an expression as one of its parameters"""
28
+ type_str = "some_event"
29
+ params = {"setMaxW": "$(this < rtgMaxW)"}
30
+ event = Event(type_str, params)
31
+
32
+ check_set_max_w = event.parameters["setMaxW"]
33
+ assert isinstance(check_set_max_w, varexps.Expression)
34
+ assert check_set_max_w.operation == varexps.OperationType.LT
35
+ assert check_set_max_w.lhs_operand == varexps.NamedVariable(varexps.NamedVariableType.DERSETTING_SET_MAX_W)
36
+ assert check_set_max_w.rhs_operand == varexps.NamedVariable(varexps.NamedVariableType.DERCAPABILITY_RTG_MAX_W)
@@ -0,0 +1,150 @@
1
+ import pytest
2
+ from cactus_test_definitions.client.test_procedures import (
3
+ TestProcedure,
4
+ TestProcedureConfig,
5
+ TestProcedureId,
6
+ )
7
+ from cactus_test_definitions.variable_expressions import (
8
+ NamedVariableType,
9
+ has_named_variable,
10
+ )
11
+
12
+ # Failures here will raise an issue in the test_from_yamlfile test
13
+ ALL_TEST_PROCEDURES: list[tuple[str, TestProcedure]] = []
14
+ try:
15
+ ALL_TEST_PROCEDURES = [(k, tp) for k, tp in TestProcedureConfig.from_resource().test_procedures.items()]
16
+ except Exception:
17
+ pass
18
+
19
+
20
+ def test_from_yamlfile():
21
+ """This test confirms the standard test procedure yaml file (intended for production use)
22
+ can be read and converted to the appropriate dataclasses.
23
+ """
24
+ test_procedures = TestProcedureConfig.from_resource()
25
+ test_procedures.validate()
26
+
27
+
28
+ def test_TestProcedureId_synchronised():
29
+ """Ensures that TestProcedureId is in sync with all available TestProcedures"""
30
+ available_tests = set(TestProcedureConfig.available_tests())
31
+ for t in available_tests:
32
+ assert t in TestProcedureId, "TestProcedureConfig has a procedure not encoded in TestProcedureId"
33
+
34
+ # By convention - test ALL-01 will be an enum ALL_01
35
+ # for t in (t.replace("_", "-") for t in TestProcedureId):
36
+ for t in TestProcedureId:
37
+ assert t.value in available_tests, "TestProcedureId has extra procedures not found in TestProcedureConfig"
38
+
39
+
40
+ def test_available_tests_populated():
41
+ """Force test procedures to load and ensure they all validate (and we at least have a few)"""
42
+ assert len(TestProcedureConfig.available_tests()) > 0
43
+
44
+
45
+ @pytest.mark.parametrize("tp_id, tp", ALL_TEST_PROCEDURES)
46
+ def test_each_step_accessible(tp_id: str, tp: TestProcedure):
47
+ """Ensures that each test procedure's steps are enabled/removed once. Failures here indicate a bad reference to
48
+ enable/remove steps"""
49
+
50
+ first_step = [k for k in tp.steps.keys()][0]
51
+ step_names = set(tp.steps.keys())
52
+
53
+ removal_count = dict([(s, 0) for s in step_names])
54
+ enabled_count = dict([(s, 0) for s in step_names])
55
+
56
+ # Tally up the number of times each step is added/removed via actions
57
+ for s in tp.steps.values():
58
+ for enabled_name in [name for a in s.actions if a.type == "enable-steps" for name in a.parameters["steps"]]:
59
+ enabled_count[enabled_name] = enabled_count[enabled_name] + 1
60
+ for removed_name in [name for a in s.actions if a.type == "remove-steps" for name in a.parameters["steps"]]:
61
+ removal_count[removed_name] = removal_count[removed_name] + 1
62
+
63
+ # Also check the preconditions for actions
64
+ if tp.preconditions and tp.preconditions.actions:
65
+ for enabled_name in [
66
+ name for a in tp.preconditions.actions if a.type == "enable-steps" for name in a.parameters["steps"]
67
+ ]:
68
+ enabled_count[enabled_name] = enabled_count[enabled_name] + 1
69
+ for removed_name in [
70
+ name for a in tp.preconditions.actions if a.type == "remove-steps" for name in a.parameters["steps"]
71
+ ]:
72
+ removal_count[removed_name] = removal_count[removed_name] + 1
73
+
74
+ # Each test should be added once and removed once
75
+ for step_name in step_names:
76
+ assert (
77
+ removal_count[step_name] == 1
78
+ ), f"{step_name}: Each test should be removed once otherwise the test cannot complete"
79
+
80
+ if step_name == first_step:
81
+ assert enabled_count[step_name] == 0, f"{step_name}: The first test step is already enabled"
82
+ else:
83
+ assert (
84
+ enabled_count[step_name] == 1
85
+ ), f"{step_name}: Each test should be added once - not expecting loops of steps"
86
+
87
+
88
+ @pytest.mark.parametrize("tp_id, tp", ALL_TEST_PROCEDURES)
89
+ def test_each_step_connected(tp_id: str, tp: TestProcedure):
90
+ """Interprets each test a graph of steps linking to other steps (via enable-steps). Passes if the test procedure
91
+ is fully connected from the first step (i.e. there are no isolated islands of steps that can't be reached from
92
+ the first step)"""
93
+
94
+ first_step = [k for k in tp.steps.keys()][0]
95
+ step_names = set(tp.steps.keys())
96
+
97
+ step_links_by_name: dict[str, list[str]] = {}
98
+ for step_name in step_names:
99
+ step = tp.steps[step_name]
100
+ child_step_names = [name for a in step.actions if a.type == "enable-steps" for name in a.parameters["steps"]]
101
+ step_links_by_name[step_name] = child_step_names
102
+
103
+ # Walk through our simple graph/tree - make sure we visit every node in that tree
104
+ def gather_connected_steps(links: dict[str, list[str]], node: str, visited_nodes: set[str]) -> None:
105
+ if node in visited_nodes:
106
+ return
107
+ visited_nodes.add(node)
108
+ for child in links[node]:
109
+ gather_connected_steps(links, child, visited_nodes)
110
+
111
+ visited_nodes: set[str] = set()
112
+ gather_connected_steps(step_links_by_name, first_step, visited_nodes)
113
+
114
+ # Also note that steps might be started as a precondition - so include those as visited too
115
+ if tp.preconditions and tp.preconditions.actions:
116
+ for n in [name for a in tp.preconditions.actions if a.type == "enable-steps" for name in a.parameters["steps"]]:
117
+ visited_nodes.add(n)
118
+
119
+ assert (
120
+ step_names == visited_nodes
121
+ ), "Missing entries here indicate a step (or steps) that can't be reached from the root node"
122
+
123
+
124
+ @pytest.mark.parametrize("_, tp", ALL_TEST_PROCEDURES)
125
+ def test_procedures_have_required_preconditions(_: str, tp: TestProcedure):
126
+
127
+ # Immediate start implies that EndDevice registration will happen during the test body - these tests
128
+ # don't require end-device-contents (as it will cause the test to fail anyway)
129
+ is_immediate_start = tp.preconditions is not None and tp.preconditions.immediate_start
130
+ if not is_immediate_start:
131
+ assert tp.preconditions is not None, "Expected precondition check 'end-device-contents'"
132
+ assert tp.preconditions.checks is not None, "Expected precondition check 'end-device-contents'"
133
+ assert any(
134
+ [check.type == "end-device-contents" for check in tp.preconditions.checks]
135
+ ), "Expected precondition check 'end-device-contents'"
136
+
137
+ # Check 'der-settings-contents' present if any precondition action parameter references setMaxW
138
+ if tp.preconditions is not None and tp.preconditions.actions is not None:
139
+ for action in tp.preconditions.actions:
140
+ if action.parameters is not None:
141
+ if any(
142
+ [
143
+ has_named_variable(
144
+ parameter_value=parameter, named_variable=NamedVariableType.DERSETTING_SET_MAX_W
145
+ )
146
+ for parameter in action.parameters.values()
147
+ ]
148
+ ):
149
+ assert tp.preconditions.checks is not None
150
+ assert any([check.type == "der-settings-contents" for check in tp.preconditions.checks])
File without changes
@@ -0,0 +1,86 @@
1
+ import pytest
2
+ from cactus_test_definitions.server.actions import ACTION_PARAMETER_SCHEMA
3
+ from cactus_test_definitions.server.test_procedures import (
4
+ TestProcedure,
5
+ TestProcedureConfig,
6
+ TestProcedureId,
7
+ )
8
+
9
+ # Failures here will raise an issue in the test_from_yamlfile test
10
+ ALL_TEST_PROCEDURES: list[tuple[str, TestProcedure]] = []
11
+ try:
12
+ ALL_TEST_PROCEDURES = [(k, tp) for k, tp in TestProcedureConfig.from_resource().test_procedures.items()]
13
+ except Exception:
14
+ pass
15
+
16
+
17
+ def test_ALL_TEST_PROCEDURES_parsed():
18
+ assert len(ALL_TEST_PROCEDURES) > 0
19
+
20
+
21
+ def test_from_yamlfile():
22
+ """This test confirms the standard test procedure yaml file (intended for production use)
23
+ can be read and converted to the appropriate dataclasses.
24
+ """
25
+ test_procedures = TestProcedureConfig.from_resource()
26
+ test_procedures.validate()
27
+
28
+
29
+ def test_TestProcedureId_synchronised():
30
+ """Ensures that TestProcedureId is in sync with all available TestProcedures"""
31
+ available_tests = set(TestProcedureConfig.available_tests())
32
+ for t in available_tests:
33
+ assert t in TestProcedureId, "TestProcedureConfig has a procedure not encoded in TestProcedureId"
34
+
35
+ # By convention - test ALL-01 will be an enum ALL_01
36
+ # for t in (t.replace("_", "-") for t in TestProcedureId):
37
+ for t in TestProcedureId:
38
+ assert t.value in available_tests, "TestProcedureId has extra procedures not found in TestProcedureConfig"
39
+
40
+
41
+ def test_available_tests_populated():
42
+ """Force test procedures to load and ensure they all validate (and we at least have a few)"""
43
+ assert len(TestProcedureConfig.available_tests()) > 0
44
+
45
+
46
+ @pytest.mark.parametrize("tp_id, tp", ALL_TEST_PROCEDURES)
47
+ def test_each_step_id_unique(tp_id: str, tp: TestProcedure):
48
+ all_ids = [s.id for s in tp.steps]
49
+ assert list(sorted(all_ids)) == list(sorted(set(all_ids)))
50
+ assert len(set((s.id for s in tp.steps))) == len(tp.steps), "All steps must have a unique id property"
51
+
52
+
53
+ @pytest.mark.parametrize("tp_id, tp", ALL_TEST_PROCEDURES)
54
+ def test_each_alias_defined(tp_id: str, tp: TestProcedure):
55
+ """Ensures that each test procedure's steps that have actions using an alias... define those aliases in advance"""
56
+
57
+ # sanity check - make sure we are looking for the action names that are valid
58
+ UPSERT_MUP_ACTION = "upsert-mup"
59
+ INSERT_MUP_ACTION = "insert-readings"
60
+ CREATE_SUB_ACTION = "create-subscription"
61
+ DELETE_SUB_ACTION = "delete-subscription"
62
+ assert UPSERT_MUP_ACTION in ACTION_PARAMETER_SCHEMA, "If this fails - the action name has changed. Update this test"
63
+ assert INSERT_MUP_ACTION in ACTION_PARAMETER_SCHEMA, "If this fails - the action name has changed. Update this test"
64
+ assert CREATE_SUB_ACTION in ACTION_PARAMETER_SCHEMA, "If this fails - the action name has changed. Update this test"
65
+ assert DELETE_SUB_ACTION in ACTION_PARAMETER_SCHEMA, "If this fails - the action name has changed. Update this test"
66
+
67
+ mup_aliases_found: set[str] = set()
68
+ sub_aliases_found: set[str] = set()
69
+ for step in tp.steps:
70
+ if step.action.parameters:
71
+ mup_id = step.action.parameters.get("mup_id", None)
72
+ sub_id = step.action.parameters.get("sub_id", None)
73
+ else:
74
+ mup_id = None
75
+ sub_id = None
76
+
77
+ if step.action.type == UPSERT_MUP_ACTION:
78
+ assert mup_id and (mup_id not in mup_aliases_found), "mup_id is already defined in this test."
79
+ mup_aliases_found.add(mup_id)
80
+ elif step.action.type == INSERT_MUP_ACTION:
81
+ assert mup_id and (mup_id in mup_aliases_found), "mup_id hasn't been defined yet."
82
+ elif step.action.type == CREATE_SUB_ACTION:
83
+ assert sub_id and (sub_id not in sub_aliases_found), "sub_id is already defined in this test."
84
+ sub_aliases_found.add(sub_id)
85
+ elif step.action.type == DELETE_SUB_ACTION:
86
+ assert sub_id and (sub_id in sub_aliases_found), "sub_id hasn't been defined yet."
@@ -0,0 +1,49 @@
1
+ import pytest
2
+ from cactus_test_definitions.csipaus import (
3
+ CSIPAusReadingLocation,
4
+ CSIPAusReadingType,
5
+ CSIPAusResource,
6
+ is_list_resource,
7
+ )
8
+
9
+
10
+ @pytest.mark.parametrize("enum_val", CSIPAusResource)
11
+ def test_CSIPAusResource_name_matches_value(enum_val: CSIPAusResource):
12
+ """We want case varying enum values (to match the values in CSIP Aus). This means we need to manage the names
13
+ manually and ensures we don't end up with a bad copy paste"""
14
+ assert enum_val.value == enum_val.name
15
+
16
+
17
+ @pytest.mark.parametrize("enum_val", CSIPAusReadingLocation)
18
+ def test_CSIPAusReadingLocation_name_matches_value(enum_val: CSIPAusReadingLocation):
19
+ """We want case varying enum values (to match the values in CSIP Aus). This means we need to manage the names
20
+ manually and ensures we don't end up with a bad copy paste"""
21
+ assert enum_val.value == enum_val.name
22
+
23
+
24
+ @pytest.mark.parametrize("enum_val", CSIPAusReadingType)
25
+ def test_CSIPAusReadingType_name_matches_value(enum_val: CSIPAusReadingType):
26
+ """We want case varying enum values (to match the values in CSIP Aus). This means we need to manage the names
27
+ manually and ensures we don't end up with a bad copy paste"""
28
+ assert enum_val.value == enum_val.name
29
+
30
+
31
+ @pytest.mark.parametrize(
32
+ "resource, expected",
33
+ [
34
+ (CSIPAusResource.ConnectionPoint, False),
35
+ (CSIPAusResource.DeviceCapability, False),
36
+ (CSIPAusResource.DERSettings, False),
37
+ (CSIPAusResource.EndDeviceList, True),
38
+ (CSIPAusResource.EndDevice, False),
39
+ (CSIPAusResource.DERList, True),
40
+ (CSIPAusResource.DERProgramList, True),
41
+ (CSIPAusResource.MirrorUsagePointList, True),
42
+ (CSIPAusResource.DERControlList, True),
43
+ (CSIPAusResource.DERControl, False),
44
+ ],
45
+ )
46
+ def test_is_list_resource(resource: CSIPAusResource, expected: bool):
47
+ actual = is_list_resource(resource)
48
+ assert isinstance(actual, bool)
49
+ assert actual is expected
@@ -0,0 +1,197 @@
1
+ from datetime import datetime, timezone
2
+ from decimal import Decimal
3
+ from itertools import product
4
+ from typing import Any
5
+
6
+ import pytest
7
+ from cactus_test_definitions.errors import TestProcedureDefinitionError
8
+ from cactus_test_definitions.parameters import (
9
+ ParameterSchema,
10
+ ParameterType,
11
+ is_valid_parameter_type,
12
+ validate_parameters,
13
+ )
14
+ from cactus_test_definitions.variable_expressions import (
15
+ Constant,
16
+ Expression,
17
+ NamedVariable,
18
+ NamedVariableType,
19
+ OperationType,
20
+ )
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ "value, type",
25
+ product(
26
+ [
27
+ None,
28
+ Expression(OperationType.ADD, Constant(1), Constant(2)),
29
+ Constant(1),
30
+ NamedVariable(NamedVariableType.NOW),
31
+ ],
32
+ list(ParameterType),
33
+ ),
34
+ )
35
+ def test_is_valid_parameter_type_skipped_values(value: Any, type: ParameterType):
36
+ """Tests that a number of values aren't considered and always return True"""
37
+ actual = is_valid_parameter_type(type, value)
38
+ assert isinstance(actual, bool)
39
+ assert actual
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ "type, value, expected",
44
+ [
45
+ (ParameterType.Boolean, True, True),
46
+ (ParameterType.Boolean, False, True),
47
+ (ParameterType.Boolean, 1, False),
48
+ (ParameterType.Boolean, [1], False),
49
+ (ParameterType.Boolean, "True", False),
50
+ (ParameterType.Float, 0, True),
51
+ (ParameterType.Float, 0.0, True),
52
+ (ParameterType.Float, 1, True),
53
+ (ParameterType.Float, 1.1, True),
54
+ (ParameterType.Float, Decimal("1.1"), True),
55
+ (ParameterType.Float, "1.1", False),
56
+ (ParameterType.Float, [1.1], False),
57
+ (ParameterType.Integer, 0, True),
58
+ (ParameterType.Integer, 0.0, True),
59
+ (ParameterType.Integer, 2, True),
60
+ (ParameterType.Integer, 2.0, True),
61
+ (ParameterType.Integer, Decimal("2.0"), True),
62
+ (ParameterType.Integer, Decimal("2.1"), False),
63
+ (ParameterType.Integer, 2.1, False),
64
+ (ParameterType.Integer, [2], False),
65
+ (ParameterType.Integer, "2", False),
66
+ (ParameterType.DateTime, datetime(2022, 11, 1, 1, 2, 3), True),
67
+ (ParameterType.DateTime, datetime(2022, 11, 1, 1, 2, 3, tzinfo=timezone.utc), True),
68
+ (ParameterType.DateTime, "2022-11-01T01:02:03Z", False),
69
+ (ParameterType.DateTime, "2022-11-01T01:02:03", False),
70
+ (ParameterType.DateTime, 2022, False),
71
+ (ParameterType.DateTime, [], False),
72
+ (ParameterType.String, "", True),
73
+ (ParameterType.String, "abc", True),
74
+ (ParameterType.String, "12", True),
75
+ (ParameterType.String, 12, False),
76
+ (ParameterType.String, ["a"], False),
77
+ (ParameterType.ListString, [], True),
78
+ (ParameterType.ListString, [""], True),
79
+ (ParameterType.ListString, ["", "b"], True),
80
+ (ParameterType.ListString, ["", 4, "b"], False),
81
+ (ParameterType.ListString, False, False),
82
+ (ParameterType.ListString, True, False),
83
+ (ParameterType.ListString, 123, False),
84
+ (ParameterType.HexBinary, "AB", True),
85
+ (ParameterType.HexBinary, "XY", False),
86
+ (ParameterType.HexBinary, 12, False),
87
+ (ParameterType.HexBinary, ["AB"], False),
88
+ (ParameterType.HexBinary, 0.0, False),
89
+ (ParameterType.HexBinary, [12], False),
90
+ (ParameterType.HexBinary, 1.2, False),
91
+ (ParameterType.HexBinary, Decimal("1.0"), False),
92
+ (ParameterType.HexBinary, True, False),
93
+ (ParameterType.CSIPAusResource, True, False),
94
+ (ParameterType.CSIPAusResource, 123, False),
95
+ (ParameterType.CSIPAusResource, "NotAResourceType", False),
96
+ (ParameterType.CSIPAusResource, "ActivePowerInstantaneous", False),
97
+ (ParameterType.CSIPAusResource, "EndDevice", True),
98
+ (ParameterType.CSIPAusResource, ["EndDevice"], False),
99
+ (ParameterType.ListCSIPAusResource, True, False),
100
+ (ParameterType.ListCSIPAusResource, 123, False),
101
+ (ParameterType.ListCSIPAusResource, "NotAResourceType", False),
102
+ (ParameterType.ListCSIPAusResource, "EndDevice", False),
103
+ (ParameterType.ListCSIPAusResource, ["EndDevice"], True),
104
+ (ParameterType.ListCSIPAusResource, ["EndDevice", "NotAResource"], False),
105
+ (ParameterType.ListCSIPAusResource, ["ActivePowerInstantaneous"], False),
106
+ (ParameterType.CSIPAusReadingType, True, False),
107
+ (ParameterType.CSIPAusReadingType, 123, False),
108
+ (ParameterType.CSIPAusReadingType, "NotAResourceType", False),
109
+ (ParameterType.CSIPAusReadingType, "EndDevice", False),
110
+ (ParameterType.CSIPAusReadingType, "ActivePowerInstantaneous", True),
111
+ (ParameterType.CSIPAusReadingType, ["ActivePowerSiteInstantaneous"], False),
112
+ (ParameterType.ListCSIPAusReadingType, True, False),
113
+ (ParameterType.ListCSIPAusReadingType, 123, False),
114
+ (ParameterType.ListCSIPAusReadingType, "NotAResourceType", False),
115
+ (ParameterType.ListCSIPAusReadingType, "EndDevice", False),
116
+ (ParameterType.ListCSIPAusReadingType, "ActivePowerInstantaneous", False),
117
+ (ParameterType.ListCSIPAusReadingType, ["ActivePowerInstantaneous"], True),
118
+ (ParameterType.ListCSIPAusReadingType, ["ActivePowerInstantaneous", "NotAResource"], False),
119
+ (ParameterType.ListCSIPAusReadingType, ["EndDevice"], False),
120
+ (ParameterType.CSIPAusReadingLocation, True, False),
121
+ (ParameterType.CSIPAusReadingLocation, 123, False),
122
+ (ParameterType.CSIPAusReadingLocation, "NotAResourceType", False),
123
+ (ParameterType.CSIPAusReadingLocation, "EndDevice", False),
124
+ (ParameterType.CSIPAusReadingLocation, "Site", True),
125
+ (ParameterType.CSIPAusReadingLocation, ["Site"], False),
126
+ (ParameterType.ReadingTypeValues, 123, False),
127
+ (ParameterType.ReadingTypeValues, "ActivePowerInstantaneous", False),
128
+ (ParameterType.ReadingTypeValues, [123], False),
129
+ (ParameterType.ReadingTypeValues, {}, False),
130
+ (
131
+ ParameterType.ReadingTypeValues,
132
+ {"ActivePowerInstantaneous": [100, 1.23], "ActivePowerAverage": [200, -300]},
133
+ True,
134
+ ),
135
+ (
136
+ ParameterType.ReadingTypeValues,
137
+ {"ActivePowerInstantaneous": [100, 200], "ActivePowerAverage": [200, 300, 400]},
138
+ False,
139
+ ),
140
+ (
141
+ ParameterType.ReadingTypeValues,
142
+ {"ActivePowerInstantaneous": [100, 200], "ActivePowerAverage": [200, 300], "Foo": [100, 200]},
143
+ False,
144
+ ),
145
+ (
146
+ ParameterType.ReadingTypeValues,
147
+ {"ActivePowerInstantaneous": [100, 200], "ActivePowerAverage": [200, "1.23"]},
148
+ False,
149
+ ),
150
+ (
151
+ ParameterType.ReadingTypeValues,
152
+ {"ActivePowerInstantaneous": 100, "ActivePowerAverage": 200},
153
+ False,
154
+ ),
155
+ ],
156
+ )
157
+ def test_is_valid_parameter_type(type: ParameterType, value: Any, expected: bool):
158
+ actual = is_valid_parameter_type(type, value)
159
+ assert isinstance(actual, bool)
160
+ assert actual == expected
161
+
162
+
163
+ @pytest.mark.parametrize(
164
+ "parameters, schema, is_valid",
165
+ [
166
+ ({}, {}, True),
167
+ ({"foo": 1}, {}, False),
168
+ ({"foo": 1}, {"foo": ParameterSchema(True, ParameterType.Integer)}, True),
169
+ ({"foo": 1}, {"foo": ParameterSchema(True, ParameterType.Float)}, True),
170
+ ({"foo": 1}, {"foo": ParameterSchema(True, ParameterType.String)}, False), # wrong type
171
+ (
172
+ {"foo": 1},
173
+ {"foo": ParameterSchema(True, ParameterType.Integer), "bar": ParameterSchema(False, ParameterType.String)},
174
+ True,
175
+ ), # Optional param is OK to miss
176
+ (
177
+ {"foo": 1},
178
+ {"foo": ParameterSchema(True, ParameterType.Integer), "bar": ParameterSchema(True, ParameterType.String)},
179
+ False,
180
+ ), # Missing mandatory param
181
+ (
182
+ {"foo": None, "bar": "2", "baz": 4},
183
+ {
184
+ "foo": ParameterSchema(True, ParameterType.Integer),
185
+ "bar": ParameterSchema(True, ParameterType.String),
186
+ "baz": ParameterSchema(False, ParameterType.Float),
187
+ },
188
+ True,
189
+ ),
190
+ ],
191
+ )
192
+ def test_validate_action_parameters(parameters: dict, schema: dict, is_valid: bool):
193
+ if is_valid:
194
+ validate_parameters("foo", parameters, schema)
195
+ else:
196
+ with pytest.raises(TestProcedureDefinitionError):
197
+ validate_parameters("foo", parameters, schema)