conftest.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Pytest test configuration file.
  6. """
  7. import pytest
  8. import boto3
  9. import shutil
  10. import os
  11. import io
  12. import threading
  13. import urllib.parse
  14. import urllib.request
  15. from pathlib import Path, PurePath
  16. from subprocess import run, list2cmdline
  17. from tempfile import NamedTemporaryFile, TemporaryDirectory
  18. def pytest_addoption(parser):
  19. """ Add custom PyTest arguments. """
  20. parser.addoption("--installer-uri", action="store", default="")
  21. parser.addoption("--install-root", action="store", default=PurePath('C:/O3DE/0.0.0.0'))
  22. # we use a directory outside of %TEMP% because otherwise MSB8029 warns it might cause incremental build issues
  23. parser.addoption("--project-path", action="store", default=PurePath('C:/Workspace/TestProject'))
  24. class SessionContext:
  25. """ Holder for test session constants and helper functions. """
  26. def __init__(self, request, temp_dir):
  27. # setup to capture subprocess output
  28. self.log_file = request.config.getoption("--log-file")
  29. self.temp_dir = temp_dir
  30. self.temp_file = NamedTemporaryFile(dir=self.temp_dir, mode='w+t', delete=False)
  31. # we do not use Path.home() or os.path.expanduser() because it may return the
  32. # shell or current environment HOME or HOMEPATH, and we want the user's
  33. # Windows user folder, because that is the normal use case for the installer
  34. home_path = Path(os.environ["SYSTEMDRIVE"], 'Users', os.getlogin()).resolve()
  35. os.environ['HOME'] = str(home_path)
  36. os.environ['HOMEPATH'] = os.environ['HOME']
  37. self.installer_path = self._get_local_installer_path(request.config.getoption("--installer-uri"))
  38. self.install_root = Path(request.config.getoption("--install-root")).resolve()
  39. self.project_path = Path(request.config.getoption("--project-path")).resolve()
  40. # create the project path or TemporaryDirectory can fail
  41. os.makedirs(self.project_path, exist_ok=True)
  42. # use a temp folder inside the project path to avoid issues where we cannot
  43. # clean up or remove the actual project folder
  44. with TemporaryDirectory(dir=self.project_path) as tmp_project_dir:
  45. self.project_path = Path(tmp_project_dir).resolve()
  46. self.cmake_runtime_path = self.install_root / 'cmake/runtime'
  47. self.engine_bin_path = self.install_root / 'bin/Windows/profile/Default'
  48. self.project_build_path_profile = self.project_path / 'build/Windows'
  49. self.project_build_path_release = self.project_path / 'build/Windows_mono'
  50. self.project_bin_path_profile = self.project_build_path_profile / 'bin/profile'
  51. self.project_bin_path_release = self.project_build_path_release / 'bin/release'
  52. # start a log reader thread to print the output to screen
  53. self.log_reader_shutdown = False
  54. self.log_reader = io.open(self.temp_file.name, 'r', buffering=1)
  55. self.log_reader_thread = threading.Thread(target=self._tail_log, daemon=True)
  56. self.log_reader_thread.start()
  57. def _tail_log(self):
  58. """ Tail the log file and print to screen for easy viewing in Jenkins. """
  59. while True:
  60. line = self.log_reader.readline()
  61. if line == '':
  62. if self.log_reader_shutdown:
  63. return
  64. else:
  65. print(line, end='')
  66. def _get_local_installer_path(self, uri):
  67. """ Return the local path to the installer, downloading the remote file if necessary """
  68. parsed_uri = urllib.parse.urlparse(uri)
  69. if parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps', 's3']:
  70. installer_path = Path(self.temp_dir) / 'installer.exe'
  71. if parsed_uri.scheme == 's3':
  72. session = boto3.session.Session()
  73. client = session.client('s3')
  74. client.download_file(parsed_uri.netloc, parsed_uri.path.lstrip('/'), str(installer_path))
  75. else:
  76. urllib.request.urlretrieve(parsed_uri.geturl(), installer_path)
  77. # verify with SignTool if available
  78. self._verify(installer_path)
  79. return installer_path
  80. else:
  81. # strip the leading slash from the file URI
  82. return Path(parsed_uri.path.lstrip('/')).resolve()
  83. def _verify(self, file):
  84. """ Assert if signtool.exe exists and cannot verify the given file. """
  85. try:
  86. signtool_path_result = run(['powershell','-Command',"(Resolve-Path \"C:\\Program Files (x86)\\Windows Kits\\10\\bin\\*\\x64\" | Select-Object -Last 1).Path"], text=True, capture_output=True)
  87. if signtool_path_result.returncode == 0:
  88. signtool_path = Path(signtool_path_result.stdout.strip()) / 'signtool.exe'
  89. signtool_result = self.run([str(signtool_path),'verify', '/pa', str(file)])
  90. assert signtool_result.returncode == 0
  91. except FileNotFoundError as e:
  92. print('Unable to verify installer is appropriately signed because SignTool.exe was not found. Verify Windows Kit is installed.')
  93. pass
  94. def run(self, command, timeout=None, cwd=None):
  95. """ Run command with the correct environment and logging settings. """
  96. self.temp_file.write('\n' + list2cmdline(command) + '\n')
  97. self.temp_file.flush()
  98. return run(command, timeout=timeout, cwd=cwd, stdout=self.temp_file, stderr=self.temp_file, text=True)
  99. def cleanup(self):
  100. """ Clean up temporary testing artifacts. """
  101. # unregister and delete project
  102. if self.project_path.is_dir():
  103. o3de_path = self.install_root / 'scripts/o3de.bat'
  104. if o3de_path.is_file():
  105. print(f"Unregistering {self.project_path}")
  106. self.run([str(o3de_path),'register','--project-path', str(self.project_path), '--remove'])
  107. print(f"Removing {self.project_path}")
  108. shutil.rmtree(self.project_path, ignore_errors=True)
  109. # uninstall engine in case we failed before the uninstall test has run
  110. if self.installer_path.is_file():
  111. self.run([str(self.installer_path) , f"InstallFolder={self.install_root}", "/quiet","/uninstall"], timeout=30*60)
  112. self.temp_file.close()
  113. self.log_reader_shutdown = True
  114. self.log_reader_thread.join()
  115. self.log_reader.close()
  116. if self.log_file:
  117. shutil.copy(self.temp_file.name, Path(self.log_file).resolve())
  118. @pytest.fixture(scope="session")
  119. def context(request, tmpdir_factory):
  120. session_context = SessionContext(request, tmpdir_factory.mktemp("installer"))
  121. yield session_context
  122. session_context.cleanup()