velxio/backend/tests/test_esp32c3_wifi.py

172 lines
6.8 KiB
Python

"""
Tests for ESP32-C3 (RISC-V) WiFi NIC argument injection and status parsing.
Verifies that:
- `-nic user,model=esp32c3_wifi,...` is used (not esp32_wifi)
- RISC-V machine name is used (esp32c3-picsimlab)
- hostfwd port is included when specified
- WiFi serial parser works with C3 output (same ESP-IDF log format)
- Free port allocation works
"""
import socket
import unittest
class TestEsp32C3QemuNicArgs(unittest.TestCase):
"""Test QEMU arg construction for ESP32-C3 with WiFi enabled."""
@staticmethod
def _simulate_args(wifi_enabled: bool, machine: str = 'esp32c3-picsimlab',
hostfwd_port: int = 0) -> list[bytes]:
"""Simulate arg-building logic from esp32_worker.main() for C3."""
args = [
b'qemu-system-riscv32',
b'-M', machine.encode(),
b'-nographic',
b'-L', b'/fake/rom',
b'-drive', b'file=/tmp/fw.bin,if=mtd,format=raw',
]
if wifi_enabled:
nic_model = 'esp32c3_wifi' if 'c3' in machine else 'esp32_wifi'
nic_arg = f'user,model={nic_model},net=192.168.4.0/24'
if hostfwd_port:
nic_arg += f',hostfwd=tcp::{hostfwd_port}-192.168.4.15:80'
args.extend([b'-nic', nic_arg.encode()])
return args
def test_c3_uses_riscv32_binary(self):
"""ESP32-C3 should use qemu-system-riscv32."""
args = self._simulate_args(wifi_enabled=False)
self.assertEqual(args[0], b'qemu-system-riscv32')
def test_c3_uses_esp32c3_picsimlab_machine(self):
"""ESP32-C3 machine name should be esp32c3-picsimlab."""
args = self._simulate_args(wifi_enabled=False)
self.assertEqual(args[2], b'esp32c3-picsimlab')
def test_c3_wifi_enabled_adds_nic(self):
"""When wifi_enabled=True, -nic should appear in C3 args."""
args = self._simulate_args(wifi_enabled=True)
self.assertIn(b'-nic', args)
def test_c3_wifi_uses_esp32c3_wifi_model(self):
"""C3 NIC model should be esp32c3_wifi, NOT esp32_wifi."""
args = self._simulate_args(wifi_enabled=True)
nic_idx = args.index(b'-nic')
nic_val = args[nic_idx + 1].decode()
self.assertIn('model=esp32c3_wifi', nic_val)
self.assertNotIn('model=esp32_wifi,', nic_val)
def test_c3_wifi_uses_192_168_4_subnet(self):
"""Same slirp subnet as Xtensa ESP32."""
args = self._simulate_args(wifi_enabled=True)
nic_idx = args.index(b'-nic')
nic_val = args[nic_idx + 1].decode()
self.assertIn('net=192.168.4.0/24', nic_val)
def test_c3_hostfwd_included_when_port_set(self):
"""hostfwd should map to ESP32-C3 port 80."""
args = self._simulate_args(wifi_enabled=True, hostfwd_port=12345)
nic_idx = args.index(b'-nic')
nic_val = args[nic_idx + 1].decode()
self.assertIn('hostfwd=tcp::12345-192.168.4.15:80', nic_val)
def test_c3_hostfwd_absent_when_port_zero(self):
"""No hostfwd when port is 0."""
args = self._simulate_args(wifi_enabled=True, hostfwd_port=0)
nic_idx = args.index(b'-nic')
nic_val = args[nic_idx + 1].decode()
self.assertNotIn('hostfwd', nic_val)
def test_c3_wifi_disabled_no_nic(self):
"""When wifi_enabled=False, -nic should NOT appear."""
args = self._simulate_args(wifi_enabled=False)
self.assertNotIn(b'-nic', args)
class TestEsp32C3WifiSerialParser(unittest.TestCase):
"""WiFi/BLE serial parser works with ESP32-C3 output (same ESP-IDF format)."""
# Simulated C3 serial output (same ESP-IDF log format as Xtensa)
C3_SERIAL_OUTPUT = """I (432) wifi:wifi sta start
I (500) wifi:new:Velxio-GUEST, old: , ASSOC
I (800) wifi:connected with Velxio-GUEST, aid = 1, channel 6
I (1200) esp_netif_handlers: sta ip: 192.168.4.2, mask: 255.255.255.0
"""
C3_BLE_OUTPUT = """I (300) BT_INIT: BT controller compile version [abcd1234]
I (500) GATTS: advertising started
"""
def test_c3_wifi_events_parsed(self):
from app.services.wifi_status_parser import parse_serial_text
wifi_events, _ = parse_serial_text(self.C3_SERIAL_OUTPUT)
self.assertGreaterEqual(len(wifi_events), 3)
def test_c3_first_event_initializing(self):
from app.services.wifi_status_parser import parse_serial_text
wifi_events, _ = parse_serial_text(self.C3_SERIAL_OUTPUT)
self.assertEqual(wifi_events[0]['status'], 'initializing')
def test_c3_connected_has_velxio_guest(self):
from app.services.wifi_status_parser import parse_serial_text
wifi_events, _ = parse_serial_text(self.C3_SERIAL_OUTPUT)
connected = [e for e in wifi_events if e['status'] == 'connected']
self.assertTrue(len(connected) > 0)
self.assertIn('Velxio-GUEST', connected[0].get('ssid', ''))
def test_c3_got_ip_correct(self):
from app.services.wifi_status_parser import parse_serial_text
wifi_events, _ = parse_serial_text(self.C3_SERIAL_OUTPUT)
got_ip = [e for e in wifi_events if e['status'] == 'got_ip']
self.assertEqual(len(got_ip), 1)
self.assertEqual(got_ip[0]['ip'], '192.168.4.2')
def test_c3_ble_events_parsed(self):
from app.services.wifi_status_parser import parse_serial_text
_, ble_events = parse_serial_text(self.C3_BLE_OUTPUT)
self.assertGreaterEqual(len(ble_events), 2)
self.assertEqual(ble_events[0]['status'], 'initialized')
self.assertEqual(ble_events[1]['status'], 'advertising')
def test_c3_no_ble_in_wifi_only_output(self):
from app.services.wifi_status_parser import parse_serial_text
_, ble_events = parse_serial_text(self.C3_SERIAL_OUTPUT)
self.assertEqual(len(ble_events), 0)
class TestEsp32C3FreePort(unittest.TestCase):
"""Free port allocation for C3 hostfwd."""
def test_find_free_port(self):
from app.api.routes.simulation import _find_free_port
port = _find_free_port()
self.assertIsInstance(port, int)
self.assertGreater(port, 0)
self.assertLess(port, 65536)
def test_free_port_is_bindable(self):
from app.api.routes.simulation import _find_free_port
port = _find_free_port()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(('127.0.0.1', port))
finally:
s.close()
class TestEsp32C3QemuManagerMapping(unittest.TestCase):
"""Test that EspQemuManager maps C3 to RISC-V correctly."""
def test_c3_maps_to_riscv32(self):
"""The _MACHINE dict should map esp32-c3 to qemu-system-riscv32."""
from app.services.esp_qemu_manager import _MACHINE
self.assertIn('esp32-c3', _MACHINE)
qemu_bin, machine_name = _MACHINE['esp32-c3']
self.assertIn('riscv32', qemu_bin)
self.assertEqual(machine_name, 'esp32c3')
if __name__ == '__main__':
unittest.main()