2026-04-01 06:53:56 +07:00
|
|
|
/**
|
|
|
|
|
* ESP32 WiFi & Bluetooth emulation tests
|
|
|
|
|
*
|
|
|
|
|
* Covers:
|
|
|
|
|
* 1. Esp32Bridge — wifi_enabled flag in start_esp32 payload
|
|
|
|
|
* 2. Esp32Bridge — wifi_status / ble_status event dispatch
|
|
|
|
|
* 3. WiFi auto-detection from sketch #include directives
|
|
|
|
|
* 4. Store updates on WiFi/BLE status events
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
fix(micropython): stop shadowing the real WiFi driver when a NIC exists
Reported in #262: on an emulated ESP32, MicroPython's WLAN.scan() always
returned [], ifconfig() reported 10.0.2.15/10.0.2.2/10.0.2.3, and
outbound sockets raised EHOSTUNREACH — while Arduino WiFi.h on the same
board and container scanned, connected on 192.168.4.x and completed real
HTTP requests. Two independent faults, both needed for the failure.
1. The MicroPython prelude injected a stub `network` module into
sys.modules unconditionally, so user code never reached the firmware
driver. The stub's hardcoded ifconfig() happens to be QEMU's SLIRP
defaults, which is why the symptom read so convincingly as a routing
bug.
2. hasWifi survived a language switch. It is a property of the compiled
sketch, set only by arduino-cli, but setBoardLanguageMode kept it — so
a board compiled once as Arduino carried that verdict into a later
MicroPython run. The source-scanning fallback only fires when hasWifi
is undefined, so it never got a chance, and the backend attaches the
`-nic` device only when wifi_enabled is true.
The stub is NOT removed, which is where this departs from the patch in
the issue. It exists for a reason the report didn't have: without a NIC,
`network.WLAN(STA_IF)` hangs forever waiting on peripheral status bits
QEMU never sets, tripping the FreeRTOS watchdog after ~26s. Deleting it
would trade a fake-but-working network for a hung board. It is now
conditional — real driver when a NIC is attached, stub when none is —
so both paths are honest.
Also kept: the ntptime and urequests stubs, which the issue's diff would
have dropped along with `network` (it removed the whole prelude block).
Those back 29 call sites in the 100-days gallery examples, which have no
emulated internet to reach regardless of the NIC.
The detector is now one shared function instead of two copies that could
disagree — the stub decision and the -nic decision MUST match. Folded in
`from network import WLAN`, which matched neither previous pattern: such
a sketch was judged WiFi-less and hung on the missing peripheral. The
existing tests re-implemented the detection inline, so they only ever
proved that copy self-consistent; the new ones call the shipped function.
2026-08-11 02:40:37 +07:00
|
|
|
import { sketchUsesWifi } from '../store/useSimulatorStore';
|
2026-04-01 06:53:56 +07:00
|
|
|
|
|
|
|
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
vi.mock('../simulation/AVRSimulator', () => ({
|
|
|
|
|
AVRSimulator: vi.fn(function (this: any) {
|
|
|
|
|
this.onSerialData = null;
|
|
|
|
|
this.onBaudRateChange = null;
|
|
|
|
|
this.onPinChangeWithTime = null;
|
|
|
|
|
this.start = vi.fn();
|
|
|
|
|
this.stop = vi.fn();
|
|
|
|
|
this.reset = vi.fn();
|
|
|
|
|
this.loadHex = vi.fn();
|
|
|
|
|
this.addI2CDevice = vi.fn();
|
|
|
|
|
this.setPinState = vi.fn();
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../simulation/RP2040Simulator', () => ({
|
|
|
|
|
RP2040Simulator: vi.fn(function (this: any) {
|
|
|
|
|
this.onSerialData = null;
|
|
|
|
|
this.onPinChangeWithTime = null;
|
|
|
|
|
this.start = vi.fn();
|
|
|
|
|
this.stop = vi.fn();
|
|
|
|
|
this.reset = vi.fn();
|
|
|
|
|
this.loadBinary = vi.fn();
|
|
|
|
|
this.addI2CDevice = vi.fn();
|
2026-06-15 13:33:28 +07:00
|
|
|
this.attachPioPeripheral = vi.fn();
|
2026-05-03 08:46:32 +07:00
|
|
|
this.spi = { onByte: null, completeTransfer: vi.fn() };
|
2026-04-01 06:53:56 +07:00
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../simulation/PinManager', () => ({
|
|
|
|
|
PinManager: vi.fn(function (this: any) {
|
|
|
|
|
this.updatePort = vi.fn();
|
|
|
|
|
this.onPinChange = vi.fn().mockReturnValue(() => {});
|
|
|
|
|
this.getListenersCount = vi.fn().mockReturnValue(0);
|
2026-05-29 00:31:43 +07:00
|
|
|
this.hardResetPinStates = vi.fn();
|
2026-04-01 06:53:56 +07:00
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
vi.mock('../simulation/I2CBusManager', async () => {
|
|
|
|
|
const actual = await vi.importActual<typeof import('../simulation/I2CBusManager')>(
|
|
|
|
|
'../simulation/I2CBusManager',
|
|
|
|
|
);
|
|
|
|
|
return actual;
|
|
|
|
|
});
|
2026-04-01 06:53:56 +07:00
|
|
|
|
|
|
|
|
vi.mock('../store/useOscilloscopeStore', () => ({
|
|
|
|
|
useOscilloscopeStore: {
|
|
|
|
|
getState: vi.fn().mockReturnValue({ channels: [], pushSample: vi.fn() }),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// WebSocket mock
|
|
|
|
|
class MockWebSocket {
|
2026-04-22 02:45:45 +07:00
|
|
|
static OPEN = 1;
|
2026-04-01 06:53:56 +07:00
|
|
|
static CLOSING = 2;
|
2026-04-22 02:45:45 +07:00
|
|
|
static CLOSED = 3;
|
2026-04-01 06:53:56 +07:00
|
|
|
|
|
|
|
|
readyState = MockWebSocket.OPEN;
|
2026-04-22 02:45:45 +07:00
|
|
|
onopen: (() => void) | null = null;
|
2026-04-01 06:53:56 +07:00
|
|
|
onmessage: ((e: { data: string }) => void) | null = null;
|
2026-04-22 02:45:45 +07:00
|
|
|
onclose: (() => void) | null = null;
|
|
|
|
|
onerror: (() => void) | null = null;
|
2026-04-01 06:53:56 +07:00
|
|
|
sent: string[] = [];
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
send(data: string) {
|
|
|
|
|
this.sent.push(data);
|
|
|
|
|
}
|
|
|
|
|
close() {
|
|
|
|
|
this.readyState = MockWebSocket.CLOSED;
|
|
|
|
|
this.onclose?.();
|
|
|
|
|
}
|
|
|
|
|
open() {
|
|
|
|
|
this.readyState = MockWebSocket.OPEN;
|
|
|
|
|
this.onopen?.();
|
|
|
|
|
}
|
2026-04-01 06:53:56 +07:00
|
|
|
receive(payload: object) {
|
|
|
|
|
this.onmessage?.({ data: JSON.stringify(payload) });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
|
|
|
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
|
|
|
|
|
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
|
|
|
|
|
|
|
|
// ── Imports (after mocks) ────────────────────────────────────────────────────
|
|
|
|
|
import { Esp32Bridge } from '../simulation/Esp32Bridge';
|
|
|
|
|
import type { WifiStatus, BleStatus } from '../simulation/Esp32Bridge';
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// 1. Esp32Bridge — wifi_enabled in start_esp32 payload
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('Esp32Bridge — WiFi flag', () => {
|
|
|
|
|
let bridge: Esp32Bridge;
|
|
|
|
|
let ws: MockWebSocket;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
bridge = new Esp32Bridge('board-1', 'esp32');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
bridge.disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('sends wifi_enabled=false by default in start_esp32', () => {
|
|
|
|
|
bridge.connect();
|
|
|
|
|
ws = (bridge as any).socket as MockWebSocket;
|
|
|
|
|
ws.open();
|
|
|
|
|
|
|
|
|
|
expect(ws.sent.length).toBeGreaterThan(0);
|
|
|
|
|
const msg = JSON.parse(ws.sent[0]);
|
|
|
|
|
expect(msg.type).toBe('start_esp32');
|
|
|
|
|
expect(msg.data.wifi_enabled).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('sends wifi_enabled=true when wifiEnabled is set before connect', () => {
|
|
|
|
|
bridge.wifiEnabled = true;
|
|
|
|
|
bridge.connect();
|
|
|
|
|
ws = (bridge as any).socket as MockWebSocket;
|
|
|
|
|
ws.open();
|
|
|
|
|
|
|
|
|
|
const msg = JSON.parse(ws.sent[0]);
|
|
|
|
|
expect(msg.type).toBe('start_esp32');
|
|
|
|
|
expect(msg.data.wifi_enabled).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('includes board type in start_esp32', () => {
|
|
|
|
|
bridge.wifiEnabled = true;
|
|
|
|
|
bridge.connect();
|
|
|
|
|
ws = (bridge as any).socket as MockWebSocket;
|
|
|
|
|
ws.open();
|
|
|
|
|
|
|
|
|
|
const msg = JSON.parse(ws.sent[0]);
|
|
|
|
|
expect(msg.data.board).toBe('esp32');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('maps esp32-s3 board kind to correct QEMU type', () => {
|
|
|
|
|
const s3Bridge = new Esp32Bridge('board-s3', 'esp32-s3');
|
|
|
|
|
s3Bridge.wifiEnabled = true;
|
|
|
|
|
s3Bridge.connect();
|
|
|
|
|
const s3ws = (s3Bridge as any).socket as MockWebSocket;
|
|
|
|
|
s3ws.open();
|
|
|
|
|
|
|
|
|
|
const msg = JSON.parse(s3ws.sent[0]);
|
|
|
|
|
expect(msg.data.board).toBe('esp32-s3');
|
|
|
|
|
s3Bridge.disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('maps esp32-c3 board kind to correct QEMU type', () => {
|
|
|
|
|
const c3Bridge = new Esp32Bridge('board-c3', 'esp32-c3');
|
|
|
|
|
c3Bridge.wifiEnabled = true;
|
|
|
|
|
c3Bridge.connect();
|
|
|
|
|
const c3ws = (c3Bridge as any).socket as MockWebSocket;
|
|
|
|
|
c3ws.open();
|
|
|
|
|
|
|
|
|
|
const msg = JSON.parse(c3ws.sent[0]);
|
|
|
|
|
expect(msg.data.board).toBe('esp32-c3');
|
|
|
|
|
c3Bridge.disconnect();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// 2. Esp32Bridge — WiFi/BLE status event dispatch
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('Esp32Bridge — WiFi/BLE status events', () => {
|
|
|
|
|
let bridge: Esp32Bridge;
|
|
|
|
|
let ws: MockWebSocket;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
bridge = new Esp32Bridge('board-1', 'esp32');
|
|
|
|
|
bridge.wifiEnabled = true;
|
|
|
|
|
bridge.connect();
|
|
|
|
|
ws = (bridge as any).socket as MockWebSocket;
|
|
|
|
|
ws.open();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
bridge.disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dispatches wifi_status event to onWifiStatus callback', () => {
|
|
|
|
|
const received: WifiStatus[] = [];
|
|
|
|
|
bridge.onWifiStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({
|
|
|
|
|
type: 'wifi_status',
|
|
|
|
|
data: { status: 'got_ip', ssid: 'Velxio-GUEST', ip: '192.168.4.2' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(received).toHaveLength(1);
|
|
|
|
|
expect(received[0].status).toBe('got_ip');
|
|
|
|
|
expect(received[0].ssid).toBe('Velxio-GUEST');
|
|
|
|
|
expect(received[0].ip).toBe('192.168.4.2');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dispatches wifi_status initializing event', () => {
|
|
|
|
|
const received: WifiStatus[] = [];
|
|
|
|
|
bridge.onWifiStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({
|
|
|
|
|
type: 'wifi_status',
|
|
|
|
|
data: { status: 'initializing' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(received).toHaveLength(1);
|
|
|
|
|
expect(received[0].status).toBe('initializing');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dispatches wifi_status disconnected event', () => {
|
|
|
|
|
const received: WifiStatus[] = [];
|
|
|
|
|
bridge.onWifiStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({
|
|
|
|
|
type: 'wifi_status',
|
|
|
|
|
data: { status: 'disconnected' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(received).toHaveLength(1);
|
|
|
|
|
expect(received[0].status).toBe('disconnected');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dispatches ble_status event to onBleStatus callback', () => {
|
|
|
|
|
const received: BleStatus[] = [];
|
|
|
|
|
bridge.onBleStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({
|
|
|
|
|
type: 'ble_status',
|
|
|
|
|
data: { status: 'initialized' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(received).toHaveLength(1);
|
|
|
|
|
expect(received[0].status).toBe('initialized');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dispatches ble_status advertising event', () => {
|
|
|
|
|
const received: BleStatus[] = [];
|
|
|
|
|
bridge.onBleStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({
|
|
|
|
|
type: 'ble_status',
|
|
|
|
|
data: { status: 'advertising' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(received).toHaveLength(1);
|
|
|
|
|
expect(received[0].status).toBe('advertising');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not crash when callbacks are not set', () => {
|
|
|
|
|
bridge.onWifiStatus = null;
|
|
|
|
|
bridge.onBleStatus = null;
|
|
|
|
|
|
|
|
|
|
// Should not throw
|
|
|
|
|
ws.receive({ type: 'wifi_status', data: { status: 'got_ip', ip: '10.0.0.1' } });
|
|
|
|
|
ws.receive({ type: 'ble_status', data: { status: 'initialized' } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles multiple sequential WiFi status changes', () => {
|
|
|
|
|
const received: WifiStatus[] = [];
|
|
|
|
|
bridge.onWifiStatus = (status) => received.push(status);
|
|
|
|
|
|
|
|
|
|
ws.receive({ type: 'wifi_status', data: { status: 'initializing' } });
|
|
|
|
|
ws.receive({ type: 'wifi_status', data: { status: 'connected', ssid: 'Velxio-GUEST' } });
|
2026-04-22 02:45:45 +07:00
|
|
|
ws.receive({
|
|
|
|
|
type: 'wifi_status',
|
|
|
|
|
data: { status: 'got_ip', ssid: 'Velxio-GUEST', ip: '192.168.4.2' },
|
|
|
|
|
});
|
2026-04-01 06:53:56 +07:00
|
|
|
|
|
|
|
|
expect(received).toHaveLength(3);
|
|
|
|
|
expect(received[0].status).toBe('initializing');
|
|
|
|
|
expect(received[1].status).toBe('connected');
|
|
|
|
|
expect(received[2].status).toBe('got_ip');
|
|
|
|
|
expect(received[2].ip).toBe('192.168.4.2');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// 3. WiFi auto-detection from sketch files
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('WiFi auto-detection', () => {
|
|
|
|
|
it('detects #include <WiFi.h> in sketch content', () => {
|
|
|
|
|
const content = `
|
|
|
|
|
#include <WiFi.h>
|
|
|
|
|
void setup() { WiFi.begin("Velxio-GUEST", ""); }
|
|
|
|
|
void loop() {}
|
|
|
|
|
`;
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasWifi =
|
|
|
|
|
content.includes('#include <WiFi.h>') ||
|
|
|
|
|
content.includes('#include <esp_wifi.h>') ||
|
|
|
|
|
content.includes('#include "WiFi.h"') ||
|
|
|
|
|
content.includes('WiFi.begin(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasWifi).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects #include <esp_wifi.h> in sketch content', () => {
|
|
|
|
|
const content = `
|
|
|
|
|
#include <esp_wifi.h>
|
|
|
|
|
void setup() { esp_wifi_init(); }
|
|
|
|
|
void loop() {}
|
|
|
|
|
`;
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasWifi =
|
|
|
|
|
content.includes('#include <WiFi.h>') ||
|
|
|
|
|
content.includes('#include <esp_wifi.h>') ||
|
|
|
|
|
content.includes('#include "WiFi.h"') ||
|
|
|
|
|
content.includes('WiFi.begin(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasWifi).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects WiFi.begin() call in sketch content', () => {
|
|
|
|
|
const content = `
|
|
|
|
|
void setup() { WiFi.begin("ssid", "pass"); }
|
|
|
|
|
void loop() {}
|
|
|
|
|
`;
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasWifi =
|
|
|
|
|
content.includes('#include <WiFi.h>') ||
|
|
|
|
|
content.includes('#include <esp_wifi.h>') ||
|
|
|
|
|
content.includes('#include "WiFi.h"') ||
|
|
|
|
|
content.includes('WiFi.begin(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasWifi).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns false for non-WiFi sketches', () => {
|
|
|
|
|
const content = `
|
|
|
|
|
void setup() { Serial.begin(115200); }
|
|
|
|
|
void loop() { delay(1000); }
|
|
|
|
|
`;
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasWifi =
|
|
|
|
|
content.includes('#include <WiFi.h>') ||
|
|
|
|
|
content.includes('#include <esp_wifi.h>') ||
|
|
|
|
|
content.includes('#include "WiFi.h"') ||
|
|
|
|
|
content.includes('WiFi.begin(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasWifi).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects #include "WiFi.h" (quotes) in sketch', () => {
|
|
|
|
|
const content = '#include "WiFi.h"\nvoid setup() {}\nvoid loop() {}';
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasWifi =
|
|
|
|
|
content.includes('#include <WiFi.h>') ||
|
|
|
|
|
content.includes('#include <esp_wifi.h>') ||
|
|
|
|
|
content.includes('#include "WiFi.h"') ||
|
|
|
|
|
content.includes('WiFi.begin(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasWifi).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
fix(micropython): stop shadowing the real WiFi driver when a NIC exists
Reported in #262: on an emulated ESP32, MicroPython's WLAN.scan() always
returned [], ifconfig() reported 10.0.2.15/10.0.2.2/10.0.2.3, and
outbound sockets raised EHOSTUNREACH — while Arduino WiFi.h on the same
board and container scanned, connected on 192.168.4.x and completed real
HTTP requests. Two independent faults, both needed for the failure.
1. The MicroPython prelude injected a stub `network` module into
sys.modules unconditionally, so user code never reached the firmware
driver. The stub's hardcoded ifconfig() happens to be QEMU's SLIRP
defaults, which is why the symptom read so convincingly as a routing
bug.
2. hasWifi survived a language switch. It is a property of the compiled
sketch, set only by arduino-cli, but setBoardLanguageMode kept it — so
a board compiled once as Arduino carried that verdict into a later
MicroPython run. The source-scanning fallback only fires when hasWifi
is undefined, so it never got a chance, and the backend attaches the
`-nic` device only when wifi_enabled is true.
The stub is NOT removed, which is where this departs from the patch in
the issue. It exists for a reason the report didn't have: without a NIC,
`network.WLAN(STA_IF)` hangs forever waiting on peripheral status bits
QEMU never sets, tripping the FreeRTOS watchdog after ~26s. Deleting it
would trade a fake-but-working network for a hung board. It is now
conditional — real driver when a NIC is attached, stub when none is —
so both paths are honest.
Also kept: the ntptime and urequests stubs, which the issue's diff would
have dropped along with `network` (it removed the whole prelude block).
Those back 29 call sites in the 100-days gallery examples, which have no
emulated internet to reach regardless of the NIC.
The detector is now one shared function instead of two copies that could
disagree — the stub decision and the -nic decision MUST match. Folded in
`from network import WLAN`, which matched neither previous pattern: such
a sketch was judged WiFi-less and hung on the missing peripheral. The
existing tests re-implemented the detection inline, so they only ever
proved that copy self-consistent; the new ones call the shipped function.
2026-08-11 02:40:37 +07:00
|
|
|
// 3b. The REAL detector (issue #262)
|
|
|
|
|
//
|
|
|
|
|
// The block above re-implements the detection inline, so it only ever
|
|
|
|
|
// proves the copy in this file is self-consistent — which is how the
|
|
|
|
|
// MicroPython gap below survived. These call the shipped function.
|
|
|
|
|
//
|
|
|
|
|
// Getting this wrong is not cosmetic: a false verdict means QEMU is
|
|
|
|
|
// started without `-nic`, and MicroPython's `network.WLAN(STA_IF)` then
|
|
|
|
|
// hangs ~26s on a peripheral that was never attached until the FreeRTOS
|
|
|
|
|
// watchdog resets the board.
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('sketchUsesWifi — the shipped detector', () => {
|
|
|
|
|
const uses = (content: string) => sketchUsesWifi([{ content }]);
|
|
|
|
|
|
|
|
|
|
it('detects the Arduino forms', () => {
|
|
|
|
|
expect(uses('#include <WiFi.h>')).toBe(true);
|
|
|
|
|
expect(uses('#include "WiFi.h"')).toBe(true);
|
|
|
|
|
expect(uses('#include <esp_wifi.h>')).toBe(true);
|
|
|
|
|
expect(uses('void setup(){ WiFi.begin("s",""); }')).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects the MicroPython import forms', () => {
|
|
|
|
|
expect(uses('import network')).toBe(true);
|
|
|
|
|
expect(uses('import network as net')).toBe(true);
|
|
|
|
|
expect(uses('wlan = network.WLAN(network.STA_IF)')).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects `from network import WLAN` (the #262 gap)', () => {
|
|
|
|
|
// Reported as MicroPython WiFi being "broken": this form matched
|
|
|
|
|
// neither /import\s+network\b/ nor /network\.WLAN/, so the sketch was
|
|
|
|
|
// judged WiFi-less and never got a NIC.
|
|
|
|
|
expect(uses('from network import WLAN\nw = WLAN(0)')).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('stays false for sketches that touch no network', () => {
|
|
|
|
|
expect(uses('void setup(){ Serial.begin(115200); }')).toBe(false);
|
|
|
|
|
expect(uses('from machine import Pin\nPin(2).on()')).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('scans every file, not just the first', () => {
|
|
|
|
|
expect(
|
|
|
|
|
sketchUsesWifi([{ content: 'from machine import Pin' }, { content: 'import network' }]),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
2026-04-01 06:53:56 +07:00
|
|
|
// 4. BLE detection from sketch files
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('BLE detection in sketches', () => {
|
|
|
|
|
it('detects BLEDevice.h include', () => {
|
|
|
|
|
const content = '#include <BLEDevice.h>\nvoid setup() { BLEDevice::init("test"); }';
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasBLE =
|
|
|
|
|
content.includes('#include <BLEDevice.h>') ||
|
|
|
|
|
content.includes('#include <esp_bt.h>') ||
|
|
|
|
|
content.includes('BLEDevice::init(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasBLE).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns false for non-BLE sketches', () => {
|
|
|
|
|
const content = 'void setup() { Serial.begin(115200); }\nvoid loop() {}';
|
2026-04-22 02:45:45 +07:00
|
|
|
const hasBLE =
|
|
|
|
|
content.includes('#include <BLEDevice.h>') ||
|
|
|
|
|
content.includes('#include <esp_bt.h>') ||
|
|
|
|
|
content.includes('BLEDevice::init(');
|
2026-04-01 06:53:56 +07:00
|
|
|
expect(hasBLE).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|