From 7d132008e311ecf47802f95cb35d65afa0e4d4f9 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 1 Aug 2025 06:48:39 +0000 Subject: [PATCH] Adding additional lambda local pytest --- python-test-samples/README.md | 2 + .../lambda-sam-helloworld/README.md | 349 +++++++++++ .../lambda-helloworld-custom-event.json | 21 + .../events/lambda-helloworld-event.json | 5 + .../img/lambda-sam-helloworld.png | Bin 0 -> 38055 bytes .../lambda_helloworld_src/app.py | 7 + .../lambda-sam-helloworld/template.yaml | 18 + .../tests/requirements.txt | 49 ++ .../tests/unit/src/test_lambda_local.py | 565 ++++++++++++++++++ .../lambda-sam-layers/README.md | 438 ++++++++++++++ .../custom-lambda-layer/requirements.txt | 1 + .../events/lambda-layers-event.json | 5 + .../img/lambda-sam-layers.png | Bin 0 -> 68083 bytes .../lambda_layers_src/app.py | 9 + .../lambda-sam-layers/tests/requirements.txt | 76 +++ .../lambda-sam-layers/tests/template.yaml | 32 + .../unit/src/test_lambda_layers_local.py | 507 ++++++++++++++++ 17 files changed, 2084 insertions(+) create mode 100644 python-test-samples/lambda-sam-helloworld/README.md create mode 100644 python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-custom-event.json create mode 100755 python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-event.json create mode 100644 python-test-samples/lambda-sam-helloworld/img/lambda-sam-helloworld.png create mode 100755 python-test-samples/lambda-sam-helloworld/lambda_helloworld_src/app.py create mode 100755 python-test-samples/lambda-sam-helloworld/template.yaml create mode 100644 python-test-samples/lambda-sam-helloworld/tests/requirements.txt create mode 100644 python-test-samples/lambda-sam-helloworld/tests/unit/src/test_lambda_local.py create mode 100644 python-test-samples/lambda-sam-layers/README.md create mode 100755 python-test-samples/lambda-sam-layers/custom-lambda-layer/requirements.txt create mode 100755 python-test-samples/lambda-sam-layers/events/lambda-layers-event.json create mode 100644 python-test-samples/lambda-sam-layers/img/lambda-sam-layers.png create mode 100755 python-test-samples/lambda-sam-layers/lambda_layers_src/app.py create mode 100644 python-test-samples/lambda-sam-layers/tests/requirements.txt create mode 100755 python-test-samples/lambda-sam-layers/tests/template.yaml create mode 100644 python-test-samples/lambda-sam-layers/tests/unit/src/test_lambda_layers_local.py diff --git a/python-test-samples/README.md b/python-test-samples/README.md index e25b76f2..6b87d7b7 100644 --- a/python-test-samples/README.md +++ b/python-test-samples/README.md @@ -10,6 +10,8 @@ This portion of the repository contains code samples for testing serverless appl |[Python Starter Project](./apigw-lambda)|This project contains introductory examples of Python tests written for AWS Lambda. This is the best place to start!| |[Integrated Application Test Kit](./integrated-application-test-kit)|This sample demonstrates how you can use the [AWS Integrated Application Test Kit (IATK)](https://awslabs.github.io/aws-iatk/) to develop integration tests for your serverless and event-driven applications.| |[Lambda local testing with Mocks](./lambda-mock)|This project contains unit tests for Lambda using mocks.| +|[Lambda hello world local testing](./lambda-sam-helloworld)|This project contains unit pytest tests for Hello World Lambda.| +|[Lambda Layers local testing](./lambda-sam-layers)|This project contains unit pytests tests for Lambda building layers.| |[Lambda Layers with Mocks](./apigw-lambda-layer)|This project contains unit tests for Lambda layers using mocks.| |[API Gateway with Lambda and DynamoDB](./apigw-lambda-dynamodb)|This project contains unit and integration tests for a pattern using API Gateway, AWS Lambda and Amazon DynamoDB.| |[Schema and Contract Testing](./schema-and-contract-testing)|This project contains sample schema and contract tests for an event driven architecture.| diff --git a/python-test-samples/lambda-sam-helloworld/README.md b/python-test-samples/lambda-sam-helloworld/README.md new file mode 100644 index 00000000..cec40df9 --- /dev/null +++ b/python-test-samples/lambda-sam-helloworld/README.md @@ -0,0 +1,349 @@ +[![python: 3.9](https://img.shields.io/badge/Python-3.9-green)](https://img.shields.io/badge/Python-3.9-green) +[![AWS: Lambda](https://img.shields.io/badge/AWS-Lambda-orange)](https://img.shields.io/badge/AWS-Lambda-orange) +[![test: pytest](https://img.shields.io/badge/Test-Pytest-red)](https://img.shields.io/badge/Test-Pytest-red) +[![test: local](https://img.shields.io/badge/Test-Local-red)](https://img.shields.io/badge/Test-Local-red) + +# Local Testing: AWS Lambda Hello World + +## Introduction + +This project demonstrates how to test AWS Lambda functions locally using the SAM CLI and PyTest. It provides a comprehensive Hello World example that showcases local testing capabilities without requiring actual AWS infrastructure, including automated test execution and validation. + +--- + +## Contents + +- [Local Testing: AWS Lambda Hello World with PyTest](#local-testing-aws-lambda-hello-world-with-pytest) + - [Introduction](#introduction) + - [Contents](#contents) + - [Architecture Overview](#architecture-overview) + - [Project Structure](#project-structure) + - [Prerequisites](#prerequisites) + - [Test Scenarios](#test-scenarios) + - [About the Test Process](#about-the-test-process) + - [Testing Workflows](#testing-workflows) + - [Common Issues](#common-issues) + - [Additional Resources](#additional-resources) + +--- + +## Architecture Overview + +

+ AWS Lambda Hello World +

+ +Components: + +- Python Hello World Lambda function +- SAM CLI for local Lambda emulation +- PyTest framework for automated testing +- Test events for various invocation scenarios + +--- + +## Project Structure + +``` +├── events/ _# folder containing json files for Lambda input events_ +│ ├── lambda-helloworld-event.json _# basic Hello World event_ +│ └── lambda-helloworld-custom-event.json _# custom message event_ +├── img/lambda-sam-helloworld.png _# Architecture diagram_ +├── lambda_helloworld_src/ _# folder containing Lambda function source code_ +│ └── app.py _# main Lambda handler function_ +├── tests/ +│ ├── unit/src/test_lambda_local.py _# python PyTest test definition_ +│ └── requirements.txt _# pytest pip requirements dependencies file_ +├── template.yaml _# sam yaml template file for Lambda function_ +└── README.md _# instructions file_ +``` + +--- + +## Prerequisites + +- AWS SAM CLI +- Docker +- Python 3.9 or newer +- AWS CLI v2 (for debugging) +- Basic understanding of AWS Lambda +- Basic understanding of PyTest framework + +--- + +## Test Scenarios + +### 1. Basic Hello World + +- Tests the basic Lambda function invocation +- Validates the default "Hello World!" message response +- Verifies correct HTTP status code (200) +- Used to validate the basic functionality of the Lambda function + +### 2. Custom Message Handling + +- Tests the Lambda function with custom input parameters +- Validates that the function can process and return custom messages +- Verifies proper input parameter handling and response formatting + +### 3. Error Handling + +- Tests the Lambda function's behavior with invalid or missing input +- Validates error responses and proper exception handling +- Ensures graceful degradation when unexpected input is provided + +--- + +## About the Test Process + +The test process leverages PyTest fixtures to manage the lifecycle of the SAM Local Lambda emulator: + +1. **SAM Local Setup**: The `lambda_container` fixture verifies that SAM Local Lambda emulator is available and running on the expected port (3001). + +2. **Lambda Client Creation**: The `lambda_client` fixture creates a Boto3 Lambda client configured to connect to the local SAM emulator endpoint. + +3. **Test Execution**: Each test invokes the Lambda function using the local client with different event payloads and validates: + - Response structure and format + - Status codes + - Response body content + - Execution metadata + +4. **Validation**: Tests verify that: + - The Lambda function executes successfully + - Response contains expected message content + - HTTP status codes are correct + - Response format matches API Gateway integration format + +5. **Cleanup**: After tests complete, the SAM Local process is gracefully terminated. + +--- + +## Testing Workflows + +### Setup Docker Environment + +> Make sure Docker engine is running before running the tests. + +```shell +lambda-sam-helloworld$ docker version +Client: Docker Engine - Community + Version: 24.0.6 + API version: 1.43 +(...) +``` + +### Run the Unit Test - End to end python test + +> Start the SAM Local Lambda emulator in a separate terminal: + +```shell +lambda-sam-helloworld$ +sam local start-lambda -p 3001 & +``` + +> Set up the python environment: + +```shell +lambda-sam-helloworld$ cd tests +export AWS_ACCESS_KEY_ID='DUMMYIDEXAMPLE' +export AWS_SECRET_ACCESS_KEY='DUMMYEXAMPLEKEY' +export AWS_REGION='us-east-1' +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt +``` + +#### Run the Unit Tests + +```shell +lambda-sam-helloworld/tests$ +python3 -m pytest -s unit/src/test_lambda_local.py +``` + +Expected output: + +``` +lambda-sam-helloworld/tests$ +python3 -m pytest -s unit/src/test_lambda_local.py +================================================================= test session starts ================================================================= +platform linux -- Python 3.10.12, pytest-7.4.4, pluggy-1.6.0 +benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) +rootdir: /home/ubuntu/environment/serverless-test-samples_lambda_pytest_try1/python-test-samples/lambda-sam-helloworld/tests +plugins: mock-3.11.1, timeout-2.1.0, Faker-24.4.0, xdist-3.3.1, metadata-3.1.1, benchmark-4.0.0, cov-4.1.0, html-3.2.0 +collected 9 items + +unit/src/test_lambda_local.py SAM Local Lambda emulator is running on port 3001 +Lambda function is responding correctly +Lambda response: {'StatusCode': 200, 'Payload': '{"statusCode": 200, "body": "{\"message\": \"Hello World! This is local Run!\"}"}'} +.Lambda response: {'StatusCode': 200, 'Message': 'Hello World! This is local Run!', 'Input_Handled': True} +.Lambda response: {'StatusCode': 200, 'Scenarios_Tested': 6, 'All_Handled_Gracefully': True} +.Lambda response format validation passed - matches API Gateway integration format +.Performance metrics: + Cold start: 403ms + Warm start average: 423ms + Performance improvement: False +Performance test completed: avg=416ms, min=403ms, max=424ms +.Concurrent invocations test passed +Results: Success_Rate=100.0%, Avg_Execution_Time=1328ms, Successful=5/5 +.Response metadata available: ['HTTPStatusCode', 'HTTPHeaders', 'RetryAttempts'] +Resource usage test passed - payload size: 81 bytes, response efficiency validated +.Edge cases test passed - 5 scenarios handled gracefully +.JSON serialization test passed - proper JSON handling validated +. +================================================================= 9 passed in 12.83s =================================================================== + +``` +#### Clean up section + +> clean pyenv environment + +```sh +lambda-sam-helloworld/tests$ +deactivate +rm -rf venv/ +``` + +> unsetting variables + +```sh +unset AWS_ACCESS_KEY_ID +unset AWS_SECRET_ACCESS_KEY +unset AWS_REGION +``` + +> cleaning sam process + +```sh +ps -axuf | grep '[s]am local start-lambda' | awk '{print $2}' | xargs -r kill +``` + +#### Debug - PyTest Debugging + +For more detailed debugging in pytest: + +```sh +# Run with verbose output +python3 -m pytest -s -v unit/src/test_lambda_local.py + +# Run with debug logging +python3 -m pytest -s -v unit/src/test_lambda_local.py --log-cli-level=DEBUG + +# Run a specific pytest test +python3 -m pytest -s -v unit/src/test_lambda_local.py::test_lambda_basic_hello_world +``` + +--- + +### Fast local development for Lambda Functions + +#### AWS CLI Commands for Manual Verification + +If you need to manually verify the Lambda function execution, you can use these commands: + +#### Configure environment variables + +```sh +lambda-sam-helloworld$ +export AWS_ACCESS_KEY_ID='DUMMYIDEXAMPLE' +export AWS_SECRET_ACCESS_KEY='DUMMYEXAMPLEKEY' +export AWS_REGION='us-east-1' +``` + +#### Start Lambda emulator + +```sh +lambda-sam-helloworld$ +sam local start-lambda -p 3001 & +``` + +#### Debug lambda functions - Manual Lambda Testing + +```sh +# Test Basic Hello World +lambda-sam-helloworld$ +aws lambda invoke \ + --function-name LambdaHelloWorld \ + --endpoint-url http://127.0.0.1:3001 \ + --payload fileb://events/lambda-helloworld-event.json \ + output.txt +cat output.txt + +# Test Custom Message +lambda-sam-helloworld$ +aws lambda invoke \ + --function-name LambdaHelloWorld \ + --endpoint-url http://127.0.0.1:3001 \ + --payload fileb://events/lambda-helloworld-custom-event.json \ + output.txt +cat output.txt +``` + +#### Direct SAM Local Invoke + +```sh +# Basic invocation +sam local invoke LambdaHelloWorld \ + --event events/lambda-helloworld-event.json + +# Custom message invocation +sam local invoke LambdaHelloWorld \ + --event events/lambda-helloworld-custom-event.json + +# Debug mode with container logs +sam local invoke LambdaHelloWorld \ + --event events/lambda-helloworld-event.json \ + --debug +``` + +--- + +## Common Issues + +### SAM Local Connection Issues + +If tests are skipped with "Lambda invocation failed, SAM might not be running properly": + +- Ensure SAM Local is running on port 3001 +- Check that you've started SAM with the correct template.yaml file +- Verify the Lambda function name matches the template definition +- Check SAM logs for any errors with `sam local start-lambda --debug` + +### Lambda Function Import Issues + +If the Lambda function fails to import or execute: + +- Verify the Python runtime version matches your local environment +- Check that all required dependencies are included in the function package +- Ensure the handler path is correctly specified in template.yaml +- Review the Lambda function logs in the SAM Local output + +### Port Conflicts + +If SAM Local fails to start due to port conflicts: + +- Check if port 3001 is already in use with `lsof -i :3001` +- Use a different port with `sam local start-lambda -p 3002` +- Update your test configuration to match the new port + +### Docker Container Issues + +If Docker containers fail to start or behave unexpectedly: + +- Ensure Docker daemon is running and accessible +- Check Docker permissions for your user account +- Verify sufficient disk space for container images +- Try pulling the latest Lambda runtime images manually + +--- + +## Additional Resources + +- [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html) +- [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) +- [SAM Local Lambda Testing Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html) +- [PyTest Documentation](https://docs.pytest.org/) +- [AWS Lambda Python Runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html) +- [SAM Template Specification](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification.html) + +[Top](#contents) \ No newline at end of file diff --git a/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-custom-event.json b/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-custom-event.json new file mode 100644 index 00000000..0e3a4d8b --- /dev/null +++ b/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-custom-event.json @@ -0,0 +1,21 @@ +{ + "test_type": "custom_message", + "custom_field": "test_value", + "user_name": "TestUser", + "timestamp": "2024-07-31T10:00:00Z", + "nested_object": { + "key1": "value345", + "key2": 123 + }, + "array_field": [1, 2, 3, "string", true, null], + "settings": { + "language": "en", + "region": "us-east-1", + "debug": true + }, + "metadata": { + "source": "custom_event", + "version": "1.0", + "environment": "local" + } +} diff --git a/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-event.json b/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-event.json new file mode 100755 index 00000000..4ccb6ff4 --- /dev/null +++ b/python-test-samples/lambda-sam-helloworld/events/lambda-helloworld-event.json @@ -0,0 +1,5 @@ +{ + "key1": "value1", + "key2": "value2", + "key3": "value3" +} \ No newline at end of file diff --git a/python-test-samples/lambda-sam-helloworld/img/lambda-sam-helloworld.png b/python-test-samples/lambda-sam-helloworld/img/lambda-sam-helloworld.png new file mode 100644 index 0000000000000000000000000000000000000000..05a982171532bf5ea377998fb6f0f23076a65cf8 GIT binary patch literal 38055 zcmeFYRa9Kh_ArPA2(H0B0fM`0aEIXT+PFIbk|4p|-KBB&5Zr@9VEHNWKk@16C{ zyv&-1nOPSex=z*JRl9a=uX92ae&L`W4K?aVB#O(7s8L*mupG?WM4WoUnvf=3Jf5;vYoO8((X9GVuyo)}tu z6b$@_-mh6k5;)j8Ex`t2>cTqCE^kb#4AtND9BaKpM4&xbZ=Q?iB>TefFnF{5us!Pc z!hJEuy)z2(f`bqb5~3UutApr|-5xR)ftrhrQJ%|V5Q4_)gkk6uAyw&boR~m|NXWiE z*}9VZQnUbwpegseewodo$DsWRK{|m}_yg-FU$7qxgx$Nr%{vG*)2N1|WJcw+E3|{b z>_9a2Qu_jY&Qkkke0{vikjO3JM;HheInpR|sH}NJ4_ALC%zY+<+D=WH7)i40pgB%P zyJqDGFSU5EYNvd)Xb{myGar0t>Y$m8ts|-Grpq85~lbDz7WCV#j zlDFUO?IyH9(WM}*6Go$zk1UnUFv(P38bZx_ENQ_fLJd->u&)}J>yl#fI=5MJR)+5w z9s9+BVX8ewZ>x0Zkl!jE7@Ojqug(cSw0VSP&sAY+V)$;7wx?ubla!2NUyD-Xo|5zi zT!rR?zLF7D8GZhxNJs8q5XIw~MwdYOtJp(FKY;|Tl=#QukYg^~(HVq;ZEz@O5SN}3 z8Yf#lDG7sooq!(QP-KMqLx{Qw;?JDv=T$f%m{otFPFQJ;$u8$8`SK*Ah;3{)3k2{P!b^Q(e9q7)nVJHE#&+1K?JX_*DK_=`)`pKl+r#@n_~53 z8u2>j6|@yBR)*5d>hDM#0q*VFJ-YcRc6NN?ebBADCA5r&Dn;hF58t~&0($Ww2zVpd zjmqbgu!;2?6kydrKDKj;J zRKyNkBMyBUXZ&tg2UmwTLRU{p9m{$>NZ#sbY6m#BXB7~)geNB_FMEhMu7qDm_*QAx z`f)}RpatpiA<_iNRR$5K2!YLLtEqvIZkdowkncL*^=?3c{5enIgh`P^{oSLG3O=3G ztnwrVCL1tlz=R5a)IjF=f>4Qk2s6FvWsk=8#mpWf?M-4QwmmfHTWKLCA6(li`|TG} z__x^EZI?vD@c%_VAc-FnB8mQi1xGV1jvNq;U;_6!@bL?wusk_V zlazuC$1YlJOKi(GN#O2K59@9&M1v3o+#;CT393j+^v3DNV2`NiUSaib5)!vZV<@@nJH{c ze72NckX|5Oc)LKfK+BVED|b^^IDWB5yT^X1dEv&gZ`QW0I)a0Q z&>h-kDr<_xGKd?5n~2A+g+&jb&qxpgw5#RMpqFcF`7FpDIV>nGcr4J?-dO36AO9xX z(>Q?H3wBP}J)GjszeoyUrcdH4h-yk|`r+|?zF20eZer`e>>y#^w_>qiv9vIErzEk6 zQ>NncNs%5LqOoSpZJEvt&5ZDj+{}YgZ*F7muiS*l5&{>ODT1eo`{#S3Cyb}3d(O)h zf=c87BuAtfTsB+~=P%?VWMd?Ad|P%t<2hC>x7r(vwcVHrIEfddulVx>J@_Nc3zkz> zQ|8u2ghq6%%O>7~jeTED*X?3#s+TvDq#8a>PR*(qH7{Q_?%7%Mb0nq@F_>qX-K5|p ze@po|;+j;K?8DT+tiZ&`vSR1E5@6L;)nBP$H(@jZu(aZya0Hr`Z!xN2(MG5yOpBK~ zso9m;l*N`MYH@0~&F?Or+923i*&NURwt2EPpHrLHYoJ(gICMO$pVO_9I9fWaIix*e z#TvH|Fb;>{AJE+LCnIEz?OB-#hbP(G}>3!NWIw5y1c5V{L5D;*G3^)tl&~N5x4ApIDUUoHZmUGtL zpIN^iSnAe2*4tS=GTe*auA1L?Cy|IbqS?iLB6W&%s@tTIyxyuBlec^Ba@}g z=f1qE*loGyIO`-jDO*{x{PzWi@_VU-Y#!a4qbdzk>r<)9)&;{Uj)o;`Q=nprLgs?E z1w_&mc^~qRL3me(AaGiE-O zF*TmHaNiQHr6n|(IpK`>a7FUk^?954-ajV1d54%lx2uC&UFnJLqBNS5`6KPe%)6&L zE=z|=jExV7iG0tLo2d&%BSs!)<@w7p4fNfKQVDhRBpu$9=VKpoVt(Y0=9}h|sKcx4 zJi3A!wpOkyhBfBv2YsezrhiTUp5~mrEicw9ZL7CG+Hk>~ovtviVAOn>{ccIX5Z9Do zvnaV#1e#MKR$6WxaGcziBcZLPu~mUpIbN}@^|;&S)E3nS+Fq?_HH^4x@73Qdeps|S zfuC2>H`P_Gk8P|m*qPJ~X&LqDy91gvtR4Se%Dk4maaQS5%LJYk70&}|-BjtdO3RLx z6Pr&4Z~Rv{XSipM=EfHEJs}7ciEodBYDsK~kp$-)gPn?wB}z#+Nz8~DiY4~VXKy%6 z*qzxZIqON@^8Wt4x#WCHRhW6rmddV9Jk8s@k9##2WBk;I7~ZceRGZLJ zNSWU3%d_pgn0(5*GX|T|K-kpZG-p5X^x3CW`(Q0|$yWfhy{zUp zzu~xnJw_{cgt-u@d)4B3uC-cMGkZ77xD?zv;mf+VeWYF7z+?Nv)@^B^?czK!Kwki) z>mF%evDrTJR1GJNTH;&x;NlnL)i)93kE%`5!{7X%aeI4**%KR?E|c*)Gl8GgM{RQJ z_cVwIb>-R~*Kzpb)I%Gn?)B{WRGQD& zeA0E$3fA$sR(5yor-+MV&_mwMn85I(9=*@^|tB{gkK7e^)#b&qKwR(BjTNBl=hee(I5t zV*)1_qNW;BW^!^6G~hNo1Qa9&1T?q>3I2x&vV?&7rwsu?3H}A=Bw0}Z>VfRdg8o+< ziu84`gn3943<6`RqT#F|C(C1OXTxApjnHiWE|Ig5zEzSNf z&|XLWp#8J2Kg;pGPR65X>27MRDQanBYU>0hO@N(^jqjgj{R9FV7^kqqu^+13TDzPD+HMN82`7vzwmsFuYCFszWq6re|o{EB7nfh_&+BsfG`lU z9{~X&1R*6Vtl|!N*ap{VD&as6ayt)m9HoZ*gh3i63hN(CUKmu>wP_(32tD_BZGK^W zaX0VV3%QtN5fcI>E51n@0FS{fV-e?sLLt}cSB|h$cf;)_{#$3{u(a)d|L}MpsHNL> zJpn9E9~~}>`}qmB9|DgQ+T;roHS~{u=s(RUNS1M20|RTFO0mDgSxijlGD3eJNSQl3 zlll!}k-%`Teb5vguM_<9gaM5d7J|ev@OM%$8t?zd&??{8*||8GnQf~r{*L;)#m*TP zCSKl_I<)BwJmdTT*5YQVO#5FEXses~%+hH$=N*zY!{-sq z*iD_urEv|oB9#zw&@8S4K z%=Idd^Ij+XgTio!>etJF$ZE%}LC2>n&Ny0NbF+^3fazBC5RjU)Oq1dsbNuih-Hr11 zXJtz2q&_P+Y#1Zn!e;cd^yr5^QBSDQ(MzDaW^KcbBdxLAC~$0pNkjG7jNQ`Lf2prl z3!umuGYkS{w;q&N#AY?umaQ~q^KINQ(we6=c?R6a4<#l|OcI9q8Kj57ucQS;<~#4;Pa13;M9ag~?HWXDU#`L0BU zqU)SPyqW1c2RdqUW$jiIhk1`eq9Lb@yV`R%zo(LFqdUi~o0J0}Bf$p?pibQ<*0yd` zflAVcVbfxh5XP4kMrz)Z)>znHBGenWbL0@J@8zW9@;98s17HiVEb6%%mA{eiJiAZko!i$8 zEuYP9VjY3!;<3aE-;KpBY3^@#S9ZRod}|I=RzlwUu_RPaEA*Vp&dN(~e&8@ND0uuf zX44?+&jBex%{dN`&jPjeuf`j@g&!uWXRWU5|ZoW4F z6h*VTKT7Gqg#{>QQ28AXZRy&1q^CJ9X|putZ#~~Mncr15aYi%&wCms$O=4Ib zIS-q!4hCPmA2+KlT`|}7z@kno-?pwr{s0 z|4Su25)vg-Kp8xX6MBR1(|5<2`JIpq%fq_a3`hFkz>PKAai`HXA-JffrK{Z*$wm3( z^@WvouakV^q>U>(&z5C&ghgo7IrSa(U-OQUEhO}AJ;!-IT;)H)RCZ!V7U}@1HdP=j z=M}G)@5^=gg{C7J+J6_`MM9!s3Xp}t=R@&N)pm_0Fm;WW-~|8L+S6baGIQF*DJNzy zGejSJ?>|(W0l6D{j6DciQv^6Pf{L51@kX)6LYL0ZizN6&tD;BJ?~hL6ODnouy72~Z zPYRwog$UaYqTLdjT*o_ak50TRNMpt3D;?^+A8Jn$klbQU+9SLbEXaQggwii#iDy4y4s$1AIDHU4vNj9>;tJxk)J z;+r37=spNvqdA0tUg>*1q<^p{15*Sixa;dEcQ)cV=-d-(x`mF)t4DRWIgpu{>Nt}y z*>ThDxZ4F6|8%|7LZ{!{SQfuS!u%5L)?}>t9&Zhn`@{WY=8N=$Gd_3v{pgKvQJVcD z^q_#3OhL1AaqS6XWCExG^gj80!1jUO%gp%@tJ*=Gl1TyGUw3qhi!0g7RSLPwnRz~U z9)j9l^SDRy1T-;h*-OuEq;S~^#Wsyxa-~NVT)RDQVf3yCk+ME92Rn_mWR+>R=+*F> zqcM`f%`ZNKaLZS{=f$WUrH{)$GN!A?nat$= zy6*sgLzA!=ouw1s4y9+I3|5dX6zSlH^Xnu9DOYpOs^NXzBCwU!=H9lIh;JFV>b9J{ zo{?1Z*l$pXwLWWi%pF-jzm*XD#4^m8=mk2mKrWD62mZ>%`K<3azqNILU^`A=MeO@C zaF9y=`7`%6&~0L}p|L19=+a^7Gi3=chW}}pex|bCX-ft5P0N^ramjE#m$SJ9efF?^3mp{S(d)=D})sXHQg`x`G|O|KAn8u!olK&?iZ&+P0uR^YdX&im_i@umx` zLZgnyX}bjF_Gfw@wvA5c>xslSpcjF@{hF{L(@`q^XHk#Qv)nCp^E3)BZ)=KM<9}F= z%L5(A6(7ngauZ{Xrb(=KalITXpvKVlDD7iH(UC?li+)5@ME_$5#u&-1V4Yji#0~DS z!;f3V_9yr8oLBIYb<^$dp(A!jBj0qBABn+XjzNJaQ?p}4j=@uo8)MVz`lVxHW{186 zg@&B{;X;ieJ5++axvlr?74(c=Tcy~TQrp$}L+>>N;_W*2FeQ>-gES_4SXgx2GpJMd zZv?~xw|8S>Nzw$zux$rQZoqkR;M)W|sjJWPq>v6*+&hCcL1NVnZG-4;e6^shQMb4t zaHzrAdUs?x2|L+$O3y|qjl#pA?^yJ*6(bnW^m6jaJ&qJ z_EotJ?y!O6M5XP+`_GC05CQQ@#08_$s#19FW}O|zInSH<_drO27w!(-K3APO=@rFE zC9Ji@l_!5bwJKqfQuj0+b5S8(Dz{``4dR(TkvpaK>k+%5r9uKW<)@=UeqkWvF&(5! zeHvv&UW@pi?@L2wK|b=LQ-4xhJ}EJ=cd;vbMHDni}ChDiLUEfaZVJ zdchdS+1sye@|E(@sE5_>BINv&y@!g37hSj4?%55!-D86vHSZwncXYh73zeJJK3xFp z+XM%ME20yDVh}awIw`hpy&DLniqc`?AQ+5_O^G%0)Zs-k7o6 zm#V-&!8PnqC#h|FlN#`*?WsdhQSbWqPQ0n(#vqxa?kO!*W?qFZw9~C_?~?1HSOuo< zHKINPYU@ecCJvYDIK-v(U4D2)rf14B8YW$ZrjzpBRZoQ54 z$rI2Ho~>TX6@yw7!_lWmZMm%3x`hLkK0X1SWqk?VHhYgUZILN9;1Mp*?;?v>g>#4!ZPRFn%obz45wN z6!d*#G9Bu(X}bN;S;GIDjLW@dRO;nIswv}+p^cl$xbm-c>^3(L4+s$ZR({H!@v(K1 z`PwyE8}Zw0`_cumgpAsCUiHB!uN7&F6mfW`01e!OI6#EjVvk9_M%3H79*0{|$S?;9FUNq7;kUZN z^y)jn;5~21X#2VGpn~^f?hv}0fQ~{jLo6LeMX?AIzta`>mhQ=2R{J4gM`soLn~n$f zbqe`8eE5Zb(m>k=q|3;O3$#HR^+1HM{dg*a#(7x8S^dgj0*!UPqFzTGI#Jm~9$n5d z+U}$d2Kk4A{l@y|Nz%$zTcPrNPjVyavp0f1-#UX2LfnU8W~u7FW{#Y{Yrw^|V>V?2 z=MV;>NT39N2eiV^ybqrPP}Wg1(97~kEJ|9NEP{4b(K%KW+Y}=_L#6P4cF4~Fbqg~~ zG@nZN|7EnHj1AoV9sJRgmU?t+@1r3!S6nDG0q;{Z6K(!PC%BZ*i|kD61q6>d-_lbF zDTTGq5zJk124r9phOMbvDs;4uTW6ac$B(Ni@wiR~Kl}O)oI7qUY++D0&`xV>!S?-0 z6+7|fY$XnQFl14dUF4b!mtq|EQt~k*Ha&vhxY9Td4EH*&M{_3h5!K}pwl2zQeu%W8 zOVQNQ=lPP=bEoVF&QGIP$Vrn3;&CsAmiivTcejsrh{5sx?4z5uj(l9E zu=mLxV32cEPmiHp-j~`vwj<6wOY=RDvVG^WXX|K1S7pX7F6e$MhrC)OjaZKk_3os~ zO5`rGa9Fyns4V`b2Dro(oQO2?B)1CxXDMpvMS3Rk%Cg1A#@4;5Ez>J(z!0s1#%I0j zto*G8KV##&h53UaulK7uE|b;O^70Z|@3_nO>xBbW_e<4_l!KT0N!1yuv~7XyO5Pm~ zO(s#CaZ73pv^e@LRXPo}cr`tSN1r_ZDHbFRzHVwQd{cd^rb9=Qq1DXBT7(W{cK*S*6`$H0OND2`8Rlmht0(0}+NlOIU+(n`Em-twlYZKA(2Pvbm4P z>00XG6H59=z)bvk0IJ4yV_(EMUBT^b>5$o=+(9z4ZQoK)@jsHlnd*N;jJvj1mZu{a z)57_3noBTvaoh&vy93Hx-h(zG^ga{2Ie%0KRTg`%7jniakLhWRHd!e(xu&<;hyxGS zoXZky9GkJZflL0)MrV{w^Jd0Pi|q$PlaabQ*Nc`_Ap4pkoetM}?`s@_&8hvI>Jxc_ z!30qFio80J;?I&H=}zd%v2NTgBfs*E<7nR*<1%_eT3=&1YNK>paQ zis(6?RKn0Q@eYB>%e~RmdcOblVTEWz zG39G!xLu-0I^w5kVv$Je+|sL~{Tmcq{LLCb?BiuEn+wZ8aI%!!#qIZ?pvfP zt0|lZq49hS9`X#l(=A>p;Ju9Z(?^1#k1->9HEP>fOgeVnAf@x|jQO6ekwRN)b)crl z;)&Gg-{RrL&(16;<1+ZJL(isy+vysd#7w>Bsx12bW~#O?$}SYQ3vOt3{(j|D`5X36jHPlVZ*%O|rC!H;aIGrhZ49PgW=}f%Akr z^u`n3dv=*kd$i57KJD@1YAdcrEsl1EuKpm+z7z4$y)(OJ%X{KMFjMn0Fd7GUIGj+e z>~DRI8VQMui6it>jj+kG&UJtNE4#+FD|ddlk?d%`pR7(#OY;2o7)4k8xcwze z2N9$Zv0z?6Nb3K&0$Q3cCex-?mZ^u*MtWT2U2uRFboSJFOOI#sRn7s&Ag{aw#YoP1 z?hmseO?8HNh^E*#+z{{iqIpc!js^G+q&o^4@!BHL-~9DCNYIEhOdKt&0zjcE>d z(svq3-9xQzI`#4a8=g(XAG;jK#O}9~i8kl&^UsIZFg&&ujZC{xwbpu*T`+#xADyvy z5Smj7z{=G3m z3N$MzGTR!T?6H(T!C>zo#3lkG39eI1ykL_; z{&|kVGcXvo{#T%wYYwiWU+P0*{Ix*I;G+02?0->N{>!HSio}04=WjasUn}wdYKv$v zu!~0u8ECX>?&nZ;&&rBvo6W3krX}Ilx*wl2@?NCMn9`WFgk2l1q?wa)b0K?@CPQOU z#|7$)(G$u%G@Xnst+<*8WVUQvT6Rgr4&T0I_% zi7G+HcUX;FxGV`B{UNY-Hp;h65o&}G04-sLskZ_l-i~i`UvSXTHHQX)WQRhHcM@Up zvB3s!*Flhq4!QUUb>awuEpW!hJ=IVrOryWE5`1k+IP;9###w{UhJoz+H9dUwMoKzPrRt>CO2pgdlFzerbT54R$Y(7%4C27D@QB9wDzck}t0H9Zm zqvR7Em*h~iZ_%@JCV-g|h15$HzNmoOO_GUtJ5YEr(e4_f%52Z7!5%OEW=1}~wKQT% zyxuq!2`Lurj}4C#Cp@w7Ho^+|*dZ^Woks}4Z^0;91Z`_Jq&jp*0Vwx|t-ez*jQ5t zQiX`WDUU1DHKgDv2RrYnwq&>P(&irL$=9lp1*XrmP_Y{@8Pf$g)WNM*$gj&;xyp|KIU!*K?WBH8>H9S!B*rT2#eY47|nnhXEEMulc7n8UxiK(H$Nt^*J}k6REDC)sF^eWHeqgk{^q>%tq*UpA=Gc^a2 zy@!zkPbP^K&@3S&&C8uwJH5VBZ5z8oIhue`5l>DHk8K5k6>xUOm?)HX1i^Unu1*mN zLOoO+yU%k(zSnkyT_U}PlbJ>SpxUPA>MrG_UGFH$XVHtX*2s{;Z~+^2SQb##li83$<9ruy?e1q`jT5X6 zeD5~v7SqwVv8<`jH$o45IbJYD*<4=rHyguuDGd88SH%qaV@jpY*B z5h1QGK*esGdsWp!R;dP;tUWCmY=1hxbtOR&C*D$gNdR}K^PDHs?-^u}j!KCxi@6Q2 zyjp@Q(X}5}UWU9=vkPh%IUX)9c&E^dWX^rKMIq@XuAT;w5nRI$HVG3%{>5ey#A|fa zwZ<+w#$~hpgoS1??a*x9(N(E_BNN>USF|Rmvc5;Z`Itec$jaiRRtHrR{aFCYkl^%5!f70NHLYB)bhGruW<1azR zTd5yLE@U@i|0FP@(0)=6IpBzG=P_kr(f}I^xE6r;VG^MR;Cs*pT~TnZ7onYMA+ko) zrE#^hgu%i&yM%${1f60@3Qx~hfiUUq_Pbfak%%#FSX4D98Q6DFukRQcGKP=z1zWWr zr#Vj%O1gahzQdu3vmHgzPotwR%$seVGV*CNZ_#C-cBxidHI+dRwv@)m(q`V%oEVzP z1aV?HfI@q?pYrM8p0Uv-C&}

_w`+YLkm)X|qjCkt(rI05G4RiX0mHHPu_?wXigmfA`)cl39>tc6dTW4n_uv0BZXSllHZD|BoT!^Le$c{fdr! zou_cl2j{hD-s3jRS;F+7@r?aal<(p=6?=>OcBAYPA}P>}%~aPisUh?%m5R6;wTguK znZ||?5*h=%wLf8d&ZDBr`qniTJ$s==RDV(W{iK;LNt~KCGE0RObFVpo)>lzmUgOd> zz2d*y^8M5hzdL*&zWAIbWKQZ#K;du9CxVc!lWRkg{^8jnfIF`xR27j+?dNZ-33V@> zKx{l*1~4PLmpC^rk=}-xh)4!&UE@0D{^}*uW_YtyDID%gH0gU}&%L%!7**=2>Cw+G zHg|2B2-m7OTMGDcGQg=Fd6&0wsy=HDV15~au%@;H!mA#zV#XTI3UJ`mW(7h%n)_v2 z;BtI#uMB!Co#2n@c!+aUiKmiDz`|q#>rUg+?`0m=PF9ta(k4sR?Q*eHoHD-)sR2hD zqp4j`v$aDg=QT24n0T>r_ZfZj7eGETuaHWf(>-g3&Z7X z$kyTAzmg*wCG}Av;kk2WIB7LWL6HTJ4+3M@_2_(-j7nhx;u+*gIBQ#Q8TJe;Ht{!f zpUEG4Yp=|f-mZp0BlQ>Ml}G975NsyAI+6@&qU&e8Y&TznX=u8I>j~M?0&Wb~JYF{8 z&lVg%`Pfu@#&>58k4TY)w6lq7AhCUzjW0?qt| zo|y-7<;s9QIJ)Dn#;FzudzzO>yNT2hiW zG2nVN1m^nVm`qtXg;CaXnFGJYp)f2nrYH$a`=vh3)Ng2H?v+3>O>@*j!Bt}wx0+VF zSZ0ovZx{vo_Eq`zAwz^U*{}^4K&IG8*$-oi1!E1zm2*fKi?w4k(8K?VI9n(KZ#~LEAR%k|3`8!XvdDz#JsJ_@$ zX1MsmWPymSR|U!YignH61xxb@s*&45!$O;6yY(z>@(NRV;|y82y5p_R1*^RxH1AAl zBZ6JKP<_=$b~DL(yqY?>!u3Xd+qo`|^lh1LMV#sPGyN$@NUs$ovXgZ5`vT370^Dfx zJRVbl8Ea#UZ!ilf)@{)zwJOwXIViMKdc#xpfW`BIr1JIqFy#tvS3X4hVj@g_b-y-U zfT#f*=#>6_^mH#0VDcACn3;2PM+%e~Vc234ps4N2YI1X7;?$ZAwoEN7g*){9k}6i< zxNX06wExLtl3wN0sUvKT zcJp1rkSOi@A!#NHiiDsfj(SY7X+DzQSd>v&A;Mwvne)=TS>+T{pFXFKkJRZIs~Gpi zBlKq*w=rxFJ_T^-A>+L9XQr&+$~o9&-gMM(*_m4Yx)Sy?73ha7#%2niN$rY^{n_~^ zPOTRSwg}a09Pq6NBGXt$>C)!NDZ<7#Q;JH+*MxOu z=b=P@O?YZ;jgUPjJWm1rkj`AhJsJ>x{t40~oEEiy2VWhumT{k!OFq9|@TK?59gerD z4lX-4+jhX9yM{6j_;wam!22T8DUF4(rN6Ti*PxfHl8D9CjZG%Rn7Lm_duw;t1xY$- zFOMr~k*p0GTKGp$LCIX@r`%})bD$1?nGr`;N=i6eM~WF0HXxQ-bQIssOVK187r~^j zoCU&K{G0To$?lB&^T5K!n0Iy6c%^3Qmh)p0FJ--!LsvP$#}U=**Ni3WmD$dHVbkrU>UsV&ahZ+eh3ABXT|RU_J~KLAr3J?f)veOp$Wcb47}%_8WES9+ zU#*wO3K3fhSv$;uPcsCoX^iC~mu3=9jV*(8sbF7+Y7F5ZlqqjBj8d=aOh`20A4=dm z7`2wFo1_NfQ%LW$nL<@UGwl{4_(HD9Ne9kcS6_AUkpaIjFu~B^D8WU;Y;v*c z5u}$Cm)c+O2IVDDyY=hkgL1f?{fh`tN1ZOpmtRxdipy-~ayltD+_g)`ndQKiVElFU zdt&@#zXZ)%q;3}c_e9GD$W1>Rgi(4$i_fzA$jR}SP4YQJD zcBaWllm$l`wkNSP$)MD;hvEx=*|GKl)+5-x(`T8#2)dx$i21PaDLHvv=+|=DRt>hDSfaejfqH;*~dJyR9{Rxu&?7$q3aLL@4Phg~NR* zHb~`BLRPWmoaaPC87xO~HDp|$fmUFPFkap$urURQAR4niPGjG93a=*BjXl7(y?qzc zzaPY{oJzSZ#tpq2KuxP&9VHnP+oE*(I|bR3NE)AMYmLAL=cb& zg}GX5@j%^&M&fd%QKe;QoVTmEiuDpEKdr)W%OPEiKp$ z_?5!2u)`3ny^tqqXU*H((u7~X!4qc6g_R$;qd-e}F&zq1(rGOjt7H@QQP~$4x?l9D z1`%qbcpj&&ekEw?2(S~@BD%a{BRV)5o%CB>5l;;tfsAZJb=n=St`k?mZrrCN05n6w zY%!W7ag;)QZq8co0cL*+mY;(W2X+Ur|4$&5ds(Kp^Xc7(J!F_siX z`}(hLPnw1CL+x|Gv!#^uFRP8`t6taJR_)X5C4z0f$!Hsrt3aN7{?o`Zhx}H9R)>j( zg8^(_rIk92H z;`rs=9W(=#OH^kctFK(nNL@mCgTj~vPdiHaX5BZu4jm%4{d#SvZS*cbd}L2`we1qQ zQE)D8<}3?cu1^U?6G@M#UtSo2#lvNS9>#@hb?V(s*}`!o?Ww}jM5TGm9unLgwux6A zvQ7bE$?2P5p_lNB9#jh?rbg`TlWr?&*C~#KdEF&y7ZgKcOsach!gTG(XhalKSgrCH z@zMiegf2*6qmWTq_w{>fiBs7Yi#30~#p4Ps8pNKZQn_Igxiv6TJs!?wQe-1p#X-c= zfCrl|hOcdGG$hfEq6-sNN66^-zw65ec%oZGhj!013g+y!oAXQFswt_c@w%@!S{cV& z2@Un-iEGx}Uk0Sw#kpiX2MLT_F8?l;=AG>Z8{TE>}N3@=trHF{>JuGz(f! zA{0<3Vq9phKyuIm9QU22tIhXNOsnN=vxNw}^ZTEnV4S%N?xb*?Q`@B&I9u5*xSBKa>EFrGpu|`z zpG*}Wg*j2b@uzwdan*DRqRF*!DVIU-viaU|gtv-v^?Dc3(BuGh3sD=j6n5!t+&7xW zszFW9$v_)p_V27m+$J|XG+ryEfUCNNiVwmG-oz*Gt-Mg_q5tgILwamWLAsl zVX(qJwubB$SC29{?rIvj zN?D=S;w&^v05v~_3QpK@8V@BQ+R2^zurVfon@RSiRL@y_?HOvGk{Rdj#KYX{x74-H z0OZ6PH-18WHv`M?_K5s74lF!uY}E^SU41nJaB(sT9NK{m6?60V;T$vE3|EbrfU@^K zq5~#l;%u}7Heo@8&E?l!D3tdp$kyGyz~(j6715T>G6#h*e1A@Ei3}$mJp=u_Y1i4( zXEWpTv9zOygswCZ}>A5F?4D&Xz`Dt+63zZ|yl}F9kV(}EXA^^dat&a7AeetC?S=|taGDQgZ48%}aa?^pR0*OdUFRcOjliSXgXGSmjir`V); zy(ryOf(OXK5lS!45ZkaLx@QuOH;z3F8-Pp`W4urIyI23Eskc}&ms=&e+)#i$QVyt1 zf5B>P%7ZPFJ%z^=x_8WOD~`_!M{Dw@C& zeP{6wtGUQ&u?BZjCh3#QM13T!dea^ktZa<_ntp?=yFb`4);3MQdDEj)l6f{gCdId_ z8x3S92Vl{=jWTt+9lgt52P`sATj94qk03FawfKs zTw@%u3FZ}J<_ysln1KOllFF;gGy7b zau8LOG4kis5bK_XTV-PJVfF%p${+txsW-(F*KRi@i;8xvweD3$cP3+;V7~l@M0nJz z=slhf^<8!rR`;5@M~UD|Rxr>wNoUh6Wn&NpgYd?4?#B=r25I+Lk)g3`&DQtF&W`2m z{l+OexNCA2|$&rQBb&S1^Hkz_O zg+JvQSyM#%6M96@S~kO*PTJH#?V8!W4OT)Ub>ot)=3vkK)#kmfWjuIleg7qGxNO*cyP}gnp8>9aP3{Z?^u|;Uxl?JvUe++k|5erR@*a7s$33ivYdn$ zpUJoULjlxDl!1``#LB1Q&Z0zt0jsyT^ABsTy68vL{rC<`RoI!+tR;Jz#$4+zKyRxG zm>F1Om#B+9l_#=|#+jlY))`X?8zQzt1C7k7BkQzZT)$a(*8y#5%YKPdh6tuJZ8(Qh zNBawdzXs$gS`s#AgFK^yu}%WHhlKV0#u_rn!VFDF)OZ&tK=cV1!0?jA+MxPfK3%c* zi4jZAul_JLU_D>o-KlA<%vIW{BB))I8H&nUQDF$*<=vGSJ!!A=f*PL8pla)dWf^nMqwuh-%$@*9;R9mC5REFbZshtC^Vc z4!D#P12*i!#|dXVrre0t$QzxTy%L)ga%9dTtX6Yb;YMoQ(J2|s$-tB%?(v}`r%;&yHdF=qs-x^=@H@B6r->gHDbO)8z6xDcR z@;rDEig~cv#+HgEA5$Ls8wX1k`jA^xC@3b`297|)=?{5X?jhhW47{Jix9;^bf%683 zWqY&u$S?QHb>ffjN0AALN{l%+o2`rmwn-#rcvY5mnr*+hR>mP69XMM5 zulByOEv}{87IzKqmH+{QySoGr1b26LcXxLP?(PySxO;GScZa*!?>^5t|KL7X`djzv z?zL*x)KRlWxfoHLG6x^(;(Li?de-yWb2=z4-ZCIj{FA5xj`ocg_gC<%R9|h(ph#8M@TgPoGSe|b(5_r zMl=J$r|f$+D?tkMQi~GAQ6Hs9q4^!i)kkp6Q+m9EG{LXW+_{;89d8RoiQ#A_QNh)gS~V@$1bpURd8dus&4Y-goPmf^ZEJ46OJEezunM2el`FKwd}EL)JGjIt z4@F!xh^}-j$*rDtB|O#ALCJh=)-5u1upRpoYqx9m3-Q@>0eAU2S$bw>dMA>NfGCdK zjY{2^9?OeWwSfR6=*^S|0KOmxfF#9l*|5nu_*+@X3503HTfqqwfC9?ZA6mwLbC(Y^ zKnsg9x*K%y@#ULi;dTgWx8@9l^BBQgCI2e8UfX}Cq2I2fu@57aX?@k24=MJbOEj8O zS4^-(rPS%cPSv1Jc5X1XfJ5D|L{MZ- z80}x|>+gp-yT3yMoXhWI`vW`xiecS9NxVBsM76UVsN34J?G-?{=Z#} z{G{@dQ~BqkqfL$ce$z;*DGQak5v$?8;>uRbgN)k9W$HYce3rBF7$ba%jL~Mwu=yG|K(g^yW4$#@{sB| z-zW(3S^t-%XCbofG~EO16f)DCV@MdhW;c+@!21Ax7qU9Oe>Zm*J84sAmsXNMHFlqp z+#>PszK<{uKBUhSfx2O4C0Q{Bd5)P8x>BaUI&1 z@>-HPx_YDQIcM{i49fFV5F+wHXjoFM)5j*NS@`Z;RwUXJ$jJUMR7dI+ba+G zlcA+FJn`jC|GcGt^_7j7Iyem0zI)2l;#*j$-RWJBqLn@YNjhVBFI@$}!c$K8ZFP)q zU@xH1J&Vd@mx|Nj368>|(`NM=(5eyDKMXC?jbaQ_5k5mEmJe92PTyG2ILMvf7duf< z)%KZTvj5=9)m!F{`ZkcXX3yEo9hIHP-jP=mqjF85z;W;y%xW z(1@9jNyTJJJqK7-7F2k*Am@;n>x0oji=cRK8m|?rMjP^XoS2l-0fu$LY!Kn{ipSrI zy$8M4gxjZuw%}P=)2T&6{8XJ3w7m2REI8~&Rxesqp!Cac582dl`^I`4MJ?i5uKM)? zl`+Ui6{JHEIVCbCuq0YXFe0gkmkPJzRj9`~;~LIT(s1|~WhCmmNJE!|XQd&;^~=bq zQo#^X(%&xFtaZNFdR$)rFKljcFr-`S1PiJ9n3n(<`OyLckN}4+VH~QFc~wm@f0WI) zY5KPc(o0a}TC?7X6==mlC(S>D1Q-!O<_&bZKJ>$7KVq6?%qIrTl+*z4GkuojqWEB8 zxvK88ZzUKz)E`QM3Pum3b*R95nJN(BjeIXT@Da4lT2J z7*z)VO?li=9!{;)G%tVa0Vy^!w~PlZVxpGoORRj+AR}B=`O=N*Rno9sY+28s=dUyr zF?Df9E`*U;>eKiLrTR`)cng8_fwTS>(wXl4x|3U|#1La%&!aG9v_nJMoMy`WNgCXa zGQdiE#;Tb?a=;O-At-ft{!oiqgHN!=fpN6arsh9RCfSyvR-~ixz`hvXgl*Ysd0>{}8teKv z-OhvW>|g=6m(J%W!W2-RJ;f{9Rxs>2XwQx>w`K+D>kSrYyzSl{1o2)}O|al1$FE5D zvT0`bzc$?dqMK**{o?v5+u+ZOL@I5BC$dEht zmy;@!Zfn79icG}TB|>j1_yR_(gXtCqBe`O6S2$;kJUn@8*)P|qO1x?CcXz(oN|x5Z z-moM=Djz-9;3jE2^_gQW^A(|5QI@J@ow18TEqerFNNGIqX{1#mDrmQhy=FgDrY{gc z4nzD=pzc`V)F`BVC0*bz+`=?uRE6MC9f1k#qh>Ti1g| z{Yox5aZ>MXkP-!)xk8m0nv?du&W+yPO6e28j82B%K)n|)|`ubH$~Oo=e7 z{@2KnLGhS5mkQ2Trv;@kmTTWci2j6cTF(8Z7|OT}s~-N+H^wU~S#v~eO)<9}B*HCfhgE!SU5MLH&jAQybVp~4InA|!SgsW5y?zXt)XuS1hBR9`1sw@?C z=?+-4HB$HYPh0%yVp(sDtA8d}&sl|sqd$&&NqRMDRm^Knp$}i7(W_T@eE{Wl$1YGS zDi1$q!>NP?pfQCU#(U5hbTY}}^QO%bB0ZEhg%}$1B15YA zidGJVDFK4IlL5KOAhR-Ef4{d^qSeZ8xJglzihtRC5SFu-z>%>Z6g+x`DApR%)|=c= zYx`1ATHmmOjH+j3TS+kWYG`8sNHS-zLpCQ(CiGmh2;=N_0%Nk!@h9m#{Be z^q}I+o7i-$iH`Y~FE*fo+>R4mGU$x>6^{%&)?XwDwbsU$7C$l1BrGTjd$8f_*D&jP zKOmv>jh0xzl=V_*k>efvZo~D!t4IxvN^cpcGEnjBTtNFJ!fsCA{jk#f6lZNgyk{3s=2%g`;-+c_Q+zcfZno0Ycr>A z@94u2RCA^Us`Z2|UmtwnAM)qI%uL>u@)0V_AiyN||R&;gbrADo?Uw?r0oCzQ(U))a>bI+Owra9`6rG_fS2HhMBN0yW! zy6ykoE6z)kAb3bw56syGGYHSNscqs(B>>cJ@CQMs+D5 z4#1|Xagmb?UI)#snnylwSh8z@{GxX>w=dJ}L;f}s09o6w{s~{1z)mBx`{h&lPEiZm z!=|PkW13!`5f4U~fldg*h2jy){cwnyencs_|{ed==%^r{=g_k9^SA)c(6soN-?10QUCeUGZO@nypsu2E`RF}(p>5Cn#?MIfnJ=xx1doxkT+}Im;U&40 zJiT;CPM*tVCn+kvt`B|(y;Sy>S{NQXed4F>CApTzz~2ueox8Cd!5u0=D}||FgC2`>3iZ1YR`&w*FQ-IPGsP4@)_D2kj$HBxjNHGiA7vewsb+SZl z-S@`rZ28-8I>>7@lnX8-?$zz~j;4+oCWT7$v-h)Bsp_DfaR_Uk#C%k^-NJpA`O5w# zontyb%R0sY1%yn$8KZ?of0r_Pb}fnMh@4(f=qrg6M;)8N05Z(1{g&|;p!M>DfeF9g zD+g;F#12WCmRTVdSjajcrS^^btBx1B5(KhXP5kjNoL3%WnAhCD(#=%_tUMW1`x3h| zzUgtWLaA?1s4mCYXPcEELX8%Fb*k+TCNpdiGH<2{VzZ^B2^7){lzbkUh=ogP35~7GA zpO9IzQ3>v}A~?8dOo!DB;%5a+*cDx*Z&CV8lCn3?^uTK9zY`(4pJhZlXna8-Qj09f z9n+GtS|PpaASF9zsec5gn?5~;$(OjMS=!&#k1o%p)a*Od-b^j0^yMWhMdP{Yzti;?Q_HpcpXY0@3CR zAXCo24Z;gim8^VD2E=lL8Nc=-;|+C=`ewCwXZHFW#S!a$AMpKVq%>t*o)z)~2s#J- zNWlcq@gEbf+-YUMtG&lBhrpe$G-c&T z1btP{Tho_~sFZ={6^WIhzNlWw(!WF%gs`YFV^j3FZU3OrHB_)w8ja&_Ro0%Vy(L>N+5hP>1Xz>fdFx?dfmaHfZ&V%%I1YW1yenQfsz%m;H|)` zptp(eX{)?xlj<*=BWZPa3^q0CAveW&qe0MdpjGGJX7fw8z=$ukXw@jSY_WbOm_L~C zCtT{amr9{Qm@d8tC^1v&@qaf^)|IOx*w{vvW1v_v3*dd;ajrOmA7qfT)r7r!g9hpI zM|u+!vBaglQB-bNBDQFCF8(cisYuict6g5WAq4tbSmw7R(4XJoM>`iB+3@p@oO6m! z-GMr2GbJ`E)ek)66A)_Ifyn@07szw8CT$6c8gwO=#9K@;5~o$SEW&%#*F->2kh8$eT z2uP!WHot)1j!X!WjCR_5Of{p>sv`-=wJC#k86$(3hzgtyQ)3PeQKl|SSglF}w1SN$ zDGbI2QhUkB-wo}`AO~LSc{lc=HxE;e7IoOnZ<;oXe?9*6?Ea*mYL<3E?g6{5_ftv# z45XwPZYkYdroAf=^}oTS?JK$Xq=Xii!Z^1CND-o-JQIR)h^@Iq9Vb|<~ zS3l7dcBB*VsimO2<}lL^=)pE6Wbw{j>~pn9S2M(H^ht^zi}d>BLJ<|Jt!jawK|K$R z@hpU18HSOS2~5n{4OH}Gh0!fPXxjc!)Rx0*9R?UbBD1-L*H0bR!T*`a zD+()-aSI!5jgf~VnOs>w#Tl}$a_&>Ygdr{16o8{N3P8F;iWZGn?dr4m`=z137+_e8 zjz5)_M|L@WISl8shxuewC;fL1*B{ET6{Rw}S?FiKA~GI0Jc2kSi9Wl7x}{c$p7BiY z9LFdxzB2b;n3Sy{pE1xDPtI!e!7hHBjR_ejINoC@7`Bbj~x5LLuzRf(~^EhM(jyti*9s4YZKS^#t^@! zv-flNLe6vb>*DxJLkh0z?%qbeYImTd6#UqN@awc@xur8LatQYuf=Cz+LHW+6p(WobI%>9B{rmEXJr`sdp4VUa+>P z%B}N#Twbewa(I;2UjZ$vb4Pr+kTi3qVHRu{t~5XP|Mri5U4d^Yyp?#uzn*32p)R}n zvpQexK-4oc)a^>TH4Cv)ngkqTdw7hsr6b_dNk zR4IsornN@o{Kj`W#M)MKsBg9}|z_k1OXyi*XSh$YBO{L9; z32`B{DA~{_3$+@FnAD>xr<9L^4BNe6lB5dPY2wvqm14r774K=o%f}Q5H_1-hEYqvS zS6i{;fkYV_4GGc+L3XSmopv?QLw-fqAVV*OZpa%?z{5oo=~KmG?uEyBn9_Gj9~k*- z0kCs?^hVvSfoF8~^7Gj2TYNS@n3_l4JGeJGl0Rjq5d)Fqpq6M0tw=1gBR=;-LOR>i zRdJ>fq36IIgNIHnC+!JfLpn%2#*@j(v-itN`C&UcF*KwT@($L~Ajl@FSlQ~gkEZ9r z_$M$C_u%ssn!-;Gay{vfZX@t*ALnsw&6zft-9bF(Y*CPuTXagn$RW23W;dIhc~oR6CM0^_y!lBiONOurr~XSa21o2EI0IxzEyo-BCYiJ5qj1OO zRzr_zYcE(l)D+<{=~bpOy?NrCisgY-$`=JfL&U^^tZYXdM^@;A!#8ST4{MtIG#a`7eiRE%8W%2xgUBP9dBQxhVw*J3m#_P=h<=4#YPD`Ue|PIKtBk)QuqDg4660R?P% z#*Zebyr?u#5KI2^!0~JemrXkow#u&ni%*SF3lmahf-2$!3L`4T_qN^X6@7_h(@;{k zIZm^wcD>t0CU{Y&(PPU7?m$U#X8;5W_u7dT3hWSYM+C*i==$3(0_YRD2({?mxnV!w zhyubn3urTz7~7J%A7Z2646zH2Fo^gMW4$0{K+`lpdCW{l&)py#0K1PrNzKNP!7sKC z!NX9nm6AQP5B@!;N$H?JWH>C8u=^TT6V9=!m+EMZUWAdJ{$s$6Lj7kl1`)P8$T-m2 zJO@t=-5I}p>(1;Q7>^d#w&XPE`dp_OT~DQ^3JKe0ej z_J~PWh)Gzr(Yy37(a?uu-9cGww&67UP6edG zHcdN`##935YTP1%L`>8OI$HY0RSG#3rzuGY-X{Ct?<&Uph+9FIg;Nc&)dZ@Jy!H57o|R8&UV;c zbMYW?yT&e;LLyUd$+9}7?nfuor4p-L_!&JlUsgfh|K|+UPmM5QsGMsuN1t7~sUgj3 znJB$mTA^A3DySdu!jpG{gReDzqy_z&&K&{eY)<$SDu4*SRBP-Iii&x*Zm=ul!z*8o?@#|rwDQrIAR+VjUkYckk74}b%9f<~4?mFv zU;`8!y2$_GDv9`LPeVQTpw0Xb%?LU0Ul}_AJpa;S=mB3AYrk%B_z#Ur%7@{`q22w< zY4d;Qjgul=PBos4uvO}eq#v)6J2hC8D%5_;ux|!X-QyW&xf(t0(s`@45xs0cClffY z)C|wyu0PE;4Pz*4$OjWX4vHleNG2<}s%=wiKYnl748n7Oc}Oumu^quQn@twQo{R7FS9f>q{rKbkX*T254 z^7gD_{Z!8N>iI-Q_}4v;Dz<$|WOX3;ae_Jg3<8T|VD+dY?NwAW1@ zS1rV{-Bu_?l5+bNM&?^fK;1ZJS^GmD9c}0Pb0=Ri+y~VVHVKd$k*yr6t6d$nI{!FO9CD?Jw}li@bD`wQ9A1aoBl{dFBH z!z|KIA?^JF`CX3paY#04MwYL`10as2eZRAQSIT<$LlG1=KTwQbx~11^ zyBNb1NfrTIzQ0@Td%w>Q;b@-)YjccvO!^bfwzTXoufI7d(Ve>taB=!pF;V(yQ~?{Q zv)>mFqtYcfGtmJibZi1}b!W-7gM-#rI&9U&$Ng+=tgvIp{VaE@#}i||GSBy0?f0Kr z*J%q0ec@QjnI<>qD1DCo8FPVJyko8RYwpR$yw|C~D0j6Tu&=pKO=aQd)V9Z`J*2PX zqumq2q3C-DXqpI}vb6+WGTlacS z1V`qs#&=$^P49@ubl&bEd^WD?xSRg6K}2d7FW3FmZbj4MVKX@S84x)dJC?SGt-!q3 zn(KaI+v=&+F5!LifYH0{)IRjPOZRH09eCW6=T58`W{W^6zT3esg-$w8_rYeFmznIh zdL8fWO8pgq2(D0TiACHut0C^$4gw~6BIhd@o?9!1dR0tXa5|T6_@m$LfKn8|yY6$X zt3FqIcLLfeqnU$`ZTw~Or)EnM+fP|5rxMPT^PPCvO2!l489F=9x}z>=j-Si&u?oK& z@v+&O06n0~PCQZIv-Uhzx2{y3OP^@_QDfC|XnOZyX_}Jn@88O$-*g#oWz<$!GHw?t zHMzpu0-v4V1)=Q+$*@|Zv|qNc>cj{f6$e;tc8HMb0DkJK<4Do>O2w7P^n1_0c~Ff8 z+q#?$zb|^=jdv676^RHhe4s~r2EMH(wl=&-J4R&s8xngDOlD0m(wQ_B_+PiE=viig zx{_Df>$vset>r)|Yq=U0y+2NP1KUq0JkRXQXpsi-*sL-p6Gw}V1~V(`>@2H>A821M zg84G4V-;En2=H(0520ls0xaeCnCRcu&R?~RBVXUEY9olJkX7Oiolop9h8w5)y<+g< z3Zcd)1tL9{)}wLoqB{&;TbArj-HvDccRUlpgLT0&k!#BReLMs92oCroP5t3 zEy_cPxE|UA8*p1_TbtwP8j7TM6d8QG!b0t~P;!L%D_A>-qe1>q1cO2$ZH^) zAi0y|rR06Pj6O%67?u{8`v!o9GHFaq0kjWKb$ritC8_D{C%>4Kks=t7kBWG2OOB4$Usu=1 z_B?Sp?Mj>c$=IZ&A&j`Q;a>ROulce?8nGK&&Y=4rH>$R=1G|}GI+0B#j8n1dm#4cQ zHE3#BCw#cWRgeW`y{_W9hR?mfv< zXw1M`z&Nx>!AG!LdTvLuDY3lxi&l2exA%Mr+rvB(0#xml9Hzb%_}BoLUly%4+C8K zGcpZ=I7OJd{h3i#qjOcZW=PP^EvotB_U5?P7})lrv=jQ{hu@JtvuCDB^3jua$+(wI ze1rpM_9N)OU86;P*_RQ6hEWRYwd;3r759HvBmjzHO_e`H_m(J7rdA1Q=fqSH*_v_x8;FGVNEh$ISJ|%&|3j z*TaJ3qs(gO)|t$k%#!A3d-o*e-&cuGln8{uu4~s>Y;zcoMB3+f8Jz3|3Nqm`p7$r9 z(D;*6UUzlbtFc0Z=cKDE*9Fb+4~MeeZYk2U-WSl`lZWvZiiYhrBgeoCtSzvhr=+aOYspQL^&{U^d12-<-T<^ zb6I@wei`sCCM4wG2kF~}dW?mGVCQy2F*Gs~T^C35@_4(>UVy%0h~mc*VH~eDnc92j z3C42W_E&$utTl&Fg;3^(PDTFX8>$oO&*IQ{vvpmadj8~#?6Cpy*`qHH4QA=VV*{Kh z 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"}, + # Event with unicode characters + {"unicode": "Hello 世界 🌍"}, + # Event with boolean and numeric values + {"boolean": True, "number": 42, "float": 3.14} + ] + + for i, test_event in enumerate(test_scenarios): + # Add scenario identifier + test_event_with_id = { + **test_event, + "scenario_id": i, + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps(test_event_with_id) + ) + + # Validate that Lambda handles all scenarios gracefully + assert response['StatusCode'] == 200, f"Scenario {i} failed with status: {response['StatusCode']}" + + # Parse response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate consistent response structure + assert lambda_response['statusCode'] == 200, f"Scenario {i}: Lambda should handle gracefully" + + # Parse body and validate message + body_data = json.loads(lambda_response['body']) + expected_message = "Hello World! This is local Run!" + assert body_data['message'] == expected_message, f"Scenario {i}: Consistent message expected" + + print(f"Lambda response: {{'StatusCode': 200, 'Scenarios_Tested': {len(test_scenarios)}, 'All_Handled_Gracefully': True}}") + + +def test_lambda_response_format_validation(lambda_client, health_check): + """ + Test that the Lambda response format matches API Gateway integration format. + """ + test_event = { + "test_type": "format_validation", + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps(test_event) + ) + + # Validate Lambda invoke response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse Lambda response payload + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate API Gateway integration format + required_fields = ['statusCode', 'body'] + for field in required_fields: + assert field in lambda_response, f"Lambda response missing required field: {field}" + + # Validate status code is numeric + assert isinstance(lambda_response['statusCode'], int), "statusCode should be an integer" + assert lambda_response['statusCode'] == 200, "statusCode should be 200" + + # Validate body is a JSON string + assert isinstance(lambda_response['body'], str), "body should be a JSON string" + + # Validate body can be parsed as JSON + try: + body_data = json.loads(lambda_response['body']) + assert isinstance(body_data, dict), "Parsed body should be a dictionary" + assert 'message' in body_data, "Body should contain message field" + except json.JSONDecodeError: + pytest.fail("Lambda response body is not valid JSON") + + # Optional fields validation (headers, isBase64Encoded, etc.) + optional_fields = ['headers', 'isBase64Encoded', 'multiValueHeaders'] + for field in optional_fields: + if field in lambda_response: + print(f"Optional field present: {field}") + + print("Lambda response format validation passed - matches API Gateway integration format") + + +def test_lambda_performance_metrics(lambda_client, health_check): + """ + Test Lambda function performance and measure execution metrics. + """ + test_event = { + "test_type": "performance", + "timestamp": datetime.now().isoformat() + } + + # Perform multiple invocations to test cold start vs warm start + execution_times = [] + responses = [] + + for i in range(3): + start_time = time.time() + + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps({**test_event, "invocation": i}) + ) + + end_time = time.time() + execution_time = int((end_time - start_time) * 1000) # Convert to milliseconds + execution_times.append(execution_time) + + # Validate each response + assert response['StatusCode'] == 200, f"Invocation {i+1} failed with status: {response['StatusCode']}" + + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + responses.append(lambda_response) + + # Small delay between invocations + if i < 2: + time.sleep(0.5) + + # Analyze performance metrics + avg_execution_time = sum(execution_times) / len(execution_times) + min_execution_time = min(execution_times) + max_execution_time = max(execution_times) + + # Performance assertions (reasonable for a simple Hello World function) + assert avg_execution_time < 5000, f"Average execution time too slow: {avg_execution_time}ms" + assert min_execution_time < 2000, f"Minimum execution time too slow: {min_execution_time}ms" + + # Validate all responses were successful and consistent + expected_message = "Hello World! This is local Run!" + for i, lambda_response in enumerate(responses): + assert lambda_response['statusCode'] == 200, f"Response {i+1} failed" + body_data = json.loads(lambda_response['body']) + assert body_data['message'] == expected_message, f"Response {i+1} message inconsistent" + + # Check for performance improvement in subsequent calls (warm starts) + if len(execution_times) >= 3: + cold_start = execution_times[0] + warm_start_avg = sum(execution_times[1:]) / len(execution_times[1:]) + performance_improvement = cold_start > warm_start_avg + + print(f"Performance metrics:") + print(f" Cold start: {cold_start}ms") + print(f" Warm start average: {int(warm_start_avg)}ms") + print(f" Performance improvement: {performance_improvement}") + + print(f"Performance test completed: avg={int(avg_execution_time)}ms, min={min_execution_time}ms, max={max_execution_time}ms") + + +def test_lambda_concurrent_invocations(lambda_client, health_check): + """ + Test concurrent Lambda invocations to validate thread safety. + """ + import threading + import queue + + test_event = { + "test_type": "concurrent", + "timestamp": datetime.now().isoformat() + } + + results = queue.Queue() + num_threads = 5 + + def invoke_lambda(thread_id): + """Helper function for concurrent Lambda invocations""" + try: + start_time = time.time() + + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps({**test_event, "thread_id": thread_id}) + ) + + end_time = time.time() + execution_time = int((end_time - start_time) * 1000) + + # Parse response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + body_data = json.loads(lambda_response['body']) + + results.put({ + 'thread_id': thread_id, + 'success': response['StatusCode'] == 200 and lambda_response['statusCode'] == 200, + 'execution_time': execution_time, + 'message': body_data.get('message'), + 'lambda_status': response['StatusCode'] + }) + + except Exception as e: + results.put({ + 'thread_id': thread_id, + 'success': False, + 'error': str(e), + 'execution_time': 0 + }) + + # Start concurrent threads + threads = [] + for i in range(num_threads): + thread = threading.Thread(target=invoke_lambda, args=(i,)) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join(timeout=30) + + # Analyze results + successful_invocations = 0 + total_execution_time = 0 + expected_message = "Hello World! This is local Run!" + + while not results.empty(): + result = results.get() + if result['success']: + successful_invocations += 1 + total_execution_time += result['execution_time'] + + # Validate message consistency + assert result['message'] == expected_message, \ + f"Thread {result['thread_id']} returned inconsistent message: {result['message']}" + else: + print(f"Thread {result['thread_id']} failed: {result.get('error', 'Unknown error')}") + + success_rate = successful_invocations / num_threads * 100 + avg_execution_time = total_execution_time / successful_invocations if successful_invocations > 0 else 0 + + # Validate concurrent performance + assert success_rate >= 90, f"Concurrent execution success rate too low: {success_rate}%" + assert successful_invocations >= num_threads - 1, f"Too many failed concurrent invocations" + + print(f"Concurrent invocations test passed") + print(f"Results: Success_Rate={success_rate}%, Avg_Execution_Time={int(avg_execution_time)}ms, Successful={successful_invocations}/{num_threads}") + + +def test_lambda_memory_and_resource_usage(lambda_client, health_check): + """ + Test Lambda function resource usage and validate efficient execution. + """ + test_event = { + "test_type": "resource_usage", + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps(test_event) + ) + + # Validate basic response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate successful execution + assert lambda_response['statusCode'] == 200, "Lambda function should execute successfully" + + body_data = json.loads(lambda_response['body']) + expected_message = "Hello World! This is local Run!" + assert body_data['message'] == expected_message, "Message should be consistent" + + # Check response metadata for resource information + response_metadata = response.get('ResponseMetadata', {}) + + # Log any available metadata + if response_metadata: + print(f"Response metadata available: {list(response_metadata.keys())}") + + # For a simple Hello World function, the response should be small and efficient + payload_size = len(payload) + assert payload_size < 1000, f"Response payload too large for Hello World: {payload_size} bytes" + + # Validate response structure efficiency + assert len(lambda_response) >= 2, "Response should have at least statusCode and body" + assert len(lambda_response) <= 5, "Response should not have excessive fields for Hello World" + + print(f"Resource usage test passed - payload size: {payload_size} bytes, response efficiency validated") + + +def test_lambda_input_edge_cases(lambda_client, health_check): + """ + Test Lambda function with various edge case inputs to ensure robustness. + """ + edge_case_events = [ + # Very large event + {"large_data": "x" * 10000, "test": "large_input"}, + + # Event with deeply nested structure + {"level1": {"level2": {"level3": {"level4": {"message": "deep_nested"}}}}}, + + # Event with array data + {"array_field": [1, 2, 3, "string", True, None], "numbers": list(range(100))}, + + # Event with special data types + {"timestamp": datetime.now().isoformat(), "boolean": False, "null_field": None}, + + # Minimal event + {"minimal": True} + ] + + expected_message = "Hello World! This is local Run!" + + for i, test_event in enumerate(edge_case_events): + # Add scenario identifier + test_event["edge_case_id"] = i + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps(test_event) + ) + + # Validate that Lambda handles all edge cases gracefully + assert response['StatusCode'] == 200, f"Edge case {i} failed with status: {response['StatusCode']}" + + # Parse and validate response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + assert lambda_response['statusCode'] == 200, f"Edge case {i}: Lambda should handle gracefully" + + body_data = json.loads(lambda_response['body']) + assert body_data['message'] == expected_message, f"Edge case {i}: Message should be consistent" + + print(f"Edge cases test passed - {len(edge_case_events)} scenarios handled gracefully") + + +def test_lambda_json_serialization(lambda_client, health_check): + """ + Test that Lambda function properly handles JSON serialization and deserialization. + """ + # Test with various JSON-serializable data types + test_event = { + "string_field": "test string", + "integer_field": 42, + "float_field": 3.14159, + "boolean_field": True, + "null_field": None, + "array_field": ["item1", "item2", 3, True], + "object_field": { + "nested_string": "nested value", + "nested_number": 100 + } + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaHelloWorld', + Payload=json.dumps(test_event) + ) + + # Validate basic response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse and validate JSON response + payload = response['Payload'].read().decode('utf-8') + + # Ensure the payload is valid JSON + try: + lambda_response = json.loads(payload) + except json.JSONDecodeError: + pytest.fail(f"Lambda response is not valid JSON: {payload}") + + # Validate response structure + assert isinstance(lambda_response, dict), "Lambda response should be a dictionary" + assert 'statusCode' in lambda_response, "Response should contain statusCode" + assert 'body' in lambda_response, "Response should contain body" + + # Validate body is valid JSON string + try: + body_data = json.loads(lambda_response['body']) + assert isinstance(body_data, dict), "Response body should contain a dictionary" + assert 'message' in body_data, "Body should contain message field" + except json.JSONDecodeError: + pytest.fail("Lambda response body is not valid JSON") + + print("JSON serialization test passed - proper JSON handling validated") \ No newline at end of file diff --git a/python-test-samples/lambda-sam-layers/README.md b/python-test-samples/lambda-sam-layers/README.md new file mode 100644 index 00000000..e5ee6ed7 --- /dev/null +++ b/python-test-samples/lambda-sam-layers/README.md @@ -0,0 +1,438 @@ +[![python: 3.9](https://img.shields.io/badge/Python-3.9-green)](https://img.shields.io/badge/Python-3.9-green) +[![AWS: Lambda](https://img.shields.io/badge/AWS-Lambda-orange)](https://img.shields.io/badge/AWS-Lambda-orange) +[![AWS: Lambda Layers](https://img.shields.io/badge/AWS-Lambda%20Layers-yellow)](https://img.shields.io/badge/AWS-Lambda%20Layers-yellow) +[![test: pytest](https://img.shields.io/badge/Test-Pytest-red)](https://img.shields.io/badge/Test-Pytest-red) +[![test: local](https://img.shields.io/badge/Test-Local-red)](https://img.shields.io/badge/Test-Local-red) + +# Local Testing: AWS Lambda with Custom Layers and PyTest + +## Introduction + +This project demonstrates how to test AWS Lambda functions with custom layers locally using SAM CLI and PyTest. It showcases the complete process of building, deploying, and testing Lambda layers without requiring actual AWS infrastructure, including automated test execution and validation of layer functionality. + +--- + +## Contents + +- [Local Testing: AWS Lambda with Custom Layers and PyTest](#local-testing-aws-lambda-with-custom-layers-and-pytest) + - [Introduction](#introduction) + - [Contents](#contents) + - [Architecture Overview](#architecture-overview) + - [Project Structure](#project-structure) + - [Prerequisites](#prerequisites) + - [Test Scenarios](#test-scenarios) + - [About the Test Process](#about-the-test-process) + - [Testing Workflows](#testing-workflows) + - [Common Issues](#common-issues) + - [Additional Resources](#additional-resources) + +--- + +## Architecture Overview + +

