Auto-discover unit-test projects in CI#2470
Conversation
The run-unit-tests target only ran 4 hardcoded projects, so test projects such as Amazon.Lambda.AspNetCoreServer.Test, Core.Tests, EventsTests.NET8 and others were never executed in CI. Replace the hardcoded list with run-unit-tests.ps1, which discovers any project referencing the test SDK (excluding *.IntegrationTests.csproj, which run in a separate phase), mirroring run-integ-tests-parallel.ps1.
These test projects were never run in CI before the discovery change, so pre-existing failures went unnoticed. Fixes by category: Real test bugs (failed deterministically, incl. locally): - BuildStreamingPreludeTests / ResponseStreamingPropertyTests: the StandalonePreludeBuilder subclassed the HTTP API v2 function (whose BuildStreamingPrelude override collapses headers into single-value Headers), but the tests assert on the multi-value MultiValueHeaders collection. Derive from the REST v1 APIGatewayProxyFunction instead, matching the assertions and the section comments. CI-environment robustness (passed locally, failed in CI): - ci.buildspec.yml: raise fs.inotify.max_user_instances (container default 128 was exhausted once all ASP.NET-host-starting unit tests run, surfacing as inotify IOExceptions across ~90 tests). - TestMinimalAPI: capture child-process stdout/stderr and handle the WaitForExit timeout so the out-of-process 'dotnet run' failure (exit 129) is diagnosable and the process cannot leak. - AddAWSLambdaBeforeSnapshotRequestTests: poll for the before-snapshot callback instead of racing a fixed 500 ms delay that was flaky under CI load.
CI run surfaced pre-existing failures (now fixed)The first CI run went red — as designed, discovery turned on test projects that were never executed in CI before, exposing their pre-existing failures ( Real test bugs (deterministic — reproduced locally)
CI-environment robustness (passed locally, failed only in CI)
All |
Previously the script ran every unit-test project and only reported the aggregate failures at the end. Exit immediately on the first failing project for quicker feedback and to avoid spending CI time on later projects once the build is already red.
Rework run-unit-tests.ps1 to mirror run-integ-tests-parallel.ps1: build all discovered projects once serially (avoids the shared ProjectReference build-output race), then run 'dotnet test --no-build' in parallel with streamed, project-prefixed output. All projects run (no short-circuit) so every failure is surfaced in one pass, and failed projects' output is reprinted as a clean block before exiting non-zero. Running the projects concurrently exposed a pre-existing intra-assembly flake in Amazon.Lambda.AspNetCoreServer.Hosting.Tests: many tests mutate the process-global AWS_LAMBDA_FUNCTION_NAME env var, and xUnit parallelizes collections within an assembly by default, so one test's value leaked into AddAWSLambdaHosting_NotInLambda_DoesNotRegisterHostingOptions (which needs it unset). Add AssemblyInfo.cs disabling in-assembly parallelization for that project so the env-var-dependent tests run serially.
PositiveHandlerTestsAsync intermittently failed (~1 in 5 under load) with 'Can't find method name in console text'. Two pre-existing issues, both aggravated by concurrency: - ExecHandlerAsync/TestHandlerFailAsync point a static logger action on the shared Amazon.Lambda.Core assembly at a per-invocation StringWriter, and LambdaBootstrap's background RunAsync writes through it. When xUnit runs collections in parallel, one test's logging can land in another's writer. Add AssemblyInfo.cs disabling in-assembly parallelization (same approach as Hosting.Tests); some classes already shared a [Collection] but that did not cover the whole assembly. - The InvokeAsync/exception waiter loops busy-spun on a field with no yield, pinning a core and starving the bootstrap thread under load. Replace the tight spins with await Task.Delay(1). Verified stable across 6 consecutive full-suite runs (was ~1 in 5).
Unit tests now run in parallel + fixed flakes it exposedReworked Note: this replaces the earlier fail-fast behavior — like the integ script, it now surfaces all failures rather than short-circuiting. Running projects concurrently surfaced two pre-existing intra-assembly flakes (xUnit parallelizes collections within an assembly by default), both fixed:
All fixes verified locally on net8.0/net10.0. (The only local-only failure is |
The last CI run's unit tests all passed but the build hung ~1h50m after
they finished, until the 2-hour AWS credential expired ('security token
expired'). Amazon.Lambda.AspNetCoreServer.Test was the one project that
never printed a completion line: TestSnapStartInitialization started a
LambdaBootstrap polling loop against a dead endpoint (localhost:123) as
fire-and-forget (_ = bootstrap.RunAsync(cts.Token)). After the assert the
leaked loop kept retrying (the log shows GET /api/SnapStart repeating), so
under parallel load dotnet test never exited.
- Keep the RunAsync task, assert before teardown, then cancel and await
the task so no background work outlives the test. The dead-endpoint loop
surfaces OperationCanceledException or HttpRequestException on teardown;
both are expected and swallowed.
- Add AssemblyInfo.cs disabling in-assembly parallelization (same approach
as Hosting.Tests and RuntimeSupport.UnitTests): this project shares the
process-global AWS_LAMBDA_* env vars and the static SnapStartController
.Invoked flag across tests.
Verified 193/193 on net8.0 and net10.0, stable across repeated runs, with
no hang.
Root-caused the CI hang ('security token expired')That run wasn't a test failure — all 286 unit tests passed. The build hung for ~1h50m after the tests finished (17:26 → 19:15) until the 2-hour AWS role credential expired, which surfaced as Cause: 13 of 14 unit-test projects printed a completion line; the one that didn't was Fix (
Verified 193/193 on net8.0 and net10.0, stable across repeated runs, no hang. |
Two CI runs hung ~1h50m (until the 2-hour credential expired) inside Amazon.Lambda.AspNetCoreServer.Test. Its TestSnapStartInitialization drives SnapStart init, which invokes the before-snapshot hooks accumulated in the process-global Amazon.Lambda.Core.SnapshotRestore registry by every test's function constructor; each hook runs in-process ASP.NET requests. Under the CPU/host contention of the parallel pool that work deadlocked. The project passes reliably on its own (193/193, ~8s), so run it serially after the parallel pool instead of concurrently. run-unit-tests.ps1 now splits discovered projects into a parallel pool and a serial list (matched by file name); all are built once up front, the pool runs with --no-build, then the serial projects run one at a time. Verified locally: AspNetCoreServer.Test passes serially with no hang.
Second hang root-caused → run AspNetCoreServer.Test seriallyThe previous run hung the same way (unit tests started, then ~1h50m of silence until the credential expired). Same project: Root cause (deeper than the last fix): Fix ( Verified locally end-to-end: the full script completes, |
Problem
CI runs unit tests via the
run-unit-teststarget inbuildtools/build.proj, which hardcoded only 4 projects:SnapshotRestore.Registry.TestsAmazon.Lambda.RuntimeSupport.UnitTestsAmazon.Lambda.Annotations.SourceGenerators.TestsAmazon.Lambda.DurableExecution.TestsAs a result, many unit-test projects were never executed in CI, including
Amazon.Lambda.AspNetCoreServer.Test,Amazon.Lambda.Core.Tests,Amazon.Lambda.AspNetCoreServer.Hosting.Tests,EventsTests.NET8,Amazon.Lambda.Logging.AspNetCore.Tests,Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests,Amazon.Lambda.DurableExecution.Testing.Tests,Amazon.Lambda.DurableExecution.Analyzers.Tests,PowerShellTests, andTestFunctionCSharp. New test projects also silently went unrun unless someone remembered to append them here.Change
Replace the hardcoded list with
buildtools/run-unit-tests.ps1, which discovers unit-test projects by convention — any*.csprojthat referencesMicrosoft.NET.Test.Sdk(or sets<IsTestProject>true</IsTestProject>), excluding*.IntegrationTests.csproj(those run in a separate phase viarun-integ-tests-parallel.ps1). This mirrors the existing integ-test discovery pattern.Discovery is content-based rather than name-based because the repo's test projects follow no single naming convention (
*.Test,*.Tests,EventsTests.NET8,PowerShellTests, ...). Non-test fixture projects (TestServerlessApp,TestWebApp,HandlerTest, ...) don't reference the test SDK, so they're cleanly excluded.The script now discovers 14 projects (the original 4 plus the 10 that were being skipped) and exits non-zero listing any that fail.