screenshot_utils.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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. """
  6. import os
  7. import string
  8. from .file_utils import move_file
  9. from ly_test_tools.environment.waiter import wait_for
  10. from ly_remote_console.remote_console_commands import capture_screenshot_command as capture_screenshot_command
  11. from ly_remote_console.remote_console_commands import send_command_and_expect_response as send_command_and_expect_response
  12. def get_next_screenshot_at_path(screenshot_path, prefix='screenshot', num_digits=4):
  13. """
  14. :param screenshot_path: Root folder where the screenshots are being generated by the Launcher pr Editor.
  15. :param prefix: Generated screenshot files are named sequentially using the prefix.
  16. e.g: screenshot0000.jpg, screenshot0001.jpg and so on.
  17. :param num_digits: How many digits are used for file name formation.
  18. :return: A string with the file name (relative to screenshot_path).
  19. """
  20. max_counter = 10**num_digits
  21. counter = 0
  22. while counter < max_counter:
  23. numberstr = "{}".format(counter)
  24. formattednumber = numberstr.zfill(num_digits)
  25. filename = "{}{}.jpg".format(prefix, formattednumber)
  26. filepath = os.path.join(screenshot_path, filename)
  27. if not os.path.exists(filepath):
  28. #This filename is available.
  29. return filename
  30. raise AssertionError("All possible screenshot names at directory {} are taken".format(screenshot_path))
  31. def take_screenshot(remote_console_instance, workspace, screenshot_name):
  32. """
  33. Takes an in game screenshot using the remote console instance passed in, validates that the screenshot exists
  34. and then renames that screenshot to something defined by the user of this function.
  35. :param remote_console_instance: Remote console instance that is attached to a specific launcher instance
  36. :param workspace: workspace instance so we can get the platform cache folder.
  37. :param screenshot_name: Name of the screenshot
  38. :return: None
  39. """
  40. screenshot_path = os.path.join(workspace.paths.platform_cache(), 'user', 'screenshots')
  41. expected_screenshot_name = get_next_screenshot_at_path(screenshot_path)
  42. capture_screenshot_command(remote_console_instance)
  43. wait_for(lambda: os.path.exists(os.path.join(screenshot_path, expected_screenshot_name)),
  44. timeout=10,
  45. exc=AssertionError('Screenshot at path:{} and with name:{} not found.'.format(screenshot_path, expected_screenshot_name)) )
  46. wait_for(lambda: rename_screenshot(screenshot_path, screenshot_name),
  47. timeout=10,
  48. exc=AssertionError('Screenshot at path:{} and with name:{} is still in use.'.format(screenshot_path, screenshot_name)))
  49. def rename_screenshot(screenshot_path, screenshot_name):
  50. """
  51. Tries to rename the screenshot when the file is done being written to
  52. :param screenshot_path: Path to the Screenshot folder
  53. :param screenshot_name: Name we wish to change the screenshot to
  54. :return: True when operation is completed, False if the file is still in use
  55. """
  56. try:
  57. src_img = os.path.join(screenshot_path, 'screenshot0000.jpg')
  58. dst_img = os.path.join(screenshot_path, '{}.jpg'.format(screenshot_name))
  59. print('Trying to rename {} to {}'.format(src_img, dst_img))
  60. os.rename(src_img, dst_img)
  61. return True
  62. except Exception as e:
  63. print('Found error {0} when trying to rename screenshot.'.format(str(e)))
  64. return False
  65. def move_screenshots(screenshot_path, file_type, logs_path):
  66. """
  67. Moves screenshots of a specific file type to the flume location so we can gather all of the screenshots we took.
  68. :param screenshot_path: Path to the screenshot folder
  69. :param file_type: Types of Files to look for. IE .jpg, .tif, etc
  70. :param logs_path: Path where flume gathers logs to be upload
  71. """
  72. for file_name in os.listdir(screenshot_path):
  73. if file_name.endswith(file_type):
  74. move_file(screenshot_path, logs_path, file_name)
  75. def move_screenshots_to_artifacts(screenshot_path, file_type, artifact_manager):
  76. """
  77. Saves screenshots of a specific file type to the artifact manager then removes the original files
  78. :param screenshot_path: Path to the screenshot folder
  79. :param file_type: Types of Files to look for. IE .jpg, .tif, etc
  80. :param artifact_manager: The artifact manager to save the artifacts to
  81. """
  82. for file_name in os.listdir(screenshot_path):
  83. if file_name.endswith(file_type):
  84. full_path_name = os.path.join(screenshot_path, file_name)
  85. artifact_manager.save_artifact(full_path_name)
  86. os.remove(full_path_name)
  87. def compare_golden_image(similarity_threshold, screenshot, screenshot_path, golden_image_name,
  88. golden_image_path=None):
  89. """
  90. This function assumes that your golden image filename contains the same base screenshot name and the word "golden"
  91. ex. pc_gamelobby_golden
  92. :param similarity_threshold: A float from 0.0 - 1.0 that determines how similar images must be or an asserts
  93. :param screenshot: A string that is the full name of the screenshot (ex. 'gamelobby_host.jpg')
  94. :param screenshot_path: A string that contains the path to the screenshots
  95. :param golden_image_path: A string that contains the path to the golden images, defaults to the screenshot_path
  96. :return:
  97. """
  98. if golden_image_path is None:
  99. golden_image_path = screenshot_path
  100. mean_similarity = compare_screenshots((os.path.join(screenshot_path, screenshot)),
  101. (os.path.join(golden_image_path, golden_image_name)))
  102. assert mean_similarity > similarity_threshold, \
  103. '{} screenshot comparison failed! Mean similarity value is: {}'\
  104. .format(screenshot, mean_similarity)
  105. def download_qa_golden_images(project_name, destination_dir, platform):
  106. """
  107. Downloads the golden images for a specified project from s3. The project_name, platform, and filetype are used to
  108. filter which images will be downloaded as the golden images.
  109. https://s3.console.aws.amazon.com/s3/buckets/ly-qae-jenkins-configs/golden-images/?region=us-west-1&tab=overview
  110. :param project_name: a string of the project name of the folder in s3. ex: 'MultiplayerSample'
  111. :param destination_dir: a string of where the images will be downloaded to
  112. :param platform: a string for the platform type ('pc', 'android', 'ios', 'darwin', 'provo')
  113. :param filetype: a string for the file type. ex: '.jpg', '.png'
  114. :return:
  115. """
  116. # Currently we import s3_utils here instead of at the top because this is the only method that needs it,
  117. # and s3_utils has an unmet dependency on boto3 that hasn't been resolved. Once s3_utils is functional again,
  118. # this can move back to the top of the file.
  119. try:
  120. from . import s3_utils as s3_utils
  121. except ImportError:
  122. raise Exception("Failed to import s3_utils")
  123. # end s3_utils import
  124. bucket_name = 'ly-qae-jenkins-configs'
  125. path = 'golden-images/{}/{}/'.format(project_name, platform)
  126. if not s3_utils.key_exists_in_bucket(bucket_name, path):
  127. raise s3_utils.KeyDoesNotExistError("Key '{}' does not exist in S3 bucket {}".format(path, bucket_name))
  128. for image in s3_utils.s3.Bucket(bucket_name).objects.filter(Prefix=path):
  129. file_name = string.replace(image.key, path, '')
  130. if file_name != '':
  131. s3_utils.download_from_bucket(bucket_name, image.key, destination_dir, file_name)
  132. def _retry_command(remote_console_instance, command, output, tries=10, timeout=10):
  133. """
  134. Retries specified console command multiple times and asserts if it still can not send.
  135. :param remote_console: the remote console connected to the launcher.
  136. :param command: the command to send to the console.
  137. :param output: The expected output to check if the command was sent successfully.
  138. :param tries: The amount of times to try before asserting.
  139. :param timeout: The amount of time in seconds to wait for each retry send.
  140. :return: True if succeeded, will assert otherwise.
  141. """
  142. while tries > 0:
  143. tries -= 1
  144. try:
  145. send_command_and_expect_response(remote_console_instance, command, output)
  146. return True
  147. except:
  148. pass #Do nothing. Let the number of tries get to 0 if necessary.
  149. assert False, "Command \"{}\" failed to run in remote console.".format(command)
  150. def prepare_for_screenshot_compare(remote_console_instance):
  151. """
  152. Prepares launcher for screenshot comparison. Removes any debug text and antialiasing that may result in interference
  153. with the comparison.
  154. :param remote_console_instance: Remote console instance that is attached to a specific launcher instance
  155. :return:
  156. """
  157. wait_for(lambda: _retry_command(remote_console_instance, 'r_displayinfo 0',
  158. '$3r_DisplayInfo = $60 $5[DUMPTODISK, RESTRICTEDMODE]$4'))