-
Notifications
You must be signed in to change notification settings - Fork 2.4k
test: Add simple integration tests #1797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
1552ea6
a890b7c
5a16b78
582262c
5117bf6
c070cdc
243edc3
8a9efd2
b35d912
eb72ddd
0f79f4b
090560b
f7ee968
2bde985
65c2ebd
b763e7e
5a11d35
a1f583e
d12abf4
d8eb9ba
083942c
e68b81e
4ae155d
4130f3d
923934b
67c2674
ee5402e
0faf6e4
dedd33b
b4eb18d
f7fc08b
0a96214
a88b80b
65bb94a
6a9fd89
dd9bec1
8c900d2
a5bfc51
bb016f5
6c85d6d
896ef40
b45a890
ae775b7
b61a189
ff18eee
e7bdf6c
c3037ae
17f3572
da61baf
dab73e7
3e3f387
cf1d3f2
e41d19d
d37fda5
befee0e
0fac1bc
1bdb60b
e1bf1f8
09e9789
ba3d54d
914cb79
9a3b154
30e841e
4eaaed0
2e14e38
7c91bb0
20e3cfd
4091d3f
c79c53b
b6e227b
a2918f6
ef7acfe
1c4c1c8
2857627
e099b0b
6a97509
453a1ca
2cb57a9
abcc4aa
f97d759
656b457
77d5b91
1871bc8
6b3ab9f
f1e0569
58e14c7
724dfdf
c1c274b
40a7827
cc19689
2fc0bb1
463e1e6
886c692
e04853f
bccea06
26eed84
8711cfa
35b5b09
8237cbf
9de384e
3aaa34c
12bc745
8f33c82
6806e78
3d380b8
d88bddd
0bddb9a
4848990
d070397
b3930c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,13 +6,16 @@ init: | |
| pip install -e '.[dev]' | ||
|
|
||
| test: | ||
| pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests | ||
| pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* | ||
|
|
||
| test-integ: | ||
| pytest --no-cov tests_integ/* | ||
|
||
|
|
||
| black: | ||
| black setup.py samtranslator/* tests/* bin/* | ||
| black setup.py samtranslator/* tests/* tests_integ/* bin/* | ||
|
|
||
| black-check: | ||
| black --check setup.py samtranslator/* tests/* bin/* | ||
| black --check setup.py samtranslator/* tests/* tests_integ/* bin/* | ||
|
|
||
| # Command to run everytime you make changes to verify everything works | ||
| dev: test | ||
|
|
@@ -27,6 +30,7 @@ Usage: $ make [TARGETS] | |
| TARGETS | ||
| init Initialize and install the requirements and dev-requirements for this project. | ||
| test Run the Unit tests. | ||
| test-integ Run the Integration tests. | ||
| dev Run all development tests after a change. | ||
| pr Perform all checks before submitting a Pull Request. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import os | ||
| from pathlib import Path | ||
| from unittest.case import TestCase | ||
|
|
||
| import boto3 | ||
| import pytest | ||
| import yaml | ||
| from samcli.lib.deploy.deployer import Deployer | ||
| from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket | ||
|
|
||
| STACK_NAME_PREFIX = "sam-integ-stack-" | ||
| S3_BUCKET_PREFIX = "sam-integ-bucket-" | ||
| CODE_KEY_TO_FILE_MAP = {"codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json"} | ||
|
|
||
|
|
||
| class BaseTest(TestCase): | ||
| @classmethod | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| def setUpClass(cls): | ||
| BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] | ||
| BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, "resources") | ||
| BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") | ||
| BaseTest.output_dir = BaseTest.tests_integ_dir | ||
| BaseTest.expected_dir = Path(BaseTest.resources_dir, "expected", "single") | ||
| code_dir = Path(BaseTest.resources_dir, "code") | ||
|
|
||
| BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() | ||
| session = boto3.session.Session() | ||
| my_region = session.region_name | ||
| create_bucket(BaseTest.s3_bucket_name, region=my_region) | ||
|
|
||
| s3_client = boto3.client("s3") | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| BaseTest.code_key_to_url = {} | ||
|
|
||
| for key, file_name in CODE_KEY_TO_FILE_MAP.items(): | ||
| code_path = str(Path(code_dir, file_name)) | ||
| s3_client.upload_file(code_path, BaseTest.s3_bucket_name, file_name) | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" | ||
| BaseTest.code_key_to_url[key] = code_url | ||
|
|
||
| @classmethod | ||
| def tearDownClass(cls) -> None: | ||
| BaseTest._clean_bucket() | ||
|
|
||
| @classmethod | ||
| def _clean_bucket(cls): | ||
| s3_client = boto3.client("s3") | ||
| response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) | ||
| for content in response["Contents"]: | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| s3_client.delete_object(Key=content["Key"], Bucket=BaseTest.s3_bucket_name) | ||
| s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) | ||
|
|
||
| def setUp(self): | ||
| self.cloudformation_client = boto3.client("cloudformation") | ||
| self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") | ||
|
|
||
| def create_and_verify_stack(self, file_name): | ||
| input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) | ||
| self.output_file_path = str(Path(BaseTest.output_dir, "cfn_" + file_name + ".yaml")) | ||
| expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) | ||
| self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| self.sub_input_file_path = self._update_template(input_file_path, file_name) | ||
| transform_template(self.sub_input_file_path, self.output_file_path) | ||
| self._deploy_stack() | ||
| self._verify_stack(expected_resource_path) | ||
|
|
||
| def tearDown(self): | ||
| self.cloudformation_client.delete_stack(StackName=self.stack_name) | ||
| if os.path.exists(self.output_file_path): | ||
| os.remove(self.output_file_path) | ||
| if os.path.exists(self.sub_input_file_path): | ||
| os.remove(self.sub_input_file_path) | ||
|
|
||
| def _update_template(self, input_file_path, file_name): | ||
| updated_template_path = str(Path(BaseTest.output_dir, "sub_" + file_name + ".yaml")) | ||
| with open(input_file_path, "r") as f: | ||
| data = f.read() | ||
| for key, s3_url in BaseTest.code_key_to_url.items(): | ||
| data = data.replace(f"${{{key}}}", s3_url) | ||
| yaml_doc = yaml.load(data, Loader=yaml.FullLoader) | ||
|
|
||
| with open(updated_template_path, "w") as f: | ||
| yaml.dump(yaml_doc, f) | ||
|
|
||
| return updated_template_path | ||
|
|
||
| def _deploy_stack(self): | ||
| with open(self.output_file_path, "r") as cfn_file: | ||
mgrandis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| result, changeset_type = self.deployer.create_and_wait_for_changeset( | ||
| stack_name=self.stack_name, | ||
| cfn_template=cfn_file.read(), | ||
| parameter_values=[], | ||
| capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], | ||
| role_arn=None, | ||
| notification_arns=[], | ||
| s3_uploader=None, | ||
| tags=[], | ||
| ) | ||
| self.deployer.execute_changeset(result["Id"], self.stack_name) | ||
| self.deployer.wait_for_execute(self.stack_name, changeset_type) | ||
|
|
||
| def _verify_stack(self, expected_resource_path): | ||
| stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) | ||
| stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) | ||
| # verify if the stack was successfully created | ||
| self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") | ||
| # verify if the stack contains the expected resources | ||
| self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import json | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a better name then "helpers"? We should try to be explicit (single principle) and in my experience, these types of classes/files just become dumping grounds There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
| import logging | ||
| import re | ||
| import random | ||
| import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 | ||
| from functools import reduce | ||
|
|
||
| import boto3 | ||
| from botocore.exceptions import ClientError | ||
|
|
||
| from samtranslator.model.exceptions import InvalidDocumentException | ||
| from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader | ||
| from samtranslator.translator.transform import transform | ||
| from samtranslator.yaml_helper import yaml_parse | ||
|
|
||
| RANDOM_SUFFIX_LENGTH = 12 | ||
|
|
||
|
|
||
| def transform_template(input_file_path, output_file_path): | ||
| LOG = logging.getLogger(__name__) | ||
| iam_client = boto3.client("iam") | ||
|
|
||
| with open(input_file_path, "r") as f: | ||
| sam_template = yaml_parse(f) | ||
|
|
||
| try: | ||
| cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) | ||
| cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) | ||
|
|
||
| with open(output_file_path, "w") as f: | ||
| f.write(cloud_formation_template_prettified) | ||
|
|
||
| print("Wrote transformed CloudFormation template to: " + output_file_path) | ||
| except InvalidDocumentException as e: | ||
| errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) | ||
| LOG.error(errorMessage) | ||
| errors = map(lambda cause: cause.message, e.causes) | ||
| LOG.error(errors) | ||
|
|
||
|
|
||
| def verify_stack_resources(expected_file_path, stack_resources): | ||
| with open(expected_file_path, "r") as expected_data: | ||
| expected_resources = _sort_resources(json.load(expected_data)) | ||
| parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) | ||
|
|
||
| if len(expected_resources) != len(parsed_resources): | ||
| return False | ||
|
|
||
| for i in range(len(expected_resources)): | ||
| exp = expected_resources[i] | ||
| parsed = parsed_resources[i] | ||
| if not re.fullmatch(exp["LogicalResourceId"] + "([0-9a-f]{10})?", parsed["LogicalResourceId"]): | ||
mgrandis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return False | ||
| if exp["ResourceType"] != parsed["ResourceType"]: | ||
| return False | ||
| return True | ||
|
|
||
|
|
||
| def generate_suffix(): | ||
| # Very basic random letters generator | ||
| return "".join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) | ||
|
|
||
|
|
||
| def _sort_resources(resources): | ||
| return sorted(resources, key=lambda d: d["LogicalResourceId"]) | ||
|
|
||
|
|
||
| def create_bucket(bucket_name, region=None): | ||
| """Create an S3 bucket in a specified region | ||
| copy code from boto3 doc example | ||
| MG: removed the try so that the exception bubbles up and interrupts the test | ||
| If a region is not specified, the bucket is created in the S3 default | ||
mingkun2020 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| region (us-east-1). | ||
| :param bucket_name: Bucket to create | ||
| :param region: String region to create bucket in, e.g., 'us-west-2' | ||
| :return: True if bucket created, else False | ||
| """ | ||
|
|
||
| # Create bucket | ||
| if region is None: | ||
mgrandis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| s3_client = boto3.client("s3") | ||
| s3_client.create_bucket(Bucket=bucket_name) | ||
| else: | ||
| s3_client = boto3.client("s3", region_name=region) | ||
| location = {"LocationConstraint": region} | ||
mgrandis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we match the naming with SAM CLI? https://github.com/aws/aws-sam-cli/blob/develop/Makefile#L17 That way we are not trying to remember which project does which thing?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the command name to integ-test.