diff --git a/.github/workflows/dockerized-test.yml b/.github/workflows/dockerized-test.yml new file mode 100644 index 0000000..b507289 --- /dev/null +++ b/.github/workflows/dockerized-test.yml @@ -0,0 +1,41 @@ +name: dockerized-test + +permissions: + contents: read + +on: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + + +jobs: + dockerized-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby_version: ['3.3', '3.4'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + + - name: Build the lib + run: make build + + - name: Build the image + run: docker build . -t local/test -f Dockerfile.test --build-arg BASE_IMAGE=public.ecr.aws/lambda/ruby:${{ matrix.ruby_version }} + + - name: Run tests + uses: aws/containerized-test-runner-for-aws-lambda@main + with: + suiteFileArray: '["./test/dockerized/suites/*.json"]' + dockerImageName: 'local/test' + taskFolder: './test/dockerized/tasks' diff --git a/.gitignore b/.gitignore index f6b393d..ba13ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ test/examples/hello-world-docker/pkg *.iml .DS_Store Gemfile.lock +# useful when using rbenv +.ruby-version +# containerized test runner clone +.test-runner/ diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..87b48de --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,8 @@ +ARG BASE_IMAGE +FROM $BASE_IMAGE +ENV GEM_HOME=/var/runtime +ADD test/dockerized/tasks /var/task +RUN gem uninstall aws_lambda_ric --executables +ADD pkg /tmp/pkg +RUN gem install /tmp/pkg/aws_lambda_ric-*.gem +RUN rm -rf /tmp/pkg \ No newline at end of file diff --git a/Makefile b/Makefile index 3efa2bb..cd58f54 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,36 @@ build: run-local-ric: scripts/run-local-ric.sh +.PHONY: test-dockerized +test-dockerized: + @echo "Running dockerized tests locally..." + @if [ -z "$(RUBY_VERSION)" ]; then \ + echo "Error: RUBY_VERSION is not set. Usage: make test-dockerized RUBY_VERSION=3.3"; \ + exit 1; \ + fi + @echo "Building the lib..." + $(MAKE) build + @echo "Building Docker image for Ruby $(RUBY_VERSION)..." + docker build . -t local/test -f Dockerfile.test --build-arg BASE_IMAGE=public.ecr.aws/lambda/ruby:$(RUBY_VERSION) + @echo "Setting up containerized test runner..." + @if [ ! -d ".test-runner" ]; then \ + echo "Copying local containerized-test-runner-for-aws-lambda..."; \ + cp -r ../containerized-test-runner-for-aws-lambda .test-runner; \ + fi + @echo "Building test runner Docker image..." + @docker build -t test-runner:local -f .test-runner/Dockerfile .test-runner + @echo "Running tests in Docker..." + @docker run --rm \ + --entrypoint suite \ + -e DOCKER_API_VERSION=1.41 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(CURDIR)/test/dockerized/tasks:$(CURDIR)/test/dockerized/tasks:ro \ + -v $(CURDIR)/test/dockerized/suites:/suites:ro \ + test-runner:local \ + --test-image local/test \ + --debug \ + /suites/*.json + .PHONY: pr pr: init test-unit test-smoke @@ -46,6 +76,7 @@ TARGETS test-integ Run Integration tests. test-unit Run Unit Tests. test-smoke Run Sanity/Smoke tests. + test-dockerized Run dockerized tests locally (requires RUBY_VERSION=X.X). run-local-ric Run local RIC changes with Runtime Interface Emulator. pr Perform all checks before submitting a Pull Request. diff --git a/README.md b/README.md index 8a2e4d4..d523104 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,16 @@ Then, * to run integration tests: `make test-integ` * to run smoke tests: `make test-smoke` +### Running Dockerized Harness Tests + +To run the containerized test harness locally, use: + +```shell script +make test-dockerized RUBY_VERSION=3.4 +``` + +This command builds your Lambda function in a Docker container using the specified Ruby version, sets up the containerized test runner, and executes the test suites defined in `test/dockerized/suites/`. + ### Troubleshooting While running integration tests, you might encounter the Docker Hub rate limit error with the following body: ``` diff --git a/test/dockerized/suites/core.json b/test/dockerized/suites/core.json new file mode 100644 index 0000000..20e8149 --- /dev/null +++ b/test/dockerized/suites/core.json @@ -0,0 +1,78 @@ +{ + "tests": [ + { + "name": "test_echo", + "handler": "core.ping", + "request": { + "msg": "message" + }, + "assertions": [ + { + "response": { + "msg": "pong[message]" + } + } + ] + }, + { + "name": "test_string_payload", + "handler": "core.str_ping", + "request": "message", + "assertions": [ + { + "response": { + "msg": "pong[message]" + } + } + ] + }, + { + "name": "test_module_echo", + "handler": "core.HandlerClass.ping", + "request": { + "msg": "MyMessage" + }, + "assertions": [ + { + "response": "Module Message: 'MyMessage'" + } + ] + }, + { + "name": "test_deep_module_echo", + "handler": "core.DeepModule::Handler.ping", + "request": { + "msg": "MyMessage" + }, + "assertions": [ + { + "response": "Deep Module Message: 'MyMessage'" + } + ] + }, + { + "name": "test_error", + "handler": "core.broken", + "request": { + "msg": "message" + }, + "assertions": [ + { + "errorType": "Function" + } + ] + }, + { + "name": "test_string", + "handler": "core.string", + "request": { + "msg": "MyMessage" + }, + "assertions": [ + { + "response": "Message: 'MyMessage'" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/dockerized/suites/ctx.json.disabled b/test/dockerized/suites/ctx.json.disabled new file mode 100644 index 0000000..86b9099 --- /dev/null +++ b/test/dockerized/suites/ctx.json.disabled @@ -0,0 +1,49 @@ +{ + "tests": [ + { + "name": "test_ctx_cognito_pool_id", + "handler": "ctx.get_cognito_pool_id", + "cognitoIdentity": { + "cognitoIdentityId": "4ab95ea510c14353a7f6da04489c43b8", + "cognitoIdentityPoolId": "35ab4794a79a4f23947d3e851d3d6578" + }, + "request": {}, + "assertions": [ + { + "response": { + "cognito_pool_id": "35ab4794a79a4f23947d3e851d3d6578" + } + } + ] + }, + { + "name": "test_ctx_cognito_identity_id", + "handler": "ctx.get_cognito_identity_id", + "cognitoIdentity": { + "cognitoIdentityId": "4ab95ea510c14353a7f6da04489c43b8", + "cognitoIdentityPoolId": "35ab4794a79a4f23947d3e851d3d6578" + }, + "request": {}, + "assertions": [ + { + "response": { + "cognito_identity_id": "4ab95ea510c14353a7f6da04489c43b8" + } + } + ] + }, + { + "name": "get_remaining_time_in_millis | elapsedTime", + "handler": "ctx.get_remaining_time_from_context", + "request": { + "sleepTimeSeconds": 0.1 + }, + "assertions": [ + { + "transform": ".elapsedTime >= 100", + "response": true + } + ] + } + ] +} \ No newline at end of file diff --git a/test/dockerized/tasks/core.rb b/test/dockerized/tasks/core.rb new file mode 100644 index 0000000..85e53e8 --- /dev/null +++ b/test/dockerized/tasks/core.rb @@ -0,0 +1,55 @@ +# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def ping(event:, context:) + resp = {} + if event.nil? + resp[:event_nil] = true + else + resp[:msg] = "pong[#{event["msg"]}]" + end + puts "Hello, loggers!" + resp + end + + def str_ping(event:, context:) + { msg: "pong[#{event}]" } + end + + def broken(_) + raise ArgumentError.new("My error message.") + end + + def string(event:, context:) + "Message: '#{event["msg"]}'" + end + + def curl(event:,context:) + resp = Net::HTTP.get(URI(event["url"])) + if resp.size > 0 + { success: true } + else + raise "Empty response!" + end + end + + def io(_) + StringIO.new("This is IO!") + end + + def execution_env(_) + { "AWS_EXECUTION_ENV" => ENV["AWS_EXECUTION_ENV"] } + end + + class HandlerClass + def self.ping(event:,context:) + "Module Message: '#{event["msg"]}'" + end + end + + module DeepModule + class Handler + def self.ping(event:,context:) + "Deep Module Message: '#{event["msg"]}'" + end + end + end \ No newline at end of file diff --git a/test/dockerized/tasks/ctx.rb b/test/dockerized/tasks/ctx.rb new file mode 100644 index 0000000..45cd2ff --- /dev/null +++ b/test/dockerized/tasks/ctx.rb @@ -0,0 +1,32 @@ +# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def get_context(event:,context:) + { + function_name: context.function_name, + deadline_ns: context.deadline_ns, + aws_request_id: context.aws_request_id, + invoked_function_arn: context.invoked_function_arn, + log_group_name: context.log_group_name, + log_stream_name: context.log_stream_name, + memory_limit_in_mb: context.memory_limit_in_mb, + function_version: context.function_version + } + end + + def get_cognito_pool_id(event:,context:) + { cognito_pool_id: context.identity&.dig("cognitoIdentityPoolId")} + end + + def get_cognito_identity_id(event:,context:) + { cognito_identity_id: context.identity&.dig("cognitoIdentityId") } + end + + def echo_context(event:,context:) + context.client_context + end + + def get_remaining_time_from_context(event:, context:) + before = context.get_remaining_time_in_millis() + sleep(event['sleepTimeSeconds']) + return { elapsedTime: before - context.get_remaining_time_in_millis() } + end \ No newline at end of file