diff --git a/src/rpdk/core/generate.py b/src/rpdk/core/generate.py index 58383055..3f99eb78 100644 --- a/src/rpdk/core/generate.py +++ b/src/rpdk/core/generate.py @@ -20,7 +20,7 @@ def generate(args): args.profile, ) project.generate_docs() - + project.generate_canary_files() LOG.warning("Generated files for %s", project.type_name) diff --git a/src/rpdk/core/generate_stack_templates.py b/src/rpdk/core/generate_stack_templates.py new file mode 100644 index 00000000..4f412e8c --- /dev/null +++ b/src/rpdk/core/generate_stack_templates.py @@ -0,0 +1,233 @@ +import json +import re +import shutil +from pathlib import Path +from typing import Any, Dict +from uuid import uuid4 + +import yaml + +CONTRACT_TEST_FOLDER = "contract-tests-artifacts" +CONTRACT_TEST_INPUT_PREFIX = "inputs_*" +CONTRACT_TEST_DEPENDENCY_FILE_NAME = "dependencies.yml" +FILE_GENERATION_ENABLED = "file_generation_enabled" +TYPE_NAME = "typeName" +CONTRACT_TEST_FILE_NAMES = "contract_test_file_names" +INPUT1_FILE_NAME = "inputs_1.json" +FN_SUB = "Fn::Sub" +FN_IMPORT_VALUE = "Fn::ImportValue" +UUID = "uuid" +DYNAMIC_VALUES_MAP = { + "region": "${AWS::Region}", + "partition": "${AWS::Partition}", + "account": "${AWS::AccountId}", +} + + +class StackTemplateGenerator: + def __init__( + self, + type_name: str, + stack_template_config: dict, + contract_test_file_names: list, + root=None, + ): + self.root = Path(root) if root else Path.cwd() + self.stack_template_config = stack_template_config + self.type_name = type_name + self.contract_test_file_names = contract_test_file_names + + @property + def file_generation_enabled(self): + return self.stack_template_config["file_generation_enabled"] + + @property + def file_prefix(self): + return self.stack_template_config["file_prefix"] + + @property + def stack_template_root_folder_path(self): + return self.stack_template_config["root_folder_path"] + + @property + def target_stack_template_folder_path(self): + return self.stack_template_config["target_folder_path"] + + @property + def stack_template_dependency_file_name(self): + return self.stack_template_config["dependency_file_name"] + + def generate_stack_templates(self) -> None: + """ + Generate stack_template files based on the contract test input files. + + This method checks if file generation is enabled and if the target contract test folder exists. + If both conditions are met, it creates the stack_template folder, copies the contract test dependencies, + and generates stack_template files for each contract test input file up to the specified count. + """ + if not self.file_generation_enabled or not self.contract_test_folder_exists(): + return + self._setup_stack_template_environment() + self._generate_stack_template_files() + + def contract_test_folder_exists(self) -> bool: + return Path(self.target_contract_test_folder_path).exists() + + def _setup_stack_template_environment(self) -> None: + stack_template_root = Path(self.stack_template_root_folder_path) + stack_template_folder = Path(self.target_stack_template_folder_path) + self.clean_and_create_stack_template_folder( + stack_template_root, stack_template_folder + ) + self.create_stack_template_bootstrap( + Path(self.target_contract_test_folder_path), stack_template_root + ) + + def _generate_stack_template_files(self) -> None: + resource_name = self.type_name + stack_template_folder = Path(self.target_stack_template_folder_path) + contract_test_files = self._get_sorted_contract_test_files() + for count, ct_file in enumerate(contract_test_files, start=1): + self.create_stack_template_file( + resource_name, ct_file, stack_template_folder, self.file_prefix, count + ) + + def _get_sorted_contract_test_files(self) -> list: + contract_test_folder = Path(self.target_contract_test_folder_path) + contract_test_files = [ + file + for file in contract_test_folder.glob(CONTRACT_TEST_INPUT_PREFIX) + if file.is_file() and file.name in self.contract_test_file_names + ] + return sorted(contract_test_files) + + def clean_and_create_stack_template_folder( + self, stack_template: Path, stack_template_folder: Path + ) -> None: + """ + Clean and create the stack_template folder. + + This method removes the existing stack_template root folder and creates a new stack_template folder. + + Args: + stack_template (Path): The path to the stack_template root folder. + stack_template_folder (Path): The path to the stack_template folder. + """ + stack_template_folder.mkdir(parents=True, exist_ok=True) + + def create_stack_template_bootstrap( + self, file_location: Path, stack_template: Path + ) -> None: + """ + Copy the contract test dependencies to the stack_template root folder. + + This method copies the contract test dependency file to the stack_template root folder + as the stack_template bootstrap file. + + Args: + file_location (Path): The path to the contract test folder. + stack_template (Path): The path to the stack_template root folder. + """ + dependencies_file = file_location / CONTRACT_TEST_DEPENDENCY_FILE_NAME + bootstrap_file = stack_template / self.stack_template_dependency_file_name + if dependencies_file.exists(): + shutil.copy(str(dependencies_file), str(bootstrap_file)) + + def create_stack_template_file( + self, + resource_type: str, + ct_file: Path, + stack_template_folder: Path, + stack_template_file_name_prefix: str, + count: int, + ) -> None: + """ + Create a stack_template file based on the contract test input file. + + This method generates a stack_template file in YAML format based on the provided contract test input file. + The stack_template file contains the resource configuration with dynamic values replaced. + + Args: + resource_type (str): The type of the resource being tested. + ct_file (Path): The path to the contract test input file. + stack_template_folder (Path): The path to the stack_template folder. + stack_template_file_name_prefix (str): The prefix for the stack_template file name. + count (int): The count of the stack_template file being generated. + """ + with ct_file.open("r") as f: + json_data = json.load(f) + resource_name = resource_type.split("::")[2] + stack_template_data = { + "Description": f"Template for {resource_type}", + "Resources": { + f"{resource_name}": { + "Type": resource_type, + "Properties": self.replace_dynamic_values( + json_data["CreateInputs"] + ), + } + }, + } + stack_template_file_name = f"{stack_template_file_name_prefix}{count}_001.yaml" + stack_template_file_path = stack_template_folder / stack_template_file_name + + with stack_template_file_path.open("w") as stack_template_file: + yaml.dump(stack_template_data, stack_template_file, indent=2) + + def replace_dynamic_values(self, properties: Dict[str, Any]) -> Dict[str, Any]: + """ + Replace dynamic values in the resource properties. + + This method recursively replaces dynamic values in the resource properties dictionary. + It handles nested dictionaries, lists, and strings with dynamic value placeholders. + + Args: + properties (Dict[str, Any]): The resource properties dictionary. + + Returns: + Dict[str, Any]: The resource properties dictionary with dynamic values replaced. + """ + for key, value in properties.items(): + if isinstance(value, dict): + properties[key] = self.replace_dynamic_values(value) + elif isinstance(value, list): + properties[key] = [self._replace_dynamic_value(item) for item in value] + else: + return_value = self._replace_dynamic_value(value) + properties[key] = return_value + return properties + + def _replace_dynamic_value(self, original_value: Any) -> Any: + """ + Replace a dynamic value with its corresponding value. + + This method replaces dynamic value placeholders in a string with their corresponding values. + It handles UUID generation, partition replacement, and Fn::ImportValue function. + + Args: + original_value (Any): The value to be replaced. + + Returns: + Any: The replaced value. + """ + pattern = r"\{\{(.*?)\}\}" + + def replace_token(match): + token = match.group(1) + if UUID in token: + return str(uuid4()) + if token in DYNAMIC_VALUES_MAP: + return DYNAMIC_VALUES_MAP[token] + return f'{{"{FN_IMPORT_VALUE}": "{token.strip()}"}}' + + replaced_value = re.sub(pattern, replace_token, str(original_value)) + + if any(value in replaced_value for value in DYNAMIC_VALUES_MAP.values()): + replaced_value = {FN_SUB: replaced_value} + if FN_IMPORT_VALUE in replaced_value: + replaced_value = json.loads(replaced_value) + return replaced_value + + @property + def target_contract_test_folder_path(self): + return self.root / CONTRACT_TEST_FOLDER diff --git a/src/rpdk/core/project.py b/src/rpdk/core/project.py index 1649bdd8..14bfade0 100644 --- a/src/rpdk/core/project.py +++ b/src/rpdk/core/project.py @@ -29,6 +29,7 @@ SpecValidationError, ) from .fragment.module_fragment_reader import _get_fragment_file +from .generate_stack_templates import StackTemplateGenerator from .jsonutils.pointer import fragment_decode, fragment_encode from .jsonutils.utils import traverse from .plugin_registry import load_plugin @@ -56,7 +57,17 @@ ARTIFACT_TYPE_RESOURCE = "RESOURCE" ARTIFACT_TYPE_MODULE = "MODULE" ARTIFACT_TYPE_HOOK = "HOOK" - +TARGET_CANARY_ROOT_FOLDER = "canary-bundle" +TARGET_CANARY_FOLDER = "canary-bundle/canary" +RPDK_CONFIG_FILE = ".rpdk-config" +CANARY_FILE_PREFIX = "canary" +CONTRACT_TEST_DEPENDENCY_FILE_NAME = "dependencies.yml" +CANARY_DEPENDENCY_FILE_NAME = "bootstrap.yaml" +CANARY_SETTINGS = "canarySettings" +TYPE_NAME = "typeName" +CONTRACT_TEST_FILE_NAMES = "contract_test_file_names" +INPUT1_FILE_NAME = "inputs_1.json" +FILE_GENERATION_ENABLED = "file_generation_enabled" DEFAULT_ROLE_TIMEOUT_MINUTES = 120 # 2 hours # min and max are according to CreateRole API restrictions # https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html @@ -145,6 +156,7 @@ def __init__(self, overwrite_enabled=False, root=None): self.test_entrypoint = None self.executable_entrypoint = None self.fragment_dir = None + self.canary_settings = {} self.target_info = {} self.env = Environment( @@ -207,6 +219,18 @@ def target_schemas_path(self): def target_info_path(self): return self.root / TARGET_INFO_FILENAME + @property + def target_canary_root_path(self): + return self.root / TARGET_CANARY_ROOT_FOLDER + + @property + def target_canary_folder_path(self): + return self.root / TARGET_CANARY_FOLDER + + @property + def rpdk_config(self): + return self.root / RPDK_CONFIG_FILE + @staticmethod def _raise_invalid_project(msg, e): LOG.debug(msg, exc_info=e) @@ -277,6 +301,7 @@ def validate_and_load_resource_settings(self, raw_settings): self.executable_entrypoint = raw_settings.get("executableEntrypoint") self._plugin = load_plugin(raw_settings["language"]) self.settings = raw_settings.get("settings", {}) + self.canary_settings = raw_settings.get("canarySettings", {}) def _write_example_schema(self): self.schema = resource_json( @@ -338,6 +363,7 @@ def _write_resource_settings(f): "testEntrypoint": self.test_entrypoint, "settings": self.settings, **executable_entrypoint_dict, + "canarySettings": self.canary_settings, }, f, indent=4, @@ -391,6 +417,10 @@ def init(self, type_name, language, settings=None): self.language = language self._plugin = load_plugin(language) self.settings = settings or {} + self.canary_settings = { + FILE_GENERATION_ENABLED: True, + CONTRACT_TEST_FILE_NAMES: [INPUT1_FILE_NAME], + } self._write_example_schema() self._write_example_inputs() self._plugin.init(self) @@ -1251,3 +1281,28 @@ def _load_target_info( ) return type_info + + def generate_canary_files(self) -> None: + """ + Generate canary files based on the contract test input files. + + This method checks if file generation is enabled and if the target contract test folder exists. + If both conditions are met, it creates the canary folder, copies the contract test dependencies, + and generates canary files for each contract test input file up to the specified count. + """ + stack_template_config = { + "root_folder_path": self.target_canary_root_path, + "target_folder_path": self.target_canary_folder_path, + "dependency_file_name": CANARY_DEPENDENCY_FILE_NAME, + "file_prefix": CANARY_FILE_PREFIX, + "file_generation_enabled": self.canary_settings.get( + FILE_GENERATION_ENABLED, False + ), + } + generate_stack_templates = StackTemplateGenerator( + self.type_name, + stack_template_config, + self.canary_settings.get(CONTRACT_TEST_FILE_NAMES, [INPUT1_FILE_NAME]), + self.root, + ) + generate_stack_templates.generate_stack_templates() diff --git a/tests/test_generate_stack_templates.py b/tests/test_generate_stack_templates.py new file mode 100644 index 00000000..0795f906 --- /dev/null +++ b/tests/test_generate_stack_templates.py @@ -0,0 +1,231 @@ +import json +import os +import re +import shutil +import uuid +from pathlib import Path +from unittest.mock import ANY, patch + +import pytest + +from rpdk.core.generate_stack_templates import ( + CONTRACT_TEST_DEPENDENCY_FILE_NAME, + CONTRACT_TEST_FOLDER, + StackTemplateGenerator, +) + + +@pytest.fixture(name="stack_template_generator") +def setup_fixture(tmp_path): + root_path = tmp_path + type_name = "AWS::Example::Resource" + stack_template_config = { + "root_folder_path": root_path / "stack-templates", + "target_folder_path": root_path / "stack-templates" / "target", + "dependency_file_name": CONTRACT_TEST_DEPENDENCY_FILE_NAME, + "file_prefix": "stack-template-", + "file_generation_enabled": True, + } + contract_test_file_names = ["inputs_1.json", "inputs_2.json"] + contract_test_folder = root_path / CONTRACT_TEST_FOLDER + contract_test_folder.mkdir(parents=True, exist_ok=True) + # Create a dummy JSON file in the contract_test_folder directory + create_dummy_json_file(contract_test_folder, "inputs_1.json") + create_dummy_json_file(contract_test_folder, "inputs_2.json") + (contract_test_folder / CONTRACT_TEST_DEPENDENCY_FILE_NAME).touch() + + return StackTemplateGenerator( + type_name, + stack_template_config, + contract_test_file_names, + root_path, + ) + + +def create_dummy_json_file(directory: Path, file_name: str): + """Create a dummy JSON file in the given directory.""" + dummy_json_file = directory / file_name + dummy_data = { + "CreateInputs": { + "Property1": "Value1", + "Property2": "Value1", + } + } + with dummy_json_file.open("w") as f: + json.dump(dummy_data, f) + + +def create_folder(folder: Path): + if os.path.exists(folder): + shutil.rmtree(folder) + folder.mkdir() + + +def test_is_file_generation_enabled(stack_template_generator): + assert stack_template_generator.file_generation_enabled is True + + +def test_contract_test_folder_exists(stack_template_generator): + assert stack_template_generator.contract_test_folder_exists() is True + + +def test_generate_stack_template_files(stack_template_generator, tmp_path): + stack_template_generator.generate_stack_templates() + + stack_template_root_path = stack_template_generator.stack_template_root_folder_path + stack_template_folder_path = ( + stack_template_generator.target_stack_template_folder_path + ) + assert stack_template_root_path.exists() + assert stack_template_folder_path.exists() + + template_files = list( + stack_template_folder_path.glob(f"{stack_template_generator.file_prefix}*") + ) + assert len(template_files) == 2 + template_files.sort() + assert template_files[0].name == f"{stack_template_generator.file_prefix}1_001.yaml" + assert template_files[1].name == f"{stack_template_generator.file_prefix}2_001.yaml" + + bootstrap_file = ( + stack_template_root_path + / stack_template_generator.stack_template_dependency_file_name + ) + assert bootstrap_file.exists() + + +@pytest.mark.usefixtures("stack_template_generator") +def test_clean_and_create_template_folder(stack_template_generator, tmp_path): + template_root_path = stack_template_generator.stack_template_root_folder_path + template_folder_path = stack_template_generator.target_stack_template_folder_path + template_root_path.mkdir() + (template_root_path / "existing_file.txt").touch() + stack_template_generator.clean_and_create_stack_template_folder( + template_root_path, template_folder_path + ) + assert template_root_path.exists() + assert not list(template_folder_path.glob("*")) + assert template_folder_path.exists() + + +def test_create_template_bootstrap(stack_template_generator, tmp_path): + contract_test_folder = tmp_path / CONTRACT_TEST_FOLDER + create_folder(contract_test_folder) + (contract_test_folder / CONTRACT_TEST_DEPENDENCY_FILE_NAME).touch() + + template_root_path = stack_template_generator.stack_template_root_folder_path + create_folder(template_root_path) + + stack_template_generator.create_stack_template_bootstrap( + contract_test_folder, template_root_path + ) + + bootstrap_file = ( + template_root_path + / stack_template_generator.stack_template_dependency_file_name + ) + assert bootstrap_file.exists() + + +@patch("rpdk.core.generate_stack_templates.yaml.dump") +def test_create_template_file(mock_yaml_dump, stack_template_generator, tmp_path): + contract_test_folder = tmp_path / CONTRACT_TEST_FOLDER + create_folder(contract_test_folder) + contract_test_file = contract_test_folder / "inputs_1.json" + contract_test_data = { + "CreateInputs": { + "Property1": "Value1", + "Property2": "{{test123}}", + "Property3": {"Nested": "{{partition}}"}, + "Property4": ["{{region}}", "Value2"], + "Property5": "{{uuid}}", + "Property6": "{{account}}", + } + } + with contract_test_file.open("w") as f: + json.dump(contract_test_data, f) + + template_folder_path = stack_template_generator.target_stack_template_folder_path + template_folder_path.mkdir(parents=True, exist_ok=True) + + stack_template_generator.create_stack_template_file( + "AWS::Example::Resource", + contract_test_file, + template_folder_path, + stack_template_generator.file_prefix, + 1, + ) + + expected_template_data = { + "Description": "Template for AWS::Example::Resource", + "Resources": { + "Resource": { + "Type": "AWS::Example::Resource", + "Properties": { + "Property1": "Value1", + "Property2": {"Fn::ImportValue": ANY}, + "Property3": {"Nested": {"Fn::Sub": "${AWS::Partition}"}}, + "Property4": [{"Fn::Sub": "${AWS::Region}"}, "Value2"], + "Property5": ANY, + "Property6": {"Fn::Sub": "${AWS::AccountId}"}, + }, + } + }, + } + args, kwargs = mock_yaml_dump.call_args + assert args[0] == expected_template_data + assert kwargs + # Assert UUID generation + replaced_properties = args[0]["Resources"]["Resource"]["Properties"] + assert isinstance(replaced_properties["Property5"], str) + assert len(replaced_properties["Property5"]) == 36 # Standard UUID length + assert re.match( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + replaced_properties["Property5"], + ) + + # Assert the generated UUID is a valid UUID + generated_uuid = replaced_properties["Property5"] + assert uuid.UUID(generated_uuid) + + +def test_replace_dynamic_values(stack_template_generator): + properties = { + "Property1": "Value1", + "Property2": "{{uuid}}", + "Property3": {"Nested": "{{partition}}"}, + "Property4": ["{{region}}", "Value2"], + "Property5": [{"Key": "{{uuid}}"}], + "Property6": "{{account}}", + "Property7": "prefix-{{uuid}}-sufix", + "Property8": "{{value8}}", + } + replaced_properties = stack_template_generator.replace_dynamic_values(properties) + assert replaced_properties["Property1"] == "Value1" + assert isinstance(replaced_properties["Property2"], str) + assert len(replaced_properties["Property2"]) == 36 + assert re.match( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + replaced_properties["Property2"], + ) + assert replaced_properties["Property3"]["Nested"] == { + "Fn::Sub": "${AWS::Partition}" + } + assert replaced_properties["Property4"][0] == {"Fn::Sub": "${AWS::Region}"} + assert replaced_properties["Property4"][1] == "Value2" + assert replaced_properties["Property6"] == {"Fn::Sub": "${AWS::AccountId}"} + property7_value = replaced_properties["Property7"] + # Assert the replaced value + assert isinstance(property7_value, str) + assert "prefix-" in property7_value + assert "-sufix" in property7_value + # Extract the UUID part + property7_value = property7_value.replace("prefix-", "").replace("-sufix", "") + # Assert the UUID format + assert len(property7_value) == 36 # Standard UUID length + assert re.match( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", property7_value + ) + # Assert the UUID is a valid UUID + assert uuid.UUID(property7_value) + assert replaced_properties["Property8"] == {"Fn::ImportValue": "value8"} diff --git a/tests/test_project.py b/tests/test_project.py index dbbb5eda..26e9fdb0 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -5,6 +5,7 @@ import logging import os import random +import shutil import string import sys import zipfile @@ -26,13 +27,21 @@ InvalidProjectError, SpecValidationError, ) +from rpdk.core.generate_stack_templates import CONTRACT_TEST_FOLDER from rpdk.core.plugin_base import LanguagePlugin from rpdk.core.project import ( + CANARY_DEPENDENCY_FILE_NAME, + CANARY_FILE_PREFIX, CFN_METADATA_FILENAME, CONFIGURATION_SCHEMA_UPLOAD_FILENAME, + CONTRACT_TEST_DEPENDENCY_FILE_NAME, + CONTRACT_TEST_FILE_NAMES, + FILE_GENERATION_ENABLED, OVERRIDES_FILENAME, SCHEMA_UPLOAD_FILENAME, SETTINGS_FILENAME, + TARGET_CANARY_FOLDER, + TARGET_CANARY_ROOT_FOLDER, TARGET_INFO_FILENAME, Project, escape_markdown, @@ -2733,3 +2742,188 @@ def test__load_target_info_for_hooks_local_only(project): sorted(test_type_info.keys()), local_schemas=ANY, local_info=test_type_info ) assert len(mock_loader.call_args[1]["local_schemas"]) == 4 + + +def setup_contract_test_data(tmp_path): + root_path = tmp_path + contract_test_folder = root_path / CONTRACT_TEST_FOLDER + contract_test_folder.mkdir(parents=True, exist_ok=True) + assert contract_test_folder.exists() + # Create a dummy JSON file in the canary_root_path directory + create_dummy_json_file(contract_test_folder, "inputs_1.json") + create_dummy_json_file(contract_test_folder, "inputs_2.json") + (contract_test_folder / CONTRACT_TEST_DEPENDENCY_FILE_NAME).touch() + assert contract_test_folder.exists() + return Project(str(root_path)) + + +def create_dummy_json_file(directory: Path, file_name: str): + """Create a dummy JSON file in the given directory.""" + dummy_json_file = directory / file_name + dummy_data = { + "CreateInputs": { + "Property1": "Value1", + "Property2": "Value1", + } + } + with dummy_json_file.open("w") as f: + json.dump(dummy_data, f) + + +def create_folder(folder: Path): + if os.path.exists(folder): + shutil.rmtree(folder) + folder.mkdir() + + +def test_generate_canary_files(project): + setup_contract_test_data(project.root) + tmp_path = project.root + plugin = object() + data = json.dumps( + { + "artifact_type": "RESOURCE", + "language": LANGUAGE, + "runtime": RUNTIME, + "entrypoint": None, + "testEntrypoint": None, + "futureProperty": "value", + "typeName": "AWS::Example::Resource", + "canarySettings": { + FILE_GENERATION_ENABLED: True, + CONTRACT_TEST_FILE_NAMES: ["inputs_1.json", "inputs_2.json"], + }, + } + ) + patch_load = patch( + "rpdk.core.project.load_plugin", autospec=True, return_value=plugin + ) + + with patch_settings(project, data) as mock_open, patch_load as mock_load: + project.load_settings() + project.generate_canary_files() + mock_open.assert_called_once_with("r", encoding="utf-8") + mock_load.assert_called_once_with(LANGUAGE) + canary_root_path = tmp_path / TARGET_CANARY_ROOT_FOLDER + canary_folder_path = tmp_path / TARGET_CANARY_FOLDER + assert canary_root_path.exists() + assert canary_folder_path.exists() + + canary_files = list(canary_folder_path.glob(f"{CANARY_FILE_PREFIX}*")) + assert len(canary_files) == 2 + canary_files.sort() + assert canary_files[0].name == f"{CANARY_FILE_PREFIX}1_001.yaml" + assert canary_files[1].name == f"{CANARY_FILE_PREFIX}2_001.yaml" + + bootstrap_file = canary_root_path / CANARY_DEPENDENCY_FILE_NAME + assert bootstrap_file.exists() + + +def setup_rpdk_config(project, rpdk_config): + root_path = project.root + plugin = object() + data = json.dumps(rpdk_config) + patch_load = patch( + "rpdk.core.project.load_plugin", autospec=True, return_value=plugin + ) + + with patch_settings(project, data) as mock_open, patch_load as mock_load: + project.load_settings() + mock_open.assert_called_once_with("r", encoding="utf-8") + mock_load.assert_called_once_with(LANGUAGE) + contract_test_folder = root_path / CONTRACT_TEST_FOLDER + contract_test_folder.mkdir(parents=True, exist_ok=True) + # Create a dummy JSON file in the canary_root_path directory + create_dummy_json_file(contract_test_folder, "inputs_1.json") + create_dummy_json_file(contract_test_folder, "inputs_2.json") + (contract_test_folder / CONTRACT_TEST_DEPENDENCY_FILE_NAME).touch() + + +def test_generate_canary_files_when_not_enabled(project): + rpdk_config = { + ARTIFACT_TYPE_RESOURCE: "RESOURCE", + "language": LANGUAGE, + "runtime": RUNTIME, + "entrypoint": None, + "testEntrypoint": None, + "futureProperty": "value", + "typeName": "AWS::Example::Resource", + "canarySettings": { + FILE_GENERATION_ENABLED: False, + "contract_test_file_names": ["inputs_1.json", "inputs_2.json"], + }, + } + tmp_path = project.root + setup_rpdk_config(project, rpdk_config) + project.generate_canary_files() + + canary_root_path = tmp_path / TARGET_CANARY_ROOT_FOLDER + canary_folder_path = tmp_path / TARGET_CANARY_FOLDER + assert not canary_root_path.exists() + assert not canary_folder_path.exists() + + +def test_generate_canary_files_no_canary_settings(project): + rpdk_config = { + ARTIFACT_TYPE_RESOURCE: "RESOURCE", + "language": LANGUAGE, + "runtime": RUNTIME, + "entrypoint": None, + "testEntrypoint": None, + "futureProperty": "value", + "typeName": "AWS::Example::Resource", + } + tmp_path = project.root + setup_rpdk_config(project, rpdk_config) + project.generate_canary_files() + + canary_root_path = tmp_path / TARGET_CANARY_ROOT_FOLDER + canary_folder_path = tmp_path / TARGET_CANARY_FOLDER + assert not canary_root_path.exists() + assert not canary_folder_path.exists() + + +def test_generate_canary_files_empty_input_files(project): + rpdk_config = { + ARTIFACT_TYPE_RESOURCE: "RESOURCE", + "language": LANGUAGE, + "runtime": RUNTIME, + "entrypoint": None, + "testEntrypoint": None, + "futureProperty": "value", + "typeName": "AWS::Example::Resource", + "canarySettings": { + FILE_GENERATION_ENABLED: True, + "contract_test_file_names": [], + }, + } + tmp_path = project.root + setup_rpdk_config(project, rpdk_config) + project.generate_canary_files() + + canary_root_path = tmp_path / TARGET_CANARY_ROOT_FOLDER + canary_folder_path = tmp_path / TARGET_CANARY_FOLDER + assert canary_root_path.exists() + assert canary_folder_path.exists() + canary_files = list(canary_folder_path.glob(f"{CANARY_FILE_PREFIX}*")) + assert not canary_files + + +def test_generate_canary_files_empty_canary_settings(project): + rpdk_config = { + ARTIFACT_TYPE_RESOURCE: "RESOURCE", + "language": LANGUAGE, + "runtime": RUNTIME, + "entrypoint": None, + "testEntrypoint": None, + "futureProperty": "value", + "typeName": "AWS::Example::Resource", + "canarySettings": {}, + } + tmp_path = project.root + setup_rpdk_config(project, rpdk_config) + project.generate_canary_files() + canary_root_path = tmp_path / TARGET_CANARY_ROOT_FOLDER + canary_folder_path = tmp_path / TARGET_CANARY_FOLDER + assert not canary_root_path.exists() + assert not canary_folder_path.exists()