diff options
| author | hathach <[email protected]> | 2026-06-17 16:15:25 +0700 |
|---|---|---|
| committer | hathach <[email protected]> | 2026-06-17 16:15:25 +0700 |
| commit | 8fc0a65ec9bba337f8f6b6f9d6a288b039f17421 (patch) | |
| tree | 96d484eb18fc759355bed13a404aed41321d68be /test | |
| parent | 50115d171321600954095fad7ac0e635e51c4c23 (diff) | |
| parent | 52035e2fa3174e85f9c43afdba05097519ae6ea9 (diff) | |
Merge remote-tracking branch 'origin/master' into ch32_fsdev
Diffstat (limited to 'test')
| -rw-r--r-- | test/hil/hil_ci_set_matrix.py | 16 | ||||
| -rwxr-xr-x | test/hil/hil_test.py | 41 | ||||
| -rw-r--r-- | test/hil/tinyusb.json | 33 | ||||
| -rw-r--r-- | test/unit-test/test/device/usbd/test_usbd.c | 52 |
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(); +} |
