From 5a87ef5155c08351b462953332dec2ff7560b8eb Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Sat, 31 Oct 2026 16:26:50 +0100 Subject: [PATCH 1/2] ps2client: Exercise C protocol tests and Windows smoke tests in CI Run the full fake-PS2Link namespace integration suite in an isolated Linux job and add native C Windows command/UDP smoke checks, alongside the existing macOS/Linux C test matrix. Keep PS2 hardware out of CI and preserve existing release binaries. --- .github/workflows/compilation.yml | 23 +++++- Makefile.mingw32 | 9 ++- README.md | 15 ++++ tests/smoke_windows.c | 123 ++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 tests/smoke_windows.c diff --git a/.github/workflows/compilation.yml b/.github/workflows/compilation.yml index e8332d9..31f59d6 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -34,9 +34,13 @@ jobs: make -j $(getconf _NPROCESSORS_ONLN) clean make -j $(getconf _NPROCESSORS_ONLN) all - - name: Run portable smoke tests + - name: Run native C regression tests if: matrix.os[0] != 'windows-latest' run: make check + + - name: Run native Windows C smoke tests + if: matrix.os[0] == 'windows-latest' + run: make -f Makefile.mingw32 check - name: Get short SHA id: slug @@ -50,7 +54,22 @@ jobs: name: ps2client-${{ steps.slug.outputs.sha8 }}-${{ matrix.os[0] }}-${{ matrix.os[1] }} path: | *tar.gz - + + integration-linux: + name: Linux PS2Link protocol integration + runs-on: ubuntu-latest + timeout-minutes: 8 + steps: + - uses: actions/checkout@v4 + - name: Build client and native C fake-PS2Link server + run: make -j2 all bin/fake_ps2link bin/crc32 + - name: Run full network-namespace integration suite + run: | + # Keep virtual interfaces and UDP ports off the runner's host network. + # GitHub-hosted Linux runners permit root-owned network namespaces. + sudo unshare --net -- env PS2CLIENT_TEST_NS=1 \ + bash tests/run-tests.sh "$GITHUB_WORKSPACE/bin/ps2client" + build-win: runs-on: ubuntu-latest container: dockcross/windows-static-x86:latest diff --git a/Makefile.mingw32 b/Makefile.mingw32 index d677303..76176fe 100644 --- a/Makefile.mingw32 +++ b/Makefile.mingw32 @@ -8,8 +8,11 @@ all: bin/fsclient.exe bin/ps2client.exe + check: bin/ps2client.exe obj/smoke_windows.exe + obj/smoke_windows.exe + clean: - rm -f obj/*.o bin/*client* + rm -f obj/*.o obj/smoke_windows.exe bin/*client* ################# ## CLIENT LIBS ## @@ -46,3 +49,7 @@ bin/ps2client.exe: $(OFILES) src/ps2client.c @mkdir -p bin $(CC) $(INC) $(OFILES) src/ps2client.c -o bin/ps2client.exe $(LIB) + + obj/smoke_windows.exe: tests/smoke_windows.c + @mkdir -p obj + $(CC) -Wall -Wextra $< -o $@ -lws2_32 diff --git a/README.md b/README.md index d08c220..1f367b2 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,21 @@ hostnames and the `console` command) on macOS and Linux without a PS2. (`tests/fake_ps2link.c`) in unprivileged Linux network namespaces, so no ps2 is needed. +CI also runs the full Linux protocol suite in isolated network namespaces +with `sudo unshare --net` on the GitHub-hosted runner. The job covers +handshakes, host-file transfers, timeouts, disconnects and console packets +against the native C fake server. On Windows, the native C smoke test runs +through `make -f Makefile.mingw32 check`. Neither test needs Python. + +The Linux integration suite can be reproduced on a machine that supports +network namespaces using: + +```sh +make all bin/fake_ps2link bin/crc32 +sudo unshare --net -- env PS2CLIENT_TEST_NS=1 \ + bash tests/run-tests.sh "$(pwd)/bin/ps2client" +``` + ## Community Links for discussion and chat are available diff --git a/tests/smoke_windows.c b/tests/smoke_windows.c new file mode 100644 index 0000000..8445868 --- /dev/null +++ b/tests/smoke_windows.c @@ -0,0 +1,123 @@ +/* Native Windows command and UDP tests without a PS2 or Python. */ +#include +#include +#include +#include +#include + +#define CONSOLE_PORT 0x4712 + +static int failures; + +static void check(int passed, const char *name) +{ + printf("%s %s\n", passed ? "PASS" : "FAIL", name); + failures += !passed; +} + +static int launch(const char *arguments, PROCESS_INFORMATION *process) +{ + STARTUPINFOA startup = {0}; + char command[1024]; + startup.cb = sizeof(startup); + memset(process, 0, sizeof(*process)); + snprintf(command, sizeof(command), "\"bin\\ps2client.exe\" %s", arguments); + return CreateProcessA(NULL, command, NULL, NULL, FALSE, CREATE_NO_WINDOW, + NULL, NULL, &startup, process) != 0; +} + +static int finish(PROCESS_INFORMATION *process, DWORD timeout) +{ + DWORD status = (DWORD)-1; + if (WaitForSingleObject(process->hProcess, timeout) != WAIT_OBJECT_0) { + TerminateProcess(process->hProcess, 1); + WaitForSingleObject(process->hProcess, 5000); + } else { + GetExitCodeProcess(process->hProcess, &status); + } + CloseHandle(process->hThread); + CloseHandle(process->hProcess); + return (int)status; +} + +static int run(const char *arguments) +{ + PROCESS_INFORMATION process; + if (!launch(arguments, &process)) return -1; + return finish(&process, 7000); +} + +static void test_console(void) +{ + PROCESS_INFORMATION process; + struct sockaddr_in address = {0}; + SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + int i; + if (sock == INVALID_SOCKET) { check(0, "UDP console setup"); return; } + if (!launch("-h 1.1.l.1 -t 3 -e smoke-console-line console", &process)) { + closesocket(sock); + check(0, "UDP console launch"); + return; + } + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(0x7f000001UL); + address.sin_port = htons(CONSOLE_PORT); + Sleep(150); + for (i = 0; i < 10; i++) { + sendto(sock, "smoke-console-line\n", 19, 0, + (struct sockaddr *)&address, sizeof(address)); + Sleep(40); + } + closesocket(sock); + check(finish(&process, 5000) == 0, + "UDP-only console accepts messages without resolving host"); +} + +static void test_reset(void) +{ + SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + struct sockaddr_in address = {0}; + unsigned char packet[64]; + DWORD timeout = 3000; + int bytes, status; + if (sock == INVALID_SOCKET) { check(0, "UDP reset setup"); return; } + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(0x7f000001UL); + address.sin_port = htons(CONSOLE_PORT); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, + (const char *)&timeout, sizeof(timeout)); + if (bind(sock, (struct sockaddr *)&address, sizeof(address)) != 0) { + closesocket(sock); + check(0, "UDP reset bind"); + return; + } + status = run("-h 127.0.0.1 reset"); + bytes = recv(sock, (char *)packet, sizeof(packet), 0); + closesocket(sock); + check(status == 0 && bytes == 6 && + packet[0] == 0xba && packet[1] == 0xbe && + packet[2] == 0x02 && packet[3] == 0x01 && + packet[4] == 0x00 && packet[5] == 0x06, + "reset uses UDP with the expected command packet"); +} + +int main(void) +{ + WSADATA sockets; + if (WSAStartup(MAKEWORD(2, 2), &sockets) != 0) return 1; + + check(run("-h 1.1.l.1 ping") == 69, "invalid hostname"); + check(run("-h 1.1.l.1 -t 1 listen") == 69, + "legacy listen requires a resolvable host"); + check(run("invalid-command") == 64, "unknown command"); + check(run("dumpmem 0x100000") == 64, "missing arguments"); + check(run("-e") == 64, "missing marker"); + SetEnvironmentVariableA("PS2HOSTNAME", "1.1.l.1"); + check(run("ping") == 69, "invalid PS2HOSTNAME environment setting"); + SetEnvironmentVariableA("PS2HOSTNAME", NULL); + test_console(); + test_reset(); + + WSACleanup(); + return failures ? 1 : 0; +} From 0798459c7805de703194ae3d499bece7a99fb2c6 Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Sat, 31 Oct 2026 16:29:18 +0100 Subject: [PATCH 2/2] ps2client: Require protocol integration tests before release --- .github/workflows/compilation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compilation.yml b/.github/workflows/compilation.yml index 31f59d6..e08be91 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -96,7 +96,7 @@ jobs: *tar.gz release: - needs: [build, build-win] + needs: [build, build-win, integration-linux] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' steps: