Skip to content

Commit

Permalink
Merge pull request #334 from dastra/directory-structure-reorg
Browse files Browse the repository at this point in the history
Validating whether metadata.json exists for each pattern, and reorganising pattern directories to support this
  • Loading branch information
brnkrygs authored May 7, 2024
2 parents 5a1af9b + c82e001 commit 6aecbfe
Show file tree
Hide file tree
Showing 133 changed files with 283 additions and 129 deletions.
18 changes: 13 additions & 5 deletions .github/metadata/metadata_json_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "Unit|Integration|Load|Canary|End to end"
"pattern": "Unit|Integration|Load|Canary|End to end|Schema|Contract"
}
},
"diagram": {
Expand Down Expand Up @@ -134,14 +134,22 @@
"pattern_source": {
"$id": "#/properties/pattern_source",
"type": "string",
"title": "Contributor of the the pattern",
"title": "Contributor type of the the pattern",
"examples": [
"AWS"
],
"pattern": "AWS|Customer",
"pattern": "AWS|Customer|Partner",
"maxLength": 60,
"minLength": 3
},
"pattern_source_name": {
"$id": "#/properties/pattern_source_name",
"type": "string",
"title": "Name of Pattern Contributor (Optional)",
"examples": [
"AWS"
]
},
"pattern_detail_tabs": {
"$id": "#/properties/pattern_detail_tabs",
"type": "array",
Expand Down Expand Up @@ -174,7 +182,7 @@
"/src/app.py",
"/tests/unit/local_emulator_test.py"
],
"maxLength": 128,
"maxLength": 1024,
"minLength": 3
}
}
Expand Down Expand Up @@ -220,7 +228,7 @@
"pattern": "^https?://"
},
{
"pattern": "^/[a-z0-9]([a-z0-9-\\.]*[a-z0-9])?(/[a-z0-9]([a-z0-9-\\.]*[a-z0-9])?)*$"
"pattern": "^/[a-z0-9]([a-z0-9-\\.]*[a-z0-9])?(.*)$"
}
]
},
Expand Down
91 changes: 64 additions & 27 deletions .github/metadata/metadata_json_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,19 @@
from os import path
import re
import json
import argparse
from github import Github, Auth
import sys

from aws_lambda_powertools.utilities.validation import validate

import os

