diff --git a/drivers/tests/test_driver_contract.py b/drivers/tests/test_driver_contract.py index 5e42ff6..5c00fcb 100644 --- a/drivers/tests/test_driver_contract.py +++ b/drivers/tests/test_driver_contract.py @@ -72,12 +72,22 @@ def test_calls_set_make_in_init(self, driver_name): f"{driver_name}: should call host.set_make() in driver_init" def test_calls_emit_in_poll(self, driver_name): - """driver_poll should call host.emit().""" + """driver_poll should report something: a DER reading or metrics. + + `host.emit` carries a DER reading and needs a DER type the host + understands. `heatpump` is in VALID_DERS, but no host has a heat-pump + reading type yet, so a heat-pump driver has nothing to emit and + reports temperatures and power through `host.emit_metric` instead. + Requiring `host.emit` of it would leave the author two options: emit + nothing, or claim to be a battery. Both are worse than a driver that + says what it measures. + """ if driver_name == "hello": pytest.skip("hello driver is a demo-only driver") code = read_driver(driver_name) - assert 'host.emit(' in code, \ - f"{driver_name}: should call host.emit() in driver_poll" + assert 'host.emit(' in code or 'host.emit_metric(' in code, \ + f"{driver_name}: should call host.emit() or host.emit_metric() " \ + f"in driver_poll" def test_no_forbidden_globals(self, driver_name): """Driver must not use forbidden sandbox-escaping functions.""" diff --git a/drivers/tests/test_mqtt_drivers.py b/drivers/tests/test_mqtt_drivers.py index c9e78e0..167dfda 100644 --- a/drivers/tests/test_mqtt_drivers.py +++ b/drivers/tests/test_mqtt_drivers.py @@ -37,22 +37,36 @@ def test_subscribes_in_driver_init(self, driver_name): f"not elsewhere" ) - def test_subscription_topics_are_strings(self, driver_name): - """All mqtt_subscribe calls should use string literal topics.""" + def test_subscription_topics_are_static(self, driver_name): + """A subscription topic must be decidable without running the driver. + + A literal is the common case. A configurable prefix is the other one: + heishamon and ctek_hybrid both build the topic from a file-local set + in driver_init, because the broker prefix is the operator's to choose. + That stays reviewable — every part is in the file. + + What this rejects is a topic taken from runtime data: an index, a + field or a call, which is how a driver ends up subscribing to whatever + a message told it to. + """ code = read_driver(driver_name) clean = strip_lua_comments(code) - # Find all subscribe calls and verify they use string literals sub_calls = re.findall( r'host\.mqtt_subscribe\s*\(\s*(.+?)\s*\)', clean, ) for call_arg in sub_calls: - # Should be a string literal (starts with ") - assert call_arg.strip().startswith('"'), ( - f"{driver_name}: mqtt_subscribe should use string literal " - f"topic, found: {call_arg}" + arg = call_arg.strip() + static = re.fullmatch( + r'(?:"[^"]*"|[A-Za-z_]\w*)' + r'(?:\s*\.\.\s*(?:"[^"]*"|[A-Za-z_]\w*))*', + arg, + ) + assert static, ( + f"{driver_name}: mqtt_subscribe topic must be a literal or a " + f"concatenation of literals and file-local names, found: {arg}" )