1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
|
import abc
from typing import Any, Final, Optional
class MockedResponse(abc.ABC):
range_start: Final[int]
range_end: Final[int]
def __init__(self, range_start: int, range_end: int):
super().__init__()
if range_start < 0 or range_end < 0:
raise ValueError(
f"Invalid range: both '{range_start}' and '{range_end}' must be positive integers."
)
if range_start != range_end and range_end < range_start + 1:
raise ValueError(
f"Invalid range: values must be equal or '{range_start}' "
f"must be at least one greater than '{range_end}'."
)
self.range_start = range_start
self.range_end = range_end
class MockedResponseReturn(MockedResponse):
payload: Final[Any]
def __init__(self, range_start: int, range_end: int, payload: Any):
super().__init__(range_start=range_start, range_end=range_end)
self.payload = payload
class MockedResponseThrow(MockedResponse):
error: Final[str]
cause: Final[str]
def __init__(self, range_start: int, range_end: int, error: str, cause: str):
super().__init__(range_start=range_start, range_end=range_end)
self.error = error
self.cause = cause
class StateMockedResponses:
state_name: Final[str]
mocked_response_name: Final[str]
mocked_responses: Final[list[MockedResponse]]
def __init__(
self,
state_name: str,
mocked_response_name: str,
mocked_responses: list[MockedResponse],
):
self.state_name = state_name
self.mocked_response_name = mocked_response_name
self.mocked_responses = []
last_range_end: int = -1
mocked_responses_sorted = sorted(
mocked_responses, key=lambda mr: mr.range_start
)
for mocked_response in mocked_responses_sorted:
if not mocked_response.range_start - last_range_end == 1:
raise RuntimeError(
f"Inconsistent event numbering detected for state '{state_name}': "
f"the previous mocked response ended at event '{last_range_end}' "
f"while the next response '{mocked_response_name}' "
f"starts at event '{mocked_response.range_start}'. "
"Mock responses must be consecutively numbered. "
f"Expected the next response to begin at event {last_range_end + 1}."
)
repeats = mocked_response.range_end - mocked_response.range_start + 1
self.mocked_responses.extend([mocked_response] * repeats)
last_range_end = mocked_response.range_end
class MockTestCase:
state_machine_name: Final[str]
test_case_name: Final[str]
state_mocked_responses: Final[dict[str, StateMockedResponses]]
def __init__(
self,
state_machine_name: str,
test_case_name: str,
state_mocked_responses_list: list[StateMockedResponses],
):
self.state_machine_name = state_machine_name
self.test_case_name = test_case_name
self.state_mocked_responses = {}
for state_mocked_response in state_mocked_responses_list:
state_name = state_mocked_response.state_name
if state_name in self.state_mocked_responses:
raise RuntimeError(
f"Duplicate definition of state '{state_name}' for test case '{test_case_name}'"
)
self.state_mocked_responses[state_name] = state_mocked_response
def _parse_mocked_response_range(string_definition: str) -> tuple[int, int]:
definition_parts = string_definition.strip().split("-")
if len(definition_parts) == 1:
range_part = definition_parts[0]
try:
range_value = int(range_part)
return range_value, range_value
except Exception:
raise RuntimeError(
f"Unknown mocked response retry range value '{range_part}', not a valid integer"
)
elif len(definition_parts) == 2:
range_part_start = definition_parts[0]
range_part_end = definition_parts[1]
try:
return int(range_part_start), int(range_part_end)
except Exception:
raise RuntimeError(
f"Unknown mocked response retry range value '{range_part_start}:{range_part_end}', "
"not valid integer values"
)
else:
raise RuntimeError(
f"Unknown mocked response retry range definition '{string_definition}', "
"range definition should consist of one integer (e.g. '0'), or a integer range (e.g. '1-2')'."
)
def _mocked_response_from_raw(
raw_response_model_range: str, raw_response_model
) -> MockedResponse:
range_start, range_end = _parse_mocked_response_range(raw_response_model_range)
if raw_response_model.Return:
payload = raw_response_model.Return.model_dump()
return MockedResponseReturn(
range_start=range_start, range_end=range_end, payload=payload
)
throw_definition = raw_response_model.Throw
return MockedResponseThrow(
range_start=range_start,
range_end=range_end,
error=throw_definition.Error,
cause=throw_definition.Cause,
)
def _mocked_responses_from_raw(
mocked_response_name: str, raw_mock_config
) -> list[MockedResponse]:
raw_response_models = raw_mock_config.MockedResponses.get(mocked_response_name)
if not raw_response_models:
raise RuntimeError(
f"No definitions for mocked response '{mocked_response_name}' in the mock configuration file."
)
mocked_responses: list[MockedResponse] = []
for raw_response_model_range, raw_response_model in raw_response_models.items():
mocked_response: MockedResponse = _mocked_response_from_raw(
raw_response_model_range=raw_response_model_range,
raw_response_model=raw_response_model,
)
mocked_responses.append(mocked_response)
return mocked_responses
def _state_mocked_responses_from_raw(
state_name: str, mocked_response_name: str, raw_mock_config
) -> StateMockedResponses:
mocked_responses = _mocked_responses_from_raw(
mocked_response_name=mocked_response_name, raw_mock_config=raw_mock_config
)
return StateMockedResponses(
state_name=state_name,
mocked_response_name=mocked_response_name,
mocked_responses=mocked_responses,
)
def _mock_test_case_from_raw(
state_machine_name: str, test_case_name: str, raw_mock_config
) -> MockTestCase:
state_machine = raw_mock_config.StateMachines.get(state_machine_name)
if not state_machine:
raise RuntimeError(
f"No definitions for state machine '{state_machine_name}' in the mock configuration file."
)
test_case = state_machine.TestCases.get(test_case_name)
if not test_case:
raise RuntimeError(
f"No definitions for test case '{test_case_name}' and "
f"state machine '{state_machine_name}' in the mock configuration file."
)
state_mocked_responses_list: list[StateMockedResponses] = []
for state_name, mocked_response_name in test_case.root.items():
state_mocked_responses = _state_mocked_responses_from_raw(
state_name=state_name,
mocked_response_name=mocked_response_name,
raw_mock_config=raw_mock_config,
)
state_mocked_responses_list.append(state_mocked_responses)
return MockTestCase(
state_machine_name=state_machine_name,
test_case_name=test_case_name,
state_mocked_responses_list=state_mocked_responses_list,
)
def load_mock_test_case_for(
state_machine_name: str, test_case_name: str
) -> Optional[MockTestCase]:
raw_mock_config = None
if raw_mock_config is None:
return None
mock_test_case: MockTestCase = _mock_test_case_from_raw(
state_machine_name=state_machine_name,
test_case_name=test_case_name,
raw_mock_config=raw_mock_config,
)
return mock_test_case
|