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')