diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp index 9dbbf26aa4..0f2e526ef6 100644 --- a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp @@ -83,6 +83,8 @@ namespace O3DE::ProjectManager { ProjectInfo projectInfo; projectInfo.m_projectName = m_projectName->lineEdit()->text(); + // currently we don't have separate fields for changing the project name and display name + projectInfo.m_displayName = projectInfo.m_projectName; projectInfo.m_path = m_projectPath->lineEdit()->text(); return projectInfo; } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index db376fb195..2de5c594e5 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -52,8 +52,10 @@ namespace Platform } // namespace Platform -#define Py_To_String(obj) obj.cast().c_str() +#define Py_To_String(obj) pybind11::str(obj).cast().c_str() #define Py_To_String_Optional(dict, key, default_string) dict.contains(key) ? Py_To_String(dict[key]) : default_string +#define QString_To_Py_String(value) pybind11::str(value.toStdString()) +#define QString_To_Py_Path(value) m_pathlib.attr("Path")(value.toStdString()) namespace RedirectOutput { @@ -298,6 +300,7 @@ namespace O3DE::ProjectManager m_enableGemProject = pybind11::module::import("o3de.enable_gem"); m_disableGemProject = pybind11::module::import("o3de.disable_gem"); m_editProjectProperties = pybind11::module::import("o3de.project_properties"); + m_pathlib = pybind11::module::import("pathlib"); // make sure the engine is registered RegisterThisEngine(); @@ -346,7 +349,7 @@ namespace O3DE::ProjectManager } } - auto result = m_register.attr("register")(m_enginePath.c_str()); + auto result = m_register.attr("register")(QString_To_Py_Path(QString(m_enginePath.c_str()))); registrationResult = (result.cast() == 0); }); @@ -388,7 +391,7 @@ namespace O3DE::ProjectManager { EngineInfo engineInfo; bool result = ExecuteWithLock([&] { - pybind11::str enginePath = m_manifest.attr("get_this_engine_path")(); + auto enginePath = m_manifest.attr("get_this_engine_path")(); auto o3deData = m_manifest.attr("load_o3de_manifest")(); if (pybind11::isinstance(o3deData)) @@ -399,7 +402,7 @@ namespace O3DE::ProjectManager engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]); engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]); - pybind11::str defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")(); + auto defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")(); engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"default_third_party_folder", Py_To_String(defaultThirdPartyFolder)); } @@ -433,14 +436,8 @@ namespace O3DE::ProjectManager bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo) { bool result = ExecuteWithLock([&] { - pybind11::str enginePath = engineInfo.m_path.toStdString(); - pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString(); - pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString(); - pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString(); - pybind11::str defaultThirdPartyFolder = engineInfo.m_thirdPartyPath.toStdString(); - auto registrationResult = m_register.attr("register")( - enginePath, // engine_path + QString_To_Py_Path(engineInfo.m_path), pybind11::none(), // project_path pybind11::none(), // gem_path pybind11::none(), // external_subdir_path @@ -448,11 +445,11 @@ namespace O3DE::ProjectManager pybind11::none(), // restricted_path pybind11::none(), // repo_uri pybind11::none(), // default_engines_folder - defaultProjectsFolder, - defaultGemsFolder, - defaultTemplatesFolder, + QString_To_Py_Path(engineInfo.m_defaultProjectsFolder), + QString_To_Py_Path(engineInfo.m_defaultGemsFolder), + QString_To_Py_Path(engineInfo.m_defaultTemplatesFolder), pybind11::none(), // default_restricted_folder - defaultThirdPartyFolder + QString_To_Py_Path(engineInfo.m_thirdPartyPath) ); if (registrationResult.cast() != 0) @@ -466,7 +463,7 @@ namespace O3DE::ProjectManager AZ::Outcome PythonBindings::GetGemInfo(const QString& path, const QString& projectPath) { - GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()), pybind11::str(projectPath.toStdString())); + GemInfo gemInfo = GemInfoFromPath(QString_To_Py_String(path), QString_To_Py_Path(projectPath)); if (gemInfo.IsValid()) { return AZ::Success(AZStd::move(gemInfo)); @@ -503,7 +500,7 @@ namespace O3DE::ProjectManager auto result = ExecuteWithLockErrorHandling([&] { - pybind11::str pyProjectPath = projectPath.toStdString(); + auto pyProjectPath = QString_To_Py_Path(projectPath); for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath)) { gems.push_back(GemInfoFromPath(path, pyProjectPath)); @@ -524,10 +521,9 @@ namespace O3DE::ProjectManager pybind11::str enabledGemsFilename; auto result = ExecuteWithLockErrorHandling([&] { - const pybind11::str pyProjectPath = projectPath.toStdString(); enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")( pybind11::none(), // project_name - pyProjectPath); // project_path + QString_To_Py_Path(projectPath)); // project_path }); if (!result.IsSuccess()) { @@ -558,7 +554,7 @@ namespace O3DE::ProjectManager bool result = ExecuteWithLock( [&] { - pybind11::str projectPath = path.toStdString(); + auto projectPath = QString_To_Py_Path(path); auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath); // Returns an exit code so boolify it then invert result @@ -574,10 +570,9 @@ namespace O3DE::ProjectManager bool result = ExecuteWithLock( [&] { - pybind11::str projectPath = path.toStdString(); auto pythonRegistrationResult = m_register.attr("register")( pybind11::none(), // engine_path - projectPath, // project_path + QString_To_Py_Path(path), // project_path pybind11::none(), // gem_path pybind11::none(), // external_subdir_path pybind11::none(), // template_path @@ -606,14 +601,12 @@ namespace O3DE::ProjectManager { ProjectInfo createdProjectInfo; bool result = ExecuteWithLock([&] { - pybind11::str projectPath = projectInfo.m_path.toStdString(); - pybind11::str projectName = projectInfo.m_projectName.toStdString(); - pybind11::str templatePath = projectTemplatePath.toStdString(); + auto projectPath = QString_To_Py_Path(projectInfo.m_path); auto createProjectResult = m_engineTemplate.attr("create_project")( projectPath, - projectName, - templatePath + QString_To_Py_String(projectInfo.m_projectName), + QString_To_Py_Path(projectTemplatePath) ); if (createProjectResult.cast() == 0) { @@ -633,7 +626,7 @@ namespace O3DE::ProjectManager AZ::Outcome PythonBindings::GetProject(const QString& path) { - ProjectInfo projectInfo = ProjectInfoFromPath(pybind11::str(path.toStdString())); + ProjectInfo projectInfo = ProjectInfoFromPath(QString_To_Py_Path(path)); if (projectInfo.IsValid()) { return AZ::Success(AZStd::move(projectInfo)); @@ -745,14 +738,11 @@ namespace O3DE::ProjectManager { return ExecuteWithLockErrorHandling([&] { - pybind11::str pyGemPath = gemPath.toStdString(); - pybind11::str pyProjectPath = projectPath.toStdString(); - m_enableGemProject.attr("enable_gem_in_project")( pybind11::none(), // gem name not needed as path is provided - pyGemPath, + QString_To_Py_Path(gemPath), pybind11::none(), // project name not needed as path is provided - pyProjectPath + QString_To_Py_Path(projectPath) ); }); } @@ -761,21 +751,19 @@ namespace O3DE::ProjectManager { return ExecuteWithLockErrorHandling([&] { - pybind11::str pyGemPath = gemPath.toStdString(); - pybind11::str pyProjectPath = projectPath.toStdString(); - m_disableGemProject.attr("disable_gem_in_project")( pybind11::none(), // gem name not needed as path is provided - pyGemPath, + QString_To_Py_Path(gemPath), pybind11::none(), // project name not needed as path is provided - pyProjectPath + QString_To_Py_Path(projectPath) ); }); } AZ::Outcome PythonBindings::UpdateProject(const ProjectInfo& projectInfo) { - return ExecuteWithLockErrorHandling([&] + bool updateProjectSucceeded = false; + auto result = ExecuteWithLockErrorHandling([&] { std::list newTags; for (const auto& i : projectInfo.m_userTags) @@ -783,23 +771,36 @@ namespace O3DE::ProjectManager newTags.push_back(i.toStdString()); } - m_editProjectProperties.attr("edit_project_props")( - pybind11::str(projectInfo.m_path.toStdString()), // proj_path + auto editResult = m_editProjectProperties.attr("edit_project_props")( + QString_To_Py_Path(projectInfo.m_path), pybind11::none(), // proj_name not used - pybind11::str(projectInfo.m_origin.toStdString()), // new_origin - pybind11::str(projectInfo.m_displayName.toStdString()), // new_display - pybind11::str(projectInfo.m_summary.toStdString()), // new_summary - pybind11::str(projectInfo.m_iconPath.toStdString()), // new_icon + QString_To_Py_String(projectInfo.m_projectName), + QString_To_Py_String(projectInfo.m_origin), + QString_To_Py_String(projectInfo.m_displayName), + QString_To_Py_String(projectInfo.m_summary), + QString_To_Py_String(projectInfo.m_iconPath), // new_icon pybind11::none(), // add_tags not used pybind11::none(), // remove_tags not used - pybind11::list(pybind11::cast(newTags))); // replace_tags + pybind11::list(pybind11::cast(newTags))); + updateProjectSucceeded = (editResult.cast() == 0); }); + + if (!result.IsSuccess()) + { + return result; + } + else if (!updateProjectSucceeded) + { + return AZ::Failure("Failed to update project."); + } + + return AZ::Success(); } ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath) { ProjectTemplateInfo templateInfo; - templateInfo.m_path = Py_To_String(pybind11::str(path)); + templateInfo.m_path = Py_To_String(path); auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath); if (pybind11::isinstance(data)) @@ -848,10 +849,9 @@ namespace O3DE::ProjectManager QVector templates; bool result = ExecuteWithLock([&] { - pybind11::str pyProjectPath = projectPath.toStdString(); for (auto path : m_manifest.attr("get_templates_for_project_creation")()) { - templates.push_back(ProjectTemplateInfoFromPath(path, pyProjectPath)); + templates.push_back(ProjectTemplateInfoFromPath(path, QString_To_Py_Path(projectPath))); } }); diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 065867a130..2bb68f1ad6 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -75,13 +75,15 @@ namespace O3DE::ProjectManager bool m_pythonStarted = false; AZ::IO::FixedMaxPath m_enginePath; - pybind11::handle m_engineTemplate; AZStd::recursive_mutex m_lock; + + pybind11::handle m_engineTemplate; pybind11::handle m_cmake; pybind11::handle m_register; pybind11::handle m_manifest; pybind11::handle m_enableGemProject; pybind11::handle m_disableGemProject; pybind11::handle m_editProjectProperties; + pybind11::handle m_pathlib; }; } diff --git a/Code/Tools/ProjectManager/project_manager_tests_files.cmake b/Code/Tools/ProjectManager/project_manager_tests_files.cmake index e340469bcc..3d297e94db 100644 --- a/Code/Tools/ProjectManager/project_manager_tests_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_tests_files.cmake @@ -13,6 +13,7 @@ set(FILES Resources/ProjectManager.qrc Resources/ProjectManager.qss tests/ApplicationTests.cpp + tests/PythonBindingsTests.cpp tests/main.cpp tests/UtilsTests.cpp ) diff --git a/Code/Tools/ProjectManager/tests/PythonBindingsTests.cpp b/Code/Tools/ProjectManager/tests/PythonBindingsTests.cpp new file mode 100644 index 0000000000..900e1cdef6 --- /dev/null +++ b/Code/Tools/ProjectManager/tests/PythonBindingsTests.cpp @@ -0,0 +1,74 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include +#include +#include +#include +#include + +#include + +namespace O3DE::ProjectManager +{ + class PythonBindingsTests + : public ::UnitTest::ScopedAllocatorSetupFixture + { + public: + + PythonBindingsTests() + { + const AZStd::string engineRootPath{ AZ::Test::GetEngineRootPath() }; + m_pythonBindings = AZStd::make_unique(AZ::IO::PathView(engineRootPath)); + } + + ~PythonBindingsTests() + { + m_pythonBindings.reset(); + } + + AZStd::unique_ptr m_pythonBindings; + }; + + TEST_F(PythonBindingsTests, PythonBindings_Start_Python_Succeeds) + { + EXPECT_TRUE(m_pythonBindings->PythonStarted()); + } + + TEST_F(PythonBindingsTests, PythonBindings_Create_Project_Succeeds) + { + ASSERT_TRUE(m_pythonBindings->PythonStarted()); + + auto templateResults = m_pythonBindings->GetProjectTemplates(); + ASSERT_TRUE(templateResults.IsSuccess()); + + QVector templates = templateResults.GetValue(); + ASSERT_FALSE(templates.isEmpty()); + + // use the first registered template + QString templatePath = templates.at(0).m_path; + + AZ::Test::ScopedAutoTempDirectory tempDir; + + ProjectInfo projectInfo; + projectInfo.m_path = QDir::toNativeSeparators(QString(tempDir.GetDirectory()) + "/" + "TestProject"); + projectInfo.m_projectName = "TestProjectName"; + + auto result = m_pythonBindings->CreateProject(templatePath, projectInfo); + EXPECT_TRUE(result.IsSuccess()); + + ProjectInfo resultProjectInfo = result.GetValue(); + EXPECT_EQ(projectInfo.m_path, resultProjectInfo.m_path); + EXPECT_EQ(projectInfo.m_projectName, resultProjectInfo.m_projectName); + } +} diff --git a/Templates/DefaultProject/Template/Code/Include/${Name}/${Name}Bus.h b/Templates/DefaultProject/Template/Code/Include/${Name}/${Name}Bus.h index 05e434ec03..672e329db4 100644 --- a/Templates/DefaultProject/Template/Code/Include/${Name}/${Name}Bus.h +++ b/Templates/DefaultProject/Template/Code/Include/${Name}/${Name}Bus.h @@ -10,7 +10,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ - // {END_LICENSE} +// {END_LICENSE} #pragma once @@ -22,7 +22,7 @@ namespace ${SanitizedCppName} class ${SanitizedCppName}Requests { public: - AZ_RTTI(${SanitizedCppName}Requests, "${Random_Uuid}"); + AZ_RTTI(${SanitizedCppName}Requests, "{${Random_Uuid}}"); virtual ~${SanitizedCppName}Requests() = default; // Put your public methods here }; diff --git a/scripts/o3de/o3de/engine_template.py b/scripts/o3de/o3de/engine_template.py index 384b8a3c0f..a72400c7c5 100755 --- a/scripts/o3de/o3de/engine_template.py +++ b/scripts/o3de/o3de/engine_template.py @@ -350,6 +350,7 @@ def _instantiate_template(template_json_data: dict, def create_template(source_path: pathlib.Path, template_path: pathlib.Path, + source_name: str = None, source_restricted_path: pathlib.Path = None, source_restricted_name: str = None, template_restricted_path: pathlib.Path = None, @@ -365,6 +366,8 @@ def create_template(source_path: pathlib.Path, :param source_path: The path to the source that you want to make into a template :param template_path: the path of the template to create, can be absolute or relative to default templates path + :param source_name: Name to replace within template folder with ${Name} placeholder + If not specified, the basename of the source_path parameter is used as the source_name instead :param source_restricted_path: path to the source restricted folder :param source_restricted_name: name of the source restricted folder :param template_restricted_path: path to the templates restricted folder @@ -395,7 +398,8 @@ def create_template(source_path: pathlib.Path, return 1 # source_name is now the last component of the source_path - source_name = os.path.basename(source_path) + if not source_name: + source_name = os.path.basename(source_path) sanitized_source_name = utils.sanitize_identifier_for_cpp(source_name) # if no template path, error @@ -410,6 +414,16 @@ def create_template(source_path: pathlib.Path, logger.error(f'Template path {template_path} already exists.') return 1 + # Make sure the output directory for the template is outside the source path directory + try: + template_path.relative_to(source_path) + except ValueError: + pass + else: + logger.error(f'Template output path {template_path} cannot be a subdirectory of the source_path {source_path}:\n' + f'{err}') + return 1 + # template name is now the last component of the template_path template_name = os.path.basename(template_path) @@ -938,7 +952,7 @@ def create_template(source_path: pathlib.Path, s.write(json.dumps(json_data, indent=4) + '\n') # copy the default preview.png - preview_png_src = this_script_parent / 'resources' /' preview.png' + preview_png_src = this_script_parent / 'resources' / 'preview.png' preview_png_dst = template_path / 'Template' / 'preview.png' if not os.path.isfile(preview_png_dst): shutil.copy(preview_png_src, preview_png_dst) @@ -987,6 +1001,7 @@ def create_template(source_path: pathlib.Path, def create_from_template(destination_path: pathlib.Path, template_path: pathlib.Path = None, template_name: str = None, + destination_name: str = None, destination_restricted_path: pathlib.Path = None, destination_restricted_name: str = None, template_restricted_path: pathlib.Path = None, @@ -1004,6 +1019,10 @@ def create_from_template(destination_path: pathlib.Path, :param destination_path: the folder you want to instantiate the template into :param template_path: the path to the template you want to instance :param template_name: the name of the template you want to instance, resolves template_path + :param destination_name: the name that will be substituted when instantiating the template. + The placeholders of ${Name} and ${SanitizedCppName} will be replaced with destination name and a sanitized + version of the destination name that is suitable as a C++ identifier. If not specified, defaults to the + last path component of the destination_path :param destination_restricted_path: path to the projects restricted folder :param destination_restricted_name: name of the projects restricted folder, resolves destination_restricted_path :param template_restricted_path: path of the templates restricted folder @@ -1043,13 +1062,14 @@ def create_from_template(destination_path: pathlib.Path, if template_name: template_path = manifest.get_registered(template_name=template_name) + if not template_path: + logger.error(f'Could not find the template path using name {template_name}.\n' + f'Has the engine been registered yet. It can be registered via the "o3de.py register --this-engine" command') + return 1 if not os.path.isdir(template_path): logger.error(f'Could not find the template {template_name}=>{template_path}') return 1 - # template folder name is now the last component of the template_path - template_folder_name = os.path.basename(template_path) - # the template.json should be in the template_path, make sure it's there a nd valid template_json = template_path / 'template.json' if not validation.valid_o3de_template_json(template_json): @@ -1130,7 +1150,7 @@ def create_from_template(destination_path: pathlib.Path, return 1 # check and make sure the restricted exists - if not os.path.isdir(template_restricted_path): + if template_restricted_path and not os.path.isdir(template_restricted_path): logger.error(f'Template restricted path {template_restricted_path} does not exist.') return 1 @@ -1183,8 +1203,9 @@ def create_from_template(destination_path: pathlib.Path, else: os.makedirs(destination_path, exist_ok=force) - # destination name is now the last component of the destination_path - destination_name = os.path.basename(destination_path) + if not destination_name: + # destination name is now the last component of the destination_path + destination_name = os.path.basename(destination_path) # destination name cannot be the same as a restricted platform name if destination_name in restricted_platforms: @@ -1340,9 +1361,6 @@ def create_project(project_path: pathlib.Path, logger.error(f'Could not find the template {template_name}=>{template_path}') return 1 - # template folder name is now the last component of the template_path - template_folder_name = os.path.basename(template_path) - # the template.json should be in the template_path, make sure it's there and valid template_json = template_path / 'template.json' if not validation.valid_o3de_template_json(template_json): @@ -1423,7 +1441,7 @@ def create_project(project_path: pathlib.Path, return 1 # check and make sure the restricted exists - if not os.path.isdir(template_restricted_path): + if template_restricted_path and not os.path.isdir(template_restricted_path): logger.error(f'Template restricted path {template_restricted_path} does not exist.') return 1 @@ -1574,6 +1592,7 @@ def create_project(project_path: pathlib.Path, return 1 # We created the project, now do anything extra that a project requires + project_json = project_path / 'project.json' # If we are not keeping the restricted in the project read the name of the restricted folder from the # restricted json and set that as this projects restricted @@ -1607,7 +1626,6 @@ def create_project(project_path: pathlib.Path, return 1 # set the "restricted_name": "restricted_name" element of the project.json - project_json = project_path / 'project.json' if not validation.valid_o3de_project_json(project_json): logger.error(f'Project json {project_json} is not valid.') return 1 @@ -1678,6 +1696,7 @@ def create_project(project_path: pathlib.Path, def create_gem(gem_path: pathlib.Path, template_path: pathlib.Path = None, template_name: str = None, + gem_name: str = None, gem_restricted_path: pathlib.Path = None, gem_restricted_name: str = None, template_restricted_path: pathlib.Path = None, @@ -1697,6 +1716,10 @@ def create_gem(gem_path: pathlib.Path, :param gem_path: the gem path, can be absolute or relative to default gems path :param template_path: the template path you want to instance, can be absolute or relative to default templates path :param template_name: the name of the registered template you want to instance, defaults to DefaultGem, resolves template_path + :param gem_name: the name that will be substituted when instantiating the template. + The placeholders of ${Name} and ${SanitizedCppName} will be replaced with gem name and a sanitized + version of the gem name that is suitable as a C++ identifier. If not specified, defaults to the + last path component of the gem_path :param gem_restricted_path: path to the gems restricted folder, can be absolute or relative to the restricted='gems' :param gem_restricted_name: str = name of the registered gems restricted path, resolves gem_restricted_path :param template_restricted_path: the templates restricted path, can be absolute or relative to the restricted='templates' @@ -1741,9 +1764,6 @@ def create_gem(gem_path: pathlib.Path, logger.error(f'Could not find the template {template_name}=>{template_path}') return 1 - # template name is now the last component of the template_path - template_folder_name = os.path.basename(template_path) - # the template.json should be in the template_path, make sure it's there and valid template_json = template_path / 'template.json' if not validation.valid_o3de_template_json(template_json): @@ -1822,7 +1842,7 @@ def create_gem(gem_path: pathlib.Path, f' and {template_json_restricted_path} will be used.') return 1 # check and make sure the restricted path exists - if not os.path.isdir(template_restricted_path): + if template_restricted_path and not os.path.isdir(template_restricted_path): logger.error(f'Template restricted path {template_restricted_path} does not exist.') return 1 @@ -1880,10 +1900,9 @@ def create_gem(gem_path: pathlib.Path, else: os.makedirs(gem_path, exist_ok=force) - # gem nam - # - # e is now the last component of the gem_path - gem_name = os.path.basename(gem_path) + # Default to the gem path basename component if gem_name has not been supplied + if not gem_name: + gem_name = os.path.basename(gem_path) if not utils.validate_identifier(gem_name): logger.error(f'Gem name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {gem_name}') @@ -2057,6 +2076,7 @@ def create_gem(gem_path: pathlib.Path, def _run_create_template(args: argparse) -> int: return create_template(args.source_path, args.template_path, + args.source_name, args.source_restricted_path, args.source_restricted_name, args.template_restricted_path, @@ -2073,6 +2093,7 @@ def _run_create_from_template(args: argparse) -> int: return create_from_template(args.destination_path, args.template_path, args.template_name, + args.destination_path, args.destination_restricted_path, args.destination_restricted_name, args.template_restricted_path, @@ -2109,6 +2130,7 @@ def _run_create_gem(args: argparse) -> int: return create_gem(args.gem_path, args.template_path, args.template_name, + args.gem_name, args.gem_restricted_path, args.gem_restricted_name, args.template_restricted_path, @@ -2159,6 +2181,16 @@ def add_args(subparsers) -> None: help='The name of the templates restricted folder. If supplied this will resolve' ' the --template-restricted-path.') + create_template_subparser.add_argument('-sn', '--source-name', + type=str, + help='Substitutes any file and path entries which match the source' + ' name within the source-path directory with the ${Name} and' + ' ${SanitizedCppName}.' + 'Ex: Path substitution' + '--source-name Foo' + '/Code/Include/FooBus.h -> /Code/Include/${Name}Bus.h' + 'Ex: File content substitution.' + 'class FooRequests -> class ${SanitizedCppName}Requests') create_template_subparser.add_argument('-srprp', '--source-restricted-platform-relative-path', type=pathlib.Path, required=False, default=None, @@ -2216,6 +2248,13 @@ def add_args(subparsers) -> None: help='The name to the registered template you want to instantiate. If supplied this will' ' resolve the --template-path.') + create_from_template_subparser.add_argument('-dn', '--destination-name', type=str, + help='The name to use when substituting the ${Name} placeholder in instantiated template,' + ' must be alphanumeric, ' + ' and can contain _ and - characters.' + ' If no name is provided, will use last component of destination path.' + ' Ex. New_Gem') + group = create_from_template_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-drp', '--destination-restricted-path', type=pathlib.Path, required=False, default=None, @@ -2372,6 +2411,12 @@ def add_args(subparsers) -> None: create_gem_subparser = subparsers.add_parser('create-gem') create_gem_subparser.add_argument('-gp', '--gem-path', type=pathlib.Path, required=True, help='The gem path, can be absolute or relative to default gems path') + create_gem_subparser.add_argument('-gn', '--gem-name', type=str, + help='The name to use when substituting the ${Name} placeholder for the gem,' + ' must be alphanumeric, ' + ' and can contain _ and - characters.' + ' If no name is provided, will use last component of gem path.' + ' Ex. New_Gem') group = create_gem_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-tp', '--template-path', type=pathlib.Path, required=False, diff --git a/scripts/o3de/o3de/project_properties.py b/scripts/o3de/o3de/project_properties.py index 52f1346b51..782ab05755 100644 --- a/scripts/o3de/o3de/project_properties.py +++ b/scripts/o3de/o3de/project_properties.py @@ -16,7 +16,7 @@ import pathlib import sys import logging -from o3de import manifest +from o3de import manifest, utils logger = logging.getLogger() logging.basicConfig() @@ -29,8 +29,16 @@ def get_project_props(name: str = None, path: pathlib.Path = None) -> dict: return None return proj_json -def edit_project_props(proj_path, proj_name, new_origin, new_display, - new_summary, new_icon, new_tags, delete_tags, replace_tags) -> int: +def edit_project_props(proj_path: pathlib.Path, + proj_name: str = None, + new_name: str = None, + new_origin: str = None, + new_display: str = None, + new_summary: str = None, + new_icon: str = None, + new_tags: str or list = None, + delete_tags: str or list = None, + replace_tags: str or list = None) -> int: proj_json = get_project_props(proj_name, proj_path) if not proj_json: @@ -38,6 +46,11 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display, if new_origin: proj_json['origin'] = new_origin + if new_name: + if not utils.validate_identifier(new_name): + logger.error(f'Project name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {new_name}') + return 1 + proj_json['project_name'] = new_name if new_display: proj_json['display_name'] = new_display if new_summary: @@ -54,9 +67,9 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display, if tag in proj_json['user_tags']: proj_json['user_tags'].remove(tag) else: - logger.warn(f'{tag} not found in user_tags for removal.') + logger.warning(f'{tag} not found in user_tags for removal.') else: - logger.warn(f'user_tags property not found for removal of {remove_tags}.') + logger.warning('user_tags property not found.') if replace_tags: tag_list = [replace_tags] if isinstance(replace_tags, str) else replace_tags proj_json['user_tags'] = tag_list @@ -67,13 +80,14 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display, def _edit_project_props(args: argparse) -> int: return edit_project_props(args.project_path, args.project_name, - args.project_origin, - args.project_display, - args.project_summary, - args.project_icon, - args.add_tags, - args.delete_tags, - args.replace_tags) + args.project_new_name, + args.project_origin, + args.project_display, + args.project_summary, + args.project_icon, + args.add_tags, + args.delete_tags, + args.replace_tags) def add_parser_args(parser): group = parser.add_mutually_exclusive_group(required=True) @@ -82,6 +96,8 @@ def add_parser_args(parser): group.add_argument('-pn', '--project-name', type=str, required=False, help='The name of the project.') group = parser.add_argument_group('properties', 'arguments for modifying individual project properties.') + group.add_argument('-pnn', '--project-new-name', type=str, required=False, + help='Sets the name for the project.') group.add_argument('-po', '--project-origin', type=str, required=False, help='Sets description or url for project origin (such as project host, repository, owner...etc).') group.add_argument('-pd', '--project-display', type=str, required=False, diff --git a/scripts/o3de/tests/CMakeLists.txt b/scripts/o3de/tests/CMakeLists.txt index 82b994e387..318673c297 100644 --- a/scripts/o3de/tests/CMakeLists.txt +++ b/scripts/o3de/tests/CMakeLists.txt @@ -41,3 +41,10 @@ ly_add_pytest( TEST_SUITE smoke EXCLUDE_TEST_RUN_TARGET_FROM_IDE ) + +ly_add_pytest( + NAME o3de_template + PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_engine_template.py + TEST_SUITE smoke + EXCLUDE_TEST_RUN_TARGET_FROM_IDE +) diff --git a/scripts/o3de/tests/unit_test_engine_template.py b/scripts/o3de/tests/unit_test_engine_template.py index 59d5e352f1..4528dbda70 100755 --- a/scripts/o3de/tests/unit_test_engine_template.py +++ b/scripts/o3de/tests/unit_test_engine_template.py @@ -8,12 +8,17 @@ # remove or modify any license notices. This file is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -import os +import json +import pathlib +import uuid + import pytest -from . import engine_template +import string + +from o3de import engine_template +from unittest.mock import patch -TEST_TEMPLATED_CONTENT_WITH_LICENSE = """\ -// {BEGIN_LICENSE} +CPP_LICENSE_TEXT = """// {BEGIN_LICENSE} /* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. @@ -26,478 +31,198 @@ TEST_TEMPLATED_CONTENT_WITH_LICENSE = """\ * */ // {END_LICENSE} -#pragma once - -#include - -namespace ${Name} -{ - class ${Name}Requests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here - }; - - using ${Name}RequestsBus = AZ::EBus<${Name}Requests>; - -} // namespace ${Name} - """ -TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE = """\ + +TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE = """ #pragma once #include +#include namespace ${Name} { class ${Name}Requests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here - }; - - using ${Name}RequestsBus = AZ::EBus<${Name}Requests>; - -} // namespace ${Name} - -""" - -TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE = """\ -#pragma once - -#include - -namespace TestTemplate -{ - class TestTemplateRequests - : public AZ::EBusTraits { public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - + AZ_RTTI(${Name}Requests, "{${Random_Uuid}}"); + virtual ~${Name}Requests() = default; // Put your public methods here }; - using TestTemplateRequestsBus = AZ::EBus; - -} // namespace TestTemplate - -""" - -TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE = """\ -// {BEGIN_LICENSE} -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ -// {END_LICENSE} -#pragma once - -#include - -namespace TestTemplate -{ - class TestTemplateRequests + class ${Name}BusTraits : public AZ::EBusTraits { public: ////////////////////////////////////////////////////////////////////////// // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here }; - using TestTemplateRequestsBus = AZ::EBus; - -} // namespace TestTemplate - -""" - -TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE = """\ -#pragma once - -#include - -namespace TestProject -{ - class TestProjectRequests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here - }; - - using TestProjectRequestsBus = AZ::EBus; - -} // namespace TestProject + using ${Name}RequestBus = AZ::EBus<${Name}Requests, ${Name}BusTraits>; + using ${Name}Interface = AZ::Interface<${Name}Requests>; +} // namespace ${Name} """ -TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE = """\ -// {BEGIN_LICENSE} -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ -// {END_LICENSE} -#pragma once +TEST_TEMPLATED_CONTENT_WITH_LICENSE = CPP_LICENSE_TEXT + TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE -#include +TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE).safe_substitute({'Name': "TestTemplate"}) -namespace TestProject -{ - class TestProjectRequests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// +TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITH_LICENSE).safe_substitute({'Name': "TestTemplate"}) - // Put your public methods here - }; +TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE).safe_substitute({'Name': "TestProject"}) - using TestProjectRequestsBus = AZ::EBus; +TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITH_LICENSE).safe_substitute({'Name': "TestProject"}) -} // namespace TestProject +TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE).safe_substitute({'Name': "TestGem"}) -""" - -TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE = """\ -#pragma once - -#include +TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE = string.Template( + TEST_TEMPLATED_CONTENT_WITH_LICENSE).safe_substitute({'Name': "TestGem"}) -namespace TestGem +TEST_TEMPLATE_JSON_CONTENTS = """\ { - class TestGemRequests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here - }; - - using TestGemRequestsBus = AZ::EBus; - -} // namespace TestGem - -""" - -TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE = """\ -// {BEGIN_LICENSE} -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ -// {END_LICENSE} -#pragma once - -#include - -namespace TestGem -{ - class TestGemRequests - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // Put your public methods here - }; - - using TestGemRequestsBus = AZ::EBus; - -} // namespace TestGem - -""" - -TEST_DEFAULTTEMPLATE_JSON_CONTENTS = """\ -{ - "inputPath": "Templates/Default/Template", + "template_name": "Templates", + "origin": "The primary repo for Templates goes here: i.e. http://www.mydomain.com", + "license": "What license Templates uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "Templates", + "summary": "A short description of Templates.", + "canonical_tags": [], + "user_tags": [ + "Templates" + ], + "icon_path": "preview.png", "copyFiles": [ { - "inFile": "Code/Include/${Name}/${Name}Bus.h", - "outFile": "Code/Include/${Name}/${Name}Bus.h", + "file": "Code/Include/${Name}/${Name}Bus.h", + "origin": "Code/Include/${Name}/${Name}Bus.h", "isTemplated": true, "isOptional": false - } - ], - "createDirectories": [ - { - "outDir": "Code" - }, - { - "outDir": "Code/Include" - }, - { - "outDir": "Code/Include/Platform" }, { - "outDir": "Code/Include/${Name}" - } - ] -}\ -""" - -TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS = """\ -{ - "inputPath": "restricted/Salem/Templates/Default/Template", - "copyFiles": [ - { - "inFile": "Code/Include/Platform/Salem/${Name}Bus.h", - "outFile": "Code/Include/Platform/Salem/${Name}Bus.h", + "file": "Code/Include/Platform/Salem/${Name}Bus.h", + "origin": "Code/Include/Platform/Salem/${Name}Bus.h", "isTemplated": true, "isOptional": false } ], "createDirectories": [ { - "outDir": "Code/Include/Platform/Salem" - } - ] -}\ -""" - -TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS = """\ -{ - "inputPath": "Templates/DefaultProject/Template", - "copyFiles": [ - { - "inFile": "Code/Include/${Name}/${Name}Bus.h", - "outFile": "Code/Include/${Name}/${Name}Bus.h", - "isTemplated": true, - "isOptional": false - } - ], - "createDirectories": [ + "dir": "Code", + "origin": "Code" + }, { - "outDir": "Code" + "dir": "Code/Include", + "origin": "Code/Include" }, { - "outDir": "Code/Include" + "dir": "Code/Include/${Name}", + "origin": "Code/Include/${Name}" }, { - "outDir": "Code/Include/Platform" + "dir": "Code/Include/Platform", + "origin": "Code/Include/Platform" }, { - "outDir": "Code/Include/${Name}" + "dir": "Code/Include/Platform/Salem", + "origin": "Code/Include/Platform/Salem" } ] -}\ +} """ -TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS = """\ -{ - "inputPath": "restricted/Salem/Templates/DefaultProject/Template", - "copyFiles": [ - { - "inFile": "Code/Include/Platform/Salem/${Name}Bus.h", - "outFile": "Code/Include/Platform/Salem/${Name}Bus.h", - "isTemplated": true, - "isOptional": false - } - ], - "createDirectories": [ - { - "outDir": "Code/Include/Platform/Salem" - } - ] -}\ -""" -TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS = """\ -{ - "inputPath": "Templates/DefaultGem/Template", - "copyFiles": [ - { - "inFile": "Code/Include/${Name}/${Name}Bus.h", - "outFile": "Code/Include/${Name}/${Name}Bus.h", - "isTemplated": true, - "isOptional": false - } - ], - "createDirectories": [ - { - "outDir": "Code" - }, - { - "outDir": "Code/Include" - }, - { - "outDir": "Code/Include/Platform" - }, - { - "outDir": "Code/Include/${Name}" - } - ] -}\ -""" +TEST_CONCRETE_TEMPLATE_JSON_CONTENTS = string.Template( + TEST_TEMPLATE_JSON_CONTENTS).safe_substitute({'Name': 'TestTemplate'}) + + +TEST_CONCRETE_PROJECT_TEMPLATE_JSON_CONTENTS = string.Template( + TEST_TEMPLATE_JSON_CONTENTS).safe_substitute({'Name': 'TestProject'}) -TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS = """\ -{ - "inputPath": "restricted/Salem/Templates/DefaultGem/Template", - "copyFiles": [ - { - "inFile": "Code/Include/Platform/Salem/${Name}Bus.h", - "outFile": "Code/Include/Platform/Salem/${Name}Bus.h", - "isTemplated": true, - "isOptional": false - } - ], - "createDirectories": [ - { - "outDir": "Code/Include/Platform/Salem" - } - ] -}\ -""" + +TEST_CONCRETE_GEM_TEMPLATE_JSON_CONTENTS = string.Template( + TEST_TEMPLATE_JSON_CONTENTS).safe_substitute({'Name': 'TestGem'}) @pytest.mark.parametrize( "concrete_contents," " templated_contents_with_license, templated_contents_without_license," - " keep_license_text, expect_failure," - " template_json_contents, restricted_template_json_contents", [ + " keep_license_text, force, expect_failure," + " template_json_contents", [ pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE, - True, False, - TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS), + True, True, False, + TEST_TEMPLATE_JSON_CONTENTS), pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE, - False, False, - TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS) + False, True, False, + TEST_TEMPLATE_JSON_CONTENTS) ] ) def test_create_template(tmpdir, concrete_contents, templated_contents_with_license, templated_contents_without_license, - keep_license_text, expect_failure, - template_json_contents, restricted_template_json_contents): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - dev_gem_code_include_testgem = f'{dev_root}/TestTemplate/Code/Include/TestTemplate' - os.makedirs(dev_gem_code_include_testgem, exist_ok=True) - - gem_bus_file = f'{dev_gem_code_include_testgem}/TestTemplateBus.h' - if os.path.isfile(gem_bus_file): - os.unlink(gem_bus_file) - with open(gem_bus_file, 'w') as s: - s.write(concrete_contents) + keep_license_text, force, expect_failure, + template_json_contents): + engine_root = (pathlib.Path(tmpdir) / 'engine-root').resolve() + engine_root.mkdir(parents=True, exist_ok=True) + + template_source_path = engine_root / 'TestTemplates' - dev_gem_code_include_platform_salem = f'{dev_root}/TestTemplate/Code/Include/Platform/Salem' - os.makedirs(dev_gem_code_include_platform_salem, exist_ok=True) + engine_gem_code_include_testgem = template_source_path / 'Code/Include/TestTemplate' + engine_gem_code_include_testgem.mkdir(parents=True, exist_ok=True) - restricted_gem_bus_file = f'{dev_gem_code_include_platform_salem}/TestTemplateBus.h' - if os.path.isfile(restricted_gem_bus_file): - os.unlink(restricted_gem_bus_file) - with open(restricted_gem_bus_file, 'w') as s: + gem_bus_file = engine_gem_code_include_testgem / 'TestTemplateBus.h' + with gem_bus_file.open('w') as s: s.write(concrete_contents) - template_folder = f'{dev_root}/Templates' - os.makedirs(template_folder, exist_ok=True) + engine_gem_code_include_platform_salem = template_source_path / 'Code/Include/Platform/Salem' + engine_gem_code_include_platform_salem.mkdir(parents=True, exist_ok=True) + + restricted_gem_bus_file = engine_gem_code_include_platform_salem / 'TestTemplateBus.h' + with restricted_gem_bus_file.open('w') as s: + s.write(concrete_contents) - restricted_folder = f'{dev_root}/restricted' - os.makedirs(restricted_folder, exist_ok=True) + template_folder = engine_root / 'Templates' + template_folder.mkdir(parents=True, exist_ok=True) - result = engine_template.create_template(dev_root, 'TestTemplate', 'Default', keep_license_text=keep_license_text) + result = engine_template.create_template(template_source_path, template_folder, source_name='TestTemplate', + keep_license_text=keep_license_text, force=force) if expect_failure: assert result != 0 else: assert result == 0 - new_template_folder = f'{template_folder}/Default' - assert os.path.isdir(new_template_folder) - new_template_json = f'{new_template_folder}/template.json' - assert os.path.isfile(new_template_json) - with open(new_template_json, 'r') as s: + new_template_folder = template_folder + assert new_template_folder.is_dir() + new_template_json = new_template_folder / 'template.json' + assert new_template_json.is_file() + with new_template_json.open('r') as s: s_data = s.read() - assert s_data == template_json_contents + assert json.loads(s_data) == json.loads(template_json_contents) - new_default_name_bus_file = f'{new_template_folder}/Template/Code/Include/' + '${Name}/${Name}Bus.h' - assert os.path.isfile(new_default_name_bus_file) - with open(new_default_name_bus_file, 'r') as s: + template_content_folder = new_template_folder / 'Template' + new_default_name_bus_file = template_content_folder / 'Code/Include/${Name}/${Name}Bus.h' + assert new_default_name_bus_file.is_file() + with new_default_name_bus_file.open('r') as s: s_data = s.read() if keep_license_text: assert s_data == templated_contents_with_license else: assert s_data == templated_contents_without_license - restricted_template_folder = f'{dev_root}/restricted/Salem/Templates' + platform_template_folder = engine_root / 'Salem/Templates' - new_restricted_template_folder = f'{restricted_template_folder}/Default' - assert os.path.isdir(new_restricted_template_folder) - new_restricted_template_json = f'{new_restricted_template_folder}/template.json' - assert os.path.isfile(new_restricted_template_json) - with open(new_restricted_template_json, 'r') as s: - s_data = s.read() - assert s_data == restricted_template_json_contents - - new_restricted_default_name_bus_file = f'{restricted_template_folder}' \ - f'/Default/Template/Code/Include/Platform/Salem/' + '${Name}Bus.h' - assert os.path.isfile(new_restricted_default_name_bus_file) - with open(new_restricted_default_name_bus_file, 'r') as s: + new_platform_default_name_bus_file = template_content_folder / 'Code/Include/Platform/Salem/${Name}Bus.h' + assert new_platform_default_name_bus_file.is_file() + with new_platform_default_name_bus_file.open('r') as s: s_data = s.read() if keep_license_text: assert s_data == templated_contents_with_license @@ -505,244 +230,164 @@ def test_create_template(tmpdir, assert s_data == templated_contents_without_license -@pytest.mark.parametrize( - "concrete_contents, templated_contents," - " keep_license_text, expect_failure," - " template_json_contents, restricted_template_json_contents", [ - pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - True, False, - TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS), - pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - False, False, - TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS) - ] -) -def test_create_from_template(tmpdir, - concrete_contents, templated_contents, - keep_license_text, expect_failure, - template_json_contents, restricted_template_json_contents): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - template_default_folder = f'{dev_root}/Templates/Default' - os.makedirs(template_default_folder, exist_ok=True) - - template_json = f'{template_default_folder}/template.json' - if os.path.isfile(template_json): - os.unlink(template_json) - with open(template_json, 'w') as s: - s.write(template_json_contents) - - default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}' - os.makedirs(default_name_bus_dir, exist_ok=True) - - default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(default_name_bus_file): - os.unlink(default_name_bus_file) - with open(default_name_bus_file, 'w') as s: - s.write(templated_contents) - - restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/Default' - os.makedirs(restricted_template_default_folder, exist_ok=True) - - restricted_template_json = f'{restricted_template_default_folder}/template.json' - if os.path.isfile(restricted_template_json): - os.unlink(restricted_template_json) - with open(restricted_template_json, 'w') as s: - s.write(restricted_template_json_contents) - - restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem' - os.makedirs(restricted_default_name_bus_dir, exist_ok=True) - - restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(restricted_default_name_bus_file): - os.unlink(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'w') as s: - s.write(templated_contents) - - result = engine_template.create_from_template(dev_root, 'TestTemplate', 'Default', - keep_license_text=keep_license_text) - if expect_failure: - assert result != 0 - else: - assert result == 0 - - test_folder = f'{dev_root}/TestTemplate' - assert os.path.isdir(test_folder) - - test_bus_file = f'{test_folder}/Code/Include/TestTemplate/TestTemplateBus.h' - assert os.path.isfile(test_bus_file) - with open(test_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents - - restricted_test_bus_folder = f'{dev_root}/restricted/Salem/TestTemplate/Code/Include/Platform/Salem' - assert os.path.isdir(restricted_test_bus_folder) - - restricted_default_name_bus_file = f'{restricted_test_bus_folder}/TestTemplateBus.h' - assert os.path.isfile(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents - - -@pytest.mark.parametrize( - "concrete_contents, templated_contents," - " keep_license_text, expect_failure," - " template_json_contents, restricted_template_json_contents", [ - pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - True, False, - TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS), - pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - False, False, - TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS) - ] -) -def test_create_project(tmpdir, - concrete_contents, templated_contents, - keep_license_text, expect_failure, - template_json_contents, restricted_template_json_contents): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - template_default_folder = f'{dev_root}/Templates/DefaultProject' - os.makedirs(template_default_folder, exist_ok=True) - - template_json = f'{template_default_folder}/template.json' - if os.path.isfile(template_json): - os.unlink(template_json) - with open(template_json, 'w') as s: - s.write(template_json_contents) - - default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}' - os.makedirs(default_name_bus_dir, exist_ok=True) - - default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(default_name_bus_file): - os.unlink(default_name_bus_file) - with open(default_name_bus_file, 'w') as s: - s.write(templated_contents) - - restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/DefaultProject' - os.makedirs(restricted_template_default_folder, exist_ok=True) - - restricted_template_json = f'{restricted_template_default_folder}/template.json' - if os.path.isfile(restricted_template_json): - os.unlink(restricted_template_json) - with open(restricted_template_json, 'w') as s: - s.write(restricted_template_json_contents) - - restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem' - os.makedirs(restricted_default_name_bus_dir, exist_ok=True) - - restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(restricted_default_name_bus_file): - os.unlink(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'w') as s: - s.write(templated_contents) - - result = engine_template.create_project(dev_root, 'TestProject', keep_license_text=keep_license_text) - - if expect_failure: - assert result != 0 - else: - assert result == 0 - - test_project_folder = f'{dev_root}/TestProject' - assert os.path.isdir(test_project_folder) - - test_project_bus_file = f'{test_project_folder}/Code/Include/TestProject/TestProjectBus.h' - assert os.path.isfile(test_project_bus_file) - with open(test_project_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents - - restricted_test_project_bus_folder = f'{dev_root}/restricted/Salem/TestProject/Code/Include/Platform/Salem' - assert os.path.isdir(restricted_test_project_bus_folder) - - restricted_default_name_bus_file = f'{restricted_test_project_bus_folder}/TestProjectBus.h' - assert os.path.isfile(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents - - -@pytest.mark.parametrize( - "concrete_contents, templated_contents," - " keep_license_text, expect_failure," - " template_json_contents, restricted_template_json_contents", [ - pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - True, False, - TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS), - pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, - False, False, - TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS) - ] -) -def test_create_gem(tmpdir, - concrete_contents, templated_contents, - keep_license_text, expect_failure, - template_json_contents, restricted_template_json_contents): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - template_default_folder = f'{dev_root}/Templates/DefaultGem' - os.makedirs(template_default_folder, exist_ok=True) - - template_json = f'{template_default_folder}/template.json' - if os.path.isfile(template_json): - os.unlink(template_json) - with open(template_json, 'w') as s: - s.write(template_json_contents) - - default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}' - os.makedirs(default_name_bus_dir, exist_ok=True) - - default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(default_name_bus_file): - os.unlink(default_name_bus_file) - with open(default_name_bus_file, 'w') as s: - s.write(templated_contents) - - restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/DefaultGem' - os.makedirs(restricted_template_default_folder, exist_ok=True) - - restricted_template_json = f'{restricted_template_default_folder}/template.json' - if os.path.isfile(restricted_template_json): - os.unlink(restricted_template_json) - with open(restricted_template_json, 'w') as s: - s.write(restricted_template_json_contents) - - restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem' - os.makedirs(restricted_default_name_bus_dir, exist_ok=True) - - restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h' - if os.path.isfile(restricted_default_name_bus_file): - os.unlink(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'w') as s: - s.write(templated_contents) - - result = engine_template.create_gem(dev_root, 'TestGem', keep_license_text=keep_license_text) - - if expect_failure: - assert result != 0 - else: - assert result == 0 - - test_gem_folder = f'{dev_root}/Gems/TestGem' - assert os.path.isdir(test_gem_folder) - - test_gem_bus_file = f'{test_gem_folder}/Code/Include/TestGem/TestGemBus.h' - assert os.path.isfile(test_gem_bus_file) - with open(test_gem_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents - - restricted_test_gem_bus_folder = f'{dev_root}/restricted/Salem/Gems/TestGem/Code/Include/Platform/Salem' - assert os.path.isdir(restricted_test_gem_bus_folder) - - restricted_default_name_bus_file = f'{restricted_test_gem_bus_folder}/TestGemBus.h' - assert os.path.isfile(restricted_default_name_bus_file) - with open(restricted_default_name_bus_file, 'r') as s: - s_data = s.read() - assert s_data == concrete_contents +class TestCreateTemplate: + def instantiate_template_wrapper(self, tmpdir, create_from_template_func, instantiated_name, + concrete_contents, templated_contents, + keep_license_text, force, expect_failure, + template_json_contents, template_file_creation_map = {}, + **create_from_template_kwargs): + # Use a SHA-1 Hash of the destination_name for every Random_Uuid for determinism in the test + concrete_contents = string.Template(concrete_contents).safe_substitute( + {'Random_Uuid': uuid.uuid5(uuid.NAMESPACE_DNS, instantiated_name)}) + + engine_root = (pathlib.Path(tmpdir) / 'engine-root').resolve() + engine_root.mkdir(parents=True, exist_ok=True) + + template_default_folder = engine_root / 'Templates/Default' + template_default_folder.mkdir(parents=True, exist_ok=True) + + template_json = template_default_folder / 'template.json' + with template_json.open('w') as s: + s.write(template_json_contents) + + for file_template_filename, file_template_content in template_file_creation_map.items(): + file_template_path = template_default_folder / 'Template' / file_template_filename + file_template_path.parent.mkdir(parents=True, exist_ok=True) + with file_template_path.open('w') as file_template_handle: + file_template_handle.write(file_template_content) + + default_name_bus_dir = template_default_folder / 'Template/Code/Include/${Name}' + default_name_bus_dir.mkdir(parents=True, exist_ok=True) + + default_name_bus_file = default_name_bus_dir / '${Name}Bus.h' + with default_name_bus_file.open('w') as s: + s.write(templated_contents) + + template_content_folder = template_default_folder / 'Template' + platform_default_name_bus_dir = template_content_folder / 'Code/Include/Platform/Salem' + platform_default_name_bus_dir.mkdir(parents=True, exist_ok=True) + + platform_default_name_bus_file = platform_default_name_bus_dir / '${Name}Bus.h' + with platform_default_name_bus_file.open('w') as s: + s.write(templated_contents) + + template_dest_path = engine_root / instantiated_name + with patch('uuid.uuid4', return_value=uuid.uuid5(uuid.NAMESPACE_DNS, instantiated_name)) as uuid4_mock: + result = create_from_template_func(template_dest_path, template_path=template_default_folder, force=True, + keep_license_text=keep_license_text, **create_from_template_kwargs) + if expect_failure: + assert result != 0 + else: + assert result == 0 + + test_folder = template_dest_path + assert test_folder.is_dir() + + test_bus_file = test_folder / f'Code/Include/{instantiated_name}/{instantiated_name}Bus.h' + assert test_bus_file.is_file() + with test_bus_file.open('r') as s: + s_data = s.read() + assert s_data == concrete_contents + + platform_test_bus_folder = test_folder / 'Code/Include/Platform/Salem' + assert platform_test_bus_folder.is_dir() + + platform_default_name_bus_file = platform_test_bus_folder / f'{instantiated_name}Bus.h' + assert platform_default_name_bus_file.is_file() + with platform_default_name_bus_file.open('r') as s: + s_data = s.read() + assert s_data == concrete_contents + + + # Use a SHA-1 Hash of the destination_name for every Random_Uuid for determinism in the test + @pytest.mark.parametrize( + "concrete_contents, templated_contents," + " keep_license_text, force, expect_failure," + " template_json_contents", [ + pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + True, True, False, + TEST_TEMPLATE_JSON_CONTENTS), + pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + False, True, False, + TEST_TEMPLATE_JSON_CONTENTS) + ] + ) + def test_create_from_template(self, tmpdir, concrete_contents, templated_contents, keep_license_text, force, + expect_failure, template_json_contents): + + self.instantiate_template_wrapper(tmpdir, engine_template.create_from_template, 'TestTemplate', concrete_contents, + templated_contents, keep_license_text, force, expect_failure, + template_json_contents, destination_name='TestTemplate') + + + @pytest.mark.parametrize( + "concrete_contents, templated_contents," + " keep_license_text, force, expect_failure," + " template_json_contents", [ + pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + True, True, False, + TEST_TEMPLATE_JSON_CONTENTS), + pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + False, True, False, + TEST_TEMPLATE_JSON_CONTENTS) + ] + ) + def test_create_project(self, tmpdir, concrete_contents, templated_contents, keep_license_text, force, + expect_failure, template_json_contents): + template_file_map = { 'project.json': + ''' + { + "project_name": "${Name}" + } + '''} + + # Append the project.json to the list of files to copy from the template + template_json_dict = json.loads(template_json_contents) + template_json_dict.setdefault('copyFiles', []).append( + { + "file": "project.json", + "origin": "project.json", + "isTemplated": True, + "isOptional": False + }) + # Convert the python dictionary back into a json string + template_json_contents = json.dumps(template_json_dict, indent=4) + self.instantiate_template_wrapper(tmpdir, engine_template.create_project, 'TestProject', concrete_contents, + templated_contents, keep_license_text, force, expect_failure, + template_json_contents, template_file_map, project_name='TestProject') + + + @pytest.mark.parametrize( + "concrete_contents, templated_contents," + " keep_license_text, force, expect_failure," + " template_json_contents", [ + pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + True, True, False, + TEST_TEMPLATE_JSON_CONTENTS), + pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE, + False, True, False, + TEST_TEMPLATE_JSON_CONTENTS) + ] + ) + def test_create_gem(self, tmpdir, concrete_contents, templated_contents, keep_license_text, force, + expect_failure, template_json_contents): + # Create a gem.json file in the template folder + template_file_map = {'gem.json': + ''' + { + "gem_name": "${Name}" + } + '''} + + # Append the gem.json to the list of files to copy from the template + template_json_dict = json.loads(template_json_contents) + template_json_dict.setdefault('copyFiles', []).append( + { + "file": "gem.json", + "origin": "gem.json", + "isTemplated": True, + "isOptional": False + }) + self.instantiate_template_wrapper(tmpdir, engine_template.create_gem, 'TestGem', concrete_contents, + templated_contents, keep_license_text, force, expect_failure, + template_json_contents, gem_name='TestGem')