velxio/test/backend/unit/test_wifi_detection_scope.py

56 lines
2.4 KiB
Python
Raw Normal View History

fix(esp32): the emulated chip keeps its radio, whatever the scan thinks (#260) `WiFi.begin()` panicked the guest: `Guru Meditation Error: Core 0 panic'ed (LoadStorePIFAddrError)`, EXCVADDR 0x60033c00. Reported against a self-hosted dev setup, and it did not reproduce on velxio.dev — which routes every supported ESP32 kind to the in-browser engines. The engines are not in this repo, so anyone self-hosting from it always runs QEMU, which is where the crash lives. It was verified away on a path the reporter cannot take. 0x60033c00 is the WiFi MAC at its APB alias, and the fork only instantiates that device when a NIC is attached (`qemu_find_nic_info(TYPE_ESP32_WIFI)` in hw/xtensa/esp32.c). The worker attached one only when `wifi_enabled` was true, so a regex over the user's source decided whether the machine had a radio at all. Reproduced against the real worker: same sketch, same firmware, only the flag differs — true connects on 192.168.4.x, false panics exactly as reported. Two things were wrong, and the second is why the first was fatal. The scan was too narrow. Arduino mode read only the entry sketch while pure-IDF mode read every file, so `#include <WiFi.h>` in a project header was judged WiFi-less. Verified against the live backend: main sketch -> has_wifi=true, the same include one file away -> false. Both branches read the whole project now. The frontend also trusted a false over its own all-files scan; the two signals are OR'd, because a stale or narrower false is the direction that hurts. But no scan should have been load-bearing. A real ESP32 has a radio whether or not the sketch uses it, so the emulated one does too: the NIC is attached unconditionally on machines that model a MAC. A misread sketch now fails to connect instead of faulting. Only those machines — the S3 machine models no radio (hw/xtensa/esp32s3.c never looks for the NIC), so handing it one would leave an unconsumed netdev. `wifi_enabled` still gates the host forward, which exposes the guest's server and does belong to the sketch. Measured before making it unconditional, on a sketch that never touches WiFi: boot time, guest-clock-vs-real-time and container CPU unchanged over 4 paired runs. The AP's beacon timer runs on QEMU_CLOCK_REALTIME — the reason -icount is off for Xtensa, b0fe5b5d — but idle beacons cost nothing measurable. One host, Linux x86-64, one sim at a time; small effects would not show. With the radio always present, the MicroPython `network` stub loses its reason to exist on this path: it was there because `WLAN(STA_IF)` hangs forever with no NIC behind it. Keeping the old condition would put the stub in front of a working device whenever the scan misread the sketch — #262, from the other direction. It now depends only on whether the backend models a radio. And when this class of fault does happen, the dump says "memory" and nothing else; a week went into #260 for that reason. LoadStorePIFAddrError is always an unmapped peripheral, so the worker now says so in the serial monitor, naming the WiFi MAC when the address is in its page. test_esp32_wifi_args re-implemented the arg logic in its own `_simulate_args`, so it would have passed unchanged through this entire bug — the same trap 1800f572 called out for the detector. The decision is now `wifi_nic_arg()` and the test calls it. Verified end to end against the QEMU worker: the multi-file WiFi project compiles with has_wifi=true and connects, and a WiFi-less sketch still boots and runs with the radio attached.
2026-08-22 02:37:50 +07:00
"""
Which files count when deciding a sketch uses WiFi (issue #260).
The Arduino path used to read only the entry sketch while the pure-IDF path
read every file, and that asymmetry was not cosmetic: a "no WiFi" verdict made
the QEMU worker leave the radio out of the machine, so the firmware's first
touch of the MAC panicked with LoadStorePIFAddrError instead of merely failing
to connect. A header that includes WiFi.h is as much a WiFi project as an .ino
that does.
"""
import unittest
from app.services.espidf_compiler import ESPIDFCompiler
class TestWifiDetectionScope(unittest.TestCase):
def setUp(self):
self.c = ESPIDFCompiler.__new__(ESPIDFCompiler) # no toolchain needed
def _joined(self, files):
"""What the compiler scans: every file's content, as one blob."""
return '\n'.join(f.get('content', '') for f in files)
def test_include_in_the_entry_sketch(self):
files = [{'name': 'main.ino', 'content': '#include <WiFi.h>\nvoid setup(){}'}]
self.assertTrue(self.c._detect_wifi_usage(self._joined(files)))
def test_include_in_a_secondary_header(self):
"""The case that reached #260 as a Guru Meditation."""
files = [
{'name': 'main.ino', 'content': '#include "net.h"\nvoid setup(){ net_start(); }'},
{'name': 'net.h', 'content': '#include <WiFi.h>\ninline void net_start(){ WiFi.begin("x",""); }'},
]
self.assertTrue(self.c._detect_wifi_usage(self._joined(files)))
def test_begin_call_in_a_secondary_source(self):
files = [
{'name': 'main.ino', 'content': 'void setup(){ net_start(); }'},
{'name': 'net.cpp', 'content': 'void net_start(){ WiFi.begin("x",""); }'},
]
self.assertTrue(self.c._detect_wifi_usage(self._joined(files)))
def test_a_project_with_no_wifi_anywhere(self):
files = [
{'name': 'main.ino', 'content': 'void setup(){ Serial.begin(115200); }'},
{'name': 'helper.h', 'content': 'inline int twice(int n){ return n * 2; }'},
]
self.assertFalse(self.c._detect_wifi_usage(self._joined(files)))
def test_idf_projects_already_read_everything(self):
files = [
{'name': 'main.c', 'content': '#include "net.h"\nvoid app_main(){}'},
{'name': 'net.h', 'content': '#include <esp_wifi.h>'},
]
self.assertTrue(self.c._detect_idf_wifi_usage(self._joined(files)))