From 21ca3a4aea3b79b530e9abc1f0e2618aae88f05d Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Wed, 4 Aug 2021 13:13:28 -0500 Subject: [PATCH] The enable-gem command registers gem with project if not registered (#2817) * The enable_gems command now registers the gem with the project if only registered with o3de_manifest.json Updated the `enable_gems` command to register the gem with the project if the gem is not registered with either the project or the engine being used. This allows the gem to be added to the build system if it wasn't registered before. Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Adding quoting around the invocation of the OpenProjectManager command The --project-path parameter now is able to pass in a path with spaces to the invocation of the Project Manager. Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> --- Code/Editor/CryEdit.cpp | 9 +- scripts/o3de/o3de/enable_gem.py | 22 ++- scripts/o3de/o3de/register.py | 6 +- scripts/o3de/tests/CMakeLists.txt | 7 + scripts/o3de/tests/unit_test_enable_gem.py | 126 ++++++++++++++++++ .../tests/unit_test_project_properties.py | 36 +++-- 6 files changed, 183 insertions(+), 23 deletions(-) create mode 100644 scripts/o3de/tests/unit_test_enable_gem.py diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 4bfc6a319d..61d4fd5aea 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -2901,7 +2901,14 @@ void CCryEditApp::OpenProjectManager(const AZStd::string& screen) { // provide the current project path for in case we want to update the project AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath(); - const AZStd::string commandLineOptions = AZStd::string::format(" --screen %s --project-path %s", screen.c_str(), projectPath.c_str()); +#if !AZ_TRAIT_OS_PLATFORM_APPLE && !AZ_TRAIT_OS_USE_WINDOWS_FILE_PATHS + const char* argumentQuoteString = R"(")"; +#else + const char* argumentQuoteString = R"(\")"; +#endif + const AZStd::string commandLineOptions = AZStd::string::format(R"( --screen %s --project-path %s%s%s)", + screen.c_str(), + argumentQuoteString, projectPath.c_str(), argumentQuoteString); bool launchSuccess = AzFramework::ProjectManager::LaunchProjectManager(commandLineOptions); if (!launchSuccess) { diff --git a/scripts/o3de/o3de/enable_gem.py b/scripts/o3de/o3de/enable_gem.py index 67e1624086..1614dc4eda 100644 --- a/scripts/o3de/o3de/enable_gem.py +++ b/scripts/o3de/o3de/enable_gem.py @@ -16,7 +16,7 @@ import os import pathlib import sys -from o3de import cmake, manifest, validation +from o3de import cmake, manifest, register, validation logger = logging.getLogger() logging.basicConfig() @@ -87,8 +87,7 @@ def enable_gem_in_project(gem_name: str = None, if not enabled_gem_file.is_file(): logger.error(f'Enabled gem file {enabled_gem_file} is not present.') return 1 - # add the gem - ret_val = cmake.add_gem_dependency(enabled_gem_file, gem_json_data['gem_name']) + project_enabled_gem_file = enabled_gem_file else: # Find the path to enabled gem file. @@ -96,8 +95,21 @@ def enable_gem_in_project(gem_name: str = None, project_enabled_gem_file = cmake.get_enabled_gem_cmake_file(project_path=project_path) if not project_enabled_gem_file.is_file(): project_enabled_gem_file.touch() - # add the gem - ret_val = cmake.add_gem_dependency(project_enabled_gem_file, gem_json_data['gem_name']) + + # Before adding the gem_dependency check if the project is registered in either the project or engine + # manifest + buildable_gems = manifest.get_engine_gems() + buildable_gems.extend(manifest.get_project_gems(project_path)) + # Convert each path to pathlib.Path object and filter out duplictes using dict.fromkeys + buildable_gems = list(dict.fromkeys(map(lambda gem_path_string: pathlib.Path(gem_path_string), buildable_gems))) + + ret_val = 0 + # If the gem is not part of buildable set, it needs to be registered + if not gem_path in buildable_gems: + ret_val = register.register(gem_path=gem_path, external_subdir_project_path=project_path) + + # add the gem if it is registered in either the project.json or engine.json + ret_val = ret_val or cmake.add_gem_dependency(project_enabled_gem_file, gem_json_data['gem_name']) return ret_val diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index 8481c5fae0..7e182b5d2d 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -285,14 +285,14 @@ def register_o3de_object_path(json_data: dict, manifest_data = None if engine_path: - manifest_data = manifest.get_engine_json_data(json_data, engine_path) + manifest_data = manifest.get_engine_json_data(None, engine_path) if not manifest_data: logger.error(f'Cannot load engine.json data at path {engine_path}') return 1 save_path = engine_path / 'engine.json' elif project_path: - manifest_data = manifest.get_project_json_data(json_data, project_path) + manifest_data = manifest.get_project_json_data(None, project_path) if not manifest_data: logger.error(f'Cannot load project.json data at path {project_path}') return 1 @@ -329,7 +329,7 @@ def register_o3de_object_path(json_data: dict, try: o3de_object_path = o3de_object_path.relative_to(save_path.parent) except ValueError: - pass # It is OK relative path cannot be formed + pass # It is OK relative path cannot be formed manifest_data[o3de_object_key].insert(0, o3de_object_path.as_posix()) if save_path: manifest.save_o3de_manifest(manifest_data, save_path) diff --git a/scripts/o3de/tests/CMakeLists.txt b/scripts/o3de/tests/CMakeLists.txt index 1cd6eac7ee..de8e9e4974 100644 --- a/scripts/o3de/tests/CMakeLists.txt +++ b/scripts/o3de/tests/CMakeLists.txt @@ -25,6 +25,13 @@ ly_add_pytest( EXCLUDE_TEST_RUN_TARGET_FROM_IDE ) +ly_add_pytest( + NAME o3de_enable_gem + PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_enable_gem.py + TEST_SUITE smoke + EXCLUDE_TEST_RUN_TARGET_FROM_IDE +) + ly_add_pytest( NAME o3de_global_project PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_global_project.py diff --git a/scripts/o3de/tests/unit_test_enable_gem.py b/scripts/o3de/tests/unit_test_enable_gem.py new file mode 100644 index 0000000000..12896a51ba --- /dev/null +++ b/scripts/o3de/tests/unit_test_enable_gem.py @@ -0,0 +1,126 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +import io +import json +import logging + +import pytest +import pathlib +from unittest.mock import patch + +from o3de import enable_gem + + +TEST_PROJECT_JSON_PAYLOAD = ''' +{ + "project_name": "TestProject", + "origin": "The primary repo for TestProject goes here: i.e. http://www.mydomain.com", + "license": "What license TestProject uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "TestProject", + "summary": "A short description of TestProject.", + "canonical_tags": [ + "Project" + ], + "user_tags": [ + "TestProject" + ], + "icon_path": "preview.png", + "engine": "o3de-install", + "restricted_name": "projects", + "external_subdirectories": [ + ] +} +''' + +TEST_GEM_JSON_PAYLOAD = ''' +{ + "gem_name": "TestGem", + "display_name": "TestGem", + "license": "What license TestGem uses goes here: i.e. https://opensource.org/licenses/MIT", + "origin": "The primary repo for TestGem goes here: i.e. http://www.mydomain.com", + "type": "Code", + "summary": "A short description of TestGem.", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "TestGem" + ], + "icon_path": "preview.png", + "requirements": "" +} +''' + + +@pytest.fixture(scope='class') +def init_enable_gem_data(request): + class EnableGemData: + def __init__(self): + self.project_data = json.loads(TEST_PROJECT_JSON_PAYLOAD) + self.gem_data = json.loads(TEST_GEM_JSON_PAYLOAD) + request.cls.enable_gem = EnableGemData() + + +@pytest.mark.usefixtures('init_enable_gem_data') +class TestEnableGemCommand: + @pytest.mark.parametrize("gem_path, project_path, gem_registered_with_project, gem_registered_with_engine," + "expected_result", [ + pytest.param(pathlib.PurePath('E:/TestGem'), pathlib.PurePath('E:/TestProject'), False, True, 0), + pytest.param(pathlib.PurePath('E:/TestGem'), pathlib.PurePath('E:/TestProject'), False, False, 0), + pytest.param(pathlib.PurePath('E:/TestGem'), pathlib.PurePath('E:/TestProject'), True, False, 0), + ] + ) + def test_enable_gem_registers_gem_as_well(self, gem_path, project_path, gem_registered_with_project, gem_registered_with_engine, + expected_result): + + def get_registered_path(project_name: str = None, gem_name: str = None) -> pathlib.Path: + if project_name: + return project_path + elif gem_name: + return gem_path + return None + + def get_registered_gem_path(gem_name: str) -> pathlib.Path: + return gem_path + + def save_o3de_manifest(new_project_data: dict, manifest_path: pathlib.Path = None) -> bool: + if manifest_path == project_path: + self.enable_gem.project_data = new_project_data + return True + + def get_project_json_data(json_data: pathlib.Path, project_path: pathlib.Path): + return self.enable_gem.project_data + + def get_gem_json_data(gem_path: pathlib.Path, project_path: pathlib.Path): + return self.enable_gem.gem_data + + def get_project_gems(project_path: pathlib.Path): + return [gem_path] if gem_registered_with_project else [] + + def get_engine_gems(): + return [gem_path] if gem_registered_with_engine else [] + + def add_gem_dependency(enable_gem_cmake_file: pathlib.Path, gem_name: str): + return 0 + + with patch('pathlib.Path.is_dir', return_value=True) as pathlib_is_dir_patch,\ + patch('pathlib.Path.is_file', return_value=True) as pathlib_is_file_patch,\ + patch('o3de.manifest.save_o3de_manifest', side_effect=save_o3de_manifest) as save_o3de_manifest_patch,\ + patch('o3de.manifest.get_registered', side_effect=get_registered_path) as get_registered_patch,\ + patch('o3de.manifest.get_gem_json_data', side_effect=get_gem_json_data) as get_gem_json_data_patch,\ + patch('o3de.manifest.get_project_json_data', side_effect=get_project_json_data) as get_gem_json_data_patch,\ + patch('o3de.manifest.get_project_gems', side_effect=get_project_gems) as get_project_gems_patch,\ + patch('o3de.manifest.get_engine_gems', side_effect=get_engine_gems) as get_engine_gems_patch,\ + patch('o3de.cmake.add_gem_dependency', side_effect=add_gem_dependency) as add_gem_dependency_patch,\ + patch('o3de.validation.valid_o3de_gem_json', return_value=True) as valid_gem_json_patch: + result = enable_gem.enable_gem_in_project(gem_path=gem_path, project_path=project_path) + assert result == expected_result + # If the gem isn't registered with the engine or project already it should now be registered with the project + if not gem_registered_with_engine and gem_registered_with_project: + assert gem_path.as_posix() in self.enable_gem.project_data.get('external_subdirectories', []) diff --git a/scripts/o3de/tests/unit_test_project_properties.py b/scripts/o3de/tests/unit_test_project_properties.py index f72a4dfe4c..0236e6cf90 100644 --- a/scripts/o3de/tests/unit_test_project_properties.py +++ b/scripts/o3de/tests/unit_test_project_properties.py @@ -6,33 +6,41 @@ # # +import json import pytest import pathlib from unittest.mock import patch from o3de import project_properties -TEST_DEFAULT_PROJECT_DATA = { - "template_name": "DefaultProject", - "restricted_name": "o3de", - "restricted_platform_relative_path": "Templates", - "origin": "The primary repo for DefaultProject goes here: i.e. http://www.mydomain.com", - "license": "What license DefaultProject uses goes here: i.e. https://opensource.org/licenses/MIT", - "display_name": "Default", - "summary": "A short description of DefaultProject.", - "included_gems": ["Atom","Camera","EMotionFX","UI","Maestro","Input","ImGui"], - "canonical_tags": [], - "user_tags": [ - "DefaultProject" +TEST_PROJECT_JSON_PAYLOAD = ''' +{ + "project_name": "TestProject", + "origin": "The primary repo for TestProject goes here: i.e. http://www.mydomain.com", + "license": "What license TestProject uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "TestProject", + "summary": "A short description of TestProject.", + "canonical_tags": [ + "Project" ], - "icon_path": "preview.png" + "user_tags": [ + "TestProject" + ], + "icon_path": "preview.png", + "engine": "o3de-install", + "restricted_name": "projects", + "external_subdirectories": [ + "D:/TestGem" + ] } +''' + @pytest.fixture(scope='class') def init_project_json_data(request): class ProjectJsonData: def __init__(self): - self.data = TEST_DEFAULT_PROJECT_DATA + self.data = json.loads(TEST_PROJECT_JSON_PAYLOAD) request.cls.project_json = ProjectJsonData() @pytest.mark.usefixtures('init_project_json_data')