From 1f7c26638485d2cc5c4c3bb32e5c28524932566c Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Wed, 30 Sep 2026 11:27:15 -0700 Subject: [PATCH] ci: test MSVC 14.44 and 14.51 on x64 and ARM64 This change replaces the MSVC jobs with these jobs: - 14.44 on x64, on the windows-2022 runner (is_earliest). CMake uses the Visual Studio 17 2022 generator. - 14.51 on x64, on the windows-2025-vs2026 runner (is_latest). CMake uses the Ninja generator. The ASan variant moves with is_latest. - 14.44 on ARM64, on the windows-11-vs2026-arm runner. This job uses the side-by-side toolset of the image. - 14.51 on ARM64, on the windows-11-vs2026-arm runner. This change removes the 14.34 jobs. No hosted runner has 14.34, and those jobs compiled with the default toolset of the image. The new msvc_toolset field in compilers.json selects a side-by-side toolset. For this field, the Configure MSVC step writes a B2 user-config. The user-config sets a setup script that gives -vcvars_ver to vcvarsall. B2 runs this script before it builds. The Configure MSVC step also compares the cl banner with the matrix. The step gets the target architecture from RUNNER_ARCH. If the version or the target architecture is different, the step stops with an error. In cancel_close_stress_test, a separate coroutine lambda now runs each iteration of the worker. This change prevents an internal compiler error (C1001) in MSVC 19.51.36257 for x64 at /O2. The compiler fails when a co_await is in a counted loop in the try block of a coroutine. The iterations, counters, cancellation mix and assertions do not change. --- .github/compilers.json | 25 +++--- .github/generate-matrix.py | 2 + .github/workflows/ci.yml | 49 ++++++++++++ test/unit/socket_stress.cpp | 155 +++++++++++++++++++----------------- 4 files changed, 145 insertions(+), 86 deletions(-) diff --git a/.github/compilers.json b/.github/compilers.json index 4443a1844..8fa3a089a 100644 --- a/.github/compilers.json +++ b/.github/compilers.json @@ -92,36 +92,37 @@ ], "msvc": [ { - "version": "14.34", + "version": "14.44", "cxxstd": "20", "latest_cxxstd": "20", "runs_on": "windows-2022", - "b2_toolset": "msvc-14.3", + "b2_toolset": "msvc-14.4", "generator": "Visual Studio 17 2022", "is_earliest": true }, { - "version": "14.44", + "version": "14.51", "cxxstd": "20", "latest_cxxstd": "20", - "runs_on": "windows-2022", - "b2_toolset": "msvc-14.4", - "generator": "Visual Studio 17 2022", + "runs_on": "windows-2025-vs2026", + "b2_toolset": "msvc-14.5", + "generator": "Ninja", "is_latest": true }, { - "version": "14.34", + "version": "14.44", "cxxstd": "20", "latest_cxxstd": "20", - "runs_on": "windows-11-arm", - "b2_toolset": "msvc-14.3" + "runs_on": "windows-11-vs2026-arm", + "b2_toolset": "msvc-14.4", + "msvc_toolset": "14.44" }, { - "version": "14.44", + "version": "14.51", "cxxstd": "20", "latest_cxxstd": "20", - "runs_on": "windows-11-arm", - "b2_toolset": "msvc-14.4" + "runs_on": "windows-11-vs2026-arm", + "b2_toolset": "msvc-14.5" } ], "clang-cl": [ diff --git a/.github/generate-matrix.py b/.github/generate-matrix.py index c58f429fb..078879bd9 100644 --- a/.github/generate-matrix.py +++ b/.github/generate-matrix.py @@ -72,6 +72,8 @@ def make_entry(compiler_family, spec, **overrides): entry["shared"] = spec["shared"] if spec.get("vcpkg_triplet"): entry["vcpkg-triplet"] = spec["vcpkg_triplet"] + if spec.get("msvc_toolset"): + entry["msvc-toolset"] = spec["msvc_toolset"] # CMake builds only on earliest/latest compilers, unless explicitly disabled if spec.get("build_cmake") is False: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b6396ed3..4443649b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,51 @@ jobs: check-latest: true trace-commands: true + # setup-cpp ignores the MSVC version and loads the image's default + # toolset, so without this check the matrix version is only a label. + # A side-by-side toolset (msvc-toolset) is pinned for B2 through a + # setup script, because B2 re-runs vcvarsall before building. + - name: Configure MSVC + id: msvc + if: matrix.compiler == 'msvc' + env: + MSVC_VERSION: ${{ matrix.version }} + MSVC_TOOLSET: ${{ matrix.msvc-toolset }} + B2_TOOLSET: ${{ matrix.b2-toolset }} + run: | + set -euo pipefail + # Not PROCESSOR_ARCHITECTURE: on ARM64 runners this bash step + # does not see ARM64 there, and picked the x64 toolset. + case "$RUNNER_ARCH" in + ARM64) arch=arm64; bin=HostARM64/ARM64; target=ARM64 ;; + X64) arch=amd64; bin=Hostx64/x64; target=x64 ;; + *) echo "::error::Unsupported runner architecture: $RUNNER_ARCH"; exit 1 ;; + esac + check="$RUNNER_TEMP/msvc-banner.bat" + if [ -n "$MSVC_TOOLSET" ]; then + setup="$RUNNER_TEMP/vcvars-$MSVC_TOOLSET.bat" + printf '@call "%sAuxiliary\\Build\\vcvarsall.bat" %%* -vcvars_ver=%s\r\n' \ + "$VCINSTALLDIR" "$MSVC_TOOLSET" > "$setup" + tools=( "$(cygpath -m "$VCINSTALLDIR")Tools/MSVC/$MSVC_TOOLSET."* ) + cl="${tools[-1]}/bin/$bin/cl.exe" + test -f "$cl" + user_config="$RUNNER_TEMP/user-config.jam" + echo "using msvc : ${B2_TOOLSET#msvc-} : \"$cl\" : \"$(cygpath -m "$setup")\" ;" \ + > "$user_config" + cat "$user_config" + echo "user-config=$(cygpath -m "$user_config")" >> "$GITHUB_OUTPUT" + printf '@call "%s" %s >nul\r\n@cl\r\n' "$(cygpath -w "$setup")" "$arch" > "$check" + else + printf '@cl\r\n' > "$check" + fi + out=$(cmd //c "$(cygpath -w "$check")" 2>&1 || true) + banner=$(printf '%s\n' "$out" | tr -d '\r' | grep -m1 'Compiler Version' || true) + echo "$banner" + case "$banner" in + *"Version 19.${MSVC_VERSION#14.}."*" for $target") ;; + *) echo "::error::Expected MSVC $MSVC_VERSION for $target, got: $banner"; exit 1 ;; + esac + - name: Install packages uses: alandefreitas/cpp-actions/package-install@v1.9.0 id: package-install @@ -494,6 +539,10 @@ jobs: source-dir: boost-root modules: corosio toolset: ${{ matrix.b2-toolset }} + # Set only for a pinned msvc-toolset. B2 then reads this file + # instead of any other user-config.jam, so a pinned x64 toolset + # must also carry the "using openssl" line. + user-config: ${{ steps.msvc.outputs.user-config }} build-variant: ${{ (matrix.compiler == 'msvc' && 'debug,release') || matrix.build-type }} cxx: ${{ matrix.cxx || steps.setup-cpp.outputs.cxx || '' }} cxxstd: ${{ matrix.cxxstd }} diff --git a/test/unit/socket_stress.cpp b/test/unit/socket_stress.cpp index 642a79666..687187c20 100644 --- a/test/unit/socket_stress.cpp +++ b/test/unit/socket_stress.cpp @@ -1,6 +1,7 @@ // // Copyright (c) 2026 Vinnie Falco // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -375,6 +376,85 @@ struct cancel_close_stress_test std::atomic cancel_writes{0}; std::atomic stop_flag{false}; + // One cancel cycle. It is a separate coroutine rather than + // inline in worker's try block because MSVC 19.51.36257 x64 + // hits an internal compiler error (C1001, Utc\src\p2\main.cpp + // line 262) at /O2 when a co_await of corosio::delay sits in + // a loop inside a coroutine's try block. + auto iteration = [&](int i) -> capy::task<> { + // Start a blocking read - use atomic for thread-safe signaling + char buf[32]; + std::atomic read_done{false}; + std::error_code read_ec; + + auto read_coro = [&read_done, &read_ec, &s2, + &buf]() -> capy::task<> { + auto [ec, n] = co_await s2.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + read_ec = ec; + read_done.store(true, std::memory_order_release); + }; + + capy::run_async(ex)(read_coro()); + + // Vary the cancellation method + switch (i % 3) + { + case 0: + { + // Yield to let the posted read_coro start + std::ignore = + co_await corosio::delay(std::chrono::microseconds(1)); + // Cancel via tcp_socket.cancel() + s2.cancel(); + ++cancels; + break; + } + case 1: + // Write data to complete the read normally + { + [[maybe_unused]] auto [ec, n] = + co_await s1.write_some(capy::const_buffer("data", 4)); + } + ++writes; + break; + case 2: + // Cancel then immediately write (race) + s2.cancel(); + { + [[maybe_unused]] auto [ec, n] = + co_await s1.write_some(capy::const_buffer("data", 4)); + } + ++cancel_writes; + break; + } + + // Poll for read completion with timeout (max 1 second) + for (int wait = 0; wait < 100; ++wait) + { + if (read_done.load(std::memory_order_acquire)) + break; + std::ignore = + co_await corosio::delay(std::chrono::milliseconds(10)); + } + + if (!read_done.load(std::memory_order_acquire)) + { + std::fprintf( + stderr, + " cancel_close_stress: read hung on case %d, " + "iter %d\n", + i % 3, i); + BOOST_TEST(read_done.load(std::memory_order_acquire)); + // Force cancel + s2.cancel(); + std::ignore = + co_await corosio::delay(std::chrono::milliseconds(100)); + } + + ++iterations; + }; + // Worker: rapidly cancel operations on pre-created sockets auto worker = [&]() -> capy::task<> { while (!stop_flag.load(std::memory_order_relaxed)) @@ -385,80 +465,7 @@ struct cancel_close_stress_test i < 50 && !stop_flag.load(std::memory_order_relaxed); ++i) { - // Start a blocking read - use atomic for thread-safe signaling - char buf[32]; - std::atomic read_done{false}; - std::error_code read_ec; - - auto read_coro = [&read_done, &read_ec, &s2, - &buf]() -> capy::task<> { - auto [ec, n] = co_await s2.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - read_ec = ec; - read_done.store(true, std::memory_order_release); - }; - - capy::run_async(ex)(read_coro()); - - // Vary the cancellation method - switch (i % 3) - { - case 0: - { - // Yield to let the posted read_coro start - std::ignore = co_await corosio::delay( - std::chrono::microseconds(1)); - // Cancel via tcp_socket.cancel() - s2.cancel(); - ++cancels; - break; - } - case 1: - // Write data to complete the read normally - { - [[maybe_unused]] auto [ec, n] = - co_await s1.write_some( - capy::const_buffer("data", 4)); - } - ++writes; - break; - case 2: - // Cancel then immediately write (race) - s2.cancel(); - { - [[maybe_unused]] auto [ec, n] = - co_await s1.write_some( - capy::const_buffer("data", 4)); - } - ++cancel_writes; - break; - } - - // Poll for read completion with timeout (max 1 second) - for (int wait = 0; wait < 100; ++wait) - { - if (read_done.load(std::memory_order_acquire)) - break; - std::ignore = co_await corosio::delay( - std::chrono::milliseconds(10)); - } - - if (!read_done.load(std::memory_order_acquire)) - { - std::fprintf( - stderr, - " cancel_close_stress: read hung on case %d, " - "iter %d\n", - i % 3, i); - BOOST_TEST( - read_done.load(std::memory_order_acquire)); - // Force cancel - s2.cancel(); - std::ignore = co_await corosio::delay( - std::chrono::milliseconds(100)); - } - - ++iterations; + co_await iteration(i); } } catch (const std::exception& e)