From 60202efe310b2c1a904078f12f8e2b1a7a6e2a46 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Mon, 10 Aug 2026 21:58:02 +0200 Subject: [PATCH 1/6] Add a portability workflow beside the existing matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmake.yml covers x86-64 Linux, Windows and macOS across four build types and four compilers, and it covers them well. What it cannot reach is anything that is not selected by the runner it happens to be on: the capability probes only ever resolve one way per machine, every runner is 64-bit, and no BSD or non-x86 architecture is present at all. The new workflow adds one job per dimension: capability-paths force each combination of SUBPROCESS_HAVE_CWD, SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS and SUBPROCESS_SPAWN_VIA_FORK on an ordinary runner linux-32-bit, windows-x86 ILP32, where uint64_t becomes unsigned long long musl a second libc foreign-arch ppc64le, riscv64, s390x, armv7 under qemu-user bsd real FreeBSD, NetBSD 9.4 and 10.1, OpenBSD VMs macos-deployment-target 10.14, 10.15 and 11.0, either side of the boundary that SUBPROCESS_HAVE_CWD turns on capability-paths is the cheapest and catches the most. The library selects its implementation at compile time, so on any given runner the other paths are never executed — including the ones added by #104 and #106, which as far as I can tell had never actually run anywhere before, only compiled. NetBSD is listed at both 9.4 and 10.1 on purpose: posix_spawn_file_actions_addchdir arrived in 10.0 and is spelled without the _np suffix, so the two take different paths through the header. foreign-arch runs under qemu-user, which is faithful for instruction-set semantics but not for posix_spawn's exec-error reporting — it does not propagate the errno, and a create_ex failure there would be an artifact rather than a platform property. That is noted in the file so nobody reads it the wrong way. --- .github/workflows/portability.yml | 154 ++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 .github/workflows/portability.yml diff --git a/.github/workflows/portability.yml b/.github/workflows/portability.yml new file mode 100644 index 0000000..6a276c6 --- /dev/null +++ b/.github/workflows/portability.yml @@ -0,0 +1,154 @@ +name: Portability + +# Complements cmake.yml, which covers x86-64 Linux, Windows and macOS well. +# Everything here reaches a dimension that matrix cannot: the capability paths +# no runner selects on its own, 32-bit, non-x86 architectures, and the BSDs. + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + # subprocess.h picks its implementation from compile-time probes, so an + # ordinary runner only ever selects one combination and the others are never + # executed. Forcing them costs nothing and exercises the paths that platforms + # without a posix_spawn chdir file action take. + capability-paths: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: fork instead of posix_spawn + defs: -DSUBPROCESS_SPAWN_VIA_FORK=1 + - name: no chdir file action, no exec error reporting + defs: -DSUBPROCESS_HAVE_CWD=0 -DSUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS=0 + - name: no chdir file action only + defs: -DSUBPROCESS_HAVE_CWD=0 + name: ${{ matrix.name }} + steps: + - uses: actions/checkout@v4 + - name: Build and test + run: | + cmake -S test -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_FLAGS="${{ matrix.defs }}" \ + -DCMAKE_CXX_FLAGS="${{ matrix.defs }}" + cmake --build build -j + ctest --test-dir build --output-on-failure + + # On ILP32, uint64_t is unsigned long long, and -Wpedantic then rejects both + # oversized literals and the ll length modifier. Nothing else here is 32-bit. + linux-32-bit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install multilib + run: sudo apt-get update && sudo apt-get install -y gcc-multilib g++-multilib + - name: Build and test + run: | + cmake -S test -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_FLAGS=-m32 \ + -DCMAKE_CXX_FLAGS=-m32 \ + -DCMAKE_EXE_LINKER_FLAGS=-m32 + cmake --build build -j + ctest --test-dir build --output-on-failure + + windows-x86: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - run: cmake -S test -B build -A Win32 + - run: cmake --build build --config RelWithDebInfo + - run: ctest --test-dir build -C RelWithDebInfo --output-on-failure + + # musl differs from glibc in the areas this library touches, and it is what + # Alpine-based consumers get. + musl: + runs-on: ubuntu-latest + container: alpine:3 + steps: + - name: Install toolchain + run: apk add --no-cache gcc g++ musl-dev cmake make linux-headers git + - uses: actions/checkout@v4 + - name: Build and test + run: | + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build -j + ctest --test-dir build --output-on-failure + + # Integer division by zero traps on x86, Arm and s390x, but not on PowerPC or + # RISC-V, so subprocess_fail_divzero behaves differently there. + # + # These run under qemu-user. That is sound for instruction-set semantics; it + # is not sound for posix_spawn's exec-error reporting, which qemu-user does + # not propagate. Do not read a create_ex failure here as a platform property. + foreign-arch: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: [ppc64le, riscv64, s390x, armv7] + name: linux ${{ matrix.arch }} + steps: + - uses: actions/checkout@v4 + - uses: uraimo/run-on-arch-action@v3 + with: + arch: ${{ matrix.arch }} + distro: ubuntu_latest + install: | + apt-get update -q + apt-get install -q -y build-essential cmake + run: | + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build -j + ctest --test-dir build --output-on-failure + + # Real BSD virtual machines rather than cross-compilation. NetBSD appears + # twice deliberately: posix_spawn_file_actions_addchdir arrived in 10.0 and is + # spelled without the _np suffix, so 9.x and 10.x take different paths. + bsd: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { os: freebsd, version: "14.3", pkg: "sudo pkg install -y cmake" } + - { os: netbsd, version: "9.4", pkg: "sudo pkgin -y install cmake" } + - { os: netbsd, version: "10.1", pkg: "sudo pkgin -y install cmake" } + - { os: openbsd, version: "7.9", pkg: "sudo pkg_add cmake" } + name: ${{ matrix.os }} ${{ matrix.version }} + steps: + - uses: actions/checkout@v4 + - uses: cross-platform-actions/action@v1.4.0 + with: + operating_system: ${{ matrix.os }} + version: ${{ matrix.version }} + run: | + ${{ matrix.pkg }} + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build + ctest --test-dir build --output-on-failure + + # SUBPROCESS_HAVE_CWD flips at macOS 10.15. Building only against the runner's + # own SDK never exercises the older side of that boundary. + macos-deployment-target: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + target: ["10.14", "10.15", "11.0"] + name: macOS deployment target ${{ matrix.target }} + steps: + - uses: actions/checkout@v4 + - name: Build and test + run: | + cmake -S test -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ matrix.target }} + cmake --build build -j + ctest --test-dir build --output-on-failure From 7f27c269e48d5f0ed502aa01f14ca7abe3feef4b Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Mon, 10 Aug 2026 22:20:44 +0200 Subject: [PATCH 2/6] Narrow the emulated-architecture jobs to what emulation reproduces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first run of this workflow failed all four foreign-arch jobs, and for a reason that has nothing to do with the architectures: qemu-user does not propagate the errno that glibc's posix_spawn uses to report a failed exec, so create_ex_subprocess_create_failure_preserves_error fails there regardless of the target. s390x failed identically to ppc64le despite having no architectural difference in play, which is what gives it away. Running the whole suite under emulation therefore cannot go green, and a job that is permanently red is worse than no job. These now build the whole suite — which still covers compilation for each architecture — and run only the tests whose outcome actually depends on the instruction set: ./subprocess_test '--filter=*divzero' That is the reason the jobs exist: integer division by zero traps on x86, Arm and s390x but not on PowerPC or RISC-V. The comment explains the narrowing so it does not read as suppressing inconvenient failures. Also noted in the file that the SUBPROCESS_SPAWN_VIA_FORK combination in capability-paths is a no-op until #112 lands, so its green tick is not mistaken for evidence today. --- .github/workflows/portability.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/portability.yml b/.github/workflows/portability.yml index 6a276c6..0d4c0d9 100644 --- a/.github/workflows/portability.yml +++ b/.github/workflows/portability.yml @@ -16,6 +16,10 @@ jobs: # ordinary runner only ever selects one combination and the others are never # executed. Forcing them costs nothing and exercises the paths that platforms # without a posix_spawn chdir file action take. + # + # SUBPROCESS_SPAWN_VIA_FORK does not exist yet; that combination is a no-op + # until #112 lands, at which point this job starts exercising the fork + # implementation on an ordinary runner. capability-paths: runs-on: ubuntu-latest strategy: @@ -81,12 +85,20 @@ jobs: cmake --build build -j ctest --test-dir build --output-on-failure - # Integer division by zero traps on x86, Arm and s390x, but not on PowerPC or - # RISC-V, so subprocess_fail_divzero behaves differently there. + # Builds the whole suite for four architectures, then runs only the tests + # whose outcome depends on the instruction set: integer division by zero traps + # on x86, Arm and s390x, but not on PowerPC or RISC-V, so what + # subprocess_fail_divzero may assert differs by target. # - # These run under qemu-user. That is sound for instruction-set semantics; it - # is not sound for posix_spawn's exec-error reporting, which qemu-user does - # not propagate. Do not read a create_ex failure here as a platform property. + # The run is narrowed on purpose. These jobs execute under qemu-user, which + # reproduces instruction-set semantics faithfully but does not propagate the + # errno that glibc's posix_spawn uses to report a failed exec. The whole suite + # therefore fails here on every architecture, including ones with no + # architectural difference at all — s390x fails identically to ppc64le, and + # emulated aarch64 fails the same way despite glibc 2.36 reporting correctly + # on real hardware. Those failures would be artifacts of the emulator, so the + # process-creation tests are not run here; the build still covers compilation + # for each architecture. foreign-arch: runs-on: ubuntu-latest strategy: @@ -106,7 +118,7 @@ jobs: run: | cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build -j - ctest --test-dir build --output-on-failure + cd build && ./subprocess_test '--filter=*divzero' # Real BSD virtual machines rather than cross-compilation. NetBSD appears # twice deliberately: posix_spawn_file_actions_addchdir arrived in 10.0 and is From fc7381712348c8d95ee359466a58545b91f50ca8 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Fri, 14 Aug 2026 22:02:20 +0200 Subject: [PATCH 3/6] Trim the workflow comments to what the YAML does not already say MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer feedback on #113: the file carried explanatory prose that repeated what the job and matrix names state. The paragraphs above capability-paths, linux-32-bit, musl and macos-deployment-target are gone entirely — "no chdir file action, no exec error reporting" and "macOS deployment target 10.14" already say it. Two survive as single lines, both where deleting the comment would invite a wrong edit rather than merely lose background: foreign-arch why it runs only *divzero. Without it the narrowing looks like an oversight and the obvious "fix" is a permanently red job. bsd why NetBSD is listed twice, so the duplicate does not get tidied away and take the 9.x path with it. The note that SUBPROCESS_SPAWN_VIA_FORK is a no-op until #112 lands is dropped as well. It was accurate when written but describes a moment rather than the workflow, and it goes stale the day #112 merges. No job, step or matrix entry is touched: 7 jobs, 17 runs, before and after. The reasoning that left the file is preserved in the two commits that added it and in the PR description. --- .github/workflows/portability.yml | 39 ++++--------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/.github/workflows/portability.yml b/.github/workflows/portability.yml index 0d4c0d9..d981f26 100644 --- a/.github/workflows/portability.yml +++ b/.github/workflows/portability.yml @@ -1,8 +1,6 @@ name: Portability -# Complements cmake.yml, which covers x86-64 Linux, Windows and macOS well. -# Everything here reaches a dimension that matrix cannot: the capability paths -# no runner selects on its own, 32-bit, non-x86 architectures, and the BSDs. +# Dimensions cmake.yml does not reach: capability paths, 32-bit, non-x86, BSD. on: push: @@ -12,14 +10,6 @@ on: jobs: - # subprocess.h picks its implementation from compile-time probes, so an - # ordinary runner only ever selects one combination and the others are never - # executed. Forcing them costs nothing and exercises the paths that platforms - # without a posix_spawn chdir file action take. - # - # SUBPROCESS_SPAWN_VIA_FORK does not exist yet; that combination is a no-op - # until #112 lands, at which point this job starts exercising the fork - # implementation on an ordinary runner. capability-paths: runs-on: ubuntu-latest strategy: @@ -44,8 +34,6 @@ jobs: cmake --build build -j ctest --test-dir build --output-on-failure - # On ILP32, uint64_t is unsigned long long, and -Wpedantic then rejects both - # oversized literals and the ll length modifier. Nothing else here is 32-bit. linux-32-bit: runs-on: ubuntu-latest steps: @@ -70,8 +58,6 @@ jobs: - run: cmake --build build --config RelWithDebInfo - run: ctest --test-dir build -C RelWithDebInfo --output-on-failure - # musl differs from glibc in the areas this library touches, and it is what - # Alpine-based consumers get. musl: runs-on: ubuntu-latest container: alpine:3 @@ -85,20 +71,9 @@ jobs: cmake --build build -j ctest --test-dir build --output-on-failure - # Builds the whole suite for four architectures, then runs only the tests - # whose outcome depends on the instruction set: integer division by zero traps - # on x86, Arm and s390x, but not on PowerPC or RISC-V, so what - # subprocess_fail_divzero may assert differs by target. - # - # The run is narrowed on purpose. These jobs execute under qemu-user, which - # reproduces instruction-set semantics faithfully but does not propagate the - # errno that glibc's posix_spawn uses to report a failed exec. The whole suite - # therefore fails here on every architecture, including ones with no - # architectural difference at all — s390x fails identically to ppc64le, and - # emulated aarch64 fails the same way despite glibc 2.36 reporting correctly - # on real hardware. Those failures would be artifacts of the emulator, so the - # process-creation tests are not run here; the build still covers compilation - # for each architecture. + # Builds everything, runs only *divzero: qemu-user does not propagate the + # errno posix_spawn reports a failed exec with, so the rest fails here on + # every architecture regardless of target. foreign-arch: runs-on: ubuntu-latest strategy: @@ -120,9 +95,7 @@ jobs: cmake --build build -j cd build && ./subprocess_test '--filter=*divzero' - # Real BSD virtual machines rather than cross-compilation. NetBSD appears - # twice deliberately: posix_spawn_file_actions_addchdir arrived in 10.0 and is - # spelled without the _np suffix, so 9.x and 10.x take different paths. + # NetBSD twice on purpose: addchdir arrived in 10.0, so 9.x takes another path. bsd: runs-on: ubuntu-latest strategy: @@ -146,8 +119,6 @@ jobs: cmake --build build ctest --test-dir build --output-on-failure - # SUBPROCESS_HAVE_CWD flips at macOS 10.15. Building only against the runner's - # own SDK never exercises the older side of that boundary. macos-deployment-target: runs-on: macos-latest strategy: From e37769ba1b4d8f9eca552e4a8629d9fadc1d525a Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Wed, 19 Aug 2026 00:36:20 +0200 Subject: [PATCH 4/6] Refresh the vendored utest.h to sheredom/utest.h@b6230bc The old copy is a pristine snapshot of utest.h@f4610ee. Since then upstream fixed the ILP32 constants and the NetBSD PRIu64 problem (#188), the test filter dropping tests it should run (#190) and added AIX support (#189). --- test/utest.h | 218 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 147 insertions(+), 71 deletions(-) diff --git a/test/utest.h b/test/utest.h index 5341a59..eb9f4ad 100644 --- a/test/utest.h +++ b/test/utest.h @@ -34,6 +34,8 @@ #define SHEREDOM_UTEST_H_INCLUDED #ifdef _MSC_VER +#pragma warning(push) + /* Disable warning about not inlining 'inline' functions. */ @@ -94,6 +96,7 @@ typedef uint32_t utest_uint32_t; #endif #include +#include #include #include #include @@ -195,6 +198,9 @@ UTEST_C_FUNC __declspec(dllimport) int __stdcall QueryPerformanceFrequency( #define UTEST_USE_CLOCKGETTIME #endif +#elif defined(_AIX) +#include + #elif defined(__APPLE__) #include #endif @@ -202,11 +208,38 @@ UTEST_C_FUNC __declspec(dllimport) int __stdcall QueryPerformanceFrequency( #if defined(_MSC_VER) && (_MSC_VER < 1920) #define UTEST_PRId64 "I64d" #define UTEST_PRIu64 "I64u" +#define UTEST_INT64_ARG(x) (x) +#define UTEST_UINT64_ARG(x) (x) #else +/* NetBSD hides the PRI macros from C++ before C++11 unless this is defined. */ +#if defined(__cplusplus) && !defined(__STDC_FORMAT_MACROS) +#define __STDC_FORMAT_MACROS 1 +#endif #include +#include +/* Where uint64_t is not unsigned long the PRI macros use the ll length + modifier, which C90 and C++98 do not have. Only those builds print through + double instead; every other target keeps using the macros unchanged. + + _MSC_VER is listed explicitly because neither of the other tests recognises + it: Windows is LLP64, so unsigned long is 32 bits even on x64, and MSVC + reports __cplusplus as 199711L without /Zc:__cplusplus and defines no + __STDC_VERSION__ in its default C mode. Anything reaching here is 1920 or + newer and prints 64-bit integers fine. */ +#if defined(_MSC_VER) || (ULONG_MAX > 0xfffffffful) || \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) #define UTEST_PRId64 PRId64 #define UTEST_PRIu64 PRIu64 +#define UTEST_INT64_ARG(x) (x) +#define UTEST_UINT64_ARG(x) (x) +#else +#define UTEST_PRId64 ".0f" +#define UTEST_PRIu64 ".0f" +#define UTEST_INT64_ARG(x) UTEST_CAST(double, x) +#define UTEST_UINT64_ARG(x) UTEST_CAST(double, x) +#endif #endif #if defined(__cplusplus) @@ -302,8 +335,8 @@ UTEST_C_FUNC __declspec(dllimport) int __stdcall QueryPerformanceFrequency( uninteresting, but for some reason MSVC's behaviour is to warn about including this system header. That *is* interesting */ -#pragma warning(disable : 4820) #pragma warning(push, 1) +#pragma warning(disable : 4820) #include #pragma warning(pop) #define UTEST_COLOUR_OUTPUT() (_isatty(_fileno(stdout))) @@ -365,6 +398,10 @@ static UTEST_INLINE utest_int64_t utest_ns(void) { #endif #endif return UTEST_CAST(utest_int64_t, ts.tv_sec) * 1000 * 1000 * 1000 + ts.tv_nsec; +#elif defined(_AIX) + struct timespec ts = {0, 0}; + clock_gettime(CLOCK_REALTIME, &ts); + return UTEST_CAST(utest_int64_t, ts.tv_sec) * 1000 * 1000 * 1000 + ts.tv_nsec; #elif __EMSCRIPTEN__ return emscripten_performance_now() * 1000000.0; #else @@ -411,12 +448,21 @@ UTEST_EXTERN struct utest_state_s utest_state; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wvariadic-macros" #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#if __has_warning("-Wformat-nonliteral") +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#endif #endif +#if defined(_MSC_VER) +UTEST_C_FUNC __declspec(noinline) int utest_printf_msvc(const char *format, + ...); +#define UTEST_PRINTF(...) ((void)utest_printf_msvc(__VA_ARGS__)) +#else #define UTEST_PRINTF(...) \ if (utest_state.output) { \ fprintf(utest_state.output, __VA_ARGS__); \ } \ printf(__VA_ARGS__) +#endif #ifdef __clang__ #pragma clang diagnostic pop #endif @@ -982,7 +1028,7 @@ utest_strncpy_gcc(char *const dst, const char *const src, const size_t size) { #define EXPECT_MEMEQ(x, y, s) UTEST_MEMEQ(x, y, s, "", 0) #define EXPECT_MEMEQ_MSG(x, y, s, msg) UTEST_MEMEQ(x, y, s, msg, 0) #define ASSERT_MEMEQ(x, y, s) UTEST_MEMEQ(x, y, s, "", 1) -#define ASSERT_MEMEQ_MSG(x, y, s, msg) UTEST_STREQ(x, y, s, msg, 1) +#define ASSERT_MEMEQ_MSG(x, y, s, msg) UTEST_MEMEQ(x, y, s, msg, 1) #define UTEST_STREQ(x, y, msg, is_assert) \ UTEST_SURPRESS_WARNING_BEGIN do { \ @@ -1215,6 +1261,15 @@ utest_strncpy_gcc(char *const dst, const char *const src, const size_t size) { UTEST_EXCEPTION_WITH_MESSAGE(x, exception_type, exception_message, msg, 1) #endif +#if defined(_MSC_VER) +#define UTEST_SURPRESS_MSVC_WARNINGS_BEGIN \ + __pragma(warning(push)) __pragma(warning(disable : 4711)) +#define UTEST_SURPRESS_MSVC_WARNINGS_END __pragma(warning(pop)) +#else +#define UTEST_SURPRESS_MSVC_WARNINGS_BEGIN +#define UTEST_SURPRESS_MSVC_WARNINGS_END +#endif + #if defined(__clang__) #if __has_warning("-Wunsafe-buffer-usage-in-libc-call") #define UTEST_SURPRESS_UNSAFE_BUFFER_USAGE \ @@ -1234,21 +1289,26 @@ utest_strncpy_gcc(char *const dst, const char *const src, const size_t size) { #define UTEST_SURPRESS_GLOBAL_CONSTRUCTORS #endif -#define UTEST_SURPRESS_WARNINGS_BEGIN \ +#define UTEST_SURPRESS_COMPILER_WARNINGS_BEGIN \ _Pragma("clang diagnostic push") \ UTEST_SURPRESS_UNSAFE_BUFFER_USAGE \ UTEST_SURPRESS_GLOBAL_CONSTRUCTORS -#define UTEST_SURPRESS_WARNINGS_END _Pragma("clang diagnostic pop") +#define UTEST_SURPRESS_COMPILER_WARNINGS_END _Pragma("clang diagnostic pop") #elif defined(__GNUC__) && __GNUC__ >= 8 && defined(__cplusplus) -#define UTEST_SURPRESS_WARNINGS_BEGIN \ +#define UTEST_SURPRESS_COMPILER_WARNINGS_BEGIN \ _Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic ignored \"-Wclass-memaccess\"") -#define UTEST_SURPRESS_WARNINGS_END _Pragma("GCC diagnostic pop") +#define UTEST_SURPRESS_COMPILER_WARNINGS_END _Pragma("GCC diagnostic pop") #else -#define UTEST_SURPRESS_WARNINGS_BEGIN -#define UTEST_SURPRESS_WARNINGS_END +#define UTEST_SURPRESS_COMPILER_WARNINGS_BEGIN +#define UTEST_SURPRESS_COMPILER_WARNINGS_END #endif +#define UTEST_SURPRESS_WARNINGS_BEGIN \ + UTEST_SURPRESS_MSVC_WARNINGS_BEGIN UTEST_SURPRESS_COMPILER_WARNINGS_BEGIN +#define UTEST_SURPRESS_WARNINGS_END \ + UTEST_SURPRESS_COMPILER_WARNINGS_END UTEST_SURPRESS_MSVC_WARNINGS_END + #define UTEST(SET, NAME) \ UTEST_SURPRESS_WARNINGS_BEGIN \ UTEST_EXTERN struct utest_state_s utest_state; \ @@ -1387,7 +1447,8 @@ utest_strncpy_gcc(char *const dst, const char *const src, const size_t size) { utest_state.tests[index].file = __FILE__; \ utest_state.tests[index].line = __LINE__; \ iUp = UTEST_CAST(utest_uint64_t, i); \ - UTEST_SNPRINTF(name, name_size, "%s/%" UTEST_PRIu64, name_part, iUp); \ + UTEST_SNPRINTF(name, name_size, "%s/%" UTEST_PRIu64, name_part, \ + UTEST_UINT64_ARG(iUp)); \ } else { \ if (utest_state.tests) { \ free(utest_state.tests); \ @@ -1417,7 +1478,7 @@ double utest_fabs(double d) { utest_uint64_t u; } both; both.d = d; - both.u &= 0x7fffffffffffffffu; + both.u &= ~(UTEST_CAST(utest_uint64_t, 1) << 63); return both.d; } @@ -1430,8 +1491,8 @@ int utest_isnan(double d) { utest_uint64_t u; } both; both.d = d; - both.u &= 0x7fffffffffffffffu; - return both.u > 0x7ff0000000000000u; + both.u &= ~(UTEST_CAST(utest_uint64_t, 1) << 63); + return both.u > (UTEST_CAST(utest_uint64_t, 0x7ff00000) << 32); } #ifdef __clang__ @@ -1456,58 +1517,34 @@ UTEST_WEAK int utest_should_filter_test(const char *filter, const char *filter_cur = filter; const char *testcase_cur = testcase; const char *filter_wildcard = UTEST_NULL; + const char *testcase_resume = UTEST_NULL; - while (('\0' != *filter_cur) && ('\0' != *testcase_cur)) { - if ('*' == *filter_cur) { - /* store the position of the wildcard */ - filter_wildcard = filter_cur; - - /* skip the wildcard character */ + while ('\0' != *testcase_cur) { + if (('*' != *filter_cur) && (*filter_cur == *testcase_cur)) { + /* literal match, consume a character from each */ filter_cur++; - - while (('\0' != *filter_cur) && ('\0' != *testcase_cur)) { - if ('*' == *filter_cur) { - /* - we found another wildcard (filter is something like *foo*) so we - exit the current loop, and return to the parent loop to handle - the wildcard case - */ - break; - } else if (*filter_cur != *testcase_cur) { - /* otherwise our filter didn't match, so reset it */ - filter_cur = filter_wildcard; - } - - /* move testcase along */ - testcase_cur++; - - /* move filter along */ - filter_cur++; - } - - if (('\0' == *filter_cur) && ('\0' == *testcase_cur)) { - return 0; - } - - /* if the testcase has been exhausted, we don't have a match! */ - if ('\0' == *testcase_cur) { - return 1; - } + testcase_cur++; + } else if ('*' == *filter_cur) { + /* remember where to resume should the rest fail to match */ + filter_wildcard = filter_cur++; + testcase_resume = testcase_cur; + } else if (UTEST_NULL != filter_wildcard) { + /* let the last wildcard swallow one more character and retry */ + filter_cur = filter_wildcard + 1; + testcase_resume++; + testcase_cur = testcase_resume; } else { - if (*testcase_cur != *filter_cur) { - /* test case doesn't match filter */ - return 1; - } else { - /* move our filter and testcase forward */ - testcase_cur++; - filter_cur++; - } + /* test case doesn't match filter */ + return 1; } } - if (('\0' != *filter_cur) || - (('\0' != *testcase_cur) && - ((filter == filter_cur) || ('*' != filter_cur[-1])))) { + /* a wildcard may stand for nothing, so any left over still match */ + while ('*' == *filter_cur) { + filter_cur++; + } + + if ('\0' != *filter_cur) { /* we have a mismatch! */ return 1; } @@ -1703,16 +1740,16 @@ int utest_main(int argc, const char *const argv[]) { } printf("%s[==========]%s Running %" UTEST_PRIu64 " test cases.\n", - colours[GREEN], colours[RESET], UTEST_CAST(utest_uint64_t, ran_tests)); + colours[GREEN], colours[RESET], UTEST_UINT64_ARG(ran_tests)); if (utest_state.output) { fprintf(utest_state.output, "\n"); fprintf(utest_state.output, "\n", - UTEST_CAST(utest_uint64_t, ran_tests)); + UTEST_UINT64_ARG(ran_tests)); fprintf(utest_state.output, "\n", - UTEST_CAST(utest_uint64_t, ran_tests)); + UTEST_UINT64_ARG(ran_tests)); } for (index = 0; index < utest_state.tests_length; index++) { @@ -1792,28 +1829,28 @@ int utest_main(int argc, const char *const argv[]) { if (UTEST_TEST_FAILURE == result) { printf("%s[ FAILED ]%s %s (%" UTEST_PRId64 "%s)\n", colours[RED], - colours[RESET], utest_state.tests[index].name, time, - units[unit_index]); + colours[RESET], utest_state.tests[index].name, + UTEST_INT64_ARG(time), units[unit_index]); } else if (UTEST_TEST_SKIPPED == result) { printf("%s[ SKIPPED ]%s %s (%" UTEST_PRId64 "%s)\n", colours[YELLOW], - colours[RESET], utest_state.tests[index].name, time, - units[unit_index]); + colours[RESET], utest_state.tests[index].name, + UTEST_INT64_ARG(time), units[unit_index]); } else { printf("%s[ OK ]%s %s (%" UTEST_PRId64 "%s)\n", colours[GREEN], - colours[RESET], utest_state.tests[index].name, time, - units[unit_index]); + colours[RESET], utest_state.tests[index].name, + UTEST_INT64_ARG(time), units[unit_index]); } } } printf("%s[==========]%s %" UTEST_PRIu64 " test cases ran.\n", colours[GREEN], - colours[RESET], ran_tests); + colours[RESET], UTEST_UINT64_ARG(ran_tests)); printf("%s[ PASSED ]%s %" UTEST_PRIu64 " tests.\n", colours[GREEN], - colours[RESET], ran_tests - failed - skipped); + colours[RESET], UTEST_UINT64_ARG(ran_tests - failed - skipped)); if (0 != skipped) { printf("%s[ SKIPPED ]%s %" UTEST_PRIu64 " tests, listed below:\n", - colours[YELLOW], colours[RESET], skipped); + colours[YELLOW], colours[RESET], UTEST_UINT64_ARG(skipped)); for (index = 0; index < skipped_testcases_length; index++) { printf("%s[ SKIPPED ]%s %s\n", colours[YELLOW], colours[RESET], utest_state.tests[skipped_testcases[index]].name); @@ -1822,7 +1859,7 @@ int utest_main(int argc, const char *const argv[]) { if (0 != failed) { printf("%s[ FAILED ]%s %" UTEST_PRIu64 " tests, listed below:\n", - colours[RED], colours[RESET], failed); + colours[RED], colours[RESET], UTEST_UINT64_ARG(failed)); for (index = 0; index < failed_testcases_length; index++) { printf("%s[ FAILED ]%s %s\n", colours[RED], colours[RESET], utest_state.tests[failed_testcases[index]].name); @@ -1861,7 +1898,42 @@ int utest_main(int argc, const char *const argv[]) { data without having to use the UTEST_MAIN macro, thus allowing them to write their own main() function. */ +#if defined(_MSC_VER) +#if defined(__clang__) +#define UTEST_DEFINE_PRINTF_CLANG_BEGIN \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wformat-nonliteral\"") +#define UTEST_DEFINE_PRINTF_CLANG_END _Pragma("clang diagnostic pop") +#else +#define UTEST_DEFINE_PRINTF_CLANG_BEGIN +#define UTEST_DEFINE_PRINTF_CLANG_END +#endif + +#define UTEST_DEFINE_PRINTF_HELPER() \ + __pragma(warning(push)) __pragma(warning(disable : 4710)) \ + UTEST_DEFINE_PRINTF_CLANG_BEGIN UTEST_C_FUNC __declspec(noinline) int \ + utest_printf_msvc(const char *format, ...) { \ + int result; \ + va_list args; \ + if (utest_state.output) { \ + va_start(args, format); \ + (void)vfprintf(utest_state.output, format, args); \ + va_end(args); \ + } \ + va_start(args, format); \ + result = vfprintf(stdout, format, args); \ + va_end(args); \ + return result; \ + } \ + UTEST_DEFINE_PRINTF_CLANG_END __pragma(warning(pop)) + +#define UTEST_STATE() \ + struct utest_state_s utest_state = {0, 0, 0}; \ + UTEST_DEFINE_PRINTF_HELPER() \ + extern int utest_state_requires_trailing_semicolon +#else #define UTEST_STATE() struct utest_state_s utest_state = {0, 0, 0} +#endif /* define a main() function to call into utest.h and start executing tests! A @@ -1876,4 +1948,8 @@ int utest_main(int argc, const char *const argv[]) { return utest_main(argc, argv); \ } +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + #endif /* SHEREDOM_UTEST_H_INCLUDED */ From 58552dee6e01dc45e284aefec2e876ece1d0c600 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Wed, 19 Aug 2026 14:32:15 +0200 Subject: [PATCH 5/6] Keep the PRI macros visible on NetBSD in C++98 , included here since #112 for __NetBSD_Version__, pulls in . That header gates on __STDC_FORMAT_MACROS for C++ before C++11 and sits behind an include guard, so anything defining the macro afterwards cannot get PRId64 and PRIu64 back. Consumers including subprocess.h first lost them silently; the test suite's C++98 translation unit is one. --- subprocess.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/subprocess.h b/subprocess.h index 67420c1..b312912 100644 --- a/subprocess.h +++ b/subprocess.h @@ -276,6 +276,11 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #endif #if defined(__NetBSD__) +/* pulls in , which hides the PRI macros from C++ + before C++11 behind an include guard, so this has to be set before it. */ +#if defined(__cplusplus) && !defined(__STDC_FORMAT_MACROS) +#define __STDC_FORMAT_MACROS 1 +#endif #include #endif From 8d78bdbdd7c7fe5c3916b843861b6368e31e39ec Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Wed, 19 Aug 2026 14:37:59 +0200 Subject: [PATCH 6/6] Fall back to the compiler default where -std=c++2a is unknown too #105 added -std=c++2a for GCC 8 and clang 9, which do not know -std=c++20. GCC 7 and older know neither, so the fallback passed a flag those compilers reject and the build stopped before running anything. NetBSD 9.4 ships GCC 7.5.0 and hit exactly this. test20.cpp now drops the standard flag when neither spelling is available, which is what already happens for test11.c without c_std_11 and for MSVC, where no -std flag is passed at all. The suite then runs that translation unit at the compiler default rather than not at all. Measured in GCC 7.5.0: -std=c++20 and -std=c++2a are both rejected, and the file compiles clean at the default gnu++14 under the flags CMakeLists sets. GCC 13 still selects -std=c++20 and the suite stays at 444/444. --- test/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 863273e..403468f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,13 +154,17 @@ subprocess_set_cxx_flags(test98.cpp "-std=gnu++98") subprocess_set_cxx_flags(test11.cpp "-std=c++11" "-Wno-c++98-compat") subprocess_set_cxx_flags(test14.cpp "-std=c++14" "-Wno-c++98-compat") subprocess_set_cxx_flags(test17.cpp "-std=c++17" "-Wno-c++98-compat") -# GCC 8 and clang 9 only accept the pre-release spelling -std=c++2a. +# GCC 8 and clang 9 only accept the pre-release spelling -std=c++2a, and GCC 7 +# and older accept neither, so the file falls back to the compiler default. include(CheckCXXCompilerFlag) check_cxx_compiler_flag("-std=c++20" SUBPROCESS_CXX_HAS_STD_CXX20) +check_cxx_compiler_flag("-std=c++2a" SUBPROCESS_CXX_HAS_STD_CXX2A) if(SUBPROCESS_CXX_HAS_STD_CXX20) subprocess_set_cxx_flags(test20.cpp "-std=c++20" "-Wno-c++98-compat") -else() +elseif(SUBPROCESS_CXX_HAS_STD_CXX2A) subprocess_set_cxx_flags(test20.cpp "-std=c++2a" "-Wno-c++98-compat") +else() + subprocess_set_cxx_flags(test20.cpp "" "-Wno-c++98-compat") endif() subprocess_set_cxx_flags(pre_windows.cpp "" "-Wno-c++98-compat") subprocess_set_cxx_flags(post_windows.cpp "" "-Wno-c++98-compat")