Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,51 @@ jobs:
# Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator).
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
run: ctest --build-config ${{ matrix.build_type }}

# mingw-w64 (GCC on Windows). NOT covered by the matrix above (that Windows
# cell is MSVC + vcpkg). This is the toolchain the stream-link TX demos'
# binary-stdin fix targets: mingw defines _WIN32 but not _MSC_VER, and it
# gets libusb from pkg-config with VCPKG_ROOT unset. Build-only would miss a
# text-mode stdin regression, so this also runs ctest (the StreamStdinSelftest
# round-trip catches it).
build-mingw:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4

- name: Set up MSYS2 / mingw-w64
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-gcc
mingw-w64-x86_64-cmake
mingw-w64-x86_64-ninja
mingw-w64-x86_64-libusb
mingw-w64-x86_64-pkgconf

- name: Configure CMake (mingw, pkg-config libusb, no vcpkg)
# VCPKG_ROOT is unset, so CMakeLists takes the pkg-config path — the
# exact configuration the mingw binary-stdin fix exists for.
run: >
cmake -B build -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER=gcc
-DCMAKE_CXX_COMPILER=g++

- name: Build (library + stream demos + self-test)
# WiFiDriverDemo / WiFiDriverTxDemo / PrecoderDemo use POSIX-only APIs
# (e.g. fork() in txdemo/main.cpp for the DEVOURER_TX_WITH_RX path) and
# are not mingw targets. This job guards the stdin-driven stream demos
# and their headless self-test — what the binary-stdin fix touches.
run: >
cmake --build build --target
WiFiDriver StreamTxDemo StreamDuplexDemo StreamStdinSelftest

- name: Test
working-directory: build
run: ctest --output-on-failure
12 changes: 8 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ cmake --build build -j

libusb-1.0 is required: `pkg-config` on Linux/macOS, vcpkg on Windows
(`VCPKG_ROOT` must be set so the toolchain file resolves). CI matrix builds
across GCC/Clang/MSVC on Ubuntu/macOS/Windows
(`.github/workflows/cmake-multi-platform.yml`). `ctest` runs in CI but no CMake
tests are registered — regression testing happens out-of-band via
`tests/regress.py`.
across GCC/Clang/MSVC on Ubuntu/macOS/Windows, plus a separate `build-mingw`
job (mingw-w64 via MSYS2, libusb from pkg-config) covering the Windows-GCC
toolchain the MSVC matrix cell doesn't
(`.github/workflows/cmake-multi-platform.yml`). `ctest` runs in every CI job;
the one registered test (`stream_stdin_binary`) round-trips the stream demos'
binary-stdin framing (`txdemo/stream_stdin.h`) headlessly, so a Windows
text-mode regression fails CI instead of only surfacing on a radio. Hardware
regression testing happens out-of-band via `tests/regress.py`.

## Regression testing

Expand Down
23 changes: 23 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ endif()

project(Rtl8812auNet)

enable_testing()

# Find pkg-config and then use it to locate libusb.
find_package(PkgConfig REQUIRED)
pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0)
Expand Down Expand Up @@ -116,6 +118,8 @@ add_executable(StreamTxDemo
txdemo/stream_tx_demo/main.cpp
)
target_link_libraries(StreamTxDemo PUBLIC WiFiDriver PRIVATE PkgConfig::libusb)
# stream_stdin.h (shared binary-stdin framing) lives in txdemo/.
target_include_directories(StreamTxDemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/txdemo)

# Stream-link DUPLEX: one binary, one chip, both directions. Combines the RX
# loop from WiFiDriverDemo and the stdin-driven TX from StreamTxDemo —
Expand All @@ -125,3 +129,22 @@ add_executable(StreamDuplexDemo
txdemo/stream_duplex_demo/main.cpp
)
target_link_libraries(StreamDuplexDemo PUBLIC WiFiDriver PRIVATE PkgConfig::libusb)
target_include_directories(StreamDuplexDemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/txdemo)

# Headless regression guard for the binary-stdin framing shared by the two
# stream demos above (txdemo/stream_stdin.h). No libusb, no radio — just the
# set_stdin_binary() + read_exact() path, so a text-mode regression (e.g. the
# _setmode gate slipping back to _MSC_VER, which is invisible to a build-only
# check) fails `ctest` on the Windows/mingw jobs. See tests/stream_stdin_test.cmake.
add_executable(StreamStdinSelftest
txdemo/stream_stdin_selftest.cpp
)
target_compile_features(StreamStdinSelftest PRIVATE cxx_std_20)
target_include_directories(StreamStdinSelftest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/txdemo)

add_test(
NAME stream_stdin_binary
COMMAND ${CMAKE_COMMAND}
-DSELFTEST_EXE=$<TARGET_FILE:StreamStdinSelftest>
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/stream_stdin_test.cmake
)
44 changes: 44 additions & 0 deletions tests/stream_stdin_test.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ctest driver for StreamStdinSelftest — see txdemo/stream_stdin_selftest.cpp.
#
# Pipes the self-test's --gen output (the canonical <u32_le len><PSDU> stream,
# which embeds 0x1A / 0x0D / 0x0A) straight into a second invocation that reads
# it back through the binary-stdin path and round-trips it. The anonymous pipe
# CMake sets up is byte-exact; the only thing that can corrupt the stream is a
# process putting its own stdio into text mode. So:
#
# * correct (_WIN32-gated) binary mode -> reader prints "...OK", exit 0, PASS
# * regressed (_MSC_VER gate on mingw) -> 0x1A read as EOF / CRLF mangled
# -> reader exits non-zero, FAIL
#
# Off Windows there is no text mode, so the test always passes there; its bite
# is on the Windows (MSVC) and mingw CI jobs. Invoked with -DSELFTEST_EXE=<path>.

if(NOT SELFTEST_EXE)
message(FATAL_ERROR "SELFTEST_EXE not set")
endif()

execute_process(
COMMAND "${SELFTEST_EXE}" --gen
COMMAND "${SELFTEST_EXE}"
OUTPUT_VARIABLE check_out
ERROR_VARIABLE check_err
RESULTS_VARIABLE all_rc)

# RESULTS_VARIABLE is a ;-list of each pipeline stage's exit code.
foreach(rc IN LISTS all_rc)
if(NOT rc EQUAL 0)
message(FATAL_ERROR
"StreamStdinSelftest pipeline stage exited ${rc} — binary stdin/stdout is "
"broken (a 0x1A or CRLF byte corrupted the length-prefixed stream). Check "
"set_stdin_binary()/set_stdout_binary() in txdemo/stream_stdin.h.\n"
"stdout:\n${check_out}\nstderr:\n${check_err}")
endif()
endforeach()

if(NOT check_out MATCHES "records=5 bytes=20 OK")
message(FATAL_ERROR
"StreamStdinSelftest produced unexpected output:\n${check_out}\n"
"stderr:\n${check_err}")
endif()

message(STATUS "stream_stdin round-trip OK: ${check_out}")
32 changes: 9 additions & 23 deletions txdemo/stream_duplex_demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include "RtlUsbAdapter.h"
#include "WiFiDriver.h"
#include "logger.h"
#include "stream_stdin.h"

#define USB_VENDOR_ID 0x0bda

Expand Down Expand Up @@ -93,22 +94,6 @@ static std::vector<uint8_t> build_dot11_probe_req() {
return h;
}

static bool read_exact(FILE *f, void *buf, size_t n) {
size_t got = 0;
auto *p = static_cast<uint8_t *>(buf);
while (got < n) {
size_t r = std::fread(p + got, 1, n - got, f);
if (r == 0) {
if (got == 0 && std::feof(f)) return false;
std::fprintf(stderr,
"stream_duplex_demo: short stdin read (%zu/%zu)\n", got, n);
return false;
}
got += r;
}
return true;
}

// RX callback — emits `<devourer-stream>` on canonical-SA matches. Wrapped in
// a mutex against the TX thread's printf calls so the two log streams don't
// interleave mid-line. (The TX thread only writes to stderr, RX to stdout, so
Expand Down Expand Up @@ -181,7 +166,8 @@ static void tx_thread(TxArgs args) {

while (!args.should_stop->load()) {
uint8_t len_bytes[4];
if (!read_exact(stdin, len_bytes, sizeof(len_bytes))) {
if (stream_stdin::read_exact(stdin, len_bytes, sizeof(len_bytes)) !=
stream_stdin::ReadResult::Ok) {
// Clean EOF or short read — TX side done. RX keeps running.
std::fprintf(stderr, "<stream-duplex>tx EOF after %ld PSDUs\n", tx_count);
break;
Expand All @@ -197,7 +183,8 @@ static void tx_thread(TxArgs args) {
break;
}
std::vector<uint8_t> psdu(len);
if (!read_exact(stdin, psdu.data(), len)) {
if (stream_stdin::read_exact(stdin, psdu.data(), len) !=
stream_stdin::ReadResult::Ok) {
std::fprintf(stderr, "<stream-duplex>tx EOF mid-PSDU (%u bytes)\n", len);
break;
}
Expand Down Expand Up @@ -239,11 +226,10 @@ int main(int argc, char **argv) {
}
}

#if defined(_WIN32)
// mingw/GCC defines _WIN32 but not _MSC_VER, yet still has _setmode — make
// stdin binary so a 0x1A/CRLF doesn't corrupt the length-prefixed PSDU stream.
_setmode(_fileno(stdin), _O_BINARY);
#endif
// Make stdin binary so a 0x1A/CRLF doesn't corrupt the length-prefixed PSDU
// stream. Gated on _WIN32 (not _MSC_VER) in the shared helper — see
// txdemo/stream_stdin.h.
stream_stdin::set_stdin_binary();

libusb_context *context = nullptr;
libusb_device_handle *handle = nullptr;
Expand Down
74 changes: 74 additions & 0 deletions txdemo/stream_stdin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Shared stdin framing for the stdin-driven stream demos (StreamTxDemo,
// StreamDuplexDemo) and their headless regression self-test
// (StreamStdinSelftest).
//
// Centralises the two things that have to stay correct on every Windows
// toolchain, so there is a single source of truth instead of one copy per
// demo:
//
// 1. set_stdin_binary() — put stdin in binary mode so a 0x1A (Ctrl-Z, which
// text-mode stdin treats as EOF) or a CRLF byte in the binary
// <u32_le len><PSDU> stream isn't translated away. Gated on _WIN32, NOT
// _MSC_VER: mingw/GCC defines _WIN32 but not _MSC_VER, yet still ships
// _setmode. A _MSC_VER gate silently leaves mingw stdin in TEXT mode and
// truncates the first PSDU ("short read on stdin (76/269)") before a
// single frame is transmitted.
//
// 2. read_exact() — the length-prefixed record reader, returning a tri-state
// so each caller keeps its own short-read policy (the TX demo aborts on a
// truncated record; the duplex demo just stops its TX thread and lets RX
// run on).
//
// StreamStdinSelftest + tests/stream_stdin_test.cmake exercise this header
// headlessly (no libusb, no hardware), so a regression in the _WIN32 gate
// fails CI on the mingw job instead of only surfacing on a real radio.
#pragma once

#include <cstddef>
#include <cstdint>
#include <cstdio>

#if defined(_WIN32)
#include <io.h>
#include <fcntl.h>
#endif

namespace stream_stdin {

// Put stdin into binary mode. No-op off Windows (POSIX has no text mode).
inline void set_stdin_binary() {
#if defined(_WIN32)
_setmode(_fileno(stdin), _O_BINARY);
#endif
}

// Put stdout into binary mode. Only the self-test's --gen path needs this, but
// it lives here so all the toolchain-gated _setmode logic stays in one place.
inline void set_stdout_binary() {
#if defined(_WIN32)
_setmode(_fileno(stdout), _O_BINARY);
#endif
}

enum class ReadResult {
Ok, // got all n bytes
Eof, // clean stream close: 0 bytes read with EOF before any byte
Short, // stream ended mid-record (truncation)
};

// Read exactly n bytes from f into buf.
inline ReadResult read_exact(std::FILE *f, void *buf, std::size_t n) {
std::size_t got = 0;
auto *p = static_cast<std::uint8_t *>(buf);
while (got < n) {
std::size_t r = std::fread(p + got, 1, n - got, f);
if (r == 0) {
if (got == 0 && std::feof(f)) return ReadResult::Eof;
return ReadResult::Short;
}
got += r;
}
return ReadResult::Ok;
}

} // namespace stream_stdin
Loading
Loading