|
14 | 14 | */ |
15 | 15 | using System; |
16 | 16 | using System.Collections.Generic; |
| 17 | +using System.IO; |
17 | 18 | using System.Linq; |
18 | 19 | using System.Net.Http; |
19 | 20 | using System.Text; |
| 21 | +using System.Threading; |
20 | 22 | using System.Threading.Tasks; |
21 | 23 | using Xunit; |
22 | 24 |
|
@@ -283,5 +285,159 @@ public void IsCallPreJitTest() |
283 | 285 | environmentVariables.SetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_LAMBDA_INITIALIZATION_TYPE, AWS_LAMBDA_INITIALIZATION_TYPE_PC); |
284 | 286 | Assert.True(UserCodeInit.IsCallPreJit(environmentVariables)); |
285 | 287 | } |
| 288 | + |
| 289 | + // --- Streaming Integration Tests --- |
| 290 | + |
| 291 | + private TestStreamingRuntimeApiClient CreateStreamingClient() |
| 292 | + { |
| 293 | + var envVars = new TestEnvironmentVariables(); |
| 294 | + var headers = new Dictionary<string, IEnumerable<string>> |
| 295 | + { |
| 296 | + { RuntimeApiHeaders.HeaderAwsRequestId, new List<string> { "streaming-request-id" } }, |
| 297 | + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List<string> { "invoked_function_arn" } }, |
| 298 | + { RuntimeApiHeaders.HeaderAwsTenantId, new List<string> { "tenant_id" } } |
| 299 | + }; |
| 300 | + return new TestStreamingRuntimeApiClient(envVars, headers); |
| 301 | + } |
| 302 | + |
| 303 | + /// <summary> |
| 304 | + /// Property 2: CreateStream Enables Streaming Mode |
| 305 | + /// When a handler calls ResponseStreamFactory.CreateStream(), the response is transmitted |
| 306 | + /// using streaming mode. LambdaBootstrap awaits the send task. |
| 307 | + /// **Validates: Requirements 1.4, 6.1, 6.2, 6.3, 6.4** |
| 308 | + /// </summary> |
| 309 | + [Fact] |
| 310 | + public async Task StreamingMode_HandlerCallsCreateStream_SendTaskAwaited() |
| 311 | + { |
| 312 | + var streamingClient = CreateStreamingClient(); |
| 313 | + |
| 314 | + LambdaBootstrapHandler handler = async (invocation) => |
| 315 | + { |
| 316 | + var stream = ResponseStreamFactory.CreateStream(); |
| 317 | + await stream.WriteAsync(Encoding.UTF8.GetBytes("hello")); |
| 318 | + return new InvocationResponse(Stream.Null, false); |
| 319 | + }; |
| 320 | + |
| 321 | + using (var bootstrap = new LambdaBootstrap(handler, null)) |
| 322 | + { |
| 323 | + bootstrap.Client = streamingClient; |
| 324 | + await bootstrap.InvokeOnceAsync(); |
| 325 | + } |
| 326 | + |
| 327 | + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); |
| 328 | + Assert.False(streamingClient.SendResponseAsyncCalled); |
| 329 | + } |
| 330 | + |
| 331 | + /// <summary> |
| 332 | + /// Property 3: Default Mode Is Buffered |
| 333 | + /// When a handler does not call ResponseStreamFactory.CreateStream(), the response |
| 334 | + /// is transmitted using buffered mode via SendResponseAsync. |
| 335 | + /// **Validates: Requirements 1.5, 7.2** |
| 336 | + /// </summary> |
| 337 | + [Fact] |
| 338 | + public async Task BufferedMode_HandlerDoesNotCallCreateStream_UsesSendResponse() |
| 339 | + { |
| 340 | + var streamingClient = CreateStreamingClient(); |
| 341 | + |
| 342 | + LambdaBootstrapHandler handler = async (invocation) => |
| 343 | + { |
| 344 | + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("buffered response")); |
| 345 | + return new InvocationResponse(outputStream); |
| 346 | + }; |
| 347 | + |
| 348 | + using (var bootstrap = new LambdaBootstrap(handler, null)) |
| 349 | + { |
| 350 | + bootstrap.Client = streamingClient; |
| 351 | + await bootstrap.InvokeOnceAsync(); |
| 352 | + } |
| 353 | + |
| 354 | + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); |
| 355 | + Assert.True(streamingClient.SendResponseAsyncCalled); |
| 356 | + } |
| 357 | + |
| 358 | + /// <summary> |
| 359 | + /// Property 14: Exception After Writes Uses Trailers |
| 360 | + /// When a handler throws an exception after writing data to an IResponseStream, |
| 361 | + /// the error is reported via trailers (ReportErrorAsync) rather than standard error reporting. |
| 362 | + /// **Validates: Requirements 5.6, 5.7** |
| 363 | + /// </summary> |
| 364 | + [Fact] |
| 365 | + public async Task MidstreamError_ExceptionAfterWrites_ReportsViaTrailers() |
| 366 | + { |
| 367 | + var streamingClient = CreateStreamingClient(); |
| 368 | + |
| 369 | + LambdaBootstrapHandler handler = async (invocation) => |
| 370 | + { |
| 371 | + var stream = ResponseStreamFactory.CreateStream(); |
| 372 | + await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); |
| 373 | + throw new InvalidOperationException("midstream failure"); |
| 374 | + }; |
| 375 | + |
| 376 | + using (var bootstrap = new LambdaBootstrap(handler, null)) |
| 377 | + { |
| 378 | + bootstrap.Client = streamingClient; |
| 379 | + await bootstrap.InvokeOnceAsync(); |
| 380 | + } |
| 381 | + |
| 382 | + // Error should be reported via trailers on the stream, not via standard error reporting |
| 383 | + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); |
| 384 | + Assert.NotNull(streamingClient.LastStreamingResponseStream); |
| 385 | + Assert.True(streamingClient.LastStreamingResponseStream.HasError); |
| 386 | + Assert.False(streamingClient.ReportInvocationErrorAsyncExceptionCalled); |
| 387 | + } |
| 388 | + |
| 389 | + /// <summary> |
| 390 | + /// Property 15: Exception Before CreateStream Uses Standard Error |
| 391 | + /// When a handler throws an exception before calling ResponseStreamFactory.CreateStream(), |
| 392 | + /// the error is reported using the standard Lambda error reporting mechanism. |
| 393 | + /// **Validates: Requirements 5.7, 7.1** |
| 394 | + /// </summary> |
| 395 | + [Fact] |
| 396 | + public async Task PreStreamError_ExceptionBeforeCreateStream_UsesStandardErrorReporting() |
| 397 | + { |
| 398 | + var streamingClient = CreateStreamingClient(); |
| 399 | + |
| 400 | + LambdaBootstrapHandler handler = async (invocation) => |
| 401 | + { |
| 402 | + await Task.Yield(); |
| 403 | + throw new InvalidOperationException("pre-stream failure"); |
| 404 | + }; |
| 405 | + |
| 406 | + using (var bootstrap = new LambdaBootstrap(handler, null)) |
| 407 | + { |
| 408 | + bootstrap.Client = streamingClient; |
| 409 | + await bootstrap.InvokeOnceAsync(); |
| 410 | + } |
| 411 | + |
| 412 | + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); |
| 413 | + Assert.True(streamingClient.ReportInvocationErrorAsyncExceptionCalled); |
| 414 | + } |
| 415 | + |
| 416 | + /// <summary> |
| 417 | + /// State Isolation: ResponseStreamFactory state is cleared after each invocation. |
| 418 | + /// **Validates: Requirements 6.5, 8.9** |
| 419 | + /// </summary> |
| 420 | + [Fact] |
| 421 | + public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation() |
| 422 | + { |
| 423 | + var streamingClient = CreateStreamingClient(); |
| 424 | + |
| 425 | + LambdaBootstrapHandler handler = async (invocation) => |
| 426 | + { |
| 427 | + var stream = ResponseStreamFactory.CreateStream(); |
| 428 | + await stream.WriteAsync(Encoding.UTF8.GetBytes("data")); |
| 429 | + return new InvocationResponse(Stream.Null, false); |
| 430 | + }; |
| 431 | + |
| 432 | + using (var bootstrap = new LambdaBootstrap(handler, null)) |
| 433 | + { |
| 434 | + bootstrap.Client = streamingClient; |
| 435 | + await bootstrap.InvokeOnceAsync(); |
| 436 | + } |
| 437 | + |
| 438 | + // After invocation, factory state should be cleaned up |
| 439 | + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(false)); |
| 440 | + Assert.Null(ResponseStreamFactory.GetSendTask(false)); |
| 441 | + } |
286 | 442 | } |
287 | 443 | } |
0 commit comments