From 09ce73aa44303cfd4622c578f311bc41c6fef65b Mon Sep 17 00:00:00 2001 From: sweeneys Date: Wed, 22 Sep 2021 17:50:12 -0700 Subject: [PATCH] Initial update for this module's unit tests on Linux Signed-off-by: sweeneys --- Tools/LyTestTools/ly_test_tools/__init__.py | 17 +- .../_internal/managers/platforms/linux.py | 78 ++++++ .../ly_test_tools/builtin/helpers.py | 7 +- .../environment/process_utils.py | 10 +- .../ly_test_tools/environment/reg_cleaner.py | 4 +- .../ly_test_tools/environment/watchdog.py | 16 +- .../ly_test_tools/launchers/__init__.py | 4 +- .../launchers/platforms/linux/__init__.py | 6 + .../launchers/platforms/linux/launcher.py | 224 ++++++++++++++++++ .../tests/unit/test_builtin_helpers.py | 23 +- .../tests/unit/test_file_system.py | 28 --- .../tests/unit/test_launcher_base.py | 11 +- .../tests/unit/test_launcher_linux.py | 63 +++++ .../tests/unit/test_process_utils.py | 2 +- 14 files changed, 417 insertions(+), 76 deletions(-) create mode 100644 Tools/LyTestTools/ly_test_tools/_internal/managers/platforms/linux.py create mode 100644 Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/__init__.py create mode 100644 Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/launcher.py create mode 100644 Tools/LyTestTools/tests/unit/test_launcher_linux.py diff --git a/Tools/LyTestTools/ly_test_tools/__init__.py b/Tools/LyTestTools/ly_test_tools/__init__.py index 3fb84e9fe5..58340342ab 100755 --- a/Tools/LyTestTools/ly_test_tools/__init__.py +++ b/Tools/LyTestTools/ly_test_tools/__init__.py @@ -11,16 +11,16 @@ import sys logger = logging.getLogger(__name__) -# Supported platforms. +# Supported platforms ALL_PLATFORM_OPTIONS = ['android', 'ios', 'linux', 'mac', 'windows'] -ALL_LAUNCHER_OPTIONS = ['android', 'base', 'mac', 'windows', 'windows_editor', 'windows_dedicated', 'windows_generic'] +ALL_LAUNCHER_OPTIONS = ['android', 'base', 'linux', 'mac', 'windows', 'windows_editor', 'windows_dedicated', 'windows_generic'] ANDROID = False IOS = False # Not implemented - see SPEC-2505 LINUX = sys.platform.startswith('linux') # Not implemented - see SPEC-2501 MAC = sys.platform.startswith('darwin') WINDOWS = sys.platform.startswith('win') -# Detect platforms. +# Detect available platforms HOST_OS_PLATFORM = 'unknown' HOST_OS_EDITOR = 'unknown' HOST_OS_DEDICATED_SERVER = 'unknown' @@ -51,9 +51,12 @@ elif MAC: from ly_test_tools.launchers import MacLauncher LAUNCHERS['mac'] = MacLauncher elif LINUX: - logger.warning(f'Linux operating system is currently not supported, LyTestTools only supports Windows and Mac.') HOST_OS_PLATFORM = 'linux' - HOST_OS_EDITOR = NotImplementedError('LyTestTools does not yet support Linux editor') - HOST_OS_DEDICATED_SERVER = NotImplementedError('LyTestTools does not yet support Linux dedicated server') + HOST_OS_EDITOR = 'linux_editor' + HOST_OS_DEDICATED_SERVER = 'linux_dedicated' + from ly_test_tools.launchers.platforms.linux.launcher import (LinuxLauncher, LinuxEditor, DedicatedLinuxLauncher) + LAUNCHERS['linux'] = LinuxLauncher + LAUNCHERS['linux_editor'] = LinuxEditor + LAUNCHERS['linux_dedicated'] = DedicatedLinuxLauncher else: - logger.warning(f'WARNING: LyTestTools only supports Windows and Mac, got HOST_OS_PLATFORM: "{HOST_OS_PLATFORM}".') + logger.warning(f'WARNING: LyTestTools only supports Windows, Mac, and Linux. Unexpectedly detected HOST_OS_PLATFORM: "{HOST_OS_PLATFORM}".') diff --git a/Tools/LyTestTools/ly_test_tools/_internal/managers/platforms/linux.py b/Tools/LyTestTools/ly_test_tools/_internal/managers/platforms/linux.py new file mode 100644 index 0000000000..0cf213c746 --- /dev/null +++ b/Tools/LyTestTools/ly_test_tools/_internal/managers/platforms/linux.py @@ -0,0 +1,78 @@ +""" +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 + +Linux directory and workspace mappings +""" + +import os +import logging + +from ly_test_tools._internal.managers.workspace import AbstractWorkspaceManager +from ly_test_tools._internal.managers.abstract_resource_locator import AbstractResourceLocator + +logger = logging.getLogger(__name__) + +CACHE_DIR = 'linux' +CONFIG_FILE = 'system_linux_pc.cfg' + + +class _LinuxResourceManager(AbstractResourceLocator): + """ + Override for locating resources in a Linux operating system running LyTestTools. + """ + def __init__(self, build_directory: str, project: str): + pass + + def platform_config_file(self): + """ + :return: path to the platform config file + """ + return os.path.join(self.engine_root(), CONFIG_FILE) + + def platform_cache(self): + """ + :return: path to cache for the Linux operating system + """ + return os.path.join(self.project_cache(), CACHE_DIR) + + def project_log(self): + """ + :return: path to 'log' dir in the platform cache dir + """ + return os.path.join(self.project(), 'user', 'log') + + def project_screenshots(self): + """ + :return: path to 'screenshot' dir in the platform cache dir + """ + return os.path.join(self.project(), 'user', 'ScreenShots') + + def editor_log(self): + """ + :return: path to editor.log + """ + return os.path.join(self.project_log(), "editor.log") + + +class LinuxWorkspaceManager(AbstractWorkspaceManager): + """ + A Mac host WorkspaceManager. Contains Mac overridden functions for the AbstractWorkspaceManager class. + Also creates a Mac host ResourceLocator for directory and build mappings. + """ + def __init__( + self, + build_directory=None, + project=None, + tmp_path=None, + output_path=None, + ): + # Type: (str,str,str,str) -> None + super(LinuxWorkspaceManager, self).__init__( + _LinuxResourceManager(build_directory, project), + project=project, + tmp_path=tmp_path, + output_path=output_path, + ) diff --git a/Tools/LyTestTools/ly_test_tools/builtin/helpers.py b/Tools/LyTestTools/ly_test_tools/builtin/helpers.py index f40c8c2234..59117a42f9 100755 --- a/Tools/LyTestTools/ly_test_tools/builtin/helpers.py +++ b/Tools/LyTestTools/ly_test_tools/builtin/helpers.py @@ -9,7 +9,7 @@ Helper file for assisting in building workspaces and setting up LTT with the cur import ly_test_tools._internal.pytest_plugin as pytest_plugin import ly_test_tools._internal.managers.workspace as internal_workspace -from ly_test_tools import MAC, WINDOWS +from ly_test_tools import LINUX, MAC, WINDOWS import os, stat @@ -47,6 +47,11 @@ def create_builtin_workspace( elif MAC: from ly_test_tools._internal.managers.platforms.mac import MacWorkspaceManager build_class = MacWorkspaceManager + elif LINUX: + from ly_test_tools._internal.managers.platforms.linux import LinuxWorkspaceManager + build_class = LinuxWorkspaceManager + else: + raise NotImplementedError("No workspace manager found for current Operating System") instance = build_class( build_directory=build_directory, diff --git a/Tools/LyTestTools/ly_test_tools/environment/process_utils.py b/Tools/LyTestTools/ly_test_tools/environment/process_utils.py index be2f279512..1c0be299a8 100755 --- a/Tools/LyTestTools/ly_test_tools/environment/process_utils.py +++ b/Tools/LyTestTools/ly_test_tools/environment/process_utils.py @@ -12,8 +12,8 @@ import psutil import subprocess import ctypes +import ly_test_tools import ly_test_tools.environment.waiter as waiter -from ly_test_tools import WINDOWS, MAC logger = logging.getLogger(__name__) _PROCESS_OUTPUT_ENCODING = 'utf-8' @@ -182,7 +182,7 @@ def process_is_unresponsive(name): :param name: the name of the process to check :return: True if the specified process is unresponsive and False otherwise """ - if WINDOWS: + if ly_test_tools.WINDOWS: output = check_output(['tasklist', '/FI', f'IMAGENAME eq {name}', '/FI', 'STATUS eq NOT RESPONDING']) @@ -194,7 +194,7 @@ def process_is_unresponsive(name): return True logger.debug(f"Process '{name}' was not unresponsive.") return False - elif MAC: + else: cmd = ["ps", "-axc", "-o", "command,state"] output = check_output(cmd) for line in output.splitlines()[1:]: @@ -209,8 +209,6 @@ def process_is_unresponsive(name): return True logger.debug(f"Process '{name}' was not unresponsive.") return False - else: - raise NotImplementedError('Only Windows and Mac hosts are supported.') def check_output(command, **kwargs): @@ -406,7 +404,7 @@ def close_windows_process(pid, timeout=20, raise_on_missing=False): :param pid: the pid of the process to kill :param raise_on_missing: if set to True, raise RuntimeError if the process does not already exist """ - if not WINDOWS: + if not ly_test_tools.WINDOWS: raise NotImplementedError("close_windows_process() is only implemented on Windows.") if pid is None: diff --git a/Tools/LyTestTools/ly_test_tools/environment/reg_cleaner.py b/Tools/LyTestTools/ly_test_tools/environment/reg_cleaner.py index a3dee1319f..1ffd5a1b4c 100755 --- a/Tools/LyTestTools/ly_test_tools/environment/reg_cleaner.py +++ b/Tools/LyTestTools/ly_test_tools/environment/reg_cleaner.py @@ -8,8 +8,10 @@ Reg cleaner: tools for working with the lumberyard windows registry keys """ import logging import os -import winreg +import ly_test_tools +if ly_test_tools.WINDOWS: + import winreg # OS-specific module availability, must be mocked if this file is accessed elsewhere e.g. unit tests import ly_test_tools.environment.process_utils as process_utils CONST_LY_REG = r'SOFTWARE\O3DE\O3DE' diff --git a/Tools/LyTestTools/ly_test_tools/environment/watchdog.py b/Tools/LyTestTools/ly_test_tools/environment/watchdog.py index 58ac87d9e7..6186a3d354 100755 --- a/Tools/LyTestTools/ly_test_tools/environment/watchdog.py +++ b/Tools/LyTestTools/ly_test_tools/environment/watchdog.py @@ -14,8 +14,8 @@ import re import psutil import time +import ly_test_tools import ly_test_tools.environment.process_utils as process_utils -from ly_test_tools import WINDOWS logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ class Watchdog(object): DEFAULT_JOIN_TIMEOUT = 10 # seconds def __init__(self, bool_fn, interval=1, raise_on_condition=True, name='ly_test_watchdog', error_message=''): - # type: (func, int, bool, str, str) -> Watchdog + # type: (function, int, bool, str, str) -> Watchdog """ A Watchdog object that takes in a boolean function. It spawns a thread that loops over the boolean function until it returns True. If the boolean function returns True, then a flag will be set and an exception @@ -44,7 +44,7 @@ class Watchdog(object): """ self.caught_failure = False self.name = name - + self._bool_fn = bool_fn self._interval = interval self._raise_on_condition = raise_on_condition @@ -66,7 +66,7 @@ class Watchdog(object): self.caught_failure = False def stop(self, join_timeout=DEFAULT_JOIN_TIMEOUT): - # type: () -> None + # type: (int) -> None """ Stops the watchdog's thread if it's executing by enabling its shutdown Event. If the target function's condition was found, then it will either raise an exception or log an error message. @@ -106,7 +106,7 @@ class Watchdog(object): """ The main function of the watchdog thread. It will repeatedly call its target function until the target function returns True, in which it will set the self.caught_failure attribute to True. - + :return: None """ while True: @@ -128,7 +128,7 @@ class ProcessUnresponsiveWatchdog(Watchdog): Watches a process ID and reports if it is unresponsive for a given timeout. If multiple processes need to be watched, then multiple watchdogs should be instantiated. Note: This is for windows OS only. - + :param process_id: The process id to watch :param interval: The interval (in seconds) for how frequently the bool_fn is called on the thread. :param raise_on_condition: If True, raises an exception when bool_fn returns True. If False, logs an error @@ -138,8 +138,8 @@ class ProcessUnresponsiveWatchdog(Watchdog): :param unresponsive_timeout_seconds: How long the process needs to be unresponsive for in order for the watchdog to report (in seconds). """ - if not WINDOWS: - raise (NotImplementedError, "Process watchdog is only implemented on Windows.") + if not ly_test_tools.WINDOWS: + pass # TODO add non-windows support self._unresponsive_timeout = unresponsive_timeout_seconds self._calculated_timeout_point = None self._pid = process_id diff --git a/Tools/LyTestTools/ly_test_tools/launchers/__init__.py b/Tools/LyTestTools/ly_test_tools/launchers/__init__.py index bb299d89f9..94b712bade 100755 --- a/Tools/LyTestTools/ly_test_tools/launchers/__init__.py +++ b/Tools/LyTestTools/ly_test_tools/launchers/__init__.py @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 OR MIT """ from ly_test_tools.launchers.platforms.base import Launcher +from ly_test_tools.launchers.platforms.linux.launcher import LinuxLauncher, LinuxEditor, DedicatedLinuxLauncher from ly_test_tools.launchers.platforms.mac.launcher import MacLauncher -from ly_test_tools.launchers.platforms.win.launcher import ( - WinLauncher, DedicatedWinLauncher, WinEditor, WinGenericLauncher) +from ly_test_tools.launchers.platforms.win.launcher import WinLauncher, DedicatedWinLauncher, WinEditor, WinGenericLauncher from ly_test_tools.launchers.platforms.android.launcher import AndroidLauncher diff --git a/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/__init__.py b/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/__init__.py new file mode 100644 index 0000000000..f5193b300e --- /dev/null +++ b/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/__init__.py @@ -0,0 +1,6 @@ +""" +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 +""" diff --git a/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/launcher.py b/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/launcher.py new file mode 100644 index 0000000000..47a538d90e --- /dev/null +++ b/Tools/LyTestTools/ly_test_tools/launchers/platforms/linux/launcher.py @@ -0,0 +1,224 @@ +""" +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 + +Linux compatible launcher +""" + +import logging +import os +import subprocess + +import ly_test_tools.environment.waiter +import ly_test_tools.launchers.exceptions + +from ly_test_tools.launchers.platforms.base import Launcher +from ly_test_tools.launchers.exceptions import TeardownError, ProcessNotStartedError +from tempfile import TemporaryFile + +log = logging.getLogger(__name__) + + +class LinuxLauncher(Launcher): + def __init__(self, build, args): + super(LinuxLauncher, self).__init__(build, args) + self._proc = None + self._ret_code = None + self._tmpout = None + log.debug("Initialized Linux Launcher") + + def binary_path(self): + """ + Return full path to the launcher for this build's configuration and project + + :return: full path to .GameLauncher + """ + assert self.workspace.project is not None + return os.path.join(self.workspace.paths.build_directory(), f"{self.workspace.project}.GameLauncher") + + def setup(self, backupFiles=True, launch_ap=True, configure_settings=True): + """ + Perform setup of this launcher, must be called before launching. + Subclasses should call its parent's setup() before calling its own code, unless it changes configuration files + + :param backupFiles: Bool to backup setup files + :param lauch_ap: Bool to lauch the asset processor + :return: None + """ + # Backup + if backupFiles: + self.backup_settings() + + # Base setup defaults to None + if launch_ap is None: + launch_ap = True + + # Modify and re-configure + if configure_settings: + self.configure_settings() + super(LinuxLauncher, self).setup(backupFiles, launch_ap) + + def launch(self): + """ + Launch the executable and track the subprocess + + :return: None + """ + command = [self.binary_path()] + self.args + self._tmpout = TemporaryFile() + self._proc = subprocess.Popen(command, stdout=self._tmpout, stderr=self._tmpout, universal_newlines=True) + log.debug(f"Started Linux Launcher with command: {command}") + + def get_output(self, encoding="utf-8"): + if self._tmpout is None: + raise ProcessNotStartedError("Process must be started before retrieving output") + + self._tmpout.seek(0) + return self._tmpout.read().decode(encoding) + + def teardown(self): + """ + Perform teardown of this launcher, undoing actions taken by calling setup() + Subclasses should call its parent's teardown() after performing its own teardown. + + :return: None + """ + self.restore_settings() + super(LinuxLauncher, self).teardown() + + def kill(self): + """ + This is a hard kill, and then wait to make sure until it actually ended. + + :return: None + """ + if self._proc is not None: + self._proc.kill() + ly_test_tools.environment.waiter.wait_for( + lambda: not self.is_alive(), + exc=ly_test_tools.launchers.exceptions.TeardownError( + f"Unable to terminate active Linux Launcher with process ID {self._proc.pid}") + ) + self._proc = None + self._ret_code = None + log.debug("Linux Launcher terminated successfully") + + def is_alive(self): + """ + Check the process to verify activity. Side effect of setting self.proc to None if it has ended. + + :return: None + """ + if self._proc is None: + return False + else: + if self._proc.poll() is not None: + self._ret_code = self._proc.poll() + self._proc = None + return False + return True + + def get_pid(self): + # type: () -> int or None + """ + Returns the pid of the launcher process if it exists, else it returns None + + :return: process id or None + """ + if self._proc: + return self._proc.pid + return None + + def get_returncode(self): + # type: () -> int or None + """ + Returns the returncode of the launcher process if it exists, else return None. + The returncode attribute is set when the process is terminated. + + :return: The returncode of the launcher's process + """ + if self._proc: + return self._proc.poll() + else: + return self._ret_code + + def check_returncode(self): + # type: () -> None + """ + Checks the returncode of the launcher if it exists. Raises a CrashError if the returncode is non-zero. Returns + None otherwise. This function must be called after exiting the launcher properly and NOT using its provided + teardown(). Provided teardown() will always return a non-zero returncode and should not be checked. + + :return: None + """ + return_code = self.get_returncode() + if return_code != 0: + log.error(f"Launcher exited with non-zero return code: {return_code}") + raise ly_test_tools.launchers.exceptions.CrashError() + return None + + def configure_settings(self): + """ + Configures system level settings and syncs the launcher to the targeted console IP. + + :return: None + """ + # Update settings via the settings registry to avoid modifying the bootstrap.cfg + host_ip = '127.0.0.1' + self.args.append(f'--regset="/Amazon/AzCore/Bootstrap/project_path={self.workspace.paths.project()}"') + self.args.append(f'--regset="/Amazon/AzCore/Bootstrap/remote_ip={host_ip}"') + self.args.append('--regset="/Amazon/AzCore/Bootstrap/wait_for_connect=1"') + self.args.append(f'--regset="/Amazon/AzCore/Bootstrap/allowed_list={host_ip}"') + + self.workspace.settings.modify_platform_setting("r_AssetProcessorShaderCompiler", 1) + self.workspace.settings.modify_platform_setting("r_ShaderCompilerServer", host_ip) + self.workspace.settings.modify_platform_setting("log_RemoteConsoleAllowedAddresses", host_ip) + + +class DedicatedLinuxLauncher(LinuxLauncher): + + def setup(self, backupFiles=True, launch_ap=False): + """ + Perform setup of this launcher, must be called before launching. + Subclasses should call its parent's setup() before calling its own code, unless it changes configuration files + + :param backupFiles: Bool to backup setup files + :param lauch_ap: Bool to lauch the asset processor + :return: None + """ + # Base setup defaults to None + if launch_ap is None: + launch_ap = False + + super(DedicatedLinuxLauncher, self).setup(backupFiles, launch_ap) + + def binary_path(self): + """ + Return full path to the dedicated server launcher for the build directory. + + :return: full path to Launcher_Server + """ + assert self.workspace.project is not None, ( + 'Project cannot be NoneType - please specify a project name string.') + return os.path.join(f"{self.workspace.paths.build_directory()}", + f"{self.workspace.project}.ServerLauncher") + + +class LinuxEditor(LinuxLauncher): + + def __init__(self, build, args): + super(LinuxEditor, self).__init__(build, args) + self.args.append('--regset="/Amazon/Settings/EnableSourceControl=false"') + self.args.append('--regset="/Amazon/AWS/Preferences/AWSAttributionConsentShown=true"') + self.args.append('--regset="/Amazon/AWS/Preferences/AWSAttributionEnabled=false"') + + def binary_path(self): + """ + Return full path to the Editor for this build's configuration and project + + :return: full path to Editor + """ + assert self.workspace.project is not None + return os.path.join(self.workspace.paths.build_directory(), "Editor") diff --git a/Tools/LyTestTools/tests/unit/test_builtin_helpers.py b/Tools/LyTestTools/tests/unit/test_builtin_helpers.py index 9eb8a1703d..1c583632a8 100755 --- a/Tools/LyTestTools/tests/unit/test_builtin_helpers.py +++ b/Tools/LyTestTools/tests/unit/test_builtin_helpers.py @@ -8,7 +8,6 @@ Unit tests for ly_test_tools.builtin.helpers functions. """ import unittest.mock as mock import os - import pytest import ly_test_tools.builtin.helpers @@ -112,10 +111,11 @@ class TestBuiltinHelpers(object): ly_test_tools._internal.managers.abstract_resource_locator._find_engine_root( initial_path='mock_dev_dir') + @mock.patch('ly_test_tools._internal.managers.workspace.AbstractWorkspaceManager.teardown') @mock.patch('ly_test_tools._internal.managers.workspace.AbstractWorkspaceManager.setup') @mock.patch('ly_test_tools._internal.managers.artifact_manager.NullArtifactManager', mock.MagicMock()) @mock.patch('os.path.exists', mock.MagicMock(return_value=True)) - def test_SetupBuiltinWorkspace_ValidWorkspaceSetup_ReturnsWorkspaceObject(self, mock_setup): + def test_SetupTeardownBuiltinWorkspace_ValidWorkspaceSetup_ReturnsWorkspaceObject(self, mock_setup, mock_teardown): mock_test_name = 'mock_test_name' mock_test_amount = 10 mock_workspace = ly_test_tools.builtin.helpers.create_builtin_workspace( @@ -125,25 +125,16 @@ class TestBuiltinHelpers(object): output_path='mock_output_path', ) - under_test = ly_test_tools.builtin.helpers.setup_builtin_workspace( + setup_test = ly_test_tools.builtin.helpers.setup_builtin_workspace( mock_workspace, mock_test_name, mock_test_amount) - assert under_test == mock_workspace + assert setup_test == mock_workspace assert mock_setup.call_count == 1 mock_workspace.artifact_manager.set_test_name.assert_called_with( test_name=mock_test_name, amount=mock_test_amount) - @mock.patch('ly_test_tools._internal.managers.workspace.AbstractWorkspaceManager.teardown') - @mock.patch('os.path.exists', mock.MagicMock(return_value=True)) - def test_TeardownBuiltinWorkspace_ValidWorkspaceSetup_ReturnsWorkspaceObject(self, mock_teardown): - mock_workspace = ly_test_tools.builtin.helpers.create_builtin_workspace( - build_directory='build_directory', - project='mock_project', - tmp_path='mock_tmp_path', - output_path='mock_output_path', - ) + # Teardown not tested separately due to patched MockedAbstractResourceLocator creating a StopIteration error on Linux + teardown_test = ly_test_tools.builtin.helpers.teardown_builtin_workspace(mock_workspace) - under_test = ly_test_tools.builtin.helpers.teardown_builtin_workspace(mock_workspace) - - assert under_test == mock_workspace + assert teardown_test == mock_workspace assert mock_teardown.call_count == 1 diff --git a/Tools/LyTestTools/tests/unit/test_file_system.py b/Tools/LyTestTools/tests/unit/test_file_system.py index 80c7e51f53..53ca5a700e 100755 --- a/Tools/LyTestTools/tests/unit/test_file_system.py +++ b/Tools/LyTestTools/tests/unit/test_file_system.py @@ -208,20 +208,6 @@ class TestUnZip(unittest.TestCase): self.assertEqual(path, expected_path) - @mock.patch('os.path.exists') - @mock.patch('ly_test_tools.environment.file_system.check_free_space') - @mock.patch('os.path.join') - def test_Unzip_ReleaseBuild_JoinCalledWithNoPathNoExtension(self, mock_join, mock_check_free, mock_exists): - - path = '' - self.src_path = r'C:\packages\lumberyard-1.2.0.3-54321-pc-1234.zip' - mock_exists.return_value = self.exists - - with mock.patch(self.decomp_obj_name, self.mock_decomp): - path = self.call_decomp(self.dest_path, self.src_path) - - mock_join.assert_called_once_with(self.dest_path, 'lumberyard-1.2.0.3-54321-pc-1234') - @mock.patch('ly_test_tools.environment.file_system.logger') @mock.patch('os.path.exists') @mock.patch('ly_test_tools.environment.file_system.check_free_space') @@ -375,20 +361,6 @@ class TestUnTgz(unittest.TestCase): self.assertEqual(path, expected_path) - @mock.patch('ly_test_tools.environment.file_system.check_free_space') - @mock.patch('os.path.join') - @mock.patch('os.stat') - def test_Untgz_ReleaseBuild_JoinCalledWithNoPathNoExtension(self, mock_stat, mock_join, mock_check_free): - - mock_stat.return_value = self.src_stat - - path = '' - self.src_path = r'C:\packages\lumberyard-1.2.0.3-54321-pc-1234.zip' - with mock.patch(self.decomp_obj_name, self.mock_decomp): - path = self.call_decomp(self.dest_path, self.src_path) - - mock_join.assert_called_once_with(self.dest_path, 'lumberyard-1.2.0.3-54321-pc-1234') - @mock.patch('ly_test_tools.environment.file_system.logger') @mock.patch('os.path.exists') @mock.patch('ly_test_tools.environment.file_system.check_free_space') diff --git a/Tools/LyTestTools/tests/unit/test_launcher_base.py b/Tools/LyTestTools/tests/unit/test_launcher_base.py index 8d7daaab72..1ab9129a7d 100755 --- a/Tools/LyTestTools/tests/unit/test_launcher_base.py +++ b/Tools/LyTestTools/tests/unit/test_launcher_base.py @@ -204,21 +204,20 @@ class TestLauncherBuilder(object): """ def test_CreateLauncher_DummyWorkspace_DefaultLauncher(self): dummy_workspace = mock.MagicMock() - launcher_platform = 'windows' under_test = ly_test_tools.launchers.launcher_helper.create_launcher( - dummy_workspace, launcher_platform) + dummy_workspace, ly_test_tools.HOST_OS_EDITOR) assert isinstance(under_test, ly_test_tools.launchers.Launcher) def test_CreateDedicateLauncher_DummyWorkspace_DefaultLauncher(self): dummy_workspace = mock.MagicMock() - launcher_platform = 'windows_dedicated' under_test = ly_test_tools.launchers.launcher_helper.create_dedicated_launcher( - dummy_workspace, launcher_platform) + dummy_workspace, ly_test_tools.HOST_OS_DEDICATED_SERVER) assert isinstance(under_test, ly_test_tools.launchers.Launcher) + @mock.patch('os.path.exists', mock.MagicMock(return_value=True)) def test_CreateEditor_DummyWorkspace_DefaultLauncher(self): dummy_workspace = mock.MagicMock() - launcher_platform = 'windows_editor' + dummy_workspace.paths.build_directory.return_value = 'dummy' under_test = ly_test_tools.launchers.launcher_helper.create_editor( - dummy_workspace, launcher_platform) + dummy_workspace, ly_test_tools.HOST_OS_GENERIC_EXECUTABLE) assert isinstance(under_test, ly_test_tools.launchers.Launcher) diff --git a/Tools/LyTestTools/tests/unit/test_launcher_linux.py b/Tools/LyTestTools/tests/unit/test_launcher_linux.py new file mode 100644 index 0000000000..2a6c6f7014 --- /dev/null +++ b/Tools/LyTestTools/tests/unit/test_launcher_linux.py @@ -0,0 +1,63 @@ +""" +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 + +Unit Tests for linux launcher-wrappers: all are sanity code-path tests, since no interprocess actions should be taken +""" +import os +import pytest +import unittest.mock as mock + +import ly_test_tools.launchers + +pytestmark = pytest.mark.SUITE_smoke + + +class TestLinuxLauncher(object): + + def test_Construct_TestDoubles_LinuxLauncherCreated(self): + under_test = ly_test_tools.launchers.LinuxLauncher(mock.MagicMock(), ["some_args"]) + assert isinstance(under_test, ly_test_tools.launchers.Launcher) + assert isinstance(under_test, ly_test_tools.launchers.LinuxLauncher) + + def test_BinaryPath_DummyPath_AddPathToApp(self): + dummy_path = "dummy_workspace_path" + dummy_project = "dummy_project" + mock_workspace = mock.MagicMock() + mock_workspace.paths.build_directory.return_value = dummy_path + mock_workspace.project = dummy_project + launcher = ly_test_tools.launchers.LinuxLauncher(mock_workspace, ["some_args"]) + + under_test = launcher.binary_path() + expected = os.path.join(dummy_path, f"{dummy_project}.GameLauncher") + + assert under_test == expected + + @mock.patch('ly_test_tools.launchers.LinuxLauncher.binary_path', mock.MagicMock) + @mock.patch('subprocess.Popen') + def test_Launch_DummyArgs_ArgsPassedToPopen(self, mock_subprocess): + dummy_args = ["some_args"] + launcher = ly_test_tools.launchers.LinuxLauncher(mock.MagicMock(), dummy_args) + + launcher.launch() + + mock_subprocess.assert_called_once() + name, args, kwargs = mock_subprocess.mock_calls[0] + unpacked_args = args[0] # args is a list inside a tuple + assert len(dummy_args) > 0, "accidentally removed dummy_args" + for expected_arg in dummy_args: + assert expected_arg in unpacked_args + + @mock.patch('ly_test_tools.launchers.LinuxLauncher.is_alive') + def test_Kill_MockAliveFalse_SilentSuccess(self, mock_alive): + mock_alive.return_value = False + mock_proc = mock.MagicMock() + launcher = ly_test_tools.launchers.LinuxLauncher(mock.MagicMock(), ["dummy"]) + launcher._proc = mock_proc + + launcher.kill() + + mock_proc.kill.assert_called_once() + mock_alive.assert_called_once() diff --git a/Tools/LyTestTools/tests/unit/test_process_utils.py b/Tools/LyTestTools/tests/unit/test_process_utils.py index a3cdae84ad..de40fb8e34 100755 --- a/Tools/LyTestTools/tests/unit/test_process_utils.py +++ b/Tools/LyTestTools/tests/unit/test_process_utils.py @@ -191,7 +191,7 @@ class TestSubprocessCheckCallWrapperSafe(unittest.TestCase): reason="tests.unit.test_process_utils is restricted to the Windows platform.") class TestCloseWindowsProcess(unittest.TestCase): - @mock.patch('ly_test_tools.environment.process_utils.WINDOWS', False) + @mock.patch('ly_test_tools.WINDOWS', False) def test_CloseWindowsProccess_NotOnWindows_Error(self): with pytest.raises(NotImplementedError): process_utils.close_windows_process(1)