|
8 | 8 |
|
9 | 9 | from __future__ import annotations |
10 | 10 |
|
| 11 | +import enum |
| 12 | +import inspect |
| 13 | + |
11 | 14 | import pytest |
12 | 15 |
|
13 | 16 | import roborock |
14 | 17 | from roborock.data.code_mappings import RoborockEnum, RoborockModeEnum |
15 | | -from tests.conformance.discovery import discover_subclasses, to_pytest_params |
| 18 | +from tests.conformance.discovery import discover_subclasses, to_pytest_params, walk_modules |
16 | 19 |
|
17 | 20 | # Baseline inventory of legacy RoborockEnum classes that do not yet define an |
18 | 21 | # explicit `unknown` member. When newer firmware emits an undocumented code, |
|
87 | 90 | _ALL_MODE_ENUMS = discover_subclasses(roborock, RoborockModeEnum, exclude=(RoborockModeEnum,)) |
88 | 91 |
|
89 | 92 |
|
| 93 | +# Enums in wire code mapping modules that intentionally remain standard Enum/StrEnum/IntEnum |
| 94 | +# (e.g., domain categories, product nicknames, or outgoing command identifiers). |
| 95 | +ALLOWED_NON_RESILIENT_CODE_MAPPING_ENUMS = { |
| 96 | + "roborock.data.b01_q10.b01_q10_code_mappings.RemoteCommand", |
| 97 | + "roborock.data.code_mappings.RoborockCategory", |
| 98 | + "roborock.data.code_mappings.RoborockProductNickname", |
| 99 | + "roborock.data.v1.v1_code_mappings.RoborockDockState", |
| 100 | +} |
| 101 | + |
| 102 | + |
| 103 | +def _discover_code_mapping_enums() -> list[type[enum.Enum]]: |
| 104 | + enums: list[type[enum.Enum]] = [] |
| 105 | + for mod in walk_modules(roborock): |
| 106 | + if "code_mapping" in mod.__name__: |
| 107 | + for _, obj in inspect.getmembers(mod, inspect.isclass): |
| 108 | + if ( |
| 109 | + obj.__module__ == mod.__name__ |
| 110 | + and issubclass(obj, enum.Enum) |
| 111 | + and obj not in (enum.Enum, enum.IntEnum, enum.StrEnum, RoborockEnum, RoborockModeEnum) |
| 112 | + ): |
| 113 | + enums.append(obj) |
| 114 | + return sorted(enums, key=lambda c: f"{c.__module__}.{c.__name__}") |
| 115 | + |
| 116 | + |
| 117 | +_ALL_CODE_MAPPING_ENUMS = _discover_code_mapping_enums() |
| 118 | + |
| 119 | + |
90 | 120 | @pytest.mark.parametrize("enum_cls", to_pytest_params(_ALL_ROBOROCK_ENUMS, marks_by_fqn=_XFAIL_MARKS)) |
91 | 121 | def test_roborock_enum_has_unknown_fallback(enum_cls: type[RoborockEnum]) -> None: |
92 | 122 | """All RoborockEnum subclasses must define an explicit 'unknown' member.""" |
93 | 123 | assert hasattr(enum_cls, "unknown"), ( |
94 | 124 | f"{enum_cls.__module__}.{enum_cls.__name__} must define an 'unknown' member to prevent " |
95 | 125 | "crashing or defaulting to arbitrary states on new firmware." |
96 | 126 | ) |
97 | | - # Also verify that resolving an unknown int code returns the unknown member |
98 | | - assert enum_cls(99999) == enum_cls.unknown |
| 127 | + # Derive an integer sentinel guaranteed to not exist in the enum |
| 128 | + sentinel = max(item.value for item in enum_cls) + 1 if list(enum_cls) else 99999 |
| 129 | + assert enum_cls(sentinel) == enum_cls.unknown |
99 | 130 |
|
100 | 131 |
|
101 | 132 | @pytest.mark.parametrize("mode_enum_cls", to_pytest_params(_ALL_MODE_ENUMS)) |
102 | 133 | def test_roborock_mode_enum_handles_unknown_code(mode_enum_cls: type[RoborockModeEnum]) -> None: |
103 | 134 | """RoborockModeEnum subclasses must return None when an unknown code is provided.""" |
104 | | - assert mode_enum_cls.from_code_optional(99999) is None |
| 135 | + sentinel = max(member.code for member in mode_enum_cls) + 1 if list(mode_enum_cls) else 99999 |
| 136 | + assert mode_enum_cls.from_code_optional(sentinel) is None |
| 137 | + |
| 138 | + |
| 139 | +@pytest.mark.parametrize("enum_cls", to_pytest_params(_ALL_CODE_MAPPING_ENUMS)) |
| 140 | +def test_code_mapping_enums_inherit_resilient_bases(enum_cls: type[enum.Enum]) -> None: |
| 141 | + """Enums in wire code mapping modules must inherit from RoborockEnum or RoborockModeEnum.""" |
| 142 | + fqn = f"{enum_cls.__module__}.{enum_cls.__name__}" |
| 143 | + if fqn in ALLOWED_NON_RESILIENT_CODE_MAPPING_ENUMS: |
| 144 | + return |
| 145 | + assert issubclass(enum_cls, (RoborockEnum, RoborockModeEnum)), ( |
| 146 | + f"{fqn} in a code mapping module does not inherit from RoborockEnum or RoborockModeEnum. " |
| 147 | + "Wire status and error code enums must use resilient enum bases to handle unknown firmware codes." |
| 148 | + ) |
105 | 149 |
|
106 | 150 |
|
107 | 151 | def test_known_missing_unknown_baseline_inventory() -> None: |
|
0 commit comments