def get_list_of_changed_test_sample_directories(changed_files: str) -> set[str]:
"""
From the list of files changed in git, extract unique test sample directories which have a metadata.json file in them.
FOLDERS_TO_IGNORE = ['img']

There are a couple of edge cases. Sometimes a directory doesn't contain a pattern (for instance /java-test-samples/img/)
and sometimes the patterns are nested (for instance dotnet-test-samples/async-architectures/async-lambda-dynamodb)
So as we can't reliably define whether the directory is a test sample directory,
we instead just test whether a metadata.json file has changed.

def get_list_of_changed_test_sample_directories(changed_files: str) -> set[str]:
"""
From the list of files changed in git, extract unique test sample directories
:param: changed_files - Comma separated list of files which have changed
:return: a set of folder names which should contain valid metadata.json files
"""
Expand All @@ -40,11 +37,51 @@ def get_list_of_changed_test_sample_directories(changed_files: str) -> set[str]:
folders = []
for file in file_list:
if re.match(r"^\w+-test-samples/[^/]+/.*", file):
res = re.search(r"(^\w+-test-samples/.*/)metadata.json$", file)
folders.append(res.group(1))
res = re.search(r"(^\w+-test-samples/[^/]+/).*", file)
# folder_name: 'dotnet-test-samples/apigw-lambda'
folder_name = res.group(1)

res = re.search(r"^\w+-test-samples/([^/]+)/.*", file)
# subfolder_name: 'apigw-lambda'
subfolder_name = res.group(1)
if subfolder_name not in FOLDERS_TO_IGNORE:
folders.append(folder_name)

return set(folders)

def github_pull_request_comment(comment: str):
# Fetch the GitHub Automation flag from the GH_AUTOMATION enviroment variable
github_automation = os.environ.get('GITHUB_AUTOMATION')
if github_automation is not None and len(github_automation) > 0:

# Fetch the Github Owner & Repo
github_repository = os.environ.get('GITHUB_REPOSITORY')
if github_repository is None or len(github_repository) == 0:
print("GITHUB_REPOSITORY environment variable not set")
sys.exit(2)

[owner, repo] = github_repository.split('/')

# Fetch the pull request number from the PR_NUMBER enviroment variable
pr_number = os.environ.get('PR_NUMBER')
if pr_number is None or len(pr_number) == 0:
print("PR_NUMBER environment variable not set. Should be the pull request number")
sys.exit(3)
else:
pr_number = int(pr_number)

# Fetch the Github Token from the GITHUB_TOKEN enviroment variable
github_token = os.environ.get('GITHUB_TOKEN')
if github_token is None or len(github_token) == 0:
print("GITHUB_TOKEN environment variable not set")
sys.exit(4)

github = Github(github_token)
repo = github.get_repo(f"{owner}/{repo}")
pr = repo.get_pull(pr_number)
pr.create_issue_comment(comment)
github.close()


def validate_metadata_json(metadata_schema: dict, metadata_json_filename: str) -> bool:
"""
Expand All @@ -54,6 +91,8 @@ def validate_metadata_json(metadata_schema: dict, metadata_json_filename: str) -
:return: Boolean indicating whether file was validated correctly
"""
try:
if not path.isfile(metadata_json_filename):
raise FileNotFoundError(f"{metadata_json_filename} does not exist - please create it")

with open(metadata_json_filename, "r", encoding="utf-8") as metadata_object:
metadata_contents = json.load(metadata_object)
Expand All @@ -66,18 +105,23 @@ def validate_metadata_json(metadata_schema: dict, metadata_json_filename: str) -

for pattern_detail in metadata_contents["pattern_detail_tabs"]:
detail_path = path.dirname(metadata_json_filename) + pattern_detail["filepath"]
if not re.match(r"^http", detail_path):
# Not checking links to external repos
continue
if path.isfile(detail_path) is False:
raise FileNotFoundError("Invalid filepath path: " + pattern_detail["filepath"])

return True
except Exception as exception:
print(str(exception))
print(f"Error in {metadata_json_filename}: {str(exception)}")
github_pull_request_comment(f"Error in {metadata_json_filename}: {str(exception)}")
return False


def validate_test_sample_folders(folders: set) -> bool:
"""
Validate all changed test sample folders
:param: pr_number - the pull request number
:param: gh_automation - the GitHub Automation flag
:param: folders - a set of folder names which should contain valid metadata.json files
:return: Boolean indicating whether all metadata files validated correctly
"""
Expand All @@ -91,29 +135,22 @@ def validate_test_sample_folders(folders: set) -> bool:
for folder in folders:
metadata_filename = path.join(folder, "metadata.json")

print(f"Validating: {metadata_filename}")

if not validate_metadata_json(metadata_schema, metadata_filename):
validated_ok = False

return validated_ok


if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog='metadata_json_validator.py',
description='Validate metadata.json for presenting the code sample on serverlessland.com.'
)

parser.add_argument('changed_files',
type=str,
help="Path to the sub-repo from the project root." + \
"Example: python-test-samples/apigw-lambda'"
)
args = parser.parse_args()

# Fetch the comma separated list of files which have changed from the ALL_CHANGED_FILES environment variable
changed_files = os.environ.get('ALL_CHANGED_FILES')
if changed_files is None or len(changed_files) == 0:
print("ALL_CHANGED_FILES environment variable not set. Should be a comma separated list of files which have changed")
sys.exit(1)

# Fetch a list of all test sample directories which have changed
test_sample_folders = get_list_of_changed_test_sample_directories(args.changed_files)
test_sample_folders = get_list_of_changed_test_sample_directories(changed_files)

# Validate each metadata.json file in turn
if not validate_test_sample_folders(test_sample_folders):
Expand Down
5 changes: 3 additions & 2 deletions .github/metadata/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fastjsonschema
jmespath
aws_lambda_powertools
PyGithub
aws_lambda_powertools
jmespath
17 changes: 12 additions & 5 deletions .github/workflows/metadata-validation.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
name: Metadata Validation

on:
push:
pull_request:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
default:
Expand All @@ -20,7 +22,7 @@ jobs:
id: get_changed
uses: tj-actions/changed-files@v44
with:
files: "*-test-samples/**/metadata.json"
files: "*-test-samples/**"
separator: ","

- name: Echo changed files
Expand All @@ -33,16 +35,21 @@ jobs:
echo "run_validate=true" >> $GITHUB_OUTPUT
fi
- name: Set up Python 3.12
- name: Set up Python 3.10
if: ${{ steps.setfiles.outputs.run_validate == 'true' }}
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.10"

- name: Install requirements
if: ${{ steps.setfiles.outputs.run_validate == 'true' }}
run: pip install -r .github/metadata/requirements.txt

- name: Validate Schema
if: ${{ steps.setfiles.outputs.run_validate == 'true' }}
run: python .github/metadata/metadata_json_validator.py ${{ steps.get_changed.outputs.all_changed_files }}
run: python .github/metadata/metadata_json_validator.py
env:
ALL_CHANGED_FILES: ${{ steps.get_changed.outputs.all_changed_files }}
PR_NUMBER: ${{ github.event.number }}
GITHUB_AUTOMATION: true
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ Event-driven architectures (EDA) are an architecture style that uses events and
---|---|---
|[S3, Lambda](./python-test-samples/async-lambda-dynamodb)|Python|This is a great starter project for learning how to test async EDA.|
|[Schemas and Contracts](./typescript-test-samples/schema-and-contract-testing)|TypeScript|Event driven architectures decouple producers and consumers at the infrastructure layer, but these resources may still be coupled at the application layer by the event contract. Learn how to test for breaking changes in the contract.|
|[S3, Lambda, DynamoDB](./dotnet-test-samples/async-architectures/async-lambda-dynamodb)|.NET|This example shows how to test async system by using DynamoDB during to store incoming asynchronous events during testing|
|[S3, Lambda, SQS](./dotnet-test-samples/async-architectures/async-lambda-sqs)|.NET|An example to how to test asynchronous workflow by long polling the queue that resulting messages are sent to.|
|[S3, Lambda, DynamoDB](./dotnet-test-samples/async-lambda-dynamodb)|.NET|This example shows how to test async system by using DynamoDB during to store incoming asynchronous events during testing|
|[S3, Lambda, SQS](./dotnet-test-samples/async-lambda-sqs)|.NET|An example to how to test asynchronous workflow by long polling the queue that resulting messages are sent to.|

## Architectural patterns
|Pattern|Services used|Language|Description|
Expand Down
Binary file removed _img/READMEIntro.gif
Binary file not shown.
Binary file removed _img/dotnet-check.png
Binary file not shown.
Binary file removed _img/java-check.png
Binary file not shown.
Binary file removed _img/pattern_04_lambda_mock_sut.png
Binary file not shown.
Binary file removed _img/pattern_05_lambda_layer_sut.png
Binary file not shown.
Binary file removed _img/python-check.png
Binary file not shown.
Binary file removed _img/typescript-check.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Automated tests are crucial components of event driven architectures. They help
## View a Generic Asynchronous Pattern
Asynchronous systems typically receive messages or events and immediately store them for future processing. Later, a secondary processing service may perform operations on the stored data. Processed data may then be sent as output to additional services or placed into another storage location. Below is a diagram illustrating a generic asynchronous pattern.

![Generic Asynchronous System](./img/generic.png)
![Generic Asynchronous System](./img/generic-async-system.png)

## Establish Logical Boundaries
Asynchronous systems rarely exist in isolation. Typically a production system will be made up of many interconnected subsystems. In order to create a reasonable testing strategy, it is useful to break complex systems into a set of logical subsystems. A subsystem may be a group of services that work together to accomplish a single task. A subsystem should have well understood inputs and outputs. A subsystem should be small enough to be able to reason about and understand. Breaking your complex architecture into smaller subsystems makes it easier to create isolated and targeted tests.
Expand All @@ -22,7 +22,8 @@ Test harnesses are usually composed of event producers and event listeners. The
Although your architecture may be asynchronous, it is still useful to establish reasonable expectations about the maximum duration your system may take to process before it considered to be in a failure state. These expectations may be explicitly defined as Service Level Agreements (SLAs). When you design your tests, you may set timeouts that match your SLA’s. If the system does not return results within the timeout period it can be considered to be in violation of the SLA and the tests will fail.

### Asynchronous Test Samples
|Project|Description|
---|---
|[Lambda with DynamoDB](./async-lambda-dynamodb/)|You may use a variety of resource types to create the event listener for your asynchronous system under test. We recommend starting with AWS Lambda and Amazon DynamoDB. DynamoDB creates a persistent storage resource that can enable long running tests or an aggregate a set of results.|
|[Lambda with SQS](./async-lambda-sqs/)|You may do not need to add a special event listener in case your asynchronous system contains a queue as the target of the lambda function. Since the existing queue is a persistant storage that can be used to verify the result of your end to end tests.
| Project |Description|
----------------------------------------------------------------|---
|[Schema & Contract Testing](./schema-and-contract-testing)|This project contains examples on how to do schema and contract testing for your event driven applications.|
| [Lambda with DynamoDB](./async-lambda-dynamodb/README.md) |You may use a variety of resource types to create the event listener for your asynchronous system under test. We recommend starting with AWS Lambda and Amazon DynamoDB. DynamoDB creates a persistent storage resource that can enable long running tests or an aggregate a set of results.|
| [Lambda with SQS](./async-lambda-sqs/README.md) |You may do not need to add a special event listener in case your asynchronous system contains a queue as the target of the lambda function. Since the existing queue is a persistant storage that can be used to verify the result of your end to end tests.
8 changes: 6 additions & 2 deletions dotnet-test-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ This portion of the repository contains code samples for testing serverless appl
|[Hexagonal Architecture](./hexagonal-architecture)|An example of hexagonal architecture implemented using AWS Lambda with tests.|
|[Kinesis Data Streams](./kinesis-lambda-dynamodb/)|This project contains unit and integration tests for a pattern using Kinesis Data Streams, AWS Lambda and Amazon DynamoDB.|
|[SQS with AWS Lambda and DynamoDb](./sqs-lambda)|This project contains unit and integration tests for AWS Lambda that is invoked by Amazon Simple Queue Service (SQS)|

|[Local testing using containers](./test-containers)|This pattern creates an Amazon API Gateway HTTP API, an AWS Lambda function, and a DynamoDB Table using SAM and .NET 6. It also demonstrates how you can use a local emulated version of DynamoDB to increase the speed of feedback loops as you make changes to your application code.|
|[Load Testing](./load-testing)|A description of how load testing can be carried out before deploying to production |

## Test Asynchronous Architectures
* In a synchronous system, a calling service makes a request to a receiving service and then blocks, waiting for the receiver to complete the operation and return a result. In contrast, in an **asynchronous system**, a caller makes a request to a receiving system, which typically returns an immediate acknowledgement but then performs the requested operation at a later time. Asynchronous systems are frequently designed using event-driven architectures. These types of systems have several advantages including increased reliability, greater control over load processing, and improved scalability. However, testing these systems can present unique challenges.

[Click this link to learn more about testing asynchronous architectures](./README-ASYNC.md).

|Project|Description|
|---|---|
|[Schema & Contract Testing](./schema-and-contract-testing)|This project contains examples on how to do schema and contract testing for your event driven applications.|
|[Async Testing Introduction](./apigw-lambda-ddb)|This project contains an introduction to asynchronous testing using Lambda, S3 & DynamoDB.|
|[Lambda with DynamoDB](./async-lambda-dynamodb) |You may use a variety of resource types to create the event listener for your asynchronous system under test. We recommend starting with AWS Lambda and Amazon DynamoDB. DynamoDB creates a persistent storage resource that can enable long running tests or an aggregate a set of results.|
|[Lambda with SQS](./async-lambda-sqs) |You may do not need to add a special event listener in case your asynchronous system contains a queue as the target of the lambda function. Since the existing queue is a persistant storage that can be used to verify the result of your end to end tests.

2 changes: 1 addition & 1 deletion dotnet-test-samples/apigw-lambda-ddb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The AWS services used in this pattern are

## Topology

<img src="../docs/apigw-lambda-ddb.jpg" alt="topology" width="80%"/>
<img src="./img/apigw-lambda-ddb.jpg" alt="topology" width="80%"/>


## Description
Expand Down
4 changes: 2 additions & 2 deletions dotnet-test-samples/apigw-lambda-ddb/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"name": "James Eastham",
"image": "https://media.licdn.com/dms/image/C4D03AQHDx6vm65vyeA/profile-displayphoto-shrink_800_800/0/1637340702988?e=1684972800&v=beta&t=ohSQHb3h2VmM4iq8EWlLVi1wfrR7AUgke8wfSqP8RPI",
"bio": "Senior Cloud Architect at AWS",
"linkedin": "https://www.linkedin.com/in/james-eastham/",
"twitter": "https://twitter.com/plantpowerjames"
"linkedin": "james-eastham",
"twitter": "plantpowerjames"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"name": "James Eastham",
"image": "https://media.licdn.com/dms/image/C4D03AQHDx6vm65vyeA/profile-displayphoto-shrink_800_800/0/1637340702988?e=1684972800&v=beta&t=ohSQHb3h2VmM4iq8EWlLVi1wfrR7AUgke8wfSqP8RPI",
"bio": "Senior Cloud Architect at AWS",
"linkedin": "https://www.linkedin.com/in/james-eastham/",
"twitter": "https://twitter.com/plantpowerjames"
"linkedin": "james-eastham",
"twitter": "plantpowerjames"
}
]
}
Loading

0 comments on commit 6aecbfe

Please sign in to comment.