From f5ea9a9f58655e0f1a4a02d4e273831276d25464 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Oct 2026 10:53:44 +0100 Subject: [PATCH 1/5] xunit.v3 4.0 on Microsoft.Testing.Platform xunit.v3 4.x depends on xunit.v3.mtp-v2, which needs Microsoft.Testing.Platform 2.5+, and that drops the VSTest bridge under the .NET 10 SDK. So: global.json selects the MTP runner for dotnet test, the four test projects set UseMicrosoftTestingPlatformRunner and stop referencing the VSTest adapters (Microsoft.NET.Test.Sdk, xunit.runner.visualstudio, xunit.v3.runner.console), and CI.yml/build.ps1 use the MTP dotnet test shape (--project / --solution, --report-xunit-trx; the GitHub Actions reporter self-registers as an extension). xunit 4 fallout: the per-protocol TheoryDiscoverer overrides the new 6-parameter CreateTestCasesForDataRow; the fault-injector tests use [Parallelization(Mode = None)]; xUnit1069 is suppressed for the round-trip tests, whose Timeout is a hang guard; net481 regains System.Reflection.Metadata and Microsoft.CSharp, previously transitive via Test.Sdk. --- .github/workflows/CI.yml | 4 +++- Directory.Packages.props | 7 ++----- build.ps1 | 4 ++-- global.json | 3 +++ tests/RESPite.Tests/RESPite.Tests.csproj | 6 +++--- .../StackExchange.Redis.Build.Tests.csproj | 5 +++-- .../AssemblyInfo.cs | 5 +++-- ...StackExchange.Redis.FaultInjector.Tests.csproj | 6 +++--- .../Helpers/Attributes.cs | 4 ++-- .../StackExchange.Redis.Tests.csproj | 15 ++++++++++++--- 10 files changed, 36 insertions(+), 23 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f4c6a6a6a4..1d58950ebf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -225,7 +225,9 @@ jobs: run: | $exitCode = 0 foreach ($tfm in @("net10.0", "net481")) { - dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true + # Microsoft.Testing.Platform `dotnet test` (selected in global.json): the GitHub Actions reporter registers itself + # as a platform extension, so only the TRX report (for dorny/test-reporter below) needs asking for + dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-xunit-trx if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE } diff --git a/Directory.Packages.props b/Directory.Packages.props index ac86b65080..7952739f99 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,7 @@ - - + @@ -55,9 +54,7 @@ - - - + \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 3ace75a065..e3fd412e34 100644 --- a/build.ps1 +++ b/build.ps1 @@ -31,9 +31,9 @@ if ($RunTests) { } Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta" if ($NetCoreOnlyTests) { - dotnet test ".\Build.csproj" -c Release -f net8.0 --no-build --logger trx + dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net8.0 --no-build --report-xunit-trx } else { - dotnet test ".\Build.csproj" -c Release --no-build --logger trx + dotnet test --solution ".\StackExchange.Redis.slnx" -c Release --no-build --report-xunit-trx } if ($LastExitCode -ne 0) { Write-Host "Error with tests, aborting build." -Foreground "Red" diff --git a/global.json b/global.json index 35e9547671..e851283dae 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ { "sdk": { "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } \ No newline at end of file diff --git a/tests/RESPite.Tests/RESPite.Tests.csproj b/tests/RESPite.Tests/RESPite.Tests.csproj index eb40683a70..70655ce0ba 100644 --- a/tests/RESPite.Tests/RESPite.Tests.csproj +++ b/tests/RESPite.Tests/RESPite.Tests.csproj @@ -6,14 +6,14 @@ false true Exe + + true - - - diff --git a/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj b/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj index 4c85bcf99d..5be251b197 100644 --- a/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj +++ b/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj @@ -2,6 +2,9 @@ net10.0 Exe + + true enable @@ -25,9 +28,7 @@ - - diff --git a/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs b/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs index 40188bd5bd..170669fc74 100644 --- a/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs +++ b/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using Xunit; +using Xunit.Sdk; +using Xunit.v3; // One cluster, one injector, and scenarios that mutate *cluster* state - node exclusions, maintenance mode, // endpoint policies. Running classes in parallel therefore breaks two ways at once: they interfere semantically @@ -8,4 +9,4 @@ // // So this tier is strictly serial. It costs wall-clock - the scenarios are minutes each - and buys results that // mean something. -[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: Parallelization(Mode = ParallelMode.None)] diff --git a/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj b/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj index 052d85e114..5884b797b4 100644 --- a/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj +++ b/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj @@ -11,15 +11,15 @@ full enable true + + true - - - diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 275101569f..4ef9b633f5 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -57,8 +57,8 @@ public override ValueTask> Discover(ITestFra public class TheoryDiscoverer : Xunit.v3.TheoryDiscoverer { - protected override ValueTask> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, ITheoryAttribute theoryAttribute, ITheoryDataRow dataRow, object?[] testMethodArguments) - => base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, testMethodArguments).ExpandAsync(); + protected override ValueTask> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, ITheoryAttribute theoryAttribute, ITheoryDataRow dataRow, object?[] testMethodArguments, string? index) + => base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, testMethodArguments, index).ExpandAsync(); protected override ValueTask> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, ITheoryAttribute theoryAttribute) => base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute).ExpandAsync(); diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index f20a2646ad..ea610334b6 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -9,9 +9,17 @@ full enable true + + true $(DefineConstants);BUILD_CURRENT + + + $(NoWarn);xUnit1069 @@ -29,15 +37,16 @@ - + + + + - - From 1f1c37536b190246892a24e74d235176375a0619 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Oct 2026 11:01:36 +0100 Subject: [PATCH 2/5] CI: the Ubuntu composite action also moves to the MTP dotnet test shape; enable the GitHub reporter --report-github is off by default under Microsoft.Testing.Platform (the VSTest logger was on by name), so both jobs ask for it explicitly, with the summary limited to failures as before. --- .github/actions/run-tests/action.yml | 11 +++++++---- .github/workflows/CI.yml | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index a6258a3ab2..f77a0f8d5d 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -71,15 +71,18 @@ runs: - name: StackExchange.Redis.Tests shell: bash + # Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform + # extension that is off by default, and the TRX report feeds dorny/test-reporter below run: >- - dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj + dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f net10.0 - --logger trx - --logger "GitHubActions;summary-include-passed=false;summary-include-skipped=false" --results-directory ./test-results/ - /p:CI=true + --report-xunit-trx + --report-github + --report-github-summary-include-passed false + --report-github-summary-include-skipped false - uses: dorny/test-reporter@v3 continue-on-error: true diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1d58950ebf..1ef0951ef4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -225,9 +225,9 @@ jobs: run: | $exitCode = 0 foreach ($tfm in @("net10.0", "net481")) { - # Microsoft.Testing.Platform `dotnet test` (selected in global.json): the GitHub Actions reporter registers itself - # as a platform extension, so only the TRX report (for dorny/test-reporter below) needs asking for - dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-xunit-trx + # Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform + # extension that is off by default, and the TRX report feeds dorny/test-reporter below + dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-xunit-trx --report-github --report-github-summary-include-passed false --report-github-summary-include-skipped false if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE } From 5f3acf521feed92df7a807824079b41b337c9bd3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Oct 2026 11:13:46 +0100 Subject: [PATCH 3/5] Follow-ups for the MTP runner: analyzer tests join the solution, build.ps1 wording and framework, docs Build.Tests was never in StackExchange.Redis.slnx, so dotnet test --solution skipped it. The -NetCoreOnlyTests path moves to net10.0: two test projects have no net8.0, and the runner fails a project lacking the requested framework instead of skipping it. AGENTS.md, the skills and the fault-injector README use the --project / --filter-class forms. --- .../SKILL.md | 4 ++-- .claude/skills/implement-resp-command/SKILL.md | 4 ++-- AGENTS.md | 12 ++++++++---- StackExchange.Redis.slnx | 1 + build.ps1 | 6 ++++-- .../README.md | 2 +- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.claude/skills/create-implementation-plan-for-redis-api-change/SKILL.md b/.claude/skills/create-implementation-plan-for-redis-api-change/SKILL.md index 4c32691ee0..a13cadc9d0 100644 --- a/.claude/skills/create-implementation-plan-for-redis-api-change/SKILL.md +++ b/.claude/skills/create-implementation-plan-for-redis-api-change/SKILL.md @@ -234,8 +234,8 @@ present (write "none" rather than omitting one): malformed), the `RoundTripUnitTests` cases (exact bytes), the live tests written-not-run with `Create(require: RedisFeatures.vX_Y_Z)`, the HLD section 8 scenarios copied and marked `expected`, and the exact commands: - `dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~"` - and, when the analyzer changed, `dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj`. + `dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class "**"` + and, when the analyzer changed, `dotnet test --project tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj`. 7. **Docs / changelog / public-API files** - `docs/.md` + `docs/index.md` yes/no, `docs/exp/SERxxx.md` yes/no, the `PublicAPI` files, and the note that `docs/ReleaseNotes.md` is frozen. diff --git a/.claude/skills/implement-resp-command/SKILL.md b/.claude/skills/implement-resp-command/SKILL.md index dff1b62f51..9223d8cad9 100644 --- a/.claude/skills/implement-resp-command/SKILL.md +++ b/.claude/skills/implement-resp-command/SKILL.md @@ -145,6 +145,6 @@ The in-process managed server (`toys/StackExchange.Redis.Server`) may also need ## Before finishing - `dotnet build Build.csproj -c Release /p:CI=true` — analyzers + `TreatWarningsAsErrors` must pass (this catches a missing `PublicAPI.Unshipped.txt` entry). -- `dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~MyCommand"` — runs your new unit tests without any server. -- `dotnet test tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj` — if you touched `TransactionAnalyzer`. Also needs no server, and takes seconds. +- `dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class "*MyCommand*"` — runs your new unit tests without any server (Microsoft.Testing.Platform filters take fully qualified names, `*` at either end). +- `dotnet test --project tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj` — if you touched `TransactionAnalyzer`. Also needs no server, and takes seconds. - Double-check no shipped signature changed (back-compat). diff --git a/AGENTS.md b/AGENTS.md index 2f07ec7b51..5f484d71e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ StackExchange.Redis — a high-performance .NET client for RESP servers (Redis, - `toys/` — runnable samples and an in-process RESP server (`StackExchange.Redis.Server`, used by tests as a managed fake server), Kestrel-hosted server, console tools. - `docs/` — published documentation site source (markdown). `docs/ReleaseNotes.md` is the changelog (frozen at 3.0 — from v3 onward release notes live in GitHub Releases). -`Build.csproj` is a `Microsoft.Build.Traversal` project that references everything under `eng/src/tests/toys` — build/test/pack it to act on the whole repo. `StackExchange.Redis.slnx` is the IDE solution (XML SLNX format, not classic `.sln`). +`Build.csproj` is a `Microsoft.Build.Traversal` project that references everything under `eng/src/tests/toys` — build/pack it to act on the whole repo. `StackExchange.Redis.slnx` is the IDE solution (XML SLNX format, not classic `.sln`); it is also what `dotnet test --solution` runs, because tests use the Microsoft.Testing.Platform runner (selected in `global.json`), which cannot see a traversal project on the .NET 10 SDK. ## Build, test, pack @@ -30,10 +30,14 @@ pwsh ./build.ps1 -StartServers docker compose --file tests/RedisConfigs/docker-compose.yml up -d --wait # Run the main test suite against one target framework (fastest inner loop) -dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release -f net10.0 +dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release -f net10.0 -# Run a single test class / method (xUnit v3 + Microsoft.Testing.Platform) -dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~ClassName.MethodName" +# Run a single test class / method (xUnit v3 on Microsoft.Testing.Platform; names are fully qualified, '*' allowed at either end) +dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-class StackExchange.Redis.Tests.ClassName +dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter-method StackExchange.Redis.Tests.ClassName.MethodName + +# Every test project at once (what build.ps1 does) +dotnet test --solution StackExchange.Redis.slnx -c Release --no-build # Pack the library dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true diff --git a/StackExchange.Redis.slnx b/StackExchange.Redis.slnx index f3a6ac32f6..55d8631613 100644 --- a/StackExchange.Redis.slnx +++ b/StackExchange.Redis.slnx @@ -40,6 +40,7 @@ + diff --git a/build.ps1 b/build.ps1 index e3fd412e34..2159d7451c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -29,9 +29,11 @@ if ($RunTests) { & .\RedisConfigs\start-all.cmd Write-Host "Servers Started." -ForegroundColor "Green" } - Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta" + Write-Host "Running tests: StackExchange.Redis.slnx (all test projects)" -ForegroundColor "Magenta" if ($NetCoreOnlyTests) { - dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net8.0 --no-build --report-xunit-trx + # net10.0, not net8.0: the analyzer and fault-injector test projects only target net10.0, and the + # Microsoft.Testing.Platform runner fails a project that lacks the requested framework rather than skipping it + dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net10.0 --no-build --report-xunit-trx } else { dotnet test --solution ".\StackExchange.Redis.slnx" -c Release --no-build --report-xunit-trx } diff --git a/tests/StackExchange.Redis.FaultInjector.Tests/README.md b/tests/StackExchange.Redis.FaultInjector.Tests/README.md index 95c3d3c05d..0ae6ff8f23 100644 --- a/tests/StackExchange.Redis.FaultInjector.Tests/README.md +++ b/tests/StackExchange.Redis.FaultInjector.Tests/README.md @@ -12,7 +12,7 @@ docker compose up -d # 10-15 minutes for the cluster to come export SER_FI_CONFIG_DIR=$PWD # or FI_CONSOLE_CONFIG_DIR, which is also honoured export E2E_SCENARIO_TESTS=true # explicit opt-in: these create and delete databases -dotnet test tests/StackExchange.Redis.FaultInjector.Tests +dotnet test --project tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj ``` **Point it at the *current* environment.** A stale directory passes every check we make: the credentials are From bd9b1d908c472c9ca1c594ab0e0c216dec511b41 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Oct 2026 11:20:16 +0100 Subject: [PATCH 4/5] Use the platform TRX reporter; observe the test-side tasks the new runner reports as unobserved xunit 4.0.2's --report-xunit-trx wrote a truncated file for the 6.8k-test run (ends mid-element; dorny/test-reporter: 'Invalid XML'), locally as well as in CI. Microsoft.Testing.Extensions.TrxReport's --report-trx produces a complete report (validated: parses, 6828 UnitTestResult elements). Microsoft.Testing.Platform also prints every UnobservedTaskException at the end of the run, which VSTest never showed. All of the ones seen are in test code, not the library: the in-process server's background work logging to xunit after its test ended (xunit v3 throws 'no currently active test'); BatchWriteFailureTests never reading its Task.WhenAll's exception; TcpForwarder's losing copy direction. Each now observes or swallows its own failure. --- .github/actions/run-tests/action.yml | 5 +++-- .github/workflows/CI.yml | 5 +++-- Directory.Packages.props | 2 ++ build.ps1 | 4 ++-- tests/RESPite.Tests/RESPite.Tests.csproj | 1 + .../StackExchange.Redis.Build.Tests.csproj | 1 + .../StackExchange.Redis.FaultInjector.Tests.csproj | 1 + .../StackExchange.Redis.Tests/BatchWriteFailureTests.cs | 3 ++- tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs | 8 +++++--- tests/StackExchange.Redis.Tests/InProcessTestServer.cs | 9 ++++++++- .../StackExchange.Redis.Tests.csproj | 1 + 11 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index f77a0f8d5d..ef016bd6b7 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -72,14 +72,15 @@ runs: - name: StackExchange.Redis.Tests shell: bash # Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform - # extension that is off by default, and the TRX report feeds dorny/test-reporter below + # extension that is off by default, and the TRX report feeds dorny/test-reporter below (the platform's + # reporter, not xunit's --report-xunit-trx, which truncated the file in 4.0.2) run: >- dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f net10.0 --results-directory ./test-results/ - --report-xunit-trx + --report-trx --report-github --report-github-summary-include-passed false --report-github-summary-include-skipped false diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1ef0951ef4..1522cf45f4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -226,8 +226,9 @@ jobs: $exitCode = 0 foreach ($tfm in @("net10.0", "net481")) { # Microsoft.Testing.Platform `dotnet test` (selected in global.json); the GitHub Actions reporter is a platform - # extension that is off by default, and the TRX report feeds dorny/test-reporter below - dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-xunit-trx --report-github --report-github-summary-include-passed false --report-github-summary-include-skipped false + # extension that is off by default, and the TRX report feeds dorny/test-reporter below (the platform's + # reporter, not xunit's --report-xunit-trx, which truncated the file in 4.0.2) + dotnet test --project tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --results-directory ./test-results/ --report-trx --report-github --report-github-summary-include-passed false --report-github-summary-include-skipped false if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE } diff --git a/Directory.Packages.props b/Directory.Packages.props index 7952739f99..2759ef146d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,6 +42,8 @@ + + diff --git a/build.ps1 b/build.ps1 index 2159d7451c..09a2a264c4 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,9 +33,9 @@ if ($RunTests) { if ($NetCoreOnlyTests) { # net10.0, not net8.0: the analyzer and fault-injector test projects only target net10.0, and the # Microsoft.Testing.Platform runner fails a project that lacks the requested framework rather than skipping it - dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net10.0 --no-build --report-xunit-trx + dotnet test --solution ".\StackExchange.Redis.slnx" -c Release -f net10.0 --no-build --report-trx } else { - dotnet test --solution ".\StackExchange.Redis.slnx" -c Release --no-build --report-xunit-trx + dotnet test --solution ".\StackExchange.Redis.slnx" -c Release --no-build --report-trx } if ($LastExitCode -ne 0) { Write-Host "Error with tests, aborting build." -Foreground "Red" diff --git a/tests/RESPite.Tests/RESPite.Tests.csproj b/tests/RESPite.Tests/RESPite.Tests.csproj index 70655ce0ba..74af96a404 100644 --- a/tests/RESPite.Tests/RESPite.Tests.csproj +++ b/tests/RESPite.Tests/RESPite.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj b/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj index 5be251b197..0600644812 100644 --- a/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj +++ b/tests/StackExchange.Redis.Build.Tests/StackExchange.Redis.Build.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj b/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj index 5884b797b4..308b3aa29a 100644 --- a/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj +++ b/tests/StackExchange.Redis.FaultInjector.Tests/StackExchange.Redis.FaultInjector.Tests.csproj @@ -21,5 +21,6 @@ + diff --git a/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs b/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs index 5ca7a27893..0243b45152 100644 --- a/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs +++ b/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs @@ -46,7 +46,8 @@ public async Task EveryBatchCommandCompletesAfterMidBatchWriteFailure(bool queue { Assert.Fail("hung: " + string.Join(", ", all.Select((t, i) => $"cmd{i}={t.Status}"))); } - // individual faults are inspected below + // individual faults are inspected below; the combined task's own exception is never read, so mark it seen + _ = everything.Exception; for (int i = 0; i < all.Length; i++) { diff --git a/tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs b/tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs index ea2b92665e..7f8ea83a77 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs @@ -69,9 +69,11 @@ private async Task PumpAsync(TcpClient client) // the buffer size is spelled out because the two-argument (stream, token) overload does not // exist on .NET Framework, which the test project still targets const int BufferSize = 4096; - await Task.WhenAny( - near.CopyToAsync(far, BufferSize, _cts.Token), - far.CopyToAsync(near, BufferSize, _cts.Token)).ForAwait(); + var inbound = near.CopyToAsync(far, BufferSize, _cts.Token); + var outbound = far.CopyToAsync(near, BufferSize, _cts.Token); + await Task.WhenAny(inbound, outbound).ForAwait(); + // the other direction fails when the sockets go away below; nobody awaits it, so observe it here + _ = Task.WhenAll(inbound, outbound).ContinueWith(static t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); } catch { diff --git a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs index 047b6d04de..726eb0bbbc 100644 --- a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs +++ b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs @@ -181,7 +181,14 @@ public ConfigurationOptions GetClientConfig(bool withPubSub = true, bool default public override void Log(string message) { - _log?.WriteLine(message); + // the server's own background work (write loops, delayed closes after MOVING) can outlive the test that + // created it; xunit v3 throws on output with no active test, and from a fire-and-forget task that only + // surfaces as an unobserved exception at the end of the run + try + { + _log?.WriteLine(message); + } + catch (InvalidOperationException) { } base.Log(message); } diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index ea610334b6..a20caaa0f0 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -47,6 +47,7 @@ + From d026f639bae718272319d8274bdfe4b1e8c10a83 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Oct 2026 11:32:20 +0100 Subject: [PATCH 5/5] VectorSetInfo: vset-uid is a zero-based counter, so do not assert it is non-zero Measured on a fresh test server: the first vector set created after startup reports vset-uid 0, the next 1, and so on. CI starts a fresh server every run, and the xunit 4 discovery order makes VectorSetInfo(Binary) under RESP2 the first test to create one, so the old assertion failed there and nowhere else. Check the parsed value against the raw VINFO reply instead. --- .../VectorSetIntegrationTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 0515451ba9..fb568a1a1d 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -252,8 +252,13 @@ public async Task VectorSetInfo(VectorSetQuantization quantization) Assert.Equal(quantization, v.Quantization); Assert.Null(v.QuantizationRaw); // Should be null for known quant types - Assert.NotEqual(0, v.VectorSetUid); - Assert.NotEqual(0, v.HnswMaxNodeUid); + // vset-uid is a zero-based per-process counter: the first vector set created after the server starts + // has uid 0, which is exactly what happens on a fresh CI server when this is the first such test to run. + // So check the parse against the raw reply rather than against zero. + var raw = (await db.ExecuteAsync("VINFO", key)).ToDictionary(); + Assert.Equal((long)raw["vset-uid"], v.VectorSetUid); + Assert.Equal((long)raw["hnsw-max-node-uid"], v.HnswMaxNodeUid); + Assert.NotEqual(0, v.HnswMaxNodeUid); // node uids start at 1, and there is one node } [Fact]