summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-06-17 16:15:25 +0700
committerhathach <[email protected]>2026-06-17 16:15:25 +0700
commit8fc0a65ec9bba337f8f6b6f9d6a288b039f17421 (patch)
tree96d484eb18fc759355bed13a404aed41321d68be /test
parent50115d171321600954095fad7ac0e635e51c4c23 (diff)
parent52035e2fa3174e85f9c43afdba05097519ae6ea9 (diff)
Merge remote-tracking branch 'origin/master' into ch32_fsdev
Diffstat (limited to 'test')
-rw-r--r--test/hil/hil_ci_set_matrix.py16
-rwxr-xr-xtest/hil/hil_test.py41
-rw-r--r--test/hil/tinyusb.json33
-rw-r--r--test/unit-test/test/device/usbd/test_usbd.c52
4 files changed, 109 insertions, 33 deletions
diff --git a/test/hil/hil_ci_set_matrix.py b/test/hil/hil_ci_set_matrix.py
index baa24afb1..13f7f1882 100644
--- a/test/hil/hil_ci_set_matrix.py
+++ b/test/hil/hil_ci_set_matrix.py
@@ -19,8 +19,12 @@ def main():
parser.add_argument('config_files', nargs='+', help='Configuration JSON file(s)')
args = parser.parse_args()
+ # Toolchain buckets must match the toolchains instantiated by the hil-build
+ # job in .github/workflows/build.yml. Keep all keys present (even if empty)
+ # so `fromJSON(hil_json)[toolchain]` always resolves to a list.
matrix = {
'arm-gcc': [],
+ 'riscv-gcc': [],
'esp-idf': []
}
@@ -38,22 +42,28 @@ def main():
for board in config['boards']:
name = board['name']
flasher = board['flasher']
+ # esptool boards must build under esp-idf; others default to arm-gcc
+ # but may opt into another bucket via an explicit "toolchain" field
+ # (e.g. RISC-V boards like ch32v20x need "riscv-gcc").
if flasher['name'] == 'esptool':
toolchain = 'esp-idf'
else:
- toolchain = 'arm-gcc'
+ toolchain = board.get('toolchain', 'arm-gcc')
build_board = f'-b {name}'
if 'build' in board and 'args' in board['build']:
build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args'])
- # Each variant builds into cmake-build-<variant.name> with its raw CFLAGS.
- # No 'variant' -> a single build named after the board.
+ # Each variant builds into cmake-build-<variant.name> with its own cmake
+ # -D defines and raw CFLAGS. No 'variant' -> a single build named after
+ # the board.
variants = board.get('variant') or [{'name': name, 'flags': ''}]
for v in variants:
arg = build_board
if v['name'] != name:
arg += f' --build-name {v["name"]}'
+ for d in v.get('defines', []):
+ arg += f' -D{d}'
for tok in v.get('flags', '').split():
arg += f' --cflag={tok}'
append_build_arg(toolchain, arg)
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index da13fcbaf..8ae9ee0dc 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -126,8 +126,9 @@ class BuildCfg(TypedDict, total=False):
class VariantCfg(TypedDict, total=False):
- name: str # build dir (cmake-build-<name>) and HIL report row
- flags: str # raw CFLAGS, e.g. "-DCFG_TUD_DWC2_DMA_ENABLE=1"
+ name: str # build dir (cmake-build-<name>) and HIL report row
+ flags: str # raw CFLAGS, e.g. "-DCFG_TUD_DWC2_DMA_ENABLE=1"
+ defines: list[str] # cmake -D defines, e.g. ["RHPORT_DEVICE=1"] (vs flags which are compiler-only)
class Board(TypedDict):
@@ -137,6 +138,7 @@ class Board(TypedDict):
flasher: FlasherCfg
build: NotRequired[BuildCfg]
variant: NotRequired[list[VariantCfg]]
+ toolchain: NotRequired[str] # CI build bucket override, e.g. "riscv-gcc" (consumed by hil_ci_set_matrix.py)
class HilConfig(TypedDict):
@@ -144,6 +146,8 @@ class HilConfig(TypedDict):
CMD_TIMEOUT = int(os.getenv('HIL_CMD_TIMEOUT', '180'))
POOL_TIMEOUT = int(os.getenv('HIL_POOL_TIMEOUT', '3000'))
+SERIAL_READ_TIMEOUT = float(os.getenv('HIL_SERIAL_READ_TIMEOUT', '5'))
+SERIAL_WRITE_TIMEOUT = float(os.getenv('HIL_SERIAL_WRITE_TIMEOUT', '10'))
def cmd_stdout_text(out: Any) -> str:
@@ -230,7 +234,8 @@ def open_serial_dev(port: str):
try:
# write_timeout: a wedged device otherwise blocks ser.write() forever,
# hanging the worker until the pool/job timeout kills the whole run
- ser = serial.Serial(port, baudrate=115200, timeout=5, write_timeout=5)
+ ser = serial.Serial(port, baudrate=115200, timeout=SERIAL_READ_TIMEOUT,
+ write_timeout=SERIAL_WRITE_TIMEOUT)
break
except serial.SerialException:
print(f'serial {port} not reaady {timeout} sec')
@@ -243,6 +248,16 @@ def open_serial_dev(port: str):
return ser
+def serial_write_all(ser: serial.Serial, data: bytes):
+ # write_timeout is a total deadline for the whole call (pyserial keeps partial progress
+ # internally). A timeout means the device stopped draining — treat it as fatal: pyserial
+ # loses the partial-write count on raise, so retrying would duplicate bytes on the wire.
+ try:
+ ser.write(data)
+ except serial.SerialTimeoutException:
+ raise AssertionError(f'Serial write timeout after {SERIAL_WRITE_TIMEOUT:.1f}s')
+
+
def read_disk_file(uid: str, lun: int, fname: str) -> bytes:
# Reads a file from a FAT volume on a block device without mounting it.
# Requires mtools: `apt install mtools` (no pip dependency).
@@ -724,8 +739,7 @@ def test_host_cdc_msc_hid(board):
offset = 0
while offset < echo_len:
chunk_size = min(random.randint(1, packet_size), echo_len - offset)
- ser.write(echo_data[offset:offset + chunk_size])
- ser.flush()
+ serial_write_all(ser, echo_data[offset:offset + chunk_size])
# wait until this chunk is echoed back
echo = b''
t_end = time.monotonic() + 1.0
@@ -774,8 +788,7 @@ def test_host_msc_file_explorer(board):
time.sleep(1)
ser.reset_input_buffer()
for ch in 'cat README.TXT\r':
- ser.write(ch.encode())
- ser.flush()
+ serial_write_all(ser, ch.encode())
time.sleep(0.002)
resp = b''
@@ -797,8 +810,7 @@ def test_host_msc_file_explorer(board):
time.sleep(0.5)
ser.reset_input_buffer()
for ch in 'dd 1024\r':
- ser.write(ch.encode())
- ser.flush()
+ serial_write_all(ser, ch.encode())
time.sleep(0.002)
# Read dd output until prompt
@@ -862,8 +874,7 @@ def test_device_cdc_dual_ports(board):
# Write in chunks of random 1-64 bytes (device has 64-byte buffer)
while offset < payload_len:
chunk_size = min(random.randint(1, 64), payload_len - offset)
- ser[writer].write(payload[offset:offset + chunk_size])
- ser[writer].flush()
+ serial_write_all(ser[writer], payload[offset:offset + chunk_size])
rd0 += ser[0].read(chunk_size)
rd1 += ser[1].read(chunk_size)
offset += chunk_size
@@ -897,8 +908,7 @@ def test_device_cdc_msc(board):
# Write in chunks of random 1-64 bytes (device has 64-byte buffer)
while offset < size:
chunk_size = min(random.randint(1, 64), size - offset)
- ser.write(test_str[offset:offset + chunk_size])
- ser.flush()
+ serial_write_all(ser, test_str[offset:offset + chunk_size])
rd_str += ser.read(chunk_size)
offset += chunk_size
assert rd_str == test_str, f'CDC wrong data ({size} bytes):\n expected: {test_str}\n received: {rd_str}'
@@ -1162,8 +1172,7 @@ def test_device_printer_to_cdc(board):
offset = 0
while offset < size:
chunk_size = min(random.randint(1, 64), size - offset)
- ser.write(test_data[offset:offset + chunk_size])
- ser.flush()
+ serial_write_all(ser, test_data[offset:offset + chunk_size])
time.sleep(0.01)
offset += chunk_size
@@ -1634,6 +1643,8 @@ def build_board(board: Board) -> tuple[str, int]:
cmd += ['-D', d]
if v['name'] != name:
cmd += ['--build-name', v['name']]
+ for d in v.get('defines', []):
+ cmd += ['-D', d]
for tok in v.get('flags', '').split():
cmd += [f'--cflag={tok}']
if verbose:
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index afe3c4d03..ea9342c03 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -458,6 +458,25 @@
"name": "stlink",
"uid": "0668FF575457657187061314"
}
+ },
+ {
+ "name": "nanoch32v203",
+ "uid": "CDAB277B0FBC03E339E339E3",
+ "toolchain": "riscv-gcc",
+ "variant": [
+ {"name": "nanoch32v203-fsdev", "defines": ["RHPORT_DEVICE=0"]},
+ {"name": "nanoch32v203-usbfs", "defines": ["RHPORT_DEVICE=1"]}
+ ],
+ "tests": {
+ "device": true,
+ "host": false,
+ "dual": false
+ },
+ "flasher": {
+ "name": "openocd_wch",
+ "uid": "EBCA8F0670AF",
+ "args": ""
+ }
}
],
"boards-skip": [
@@ -478,20 +497,6 @@
"uid": "000778170924",
"args": "-device stm32f769ni"
}
- },
- {
- "name": "nanoch32v203",
- "uid": "CDAB277B0FBC03E339E339E3",
- "tests": {
- "device": true,
- "host": false,
- "dual": false
- },
- "flasher": {
- "name": "openocd_wch",
- "uid": "EBCA8F0670AF",
- "args": ""
- }
}
]
}
diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c
index 3a2cf3217..7f3c3f5b2 100644
--- a/test/unit-test/test/device/usbd/test_usbd.c
+++ b/test/unit-test/test/device/usbd/test_usbd.c
@@ -29,7 +29,7 @@
#include "tusb_fifo.h"
#include "tusb.h"
#include "usbd.h"
-TEST_SOURCE_FILE("usbd_control.c")
+TEST_SOURCE_FILE("usbd.c")
// Mock File
#include "mock_dcd.h"
@@ -100,6 +100,16 @@ tusb_control_request_t const req_get_desc_configuration =
.wLength = 256
};
+// Vendor OUT control request (direction OUT, type Vendor, recipient Device), 8-byte data stage
+tusb_control_request_t const req_vendor_out =
+{
+ .bmRequestType = 0x40,
+ .bRequest = 0x01,
+ .wValue = 0x0000,
+ .wIndex = 0x0000,
+ .wLength = 8
+};
+
uint8_t const* desc_device;
uint8_t const* desc_configuration;
@@ -120,6 +130,19 @@ uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
return NULL;
}
+// Backing buffer for the vendor OUT data stage. Sized to EP0 max packet so an (untested) regression
+// that drops the clamp can't corrupt memory here; the regression is caught by the expectation below.
+static uint8_t vendor_out_buf[CFG_TUD_ENDPOINT0_SIZE];
+
+bool tud_vendor_control_xfer_cb(uint8_t rhport_, uint8_t stage, tusb_control_request_t const* request) {
+ (void) request;
+ if (stage == CONTROL_STAGE_SETUP) {
+ // Offer only an 8-byte capacity even though the data stage may receive a larger packet
+ return tud_control_xfer(rhport_, request, vendor_out_buf, 8);
+ }
+ return true;
+}
+
void setUp(void) {
dcd_int_disable_Ignore();
dcd_int_enable_Ignore();
@@ -246,3 +269,30 @@ void test_usbd_control_in_zlp(void)
tud_task();
}
+
+//--------------------------------------------------------------------+
+// Control OUT data stage host overrun
+//--------------------------------------------------------------------+
+
+// A non-compliant host sends an OUT data packet larger than the buffer the class offered:
+// wLength = 8, but the DCD reports a full CFG_TUD_ENDPOINT0_SIZE packet. usbd must clamp the
+// copy/accounting to the 8-byte capacity so total_xferred reaches wLength, ends the data stage,
+// and queues the IN status stage. Without the clamp total_xferred overshoots wLength and usbd
+// re-arms an OUT data packet (EDPT_CTRL_OUT) instead, failing the EDPT_CTRL_IN expectation below.
+void test_usbd_control_out_overrun_clamp(void)
+{
+ dcd_event_setup_received(rhport, (uint8_t*) &req_vendor_out, false);
+
+ // Data stage: usbd arms an 8-byte OUT into its internal bounce buffer (buffer ptr is internal)
+ dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_OUT, NULL, 8, false, true);
+ dcd_edpt_xfer_IgnoreArg_buffer();
+ // Host overrun: DCD reports a full max packet, larger than the 8-byte capacity
+ dcd_event_xfer_complete(rhport, EDPT_CTRL_OUT, CFG_TUD_ENDPOINT0_SIZE, XFER_RESULT_SUCCESS, false);
+
+ // Clamp -> total_xferred == wLength -> data stage done -> IN status stage queued
+ dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_IN, NULL, 0, false, true);
+ dcd_event_xfer_complete(rhport, EDPT_CTRL_IN, 0, 0, false);
+ dcd_edpt0_status_complete_ExpectWithArray(rhport, &req_vendor_out, 1);
+
+ tud_task();
+}