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 4c32691ee..a13cadc9d 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 dff1b62f5..9223d8cad 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/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index a6258a3ab..ef016bd6b 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -71,15 +71,19 @@ 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 (the platform's + # reporter, not xunit's --report-xunit-trx, which truncated the file in 4.0.2) 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-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 f4c6a6a6a..1522cf45f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -225,7 +225,10 @@ 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 is a platform + # 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/AGENTS.md b/AGENTS.md index 2f07ec7b5..5f484d71e 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/Directory.Packages.props b/Directory.Packages.props index ac86b6508..2759ef146 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,9 @@ - - + + + @@ -55,9 +56,7 @@ - - - + \ No newline at end of file diff --git a/StackExchange.Redis.slnx b/StackExchange.Redis.slnx index f3a6ac32f..55d863161 100644 --- a/StackExchange.Redis.slnx +++ b/StackExchange.Redis.slnx @@ -40,6 +40,7 @@ + diff --git a/build.ps1 b/build.ps1 index 3ace75a06..09a2a264c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -29,11 +29,13 @@ 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 ".\Build.csproj" -c Release -f net8.0 --no-build --logger 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-trx } else { - dotnet test ".\Build.csproj" -c Release --no-build --logger 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/global.json b/global.json index 35e954767..e851283da 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 eb40683a7..74af96a40 100644 --- a/tests/RESPite.Tests/RESPite.Tests.csproj +++ b/tests/RESPite.Tests/RESPite.Tests.csproj @@ -6,14 +6,15 @@ 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 4c85bcf99..060064481 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,8 @@ - - + diff --git a/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs b/tests/StackExchange.Redis.FaultInjector.Tests/AssemblyInfo.cs index 40188bd5b..170669fc7 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/README.md b/tests/StackExchange.Redis.FaultInjector.Tests/README.md index 95c3d3c05..0ae6ff8f2 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 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 052d85e11..308b3aa29 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,16 @@ full enable true + + true - - - + diff --git a/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs b/tests/StackExchange.Redis.Tests/BatchWriteFailureTests.cs index 5ca7a2789..0243b4515 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/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 275101569..4ef9b633f 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/Helpers/TcpForwarder.cs b/tests/StackExchange.Redis.Tests/Helpers/TcpForwarder.cs index ea2b92665..7f8ea83a7 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 047b6d04d..726eb0bbb 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 f20a2646a..a20caaa0f 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,17 @@ - + + + + - - + diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 0515451ba..fb568a1a1 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]