#!/usr/bin/env python3 """Exercise CPU-speed snapshots, canonical config writes and the shipped form.""" import json from pathlib import Path import unittest import test_webconfig_lost_reply as web_fixture ROOT = Path(__file__).resolve().parents[1] class CpuFrequencyWebConfigTest(unittest.TestCase): compile_run = web_fixture.LostReplyWebConfigTest.compile_run setup_values = web_fixture.LostReplyWebConfigTest.setup_values run_page = web_fixture.LostReplyWebConfigTest.run_page def test_actual_json_snapshot_and_canonical_cli_batch(self): # Reuse the real authenticated GET/POST handlers, not a second JSON # implementation. Only their peripheral/HTTP boundaries are faked. source = web_fixture.LostReplyWebConfigTest.fixture(self) source = source.split("int main() {", 1)[0] + r''' #include struct CpuBoard { uint16_t actual = 160; int applies = 0; bool supportsCpuFrequencyMHz(uint16_t mhz) const { return mhz == 80 || mhz == 160; } uint16_t getCpuFrequencyMHz() const { return actual; } bool applyCpuFrequencyMHz(uint16_t selected, bool) { ++applies; actual = selected ? selected : 80; return true; } }; String cpuWriteBody(const char* value, const char* key = "cpu.freq") { return String("{\"reqid\":\"0123456789abcdef\",\"set\":{\"") + key + "\":" + value + "}}"; } int main() { assert(mesh::esp32::managementPassword::publish("password")); for (unsigned mask = 0; mask < 16; ++mask) { WebConfigServer server; server.callbacks.node.cpu_freq_mhz = 80; server.callbacks.node.cpu_actual_mhz = 160; server.callbacks.node.cpu_supported_mask = mask; AsyncWebServerRequest get; server.handleConfigGet(&get); JsonDocument snapshot; assert(get.status == 200 && deserializeJson(snapshot, get.output) == DeserializationError::Ok); if (!(mask & 7)) { assert(snapshot["cpu"].isNull()); continue; } assert(snapshot["cpu"]["selected_mhz"].as() == 80); assert(snapshot["cpu"]["actual_mhz"].as() == 160); JsonArray options = snapshot["cpu"]["supported_mhz"]; const unsigned frequencies[] = {80, 160, 240}; unsigned count = 0; for (unsigned bit = 0; bit < 3; ++bit) { if (mask & (1U << bit)) assert(options[count++].as() == frequencies[bit]); } assert(options.size() == count); } for (const char* text : {"auto", "80", "160", "240", "81", "80;reboot", ""}) { WebConfigServer server; const String value = String("\"") + text + "\""; AsyncWebServerRequest save(cpuWriteBody(value.c_str())); server.handleConfigPost(&save); assert(save.status == 202 && server._batch_count == 1 && !server._batch_reboot); assert(strcmp(server._batch[0].key, "cpu.freq") == 0); assert(String(server._batch[0].cmd) == String("set cpu.freq ") + text); CpuBoard board; uint16_t selected = 160; unsigned saves = 0; char reply[160] = {}; assert(mesh::esp32::cpuclock::handleCommand(board, selected, true, server._batch[0].cmd, reply, sizeof(reply), [&]() { ++saves; return true; })); const bool valid = strcmp(text, "auto") == 0 || strcmp(text, "80") == 0 || strcmp(text, "160") == 0; if (valid) { assert(strncmp(reply, "OK - saved", 10) == 0 && saves == 1 && board.applies == 1); assert(selected == (strcmp(text, "auto") == 0 ? 0 : atoi(text))); } else { assert(strncmp(reply, "Error:", 6) == 0 && !saves && !board.applies); assert(selected == 160 && board.actual == 160); } } for (const char* invalid : {"null", "true", "80"}) { WebConfigServer server; AsyncWebServerRequest save(cpuWriteBody(invalid)); server.handleConfigPost(&save); assert(save.status == 400 && server._batch_count == 0); } WebConfigServer server; AsyncWebServerRequest extra(cpuWriteBody("\"80\"", "cpu.freq.extra")); server.handleConfigPost(&extra); assert(extra.status == 400 && server._batch_count == 0); server.clearSessionLocked(); AsyncWebServerRequest unauthenticated(cpuWriteBody("\"80\"")); server.handleConfigPost(&unauthenticated); assert(unauthenticated.status == 401 && server._batch_count == 0); assert(wcIsAllowedSetKey("cpu.freq") && !wcSetKeyRequiresReboot("cpu.freq")); } ''' self.compile_run(source) def test_shipped_form_uses_supported_options_and_saves_through_normal_batch(self): status, config = self.setup_values() status.update(mode="lan", capabilities=8192) config["cpu"] = {"selected_mhz": 0, "actual_mhz": 80, "supported_mhz": [80, 160]} prelude = r'''''' % (json.dumps(status), json.dumps(config)) dom = self.run_page(prelude, virtual_time=4000) for key, value in (("visible", "true"), ("enabled", "true"), ("initial", "auto"), ("options", "auto,80,160"), ("actual", "Running at 80 MHz"), ("clean", "true"), ("dirty", "160"), ("saved", "160"), ("only-key", "cpu.freq"), ("no-reboot", "true"), ("after-actual", "Running at 160 MHz"), ("auto-saved", "auto"), ("rejected-dirty", "160"), ("rejected-saved", "auto"), ("rejected-field", "160"), ("rejected-actual", "Running at 80 MHz"), ("requests", "3")): self.assertIn('data-test-cpu-%s="%s"' % (key, value), dom) def test_shipped_form_hides_unavailable_clock_and_can_load_240(self): for cpu, values, visible, selected in ( (None, "auto", False, "auto"), ({"selected_mhz": 240, "actual_mhz": 240, "supported_mhz": [240, 80, 160, 240, 20]}, "auto,80,160,240", True, "240")): with self.subTest(cpu=cpu): status, config = self.setup_values() status.update(mode="lan") if cpu is not None: config["cpu"] = cpu else: config.pop("cpu", None) prelude = r'''''' % (json.dumps(status), json.dumps(config)) dom = self.run_page(prelude) for key, value in (("visible", str(visible).lower()), ("disabled", str(not visible).lower()), ("options", values), ("selected", selected), ("dirty", "0")): self.assertIn('data-test-cpu-%s="%s"' % (key, value), dom) if __name__ == "__main__": unittest.main()