Files
o3de/Tools/LyTestTools/ly_test_tools/o3de/settings.py
T

322 lines
13 KiB
Python

"""
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.
LySettings provides an API for modifying settings files and creating/restoring backups of them.
"""
import fileinput
import json
import logging
import re
import os
import ly_test_tools.environment.file_system
logger = logging.getLogger(__name__)
class LySettings(object):
"""
LySettings provides an API for modifying settings files and creating/restoring backups of them.
"""
def __init__(self, temp_path, resource_locator):
self._temp_path = temp_path
self._resource_locator = resource_locator
def get_temp_path(self):
return self._temp_path
def modify_asset_processor_setting(self, setting, value):
logger.info(f'Updating setting {setting} to {value}')
_edit_text_settings_file(self._resource_locator.asset_processor_config_file(), setting, value)
def modify_platform_setting(self, setting, value):
logger.info(f'Updating setting {setting} to {value}')
_edit_text_settings_file(self._resource_locator.platform_config_file(), setting, value)
def modify_shader_compiler_setting(self, setting, value):
logger.info(f'Updating setting {setting} to {value}')
_edit_text_settings_file(self._resource_locator.shader_compiler_config_file(), setting, value)
def backup_asset_processor_settings(self, backup_path=None):
self._backup_settings(self._resource_locator.asset_processor_config_file(), backup_path)
def backup_platform_settings(self, backup_path=None):
"""
Creates a backup of the platform settings file (~/dev/system_[platform].cfg) in the backup_path. If no path is
provided, it will store in the workspace temp path (the contents of the workspace temp directory are removed
during workspace teardown)
"""
self._backup_settings(self._resource_locator.platform_config_file(), backup_path)
def backup_bootstrap_settings(self, backup_path=None):
"""
Creates a backup of the bootstrap settings file (~/dev/bootstrap.cfg) in the backup_path. If no path is
provided, it will store in the workspace temp path (the contents of the workspace temp directory are removed
during workspace teardown)
"""
self._backup_settings(self._resource_locator.bootstrap_config_file(), backup_path)
def backup_shader_compiler_settings(self, backup_path=None):
self._backup_settings(self._resource_locator.shader_compiler_config_file(), backup_path)
def restore_asset_processor_settings(self, backup_path):
self._restore_settings(self._resource_locator.asset_processor_config_file(), backup_path)
def restore_platform_settings(self, backup_path=None):
"""
Restores the platform settings file (~/dev/system_[platform].cfg) from its backup.
The backup is stored in the backup_path.
If no backup_path is provided, it will attempt to retrieve the backup from the workspace temp path.
"""
self._restore_settings(self._resource_locator.platform_config_file(), backup_path)
def restore_bootstrap_settings(self, backup_path=None):
"""
Restores the bootstrap settings file (~/dev/bootstrap.cfg) from its backup.
The backup is stored in the backup_path.
If no backup_path is provided, it will attempt to retrieve the backup from the workspace temp path.
"""
self._restore_settings(self._resource_locator.bootstrap_config_file(), backup_path)
def restore_shader_compiler_settings(self, backup_path=None):
self._restore_settings(self._resource_locator.shader_compiler_config_file(), backup_path)
def backup_json_settings(self, json_settings_file, backup_path=None):
"""
Creates a backup of a Json/Registry Settings file in the backup_path. If no path is provided, it will store in
the workspace temp path (the contents of the workspace temp directory are removed during workspace teardown)
:param json_settings_file:
:param backup_path:
:return:
"""
self._backup_settings(json_settings_file, backup_path)
def restore_json_settings(self, json_settings_file, backup_path=None):
"""
Restores a Json/Registry settings file from its backup.
The backup is stored in the backup_path.
If no backup_path is provided, it will attempt to retrieve the backup from the workspace temp path.
"""
self._restore_settings(json_settings_file, backup_path)
def _backup_settings(self, settings_file, backup_path):
"""
Creates a backup of the settings file in the backup_path. If no path is
provided, it will store in the workspace temp path (the contents of the workspace temp directory are removed
during workspace teardown)
"""
if not backup_path:
backup_path = self._temp_path
ly_test_tools.environment.file_system.create_backup(settings_file, backup_path)
def _restore_settings(self, settings_file, backup_path):
"""
Restores the settings file from its backup stored in backup_path. If no path is provided, it will attempt
to retrieve the backup from the workspace temp path.
"""
if not backup_path:
backup_path = self._temp_path
ly_test_tools.environment.file_system.restore_backup(settings_file, backup_path)
class JsonSettings(object):
"""
JsonSettings provides an API for reading, modifying and writing Json settings files.
Can be used as a Context Manager.
"""
def __init__(self, file_path, line_clean_func=None):
self._data = None
self._file_path = file_path
self._line_clean_func = line_clean_func
self._modified = False
self._path_separator = '/'
def __enter__(self):
self.load()
return self
def __exit__(self, exception_type, exception_value, traceback):
if self._modified:
self.dump()
def _get_path_tokens(self, key_path):
"""
Splits the path using the separator and returns a list of tokens.
The first empty token is skipped, since it points to the root of the document.
:param key_path: Path to the key
:return: List of path tokens
"""
path_tokens = key_path.split(self._path_separator)
# Skip first empty token, it points to the root of the document
path_tokens = path_tokens[1:]
return path_tokens
def load(self):
"""
Load the Json file into memory
:return: None
"""
with open(self._file_path, 'r') as contents:
if not self._line_clean_func:
self._data = json.load(contents)
else:
cleaned_lines = []
for line in contents:
clean_line = self._line_clean_func(line)
cleaned_lines.append(clean_line)
self._data = json.loads(cleaned_lines)
def dump(self):
"""
Write the content into the Json file
:return: None
"""
with open(self._file_path, 'w') as out_file:
json.dump(self._data, out_file, indent=4)
def get_key(self, key_path, default_value=None):
"""
Retrieve a key by path from the Json
:param key_path: Path to the key
:param default_value: Default value to return if the key is not found
:return: Value of the key or default_value if not found
"""
path_tokens = self._get_path_tokens(key_path)
obj = self._data
try:
for token in path_tokens:
obj = obj[token]
return obj
except KeyError as err:
logger.error(f'KeyError when retrieving {key_path} from file {self._file_path}: {err}')
return default_value
def set_key(self, key_path, value):
"""
Set a key to a value
:param key_path: Path to the key
:param value: The value
:return: None
"""
path_tokens = self._get_path_tokens(key_path)
obj = self._data
try:
for token in path_tokens[0:-1]:
obj = obj.setdefault(token, {})
obj[path_tokens[-1]] = value
self._modified = True
except KeyError as err:
logger.error(f'KeyError when setting {key_path} from file {self._file_path}: {err}')
def remove_key(self, key_path):
"""
Remove a key by path from the Json
:param key_path: Path to the key
:return: None
"""
path_tokens = self._get_path_tokens(key_path)
obj = self._data
parent = None
key = None
try:
for token in path_tokens:
key = token
parent = obj
obj = obj[token]
if parent:
del parent[key]
self._modified = True
else:
logger.warning(f'Could not remove key {key} from file {self._file_path}')
except KeyError as err:
logger.error(f'KeyError when removing {key_path} from file {self._file_path}: {err}')
class RegistrySettings(JsonSettings):
"""
JsonSettings provides an API for reading, modifying and writing Registry settings files.
Files must be Json-compatible with optional line comments starting with '//'.
"""
def __init__(self, file_path):
super(RegistrySettings, self).__init__(file_path, RegistrySettings._clean_line)
@staticmethod
def _clean_line(line):
comment_index = line.lstrip.startswith('//')
return line if comment_index == -1 else line[:comment_index]
def _edit_text_settings_file(settings_file, setting, value, comment_char=""):
"""
Find and set a specific setting in a text based settings file. Uses "setting = value" syntax to identify setting,
ignoring whitespace. Will append "setting = value" to a text file if it can not find the setting already.
Note all found instances of the setting key in the file will be changed.
Unintentional setting changes may happen for files with multiple settings named the same
Using the comment_char, users can set a value on a corresponding setting but leave it commented out
--setting=value
:param settings_file: The path to a settings file
:param setting: The target key setting to update
:param value: The new key value
:param comment_char: A character identifier for commenting out data in a settings file
"""
if not os.path.isfile(settings_file):
raise IOError(f"Invalid file and/or path {settings_file}.")
match_obj = None
document = None
try:
# fileinput can be very destructive when used in conjunction with "with as" due to temp file creation.
# Allows using print to rewrite lines.
logger.debug(f"Opening {settings_file}")
document = fileinput.input(settings_file, inplace=True)
for line in document:
# Remove whitespace to avoid double spacing the output.
line = line.rstrip()
if setting in line:
# Run regex on each line, and make the change if we have an exact match.
setting_regex = re.compile("([-;]+)?(.*)=(.*)")
possible_match = setting_regex.match(line)
if possible_match is None or setting != possible_match.group(2).strip():
print(line)
continue
match_obj = possible_match
logger.debug(f"Found {setting} in {settings_file}")
# Print the new value for the setting.
if value == "":
print("")
else:
print(f"{comment_char}{setting}={value}")
logger.info(f"Updated setting {setting} in {settings_file} to {value} from {match_obj.group(3)}")
else:
print(line)
except PermissionError as error:
logger.warning(f"PermissionError, possibly due to ({settings_file}) already being open. Error: {error}")
finally:
if document is not None:
document.close()
# Append the settings change if setting doesn't exist in file.
if match_obj is None:
logger.info(
"Unable to locate setting in file. "
f"Appending {comment_char}{setting}={value} to {settings_file}.")
with open(settings_file, "a") as document:
document.write(f"{comment_char}{setting}={value}")