From 7c411c7a26731071b1849e9706308ddbf18a6d3c Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 15:33:55 -0400 Subject: [PATCH 01/10] ci(github): serialize coverage test TFMs --- .github/workflows/pr-build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 06c1aabd..05dbb3ee 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -54,6 +54,7 @@ jobs: dotnet test MinimalLambda.sln --configuration Release --no-build --results-directory ./coverage --coverage --coverage-output-format cobertura --no-progress --no-ansi + -p:TestTfmsInParallel=false - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v6 From 4c96f9eee4c817e7d673e4013980e66f99359d95 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 15:41:51 -0400 Subject: [PATCH 02/10] test(tests): observe application start delegation --- .../Builder/LambdaApplicationTests.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs b/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs index 5856b156..0a794d12 100644 --- a/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs +++ b/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs @@ -177,32 +177,38 @@ public void Logger_WithoutLoggerFactory_ReturnsNullLogger() } [Fact] - public void StartAsync_ReturnsAwaitableTask() + public async Task StartAsync_DelegatesToHost() { // Arrange - var host = CreateHostWithServices(); + using var servicesHost = CreateHostWithServices(); + var host = Substitute.For(); + host.Services.Returns(servicesHost.Services); + host.StartAsync(TestContext.Current.CancellationToken).Returns(Task.CompletedTask); var app = new LambdaApplication(host); // Act - var task = app.StartAsync(TestContext.Current.CancellationToken); + await app.StartAsync(TestContext.Current.CancellationToken); // Assert - task.Should().NotBeNull(); + await host.Received(1).StartAsync(TestContext.Current.CancellationToken); } [Fact] - public void StartAsync_WithCancellationToken_ReturnsAwaitableTask() + public async Task StartAsync_PassesCancellationTokenToHost() { // Arrange - var host = CreateHostWithServices(); + using var servicesHost = CreateHostWithServices(); + var host = Substitute.For(); + host.Services.Returns(servicesHost.Services); + using var cts = new CancellationTokenSource(); + host.StartAsync(cts.Token).Returns(Task.CompletedTask); var app = new LambdaApplication(host); // Act - using var cts = new CancellationTokenSource(); - var task = app.StartAsync(cts.Token); + await app.StartAsync(cts.Token); // Assert - task.Should().NotBeNull(); + await host.Received(1).StartAsync(cts.Token); } [Fact] From 34a977b9edf1e62e0d1ebceb0f2116f208ca6d6b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 16:09:20 -0400 Subject: [PATCH 03/10] fix(testing): order test-server shutdown Cancel test transactions only after host shutdown begins, so the Lambda bootstrap recognizes intentional cancellation. --- src/MinimalLambda.Testing/LambdaTestServer.cs | 26 ++++++++++++++----- .../SimpleLambdaTests.cs | 2 ++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/MinimalLambda.Testing/LambdaTestServer.cs b/src/MinimalLambda.Testing/LambdaTestServer.cs index 481685d7..707b3dc2 100644 --- a/src/MinimalLambda.Testing/LambdaTestServer.cs +++ b/src/MinimalLambda.Testing/LambdaTestServer.cs @@ -132,6 +132,9 @@ public async ValueTask DisposeAsync() if (_disposed) return; + if (State == ServerState.Running) + await StopAsync().ConfigureAwait(false); + // Complete both channels to prevent new items _transactionChannel.Writer.TryComplete(); _pendingInvocationIds.Writer.TryComplete(); @@ -139,9 +142,6 @@ public async ValueTask DisposeAsync() // Cancel the shutdown token await _shutdownCts.CancelAsync(); - if (State == ServerState.Running) - await StopAsync().ConfigureAwait(false); - // Dispose the CancellationTokenSource _shutdownCts.Dispose(); @@ -236,7 +236,10 @@ await TaskHelpers if (_initCompletionTcs.Task.Result.InitStatus == InitStatus.InitCompleted) State = ServerState.Running; else + { + await _shutdownCts.CancelAsync(); await StopAsync(CancellationToken.None); + } return _initCompletionTcs.Task.Result; } @@ -377,9 +380,9 @@ public async Task> InvokeAsync( /// /// This method performs the following shutdown sequence: /// - /// Transitions the server state to - /// Cancels the internal shutdown token to signal background tasks /// Stops the application host via + /// Waits for the entry point or processing task to complete + /// Cancels the internal shutdown token to stop transaction processing /// Waits for the entry point and processing tasks to complete /// Transitions the server state to /// @@ -396,10 +399,19 @@ public async Task StopAsync(CancellationToken cancellationToken = default) State = ServerState.Stopping; - await _shutdownCts.CancelAsync(); - _applicationLifetime?.StopApplication(); + try + { + await TaskHelpers + .WhenAny(_entryPointCompletion, _processingTask ?? Task.CompletedTask) + .WaitAsync(cancellationToken); + } + finally + { + await _shutdownCts.CancelAsync(); + } + await TaskHelpers .WhenAll(_entryPointCompletion, _processingTask ?? Task.CompletedTask) .UnwrapAndThrow("Exception(s) encountered while running StopAsync") diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs index 88143e92..0151ffaf 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs @@ -115,6 +115,8 @@ public async Task SimpleLambda_ReturnsError() response.WasSuccess.Should().BeFalse(); response.Error.Should().NotBeNull(); response.Error.ErrorMessage.Should().Be("Name is required"); + + await factory.TestServer.DisposeAsync(); } [Fact] From 03eb7b662c90cb364f70694bae64e4235f03dcca Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 16:23:37 -0400 Subject: [PATCH 04/10] fix(runtime): cancel bootstrap on host stop Propagate ApplicationStopping before test-server transactions cancel, preventing RuntimeSupport from retrying a canceled poll. --- src/MinimalLambda.Testing/LambdaTestServer.cs | 14 ++------ .../Runtime/LambdaHostedService.cs | 5 +++ .../Core/Runtime/LambdaHostedServiceTests.cs | 33 +++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/MinimalLambda.Testing/LambdaTestServer.cs b/src/MinimalLambda.Testing/LambdaTestServer.cs index 707b3dc2..1e1dbe38 100644 --- a/src/MinimalLambda.Testing/LambdaTestServer.cs +++ b/src/MinimalLambda.Testing/LambdaTestServer.cs @@ -381,7 +381,6 @@ public async Task> InvokeAsync( /// This method performs the following shutdown sequence: /// /// Stops the application host via - /// Waits for the entry point or processing task to complete /// Cancels the internal shutdown token to stop transaction processing /// Waits for the entry point and processing tasks to complete /// Transitions the server state to @@ -401,16 +400,7 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _applicationLifetime?.StopApplication(); - try - { - await TaskHelpers - .WhenAny(_entryPointCompletion, _processingTask ?? Task.CompletedTask) - .WaitAsync(cancellationToken); - } - finally - { - await _shutdownCts.CancelAsync(); - } + await _shutdownCts.CancelAsync(); await TaskHelpers .WhenAll(_entryPointCompletion, _processingTask ?? Task.CompletedTask) @@ -478,7 +468,7 @@ private async Task HandleGetNextInvocationAsync(LambdaHttpTransaction transactio { var requestId = await _pendingInvocationIds.Reader.ReadAsync(_shutdownCts.Token); _pendingInvocations.GetRequired(requestId, out var pendingInvocation); - transaction.ResponseTcs.SetResult(pendingInvocation.EventResponse); + transaction.Respond(pendingInvocation.EventResponse); } } diff --git a/src/MinimalLambda/Runtime/LambdaHostedService.cs b/src/MinimalLambda/Runtime/LambdaHostedService.cs index 55d09167..a0a26c0a 100644 --- a/src/MinimalLambda/Runtime/LambdaHostedService.cs +++ b/src/MinimalLambda/Runtime/LambdaHostedService.cs @@ -18,6 +18,7 @@ internal sealed class LambdaHostedService : IHostedService, IDisposable private readonly LambdaHostedServiceOptions _options; private bool _disposed; + private CancellationTokenRegistration _applicationStoppingRegistration; private Task? _executeTask; private Func? _shutdownHandler; private CancellationTokenSource? _stoppingCts; @@ -48,6 +49,7 @@ public LambdaHostedService( /// public void Dispose() { + _applicationStoppingRegistration.Dispose(); _stoppingCts?.Cancel(); if (_disposed) @@ -69,6 +71,9 @@ public Task StartAsync(CancellationToken cancellationToken) // Create a linked token to allow cancelling the executing task from the provided token _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _applicationStoppingRegistration = + _lifetime.ApplicationStopping.Register(_stoppingCts.Cancel); + // Create a fully composed handler with middleware and request processing. var requestHandler = _handlerFactory.CreateHandler(_stoppingCts.Token); diff --git a/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs b/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs index cfc2f0a7..aa2df0e8 100644 --- a/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs +++ b/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs @@ -78,6 +78,39 @@ await bootstrapOrchestrator Arg.Any()); } + [Theory] + [AutoNSubstituteData] + internal async Task StartAsync_CancelsBootstrapWhenApplicationStops( + [Frozen] ILambdaBootstrapOrchestrator bootstrap, + [Frozen] IHostApplicationLifetime lifetime, + LambdaHostedService service) + { + // Arrange + using var applicationStoppingCts = new CancellationTokenSource(); + var bootstrapToken = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + lifetime.ApplicationStopping.Returns(applicationStoppingCts.Token); + bootstrap + .RunAsync( + Arg.Any>>(), + Arg.Any>>(), + Arg.Any()) + .Returns(callInfo => + { + var cancellationToken = callInfo.ArgAt(2); + bootstrapToken.SetResult(cancellationToken); + return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + }); + + // Act + await service.StartAsync(CancellationToken.None); + applicationStoppingCts.Cancel(); + + // Assert + (await bootstrapToken.Task).IsCancellationRequested.Should().BeTrue(); + await service.StopAsync(CancellationToken.None); + } + [Theory] [AutoNSubstituteData] internal async Task StartAsync_CreatesRequestHandler( From bc940524e68254cf87c9d9ccf428eb9cc9b170c8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 17:20:09 -0400 Subject: [PATCH 05/10] ci(github): serialize coverage test modules --- .github/workflows/pr-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 05dbb3ee..249fe8f7 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -54,7 +54,7 @@ jobs: dotnet test MinimalLambda.sln --configuration Release --no-build --results-directory ./coverage --coverage --coverage-output-format cobertura --no-progress --no-ansi - -p:TestTfmsInParallel=false + --max-parallel-test-modules 1 -p:TestTfmsInParallel=false - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v6 From d9439ed5f9fd4721393bea28e1897599f36450f7 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 18:11:32 -0400 Subject: [PATCH 06/10] fix(testing): dispose host after shutdown error Prevent failed test-server shutdown from leaving Lambda hosts alive until CI times out. --- .../LambdaApplicationFactory.cs | 26 +++++++++++-------- .../DiLambdaTests.cs | 7 ++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/MinimalLambda.Testing/LambdaApplicationFactory.cs b/src/MinimalLambda.Testing/LambdaApplicationFactory.cs index d7cf1c42..672461e0 100644 --- a/src/MinimalLambda.Testing/LambdaApplicationFactory.cs +++ b/src/MinimalLambda.Testing/LambdaApplicationFactory.cs @@ -105,20 +105,24 @@ public virtual async ValueTask DisposeAsync() if (_disposedAsync) return; - foreach (var factory in _derivedFactories) - await ((IAsyncDisposable)factory).DisposeAsync().ConfigureAwait(false); - - // TestServer handles disposing both processor and host - if (_server != null) - await _server.DisposeAsync().ConfigureAwait(false); - - _host?.Dispose(); + try + { + foreach (var factory in _derivedFactories) + await ((IAsyncDisposable)factory).DisposeAsync().ConfigureAwait(false); - _disposedAsync = true; + // TestServer handles disposing its processor. + if (_server != null) + await _server.DisposeAsync().ConfigureAwait(false); + } + finally + { + _host?.Dispose(); + _disposedAsync = true; - Dispose(true); + Dispose(true); - GC.SuppressFinalize(this); + GC.SuppressFinalize(this); + } } /// diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs index 11913673..5add93d8 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs @@ -104,15 +104,16 @@ internal async Task DiLambda_ShutdownThrowsException(ILifecycleService lifecycle var initResult = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); initResult.InitStatus.Should().Be(InitStatus.InitCompleted); - var act = async () => - // ReSharper disable once AccessToDisposedClosure - await factory.TestServer.StopAsync(TestContext.Current.CancellationToken); + var act = async () => await factory.DisposeAsync(); (await act.Should().ThrowAsync()) .WithInnerException() .WithInnerException() .WithInnerException() .WithMessage("Test init error"); + + var getService = () => factory.Services.GetRequiredService(); + getService.Should().Throw(); } [Theory] From b9f086f91c6a06d906cc9a89d3841073587baef8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 18:52:00 -0400 Subject: [PATCH 07/10] test(testing): set Lambda logs to warning --- .../appsettings.json | 8 +++++++- .../appsettings.json | 8 +++++++- .../appsettings.json | 8 +++++++- .../appsettings.json | 8 +++++++- .../appsettings.json | 8 +++++++- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json index 9e26dfee..b927333b 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json @@ -1 +1,7 @@ -{} \ No newline at end of file +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json index 0967ef42..6cf0b42b 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json @@ -1 +1,7 @@ -{} +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json index 9e26dfee..b927333b 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json @@ -1 +1,7 @@ -{} \ No newline at end of file +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json index 9e26dfee..b927333b 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json @@ -1 +1,7 @@ -{} \ No newline at end of file +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json index 9e26dfee..b927333b 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json @@ -1 +1,7 @@ -{} \ No newline at end of file +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file From 57c8bb8a11a11ce25b298983f909a5b447cf60c1 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 21:58:43 -0400 Subject: [PATCH 08/10] test(testing): suppress Lambda host logs --- .../appsettings.json | 8 +---- .../appsettings.json | 8 +---- .../appsettings.json | 8 +---- .../appsettings.json | 8 +---- .../appsettings.json | 8 +---- .../DiLambdaTests.cs | 35 +++++++++++++++---- .../DurableLambdaTests.cs | 10 +++--- .../LambdaTestFactory.cs | 29 +++++++++++++++ .../NoEventLambdaTests.cs | 17 +++++---- .../NoResponseLambdaTests.cs | 15 ++++---- .../SimpleLambdaTests.cs | 33 ++++++++++------- 11 files changed, 107 insertions(+), 72 deletions(-) create mode 100644 tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json index b927333b..9e26dfee 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DiLambda/appsettings.json @@ -1,7 +1 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json index 6cf0b42b..0967ef42 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.DurableLambda/appsettings.json @@ -1,7 +1 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - } -} +{} diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json index b927333b..9e26dfee 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoEventLambda/appsettings.json @@ -1,7 +1 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json index b927333b..9e26dfee 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.NoResponseLambda/appsettings.json @@ -1,7 +1 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json index b927333b..9e26dfee 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json +++ b/tests/MinimalLambda.Testing.UnitTests/Lambdas/MinimalLambda.Testing.UnitTests.SimpleLambda/appsettings.json @@ -1,7 +1 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs index 5add93d8..97373e69 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs @@ -2,6 +2,7 @@ using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using MinimalLambda.UnitTests; using NSubstitute.ExceptionExtensions; @@ -13,8 +14,9 @@ public class DiLambdaTests public async Task DiLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( new DiLambdaRequest("World"), @@ -26,11 +28,26 @@ public async Task DiLambda_ReturnsExpectedValue() response.Response.Message.Should().Be("Hello World!"); } + [Fact] + public async Task DiLambda_HostLoggingIsDisabled() + { + await using var factory = + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); + + var logger = factory.Services.GetRequiredService>(); + + foreach (var level in Enum.GetValues()) + logger.IsEnabled(level).Should().BeFalse(); + } + [Fact] internal async Task DiLambda_InitStopped() { var lifecycleService = Substitute.For(); - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -59,7 +76,8 @@ internal async Task DiLambda_InitThrowsException() { var lifecycleService = Substitute.For(); - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -90,7 +108,8 @@ internal async Task DiLambda_InitThrowsException() [AutoNSubstituteData] internal async Task DiLambda_ShutdownThrowsException(ILifecycleService lifecycleService) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -122,7 +141,8 @@ internal async Task DiLambda_DiContainerCanBeReplaced( ILifecycleService lifecycleService, IService service) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder .ConfigureContainer((_, containerBuilder) => @@ -150,7 +170,8 @@ internal async Task DiLambda_DiContainerCanBeReplacedWithFactory( ILifecycleService lifecycleService, IService service) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder .ConfigureContainer((_, containerBuilder) => diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs index 6342764a..39df8310 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs @@ -14,8 +14,9 @@ public async Task DurableLambda_Success_RoundTripsGeneratedAdapterPipeline() { // Arrange await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var input = CreateInvocationInput(shouldFail: false); // Act @@ -45,8 +46,9 @@ public async Task DurableLambda_Failure_ReturnsFailedOuterEnvelope() { // Arrange await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var input = CreateInvocationInput(shouldFail: true); // Act diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs new file mode 100644 index 00000000..0613ef11 --- /dev/null +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace MinimalLambda.Testing.UnitTests; + +internal static class LambdaTestFactory +{ + private const string LambdaLogLevelEnvironmentVariable = "AWS_LAMBDA_LOG_LEVEL"; + + static LambdaTestFactory() + { + if (string.IsNullOrEmpty( + Environment.GetEnvironmentVariable(LambdaLogLevelEnvironmentVariable))) + Environment.SetEnvironmentVariable(LambdaLogLevelEnvironmentVariable, "Critical"); + } + + public static LambdaApplicationFactory Create() + where TEntryPoint : class => + new LambdaApplicationFactory().WithHostBuilder(builder => + { + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddSingleton(NullLoggerFactory.Instance); + }); + }); +} diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs index f8044c1a..67aa7110 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs @@ -8,8 +8,9 @@ public class NoEventLambdaTests public async Task NoEvent_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeNoEventAsync( @@ -24,11 +25,13 @@ await factory.TestServer.InvokeNoEventAsync( [Fact] public async Task NoEvent_ConfigurationCanBeOverwritten() { - await using var factory = new LambdaApplicationFactory() - .WithCancellationToken(TestContext.Current.CancellationToken) - .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) => - config.AddInMemoryCollection( - new Dictionary { ["MESSAGE"] = "Hello Mars!" }!))); + await using var factory = + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken) + .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) + => config.AddInMemoryCollection( + new Dictionary { ["MESSAGE"] = "Hello Mars!" }!))); var response = await factory.TestServer.InvokeNoEventAsync( diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs index f92e58d0..8ba9ff1a 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs @@ -11,8 +11,9 @@ public class NoResponseLambdaTests public async Task NoResponseLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeNoResponseAsync( new NoResponseLambdaRequest("World"), @@ -26,8 +27,9 @@ public async Task NoResponseLambda_ReturnsExpectedValue() public async Task NoResponseLambda_ServicesIsAccessible() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var act = () => factory.TestServer.Services.GetRequiredService(); @@ -38,8 +40,9 @@ public async Task NoResponseLambda_ServicesIsAccessible() public async Task NoResponseLambda_DisposeCanBeCalledMultipleTimes() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var act = async () => { diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs index 0151ffaf..aa377d46 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs @@ -9,8 +9,9 @@ public class SimpleLambdaTests public async Task SimpleLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var setup = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); setup.InitStatus.Should().Be(InitStatus.InitCompleted); @@ -27,8 +28,9 @@ public async Task SimpleLambda_ReturnsExpectedValue() public async Task SimpleLambda_WorksWhenStartIsNotCalled() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( "World", @@ -43,8 +45,9 @@ public async Task SimpleLambda_WorksWhenStartIsNotCalled() public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); // Launch 5 concurrent invocations var tasks = Enumerable @@ -73,8 +76,9 @@ public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes() public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes_WithoutStart() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); // Launch 5 concurrent invocations @@ -104,8 +108,9 @@ public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes_WithoutStart() public async Task SimpleLambda_ReturnsError() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( "", @@ -122,7 +127,8 @@ public async Task SimpleLambda_ReturnsError() [Fact] public async Task SimpleLambda_ErrorsArePropagated() { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => { @@ -150,8 +156,9 @@ await act public async Task SimpleLambda_WithPreCanceledToken_CancelsInvocation() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); using var cts = new CancellationTokenSource(); From d4e88040df3529f52f501dbe6e6aa6e0e1b5ecc3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 22:08:37 -0400 Subject: [PATCH 09/10] test(testing): configure host log level --- .../LambdaTestFactory.cs | 20 +++++++++---------- .../NoEventLambdaTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs index 0613ef11..b5f5cb83 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs @@ -1,7 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; namespace MinimalLambda.Testing.UnitTests; @@ -9,6 +7,11 @@ internal static class LambdaTestFactory { private const string LambdaLogLevelEnvironmentVariable = "AWS_LAMBDA_LOG_LEVEL"; + private static readonly KeyValuePair[] HostConfiguration = + [ + new("Logging:LogLevel:Default", "None"), + ]; + static LambdaTestFactory() { if (string.IsNullOrEmpty( @@ -19,11 +22,6 @@ static LambdaTestFactory() public static LambdaApplicationFactory Create() where TEntryPoint : class => new LambdaApplicationFactory().WithHostBuilder(builder => - { - builder.ConfigureServices((_, services) => - { - services.RemoveAll(); - services.AddSingleton(NullLoggerFactory.Instance); - }); - }); + builder.ConfigureAppConfiguration((_, configuration) => + configuration.AddInMemoryCollection(HostConfiguration))); } diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs index 67aa7110..bef667cf 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs @@ -29,8 +29,8 @@ public async Task NoEvent_ConfigurationCanBeOverwritten() LambdaTestFactory .Create() .WithCancellationToken(TestContext.Current.CancellationToken) - .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) - => config.AddInMemoryCollection( + .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) => + config.AddInMemoryCollection( new Dictionary { ["MESSAGE"] = "Hello Mars!" }!))); var response = From 0a1c3956395e689728cf77b1c95d2bfad07b58cd Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 17 Aug 2026 22:16:57 -0400 Subject: [PATCH 10/10] test(core): silence formatter init logs --- ...OutputFormattingLambdaApplicationExtensionsTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs b/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs index 7c049064..c2058b14 100644 --- a/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs +++ b/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs @@ -1,12 +1,18 @@ using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace MinimalLambda.UnitTests.Application.Extensions; [TestSubject(typeof(OutputFormattingLambdaApplicationExtensions))] public class OutputFormattingLambdaApplicationExtensionsTests { - private static IHost CreateHostWithServices() => - new LambdaApplicationBuilder(new LambdaApplicationOptions()).Build(); + private static IHost CreateHostWithServices() + { + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.None); + + return builder.Build(); + } [Fact] public void OnInitClearLambdaOutputFormatting_WithNullApplication_ThrowsArgumentNullException()