From 74464afbf3a31ea0e82c766444fb34e5bd4c451a Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Thu, 27 May 2021 21:20:19 -0500 Subject: [PATCH] Updated the global_project.py script to be able to specify and output path when setting the global project path Also updated that script to support an input path when reading the global project path. Added a unit test for the global_projecy.py "set-global-project" command --- scripts/o3de/o3de/disable_gem.py | 2 +- scripts/o3de/o3de/enable_gem.py | 2 +- scripts/o3de/o3de/global_project.py | 221 +++++++++++------- scripts/o3de/o3de/manifest.py | 12 +- scripts/o3de/o3de/register.py | 8 +- scripts/o3de/o3de/sha256.py | 2 +- scripts/o3de/tests/CMakeLists.txt | 7 + scripts/o3de/tests/unit_global_project.py | 40 ++++ .../o3de/tests/unit_test_current_project.py | 102 -------- 9 files changed, 192 insertions(+), 204 deletions(-) create mode 100644 scripts/o3de/tests/unit_global_project.py delete mode 100755 scripts/o3de/tests/unit_test_current_project.py diff --git a/scripts/o3de/o3de/disable_gem.py b/scripts/o3de/o3de/disable_gem.py index 6c466c3631..61d71445f0 100644 --- a/scripts/o3de/o3de/disable_gem.py +++ b/scripts/o3de/o3de/disable_gem.py @@ -83,7 +83,7 @@ def disable_gem_in_project(gem_name: str = None, project_path = manifest.get_registered(project_name=project_name) if not project_path: logger.error(f'Unable to locate project path from the registered manifest.json files:' - f' {str(pathlib.Path.home() / ".o3de/manifest.json")}, engine.json') + f' {str(pathlib.Path("~/.o3de/o3de_manifest.json").expanduser())}, engine.json') return 1 project_path = pathlib.Path(project_path).resolve() diff --git a/scripts/o3de/o3de/enable_gem.py b/scripts/o3de/o3de/enable_gem.py index 73fc2ea3cf..0dee01e05e 100644 --- a/scripts/o3de/o3de/enable_gem.py +++ b/scripts/o3de/o3de/enable_gem.py @@ -117,7 +117,7 @@ def enable_gem_in_project(gem_name: str = None, gem_path = manifest.get_registered(gem_name=gem_name) if not gem_path: logger.error(f'Unable to locate gem path from the registered manifest.json files:' - f' {str(pathlib.Path.home() / ".o3de/manifest.json")},' + f' {str(pathlib.Path( "~/.o3de/o3de_manifest.json").expanduser())},' f' {project_path / "project.json"}, engine.json') return 1 diff --git a/scripts/o3de/o3de/global_project.py b/scripts/o3de/o3de/global_project.py index 787e676a7e..d165510250 100644 --- a/scripts/o3de/o3de/global_project.py +++ b/scripts/o3de/o3de/global_project.py @@ -16,94 +16,122 @@ import sys import re import pathlib import json -from o3de import manifest + +from o3de import manifest, validation logger = logging.getLogger() logging.basicConfig() +DEFAULT_BOOTSTRAP_SETREG = pathlib.Path('~/.o3de/Registry/bootstrap.setreg').expanduser() +PROJECT_PATH_KEY = ('Amazon', 'AzCore', 'Bootstrap', 'project_path') -def set_global_project(project_name: str or None, - project_path: str or pathlib.Path or None) -> int: +def get_json_data(input_path: pathlib.Path): + setreg_json_data = {} + # If the output_path exist validate that it is a valid json file + if input_path.is_file(): + with input_path.open('r') as f: + try: + setreg_json_data = json.load(f) + except json.JSONDecodeError as e: + logger.error(f'The file: {input_path} is not a valid json file: {str(e)}') + + return setreg_json_data + +def set_global_project(output_path: pathlib.Path, + project_name: str = None, + project_path: pathlib.Path = None, + force: bool = False) -> int: """ - set what the current project is - :param project_name: the name of the project you want to set, resolves project_path - :param project_path: the path of the project you want to set + Adds a project path the a settings registry file in the users ~/.o3de/Registry directory + :param output_path: path to .setreg file to store project_path value into + :param project_name: name of the project to lookup path for + :param project_path: path to the project to add to .setreg file + :param force: if set, the project path will be set within the .setreg file regardless of if the path doesn't exist :return: 0 for success or non 0 failure code """ - if project_path and project_name: - logger.error(f'Project Name and Project Path provided, these are mutually exclusive.') - return 1 - + # we need either a project name or path if not project_name and not project_path: - logger.error('Must specify either a Project name or Project Path.') + logger.error(f'Must either specify a Project path or Project Name.') return 1 + # if project name resolve it into a path if project_name and not project_path: project_path = manifest.get_registered(project_name=project_name) if not project_path: - logger.error(f'Project Path {project_path} has not been registered.') + logger.error( + f'The project name has been supplied. Unable to locate project path from the registered manifest.json files:' + f' {str(pathlib.Path("~/.o3de/o3de_manifest.json").expanduser())}, engine.json\n' + 'A The --project-path parameter can be used directly to skip checking the manifest') return 1 - project_path = pathlib.Path(project_path).resolve() + # Only perform project path validations when force=False + if not force: + if not project_path.is_dir(): + logger.error(f'Project path {project_path} is not a folder.') + return 1 - bootstrap_setreg_file = manifest.get_o3de_registry_folder() / 'bootstrap.setreg' - if bootstrap_setreg_file.is_file(): - with bootstrap_setreg_file.open('r') as f: + # Validate that the supplied path points contains a valid project.json + if not validation.valid_o3de_project_json(project_path / 'project.json'): + logger.error(f'The supplied project path does not contain a valid project.json.\n' + f'The Path will not be set') + return 1 + + # If the output_path exist validate that it is a valid json file and read it's json data + setreg_json_data = get_json_data(output_path) + if output_path.is_file(): + with output_path.open('r') as f: try: - json_data = json.load(f) - except json.JSONDecodeError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - json_data["Amazon"]["AzCore"]["Bootstrap"]["project_path"] = project_path - except KeyError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - os.unlink(bootstrap_setreg_file) - except OSError as e: - logger.error(f'Failed to unlink bootstrap file {bootstrap_setreg_file}: {str(e)}') - return 1 - else: - json_data = {} - json_data.update({"Amazon":{"AzCore":{"Bootstrap":{"project_path":project_path.as_posix()}}}}) + setreg_json_data = json.load(f) + except (json.JSONDecodeError) as e: + logger.error(f'The output file: {output_path} is not a valid json file: {str(e)}') + return 1 - with bootstrap_setreg_file.open('w') as s: - s.write(json.dumps(json_data, indent=4)) + # Add a json dictionary that will be merged with any existing json data from the .setreg file + merge_json_data = {} + json_object_iter = merge_json_data + for json_key in PROJECT_PATH_KEY[:-1]: + # Add the parent json object for the key to update + json_object_iter = json_object_iter.setdefault(json_key, {}) + + # Set the project path value here + json_object_iter[PROJECT_PATH_KEY[-1]] = project_path.as_posix() + setreg_json_data.update(merge_json_data) + + # Create the parent directories + if output_path.parent: + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + with output_path.open('w') as s: + s.write(json.dumps(setreg_json_data, indent=4) + '\n') + except OSError as e: + logger.error(f'Failed to write project path {project_path} to file {output_path}: {str(e)}') + return 1 return 0 -def get_global_project() -> pathlib.Path or None: +def get_global_project(input_path: pathlib.Path) -> pathlib.Path or None: """ - get what the current project set is + Retrieves the /Amazon/AzCore/Bootstrap/project_path key from the supplied file path :return: project_path or None on failure """ - bootstrap_setreg_file = manifest.get_o3de_registry_folder() / 'bootstrap.setreg' - if not bootstrap_setreg_file.is_file(): - logger.error(f'Bootstrap.setreg file {bootstrap_setreg_file} does not exist.') - return None + setreg_json_data = get_json_data(input_path) - with bootstrap_setreg_file.open('r') as f: - try: - json_data = json.load(f) - except json.JSONDecodeError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - project_path = json_data["Amazon"]["AzCore"]["Bootstrap"]["project_path"] - except KeyError as e: - logger.error(f'Bootstrap.setreg cannot find Amazon:AzCore:Bootstrap:project_path: {str(e)}') - else: - return pathlib.Path(project_path).resolve() + try: + # Iterate over each element of the tuple and read the json key from each successive json object + json_object_iter = setreg_json_data + for json_key in PROJECT_PATH_KEY: + json_object_iter = json_object_iter[json_key] + except KeyError as e: + logger.error(f'Cannot read key /{"/".join(PROJECT_PATH_KEY)} from file {input_path.as_posix()}: {str(e)}') + else: + project_path = json_object_iter + return pathlib.Path(project_path).resolve() return None def _run_get_global_project(args: argparse) -> int: - if args.override_home_folder: - manifest.override_home_folder = args.override_home_folder - - project_path = get_global_project() + project_path = get_global_project(args.input_path) if project_path: print(project_path.as_posix()) return 0 @@ -111,51 +139,66 @@ def _run_get_global_project(args: argparse) -> int: def _run_set_global_project(args: argparse) -> int: - if args.override_home_folder: - manifest.override_home_folder = args.override_home_folder + return set_global_project(args.output_path, + args.project_name, + args.project_path, + args.force) - return set_global_project(args.project_name, - args.project_path) +def add_parser_args(get_project_parser, set_project_parser): + """ + add_parser_args is called to add arguments to each command such that it can be + invoked locally or added by a central python file. + Ex. Directly run from this file alone with: python global_project.py --project-path "D:/TestProject" + :param parser: the caller passes an argparse parser like instance to this method + """ + + # get-current-project + get_project_parser.add_argument('-i', '--input-path', type=pathlib.Path, required=False, default=DEFAULT_BOOTSTRAP_SETREG, + help=f'Optional path to file to read /{"/".join(PROJECT_PATH_KEY)} key from.' + f' If not supplied, then {DEFAULT_BOOTSTRAP_SETREG} is used instead') + get_project_parser.set_defaults(func=_run_get_global_project) + + # set-current-project + group = set_project_parser.add_mutually_exclusive_group(required=True) + group.add_argument('-pp', '--project-path', type=pathlib.Path, required=False, + help='The path to the project.') + group.add_argument('-pn', '--project-name', type=str, required=False, + help='The name of the project.') + set_project_parser.add_argument('-o', '--output-path', type=pathlib.Path, required=False, + default=DEFAULT_BOOTSTRAP_SETREG, + help=f'Optional path to output file to write project_path key to. ' + f'If not supplied, then {DEFAULT_BOOTSTRAP_SETREG} is used instead') + set_project_parser.add_argument('-f', '--force', action='store_true', default=False, + help=f'Force the setting of the project path in the supplied setreg file') + set_project_parser.set_defaults(func=_run_set_global_project) def add_args(subparsers) -> None: """ - add_args is called to add expected parser arguments and subparsers arguments to each command such that it can be - invoked locally or aggregated by a central python file. - Ex. Directly run from this file alone with: python global_project.py set_global_project --project-name TestProject - OR - o3de.py can aggregate commands by importing global_project, call add_args and - execute: python o3de.py set_global_project --project-path C:/TestProject + add_args is called to add subparsers arguments to each command such that it can be + a central python file such as o3de.py. + It can be run from the o3de.py script as follows + call add_args and execute: python o3de.py set-global-project --project-path "D:/TestProject" :param subparsers: the caller instantiates subparsers and passes it in here """ - get_global_project_subparser = subparsers.add_parser('get-global-project') - get_global_project_subparser.add_argument('-ohf', '--override-home-folder', type=str, required=False, - help='By default the home folder is the user folder, override it to this folder.') - - get_global_project_subparser.set_defaults(func=_run_get_global_project) - - set_global_project_subparser = subparsers.add_parser('set-global-project') - group = set_global_project_subparser.add_mutually_exclusive_group(required=True) - group.add_argument('-pn', '--project-name', required=False, - help='The name of the project. If supplied this will resolve the --project-path.') - group.add_argument('-pp', '--project-path', required=False, - help='The path to the project') - - set_global_project_subparser.add_argument('-ohf', '--override-home-folder', type=str, required=False, - help='By default the home folder is the user folder, override it to this folder.') - - set_global_project_subparser.set_defaults(func=_run_set_global_project) + get_project_subparser = subparsers.add_parser('get-global-project') + set_project_subparser = subparsers.add_parser('set-global-project') + add_parser_args(get_project_subparser, set_project_subparser) -if __name__ == "__main__": +def main(): + """ + Runs this script as standalone script + """ # parse the command line args the_parser = argparse.ArgumentParser() # add subparsers - the_subparsers = the_parser.add_subparsers(help='sub-command help', dest='command', required=True) + project_subparsers = the_parser.add_subparsers(help="Commands for modifying the project path in the user's home" + " setreg files") # add args to the parser - add_args(the_subparsers) + add_args(project_subparsers) # parse args the_args = the_parser.parse_args() @@ -165,3 +208,7 @@ if __name__ == "__main__": # return sys.exit(ret) + + +if __name__ == "__main__": + main() diff --git a/scripts/o3de/o3de/manifest.py b/scripts/o3de/o3de/manifest.py index 092430500f..9436e1dd29 100644 --- a/scripts/o3de/o3de/manifest.py +++ b/scripts/o3de/o3de/manifest.py @@ -142,7 +142,7 @@ def get_o3de_manifest() -> pathlib.Path: with default_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'o3de'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') json_data.update({'default_restricted_folder': default_restricted_folder.as_posix()}) default_projects_restricted_folder_json = default_projects_restricted_folder / 'restricted.json' @@ -150,24 +150,24 @@ def get_o3de_manifest() -> pathlib.Path: with default_projects_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'projects'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') default_gems_restricted_folder_json = default_gems_restricted_folder / 'restricted.json' if not default_gems_restricted_folder_json.is_file(): with default_gems_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'gems'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') default_templates_restricted_folder_json = default_templates_restricted_folder / 'restricted.json' if not default_templates_restricted_folder_json.is_file(): with default_templates_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'templates'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') with manifest_path.open('w') as s: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') return manifest_path @@ -201,7 +201,7 @@ def save_o3de_manifest(json_data: dict, manifest_path: pathlib.Path = None) -> N manifest_path = get_o3de_manifest() with manifest_path.open('w') as s: try: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') except OSError as e: logger.error(f'Manifest json failed to save: {str(e)}') diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index 1b30448db9..68575488dc 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -410,12 +410,8 @@ def register_project_path(json_data: dict, if update_project_json: project_json_data['engine'] = this_engine_json['engine_name'] utils.backup_file(project_json) - with project_json.open('w') as s: - try: - s.write(json.dumps(project_json_data, indent=4)) - except OSError as e: - logger.error(f'Project json failed to save: {str(e)}') - return 1 + if not manifest.save_o3de_manifest(project_json_data, project_path): + return 1 return 0 diff --git a/scripts/o3de/o3de/sha256.py b/scripts/o3de/o3de/sha256.py index bbec7696d6..db0a1fe834 100644 --- a/scripts/o3de/o3de/sha256.py +++ b/scripts/o3de/o3de/sha256.py @@ -51,7 +51,7 @@ def sha256(file_path: str or pathlib.Path, utils.backup_file(json_path) with json_path.open('w') as s: try: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') except OSError as e: logger.error(f'Failed to write Json path {json_path}: {str(e)}') return 1 diff --git a/scripts/o3de/tests/CMakeLists.txt b/scripts/o3de/tests/CMakeLists.txt index 5c83f4112b..0526c7740d 100644 --- a/scripts/o3de/tests/CMakeLists.txt +++ b/scripts/o3de/tests/CMakeLists.txt @@ -27,3 +27,10 @@ ly_add_pytest( 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 + TEST_SUITE smoke + EXCLUDE_TEST_RUN_TARGET_FROM_IDE +) diff --git a/scripts/o3de/tests/unit_global_project.py b/scripts/o3de/tests/unit_global_project.py new file mode 100644 index 0000000000..1d3a4dd4f4 --- /dev/null +++ b/scripts/o3de/tests/unit_global_project.py @@ -0,0 +1,40 @@ +# +# 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. +# + +import io +import json +import logging +import pytest +import pathlib +from unittest.mock import patch + +from o3de import global_project + + +logger = logging.getLogger() +logging.basicConfig() + +DEFAULT_BOOTSTRAP_SETREG = pathlib.Path('~/.o3de/Registry/bootstrap.setreg').expanduser() +PROJECT_PATH_KEY = ('Amazon', 'AzCore', 'Bootstrap', 'project_path') + +class TestSetGlobalProject: + @pytest.mark.parametrize( + "output_path, project_path, force, expected_result", [ + pytest.param(pathlib.Path('~/.o3de/Registry/bootstrap.setreg'), pathlib.Path('A:/'), False, False), + pytest.param(pathlib.Path('~/.o3de/Registry/bootstrap.setreg'), pathlib.Path('A:/'), True, True) + ] + ) + def test_set_global_project_non_existent_project_path(self, output_path, project_path, force, expected_result): + with patch('pathlib.Path.open', return_value=io.StringIO()) as pathlib_open_mock: + result = global_project.set_global_project(output_path, project_path=project_path, force=force) == 0 + + + assert result == expected_result diff --git a/scripts/o3de/tests/unit_test_current_project.py b/scripts/o3de/tests/unit_test_current_project.py deleted file mode 100755 index 7db48b62aa..0000000000 --- a/scripts/o3de/tests/unit_test_current_project.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# 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. -# - -import os -import pytest - -from . import current_project - -TEST_BOOTSTRAP_CONTENT_1 = """ -project_path = Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_2 = """ -project_path=Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_3 = """ -project_path= Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_4 = """ -project_path =Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_5 = """ -project_path = Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" - -@pytest.mark.parametrize( - "contents, expected_result", [ - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_2, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_3, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_4, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_5, 'Game1'), - ] -) -def test_get_current_project(tmpdir, contents, expected_result): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - bootstrap_file = f'{dev_root}/bootstrap.cfg' - if os.path.isfile(bootstrap_file): - os.unlink(bootstrap_file) - with open(bootstrap_file, 'a') as s: - s.write(contents) - - result = current_project.get_current_project(dev_root) - assert expected_result == result - - -@pytest.mark.parametrize( - "contents, project_to_set, expected_result", [ - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test1', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, ' Test2', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test3 ', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, '/Test4', 1), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, '=Test5', 1), - ] -) -def test_set_current_project(tmpdir, contents, project_to_set, expected_result): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - bootstrap_file = f'{dev_root}/bootstrap.cfg' - if os.path.isfile(bootstrap_file): - os.unlink(bootstrap_file) - with open(bootstrap_file, 'a') as s: - s.write(contents) - - result = current_project.set_current_project(dev_root, project_to_set) - assert expected_result == result - - if result == 0: - project_that_is_set = current_project.get_current_project(dev_root) - print(project_that_is_set) - print(project_to_set) - assert project_to_set.strip() == project_that_is_set \ No newline at end of file