+ AWS Lambda with Custom Layers +

+ +Components: + +- Python Lambda function with external API integration +- Custom Lambda layer containing third-party dependencies (requests library) +- SAM CLI for local building, layer management, and execution +- PyTest framework for automated testing and validation +- Test events for various layer functionality scenarios + +--- + +## Project Structure + +``` +├── custom-lambda-layer/ _# folder containing python dependencies for the layer_ +│ └── requirements.txt _# layer dependencies specification_ +├── events/ _# folder containing json files for Lambda Layers input events_ +│ ├── lambda-layers-event.json _# basic layer functionality event_ +│ └── lambda-layers-api-event.json _# API integration test event_ +├── img/lambda-sam-layers.png _# Architecture diagram_ +├── lambda_layers_src/ _# folder containing Lambda function source code_ +│ └── app.py _# main Lambda handler function_ +├── tests/ +│ ├── unit/src/test_lambda_layers_local.py _# python PyTest test definition_ +│ ├── requirements.txt _# pytest pip requirements dependencies file_ +│ └── template.yaml _# sam yaml template file for Lambda function and layer_ +└── README.md _# instructions file_ +``` + +--- + +## Prerequisites + +- AWS SAM CLI +- Docker +- Python 3.9 or newer +- pip package manager +- zip utilities +- AWS CLI v2 (for debugging) +- Basic understanding of AWS Lambda +- Basic understanding of Lambda Layers +- Basic understanding of PyTest framework + +--- + +## Test Scenarios + +### 1. Layer Dependency Loading + +- Tests that the Lambda function can successfully import dependencies from the custom layer +- Validates that the requests library is available and functional +- Verifies the correct version of dependencies is loaded from the layer +- Used to validate basic layer functionality and dependency resolution + +### 2. External API Integration + +- Tests the Lambda function's ability to make HTTP requests using the layer's requests library +- Validates successful API calls to external services (GitHub API endpoints) +- Verifies proper error handling when external services are unavailable +- Ensures layer dependencies work correctly in real-world scenarios + +### 3. Layer Version Compatibility + +- Tests that the layer dependencies are compatible with the Lambda runtime +- Validates that no version conflicts exist between layer and runtime +- Verifies that all required dependencies are properly packaged in the layer + +### 4. Performance with Layers + +- Tests the Lambda function's performance when using layers +- Validates that layer loading doesn't significantly impact cold start times +- Measures memory usage with layer dependencies loaded + +--- + +## About the Test Process + +The test process leverages PyTest fixtures and SAM CLI to manage the complete lifecycle of Lambda layers and functions: + +1. **Layer Building**: The `layer_build` fixture ensures the custom Lambda layer is built with all dependencies before testing begins. + +2. **SAM Local Setup**: The `lambda_container` fixture verifies that SAM Local Lambda emulator is available and running with layer support enabled. + +3. **Lambda Client Creation**: The `lambda_client` fixture creates a Boto3 Lambda client configured to connect to the local SAM emulator endpoint with layer-enabled functions. + +4. **Test Execution**: Each test: + - Builds the layer with required dependencies + - Invokes the Lambda function using the local client + - Validates layer dependency availability + - Tests specific functionality enabled by the layer + +5. **Validation**: Tests verify that: + - Layer dependencies are correctly loaded and accessible + - External library functionality works as expected + - API calls using layer dependencies execute successfully + - Response format and content match expectations + - No import errors or dependency conflicts occur + +6. **Cleanup**: After tests complete, built artifacts and containers are cleaned up. + +--- + +## Testing Workflows + +### Setup Docker Environment + +> Make sure Docker engine is running before running the tests. + +```shell +lambda-sam-layers$ docker version +Client: Docker Engine - Community + Version: 24.0.6 + API version: 1.43 +(...) +``` + +### Build Lambda Layer + +> Build the custom layer with dependencies: + +```shell +lambda-sam-layers$ +cd tests +sam build LambdaLayersLayer \ + --use-container \ + --build-image amazon/aws-sam-cli-build-image-python3.9 +``` + +Expected output: + +``` +Starting Build inside a container +Building layer 'LambdaLayersLayer' +(...) +Build Succeeded + +Running PythonPipBuilder:ResolveDependencies +Running PythonPipBuilder:CopySource +``` + +### Run the Unit Test - End to end python test + +> Start the SAM Local Lambda emulator in a separate terminal: + +```shell +lambda-sam-layers/tests$ +sam local start-lambda -p 3001 & +``` + +> Set up the python environment: + +```shell +lambda-sam-layers/tests$ +export AWS_ACCESS_KEY_ID='DUMMYIDEXAMPLE' +export AWS_SECRET_ACCESS_KEY='DUMMYEXAMPLEKEY' +export AWS_REGION='us-east-1' +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt +``` + +#### Run the Unit Tests + +```shell +lambda-sam-layers/tests$ +python3 -m pytest -s unit/src/test_lambda_layers_local.py +``` + +Expected output: + +``` +lambda-sam-layers/tests$ +python3 -m pytest -s unit/src/test_lambda_layers_local.py +================================================================= test session starts ================================================================= +platform linux -- Python 3.10.12, pytest-8.3.5, pluggy-1.6.0 +benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) +rootdir: /home/ubuntu/environment/serverless-test-samples_lambda_pytest_try1/python-test-samples/lambda-sam-layers/tests +plugins: Faker-24.4.0, parallel-0.1.1, allure-pytest-2.13.5, profiling-1.7.0, subprocess-1.5.0, metadata-3.1.1, benchmark-4.0.0, html-4.1.1, mock-3.12.0, pytest_httpserver-1.0.10, anyio-4.9.0, xdist-3.5.0, timeout-2.3.1, cov-5.0.0 +collected 7 items + +unit/src/test_lambda_layers_local.py SAM Local Lambda emulator is running on port 3001 +Building Lambda layer with SAM... +Lambda layer built successfully +Lambda function with layers is responding correctly +Layer dependency loading test passed - requests library is working +Lambda response: {'StatusCode': 200, 'GitHub_API_Success': True, 'Response_Length': 2396} +.External API integration test passed +Lambda response: {'StatusCode': 200, 'GitHub_Endpoints': 33, 'Execution_Time_ms': 834} +.Layer version compatibility test passed +Lambda response: {'StatusCode': 200, 'Requests_Working': True, 'Python_Version': '3.10.12', 'Layer_Compatible': True} +.Performance analysis: + Cold start: 852ms + Warm start average: 854ms + Performance improvement: False +Performance with layers test passed +Lambda response: {'StatusCode': 200, 'Avg_Execution_Time_ms': 853, 'Min_Time_ms': 848, 'Max_Time_ms': 861} +.Layer isolation and dependencies test passed +Lambda response: {'StatusCode': 200, 'Dependencies_Working': True, 'GitHub_API_Success': True} +.Error handling test 1: StatusCode=200, Body_Length=2396 +Error handling test 2: StatusCode=200, Body_Length=2396 +Error handling with layers test passed - Lambda handles scenarios gracefully +.Concurrent layer usage test passed +Results: Success_Rate=100.0%, Avg_Execution_Time=1976ms, Successful_Invocations=3/3 +. + +================================================================= 7 passed in 32.28s ================================================================= +``` + +#### Clean up section + +> clean pyenv environment + +```sh +lambda-sam-layers/tests$ +deactivate +rm -rf venv/ +``` + +> unsetting variables + +```sh +unset AWS_ACCESS_KEY_ID +unset AWS_SECRET_ACCESS_KEY +unset AWS_REGION +``` + +> cleaning sam process + +```sh +ps -axuf | grep '[s]am local start-lambda' | awk '{print $2}' | xargs -r kill +``` + +> cleaning build artifacts + +```sh +lambda-sam-layers/tests$ +rm -rf .aws-sam/ +``` + +#### Debug - PyTest Debugging + +For more detailed debugging in pytest: + +```sh +# Run with verbose output +python3 -m pytest -s -v unit/src/test_lambda_layers_local.py + +# Run with debug logging +python3 -m pytest -s -v unit/src/test_lambda_layers_local.py --log-cli-level=DEBUG + +# Run a specific pytest test +python3 -m pytest -s -v unit/src/test_lambda_layers_local.py::test_layer_dependency_loading +``` + +--- + +### Fast local development for Lambda Layers + +#### AWS CLI Commands for Manual Verification + +If you need to manually verify the Lambda function with layers, you can use these commands: + +#### Configure environment variables + +```sh +lambda-sam-layers$ +export AWS_ACCESS_KEY_ID='DUMMYIDEXAMPLE' +export AWS_SECRET_ACCESS_KEY='DUMMYEXAMPLEKEY' +export AWS_REGION='us-east-1' +``` + +#### Build and Start Lambda emulator + +```sh +# Build the layer +lambda-sam-layers$ +cd tests +sam build LambdaLayersLayer \ + --use-container \ + --build-image amazon/aws-sam-cli-build-image-python3.9 + +# Start Lambda emulator +lambda-sam-layers/tests$ +sam local start-lambda -p 3001 & +``` + +#### Debug lambda functions - Manual Lambda Testing with Layers + +```sh +# Test Layer Dependency Loading +lambda-sam-layers/tests$ +aws lambda invoke \ + --function-name LambdaLayersFunction \ + --endpoint-url http://127.0.0.1:3001 \ + --payload fileb://../events/lambda-layers-event.json \ + output.txt +cat output.txt + +# Test API Integration +lambda-sam-layers/tests$ +aws lambda invoke \ + --function-name LambdaLayersFunction \ + --endpoint-url http://127.0.0.1:3001 \ + --payload fileb://../events/lambda-layers-event.json \ + output.txt +cat output.txt +``` + +#### Direct SAM Local Invoke with Layers + +```sh +# Basic layer functionality invocation +lambda-sam-layers/tests$ +sam local invoke LambdaLayersFunction \ + --event ../events/lambda-layers-event.json + +# API integration test +lambda-sam-layers/tests$ +sam local invoke LambdaLayersFunction \ + --event ../events/lambda-layers-api-event.json + +# Debug mode with container logs +lambda-sam-layers/tests$ +sam local invoke LambdaLayersFunction \ + --event ../events/lambda-layers-event.json \ + --debug +``` + +#### Layer Build Verification + +```sh +# Check layer contents +lambda-sam-layers/tests$ +unzip -l .aws-sam/build/LambdaLayersLayer/python.zip | head -20 + +# Verify layer structure +tree .aws-sam/build/LambdaLayersLayer/ +``` + +--- + +## Common Issues + +### Layer Build Failures + +If the layer build fails: + +- Ensure Docker is running and accessible +- Verify the requirements.txt file exists in the custom-lambda-layer directory +- Check that the build image matches your Python runtime version +- Review build logs for specific dependency resolution errors +- Try building with `--debug` flag for detailed output + +### Layer Dependency Import Errors + +If the Lambda function can't import layer dependencies: + +- Verify the layer is correctly referenced in template.yaml +- Check that the layer structure follows AWS Lambda requirements (python/lib/python3.9/site-packages/) +- Ensure compatibility between dependency versions and Python runtime +- Validate that all required dependencies are included in requirements.txt + +### SAM Local Layer Loading Issues + +If SAM Local fails to load layers: + +- Ensure the layer is built before starting the emulator +- Check that the layer ARN is correctly configured in the function definition +- Verify the layer compatibility with the function's runtime +- Try rebuilding the layer with `--use-container` flag + +### Performance Issues with Layers + +If Lambda functions with layers are slow: + +- Check the layer size - large layers increase cold start times +- Consider splitting large layers into smaller, more focused layers +- Monitor memory usage - layers consume Lambda memory allocation +- Optimize dependencies by removing unused packages + +### Docker Container Issues for Layer Building + +If Docker containers fail during layer building: + +- Ensure sufficient disk space for container images and build artifacts +- Check Docker daemon permissions and configuration +- Verify the build image is available and compatible +- Try pulling the build image manually: `docker pull amazon/aws-sam-cli-build-image-python3.9` + +--- + +## Additional Resources + +- [SAM CLI Layer Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-layers.html) +- [AWS Lambda Layers Guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) +- [SAM Local Testing Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html) +- [PyTest Documentation](https://docs.pytest.org/) +- [AWS Lambda Python Runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html) +- [Lambda Layer Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html) +- [SAM Template Layer Specification](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-layerversion.html) + +[Top](#contents) diff --git a/python-test-samples/lambda-sam-layers/custom-lambda-layer/requirements.txt b/python-test-samples/lambda-sam-layers/custom-lambda-layer/requirements.txt new file mode 100755 index 00000000..2c24336e --- /dev/null +++ b/python-test-samples/lambda-sam-layers/custom-lambda-layer/requirements.txt @@ -0,0 +1 @@ +requests==2.31.0 diff --git a/python-test-samples/lambda-sam-layers/events/lambda-layers-event.json b/python-test-samples/lambda-sam-layers/events/lambda-layers-event.json new file mode 100755 index 00000000..4ccb6ff4 --- /dev/null +++ b/python-test-samples/lambda-sam-layers/events/lambda-layers-event.json @@ -0,0 +1,5 @@ +{ + "key1": "value1", + "key2": "value2", + "key3": "value3" +} \ No newline at end of file diff --git a/python-test-samples/lambda-sam-layers/img/lambda-sam-layers.png b/python-test-samples/lambda-sam-layers/img/lambda-sam-layers.png new file mode 100644 index 0000000000000000000000000000000000000000..05b57993a8e1f88b4be3226c480c5ea6eb5e7a7b GIT binary patch literal 68083 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 0, "GitHub API response should not be empty" + + # Check if the response contains typical GitHub API content + github_data_indicators = ['github', 'api', 'current_user_url', 'authorizations_url'] + response_lower = github_response.lower() + found_indicators = [indicator for indicator in github_data_indicators if indicator in response_lower] + + assert len(found_indicators) >= 2, f"Response doesn't seem to be from GitHub API: {github_response[:200]}..." + + print(f"Layer dependency loading test passed - requests library is working") + print(f"Lambda response: {{'StatusCode': {response['StatusCode']}, 'GitHub_API_Success': True, 'Response_Length': {len(github_response)}}}") + + +def test_external_api_integration(lambda_client, health_check): + """ + Test the Lambda function's ability to make HTTP requests using the layer's requests library. + Validates successful API calls to external services (GitHub API). + """ + # Test event for API integration + test_event = { + "test_type": "api_integration", + "target_api": "github", + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + start_time = time.time() + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps(test_event) + ) + end_time = time.time() + + execution_time = int((end_time - start_time) * 1000) + + # Validate Lambda invoke response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse Lambda response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate successful GitHub API call + assert lambda_response['statusCode'] == 200, f"GitHub API call failed with status: {lambda_response['statusCode']}" + + # Parse GitHub API response to extract meaningful data + github_response = lambda_response['body'] + + try: + # Try to parse the GitHub API response as JSON + github_data = json.loads(github_response) + + # Count available endpoints in GitHub API response + endpoints_count = 0 + for key, value in github_data.items(): + if key.endswith('_url') and isinstance(value, str): + endpoints_count += 1 + + assert endpoints_count > 10, f"Expected multiple GitHub API endpoints, found {endpoints_count}" + + print(f"External API integration test passed") + print(f"Lambda response: {{'StatusCode': {response['StatusCode']}, 'GitHub_Endpoints': {endpoints_count}, 'Execution_Time_ms': {execution_time}}}") + + except json.JSONDecodeError: + # If the response is not JSON, validate it's still a valid response + assert len(github_response) > 100, "GitHub API response seems too short" + assert 'api.github.com' in github_response.lower() or 'github' in github_response.lower(), \ + "Response doesn't appear to be from GitHub API" + + print(f"External API integration test passed (non-JSON response)") + print(f"Lambda response: {{'StatusCode': {response['StatusCode']}, 'Response_Length': {len(github_response)}, 'Execution_Time_ms': {execution_time}}}") + + +def test_layer_version_compatibility(lambda_client, health_check): + """ + Test that the layer dependencies are compatible with the Lambda runtime. + Validates that no version conflicts exist between layer and runtime. + """ + # Test event for version compatibility + test_event = { + "test_type": "version_compatibility", + "check_versions": True, + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps(test_event) + ) + + # Validate Lambda invoke response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse Lambda response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate that the Lambda function executed successfully + assert lambda_response['statusCode'] == 200, f"Lambda function failed with status: {lambda_response['statusCode']}" + + # The fact that the function executed successfully means the layer is compatible + # We can verify this by checking that the GitHub API call succeeded + github_response = lambda_response['body'] + assert len(github_response) > 0, "Empty response suggests layer compatibility issues" + + # Additional validation: check that we can extract version information from logs if available + # Note: The current Lambda code prints the requests version, but we can't easily capture that in tests + # So we validate functionality instead + + # Validate that the requests library from the layer is working correctly + try: + # If response is JSON, it means requests worked properly + github_data = json.loads(github_response) + requests_working = True + version_compatible = True + except json.JSONDecodeError: + # Even if not JSON, if we got a response, requests is working + requests_working = len(github_response) > 0 + version_compatible = True + + assert requests_working, "Requests library from layer is not functioning properly" + assert version_compatible, "Layer dependencies appear to have compatibility issues" + + # Extract Python version info if possible (this would be from the Lambda runtime) + python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + + print(f"Layer version compatibility test passed") + print(f"Lambda response: {{'StatusCode': {response['StatusCode']}, 'Requests_Working': {requests_working}, 'Python_Version': '{python_version}', 'Layer_Compatible': {version_compatible}}}") + + +def test_performance_with_layers(lambda_client, health_check): + """ + Test the Lambda function's performance when using layers. + Validates that layer loading doesn't significantly impact cold start times. + """ + # Test event for performance testing + test_event = { + "test_type": "performance", + "measure_execution": True, + "timestamp": datetime.now().isoformat() + } + + # Multiple invocations to test both cold and warm starts + execution_times = [] + responses = [] + + for i in range(3): + start_time = time.time() + + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps(test_event) + ) + + end_time = time.time() + execution_time = int((end_time - start_time) * 1000) + execution_times.append(execution_time) + + # Validate each response + assert response['StatusCode'] == 200, f"Lambda invocation {i+1} failed with status: {response['StatusCode']}" + + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + responses.append(lambda_response) + + # Small delay between invocations + if i < 2: + time.sleep(0.5) + + # Analyze performance metrics + avg_execution_time = sum(execution_times) / len(execution_times) + min_execution_time = min(execution_times) + max_execution_time = max(execution_times) + + # Validate performance is reasonable (cold start might be slower) + assert avg_execution_time < 10000, f"Average execution time too slow: {avg_execution_time}ms" + assert min_execution_time < 5000, f"Fastest execution time too slow: {min_execution_time}ms" + + # Validate all responses were successful + for i, lambda_response in enumerate(responses): + assert lambda_response['statusCode'] == 200, f"Response {i+1} failed with status: {lambda_response['statusCode']}" + assert len(lambda_response['body']) > 0, f"Response {i+1} had empty body" + + # Check if performance improved with warm starts (second and third calls should be faster) + if len(execution_times) >= 3: + warm_start_avg = sum(execution_times[1:]) / len(execution_times[1:]) + performance_improvement = execution_times[0] > warm_start_avg + + print(f"Performance analysis:") + print(f" Cold start: {execution_times[0]}ms") + print(f" Warm start average: {int(warm_start_avg)}ms") + print(f" Performance improvement: {performance_improvement}") + + print(f"Performance with layers test passed") + print(f"Lambda response: {{'StatusCode': 200, 'Avg_Execution_Time_ms': {int(avg_execution_time)}, 'Min_Time_ms': {min_execution_time}, 'Max_Time_ms': {max_execution_time}}}") + + +def test_layer_isolation_and_dependencies(lambda_client, health_check): + """ + Test that the layer provides proper isolation and all required dependencies. + Validates that the layer contains only the expected dependencies. + """ + # Test event + test_event = { + "test_type": "isolation", + "validate_dependencies": True, + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps(test_event) + ) + + # Validate Lambda invoke response + assert response['StatusCode'] == 200, f"Lambda invocation failed with status: {response['StatusCode']}" + + # Parse Lambda response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Validate successful execution + assert lambda_response['statusCode'] == 200, f"Lambda execution failed with status: {lambda_response['statusCode']}" + + # Validate that requests library is working (this is our main layer dependency) + github_response = lambda_response['body'] + + # Test that the requests library is available and working + assert len(github_response) > 0, "Empty response suggests requests library issues" + + # Validate that the API call was successful (which requires requests) + if lambda_response['statusCode'] == 200: + # Check if we can identify this as a GitHub API response + github_indicators = ['api', 'github', 'url', 'current_user'] + response_text = github_response.lower() + found_indicators = sum(1 for indicator in github_indicators if indicator in response_text) + + assert found_indicators >= 2, "Response doesn't appear to be from GitHub API, suggesting layer issues" + + print(f"Layer isolation and dependencies test passed") + print(f"Lambda response: {{'StatusCode': {response['StatusCode']}, 'Dependencies_Working': True, 'GitHub_API_Success': True}}") + + +def test_error_handling_with_layers(lambda_client, health_check): + """ + Test error handling scenarios when using layers. + Validates graceful handling of network issues and layer-related errors. + """ + # Test event that might cause different behaviors + test_event = { + "test_type": "error_handling", + "simulate_scenarios": True, + "timestamp": datetime.now().isoformat() + } + + # Invoke Lambda function multiple times to test consistency + for i in range(2): + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps(test_event) + ) + + # Validate Lambda invoke response + assert response['StatusCode'] == 200, f"Lambda invocation {i+1} failed with status: {response['StatusCode']}" + + # Parse Lambda response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + # Even if the GitHub API call fails, the Lambda should handle it gracefully + # The status code should be present in the response + assert 'statusCode' in lambda_response, f"Response {i+1} missing statusCode" + assert 'body' in lambda_response, f"Response {i+1} missing body" + + # Log the response for debugging + print(f"Error handling test {i+1}: StatusCode={lambda_response.get('statusCode')}, Body_Length={len(str(lambda_response.get('body', '')))}") + + print("Error handling with layers test passed - Lambda handles scenarios gracefully") + + +def test_concurrent_layer_usage(lambda_client, health_check): + """ + Test concurrent usage of Lambda functions with layers. + Validates that layers work correctly under concurrent load. + """ + import threading + import queue + + # Test event + test_event = { + "test_type": "concurrent", + "thread_test": True, + "timestamp": datetime.now().isoformat() + } + + results = queue.Queue() + num_threads = 3 + + def invoke_lambda(thread_id): + """Helper function for concurrent Lambda invocations""" + try: + start_time = time.time() + + response = lambda_client.invoke( + FunctionName='LambdaLayersFunction', + Payload=json.dumps({**test_event, "thread_id": thread_id}) + ) + + end_time = time.time() + execution_time = int((end_time - start_time) * 1000) + + # Parse response + payload = response['Payload'].read().decode('utf-8') + lambda_response = json.loads(payload) + + results.put({ + 'thread_id': thread_id, + 'success': response['StatusCode'] == 200 and lambda_response.get('statusCode') == 200, + 'execution_time': execution_time, + 'lambda_status': response['StatusCode'], + 'api_status': lambda_response.get('statusCode') + }) + + except Exception as e: + results.put({ + 'thread_id': thread_id, + 'success': False, + 'error': str(e), + 'execution_time': 0 + }) + + # Start concurrent threads + threads = [] + for i in range(num_threads): + thread = threading.Thread(target=invoke_lambda, args=(i,)) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join(timeout=30) + + # Analyze results + successful_invocations = 0 + total_execution_time = 0 + + while not results.empty(): + result = results.get() + if result['success']: + successful_invocations += 1 + total_execution_time += result['execution_time'] + else: + print(f"Thread {result['thread_id']} failed: {result.get('error', 'Unknown error')}") + + success_rate = successful_invocations / num_threads * 100 + avg_execution_time = total_execution_time / successful_invocations if successful_invocations > 0 else 0 + + # Validate concurrent performance + assert success_rate >= 80, f"Concurrent execution success rate too low: {success_rate}%" + assert avg_execution_time < 15000, f"Average concurrent execution time too slow: {avg_execution_time}ms" + + print(f"Concurrent layer usage test passed") + print(f"Results: Success_Rate={success_rate}%, Avg_Execution_Time={int(avg_execution_time)}ms, Successful_Invocations={successful_invocations}/{num_threads}") \ No newline at end of file