ap_missing_dependency_fixture.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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 fixture missing dependency scanner tests
  6. """
  7. import logging
  8. import os
  9. import pytest
  10. import re
  11. import tempfile
  12. from typing import Dict, List, Tuple, Any, Set
  13. from ly_test_tools.environment.file_system import create_backup, restore_backup, unlock_file
  14. from automatedtesting_shared import asset_database_utils as db_utils
  15. from ly_test_tools.o3de.ap_log_parser import APLogParser
  16. from . import ap_setup_fixture as ap_setup_fixture
  17. logger = logging.getLogger(__name__)
  18. @pytest.fixture
  19. def ap_missing_dependency_fixture(request, workspace, ap_setup_fixture) -> Any:
  20. ap_setup = ap_setup_fixture
  21. class MissingDependencyHelper:
  22. def __init__(self):
  23. # Indexes for MissingProductDependencies rows in DB
  24. self.UUID_INDEX = 5
  25. self.SUB_ID_INDEX = 6
  26. self.STR_INDEX = 7
  27. # Constant tuple to represent a 'No missing dependencies' in a database entry
  28. self.DB_NO_MISSING_DEP = ("No missing dependencies found", "{00000000-0000-0000-0000-000000000000}:0")
  29. self.log_file = workspace.paths.ap_batch_log()
  30. self.ap_batch = os.path.join(workspace.paths.build_directory(), "AssetProcessorBatch")
  31. self.asset_db = workspace.paths.asset_db()
  32. self.backup_dir = os.path.join(tempfile.gettempdir(), "MissingDependencyBackup")
  33. def extract_missing_dependencies_from_output(self, log_output: List[str or List[str]]) -> Dict[
  34. str, List[Tuple[str, str]]]:
  35. """
  36. Extracts missing dependencies for each product file scanned from the console output.
  37. log_output: List[str or List[str]],
  38. Returns a dictionary, where:
  39. each key: product file with missing dependencies
  40. each value: a list of tuples where:
  41. each tuple[0] - Missing dependency string
  42. each tuple[1] - Missing dependency asset id
  43. """
  44. # Useful regex workers
  45. re_file_path = re.compile(r"Scanning for missing dependencies:\s*(.*)")
  46. re_content = re.compile(r'Missing dependency: String "(.*)" matches asset: (.*)\Z')
  47. result = {} # The dictionary to return
  48. missing_dependencies = None # Temp list to hold current_products missing dependency tuples
  49. current_product = None # Current product in the log file
  50. for line in log_output:
  51. next_file = re_file_path.search(line, re.I) # Attempt pattern search for next 'product'
  52. if next_file:
  53. # Found next product in file
  54. if current_product is not None:
  55. # If already in a dependency file log section, save it before starting the next
  56. result[current_product] = missing_dependencies
  57. # Start a new product in this file
  58. missing_dependencies = []
  59. current_product = next_file.group(1)
  60. assert current_product, "REGEX 'current_product' parsing error-- Did the log format change?"
  61. else:
  62. # Not a new product yet in log:
  63. # Try to parse dependency "String" and "missing asset"
  64. content = re_content.search(line, re.I)
  65. if content:
  66. # Regex success, extract groups and add to [missing_dependencies]
  67. missing_dependencies.append((content.group(1), content.group(2)))
  68. # End: for line in log_file
  69. # Hit the end of file, we might still have one more product to save
  70. if current_product is not None:
  71. # Append last dependency file log section
  72. result[current_product] = missing_dependencies
  73. return result
  74. def extract_missing_dependencies_from_log(self) -> Dict[str, List[Tuple[str, str]]]:
  75. """
  76. Extracts missing dependencies for each product file scanned from ONLY THE LAST AP Batch run.
  77. Returns a dictionary, where:
  78. each key: product file with missing dependencies
  79. each value: a list of tuples where:
  80. each tuple[0] - Missing dependency string
  81. each tuple[1] - Missing dependency asset id
  82. """
  83. return self.extract_missing_dependencies_from_output(APLogParser(self.log_file).get_lines(run=-1))
  84. def extract_missing_dependencies_from_database(self, product: str, check_platforms: List[str] = None) -> Dict[
  85. str, Set[Tuple[str, str]]]:
  86. """
  87. Extracts missing dependencies for the [product] from the database.
  88. Returns a dictionary, where:
  89. each key: product file with cache platform path. (i.e. pc, ios... etc.)
  90. each value: a set of tuples where:
  91. each tuple[0] - Missing dependency string
  92. each tuple[1] - Missing dependency asset id
  93. """
  94. platforms = check_platforms or db_utils.get_active_platforms_from_db(self.asset_db)
  95. logger.info(f"Searching for product {product} in active platforms: {platforms}")
  96. product_names = [ # Get product name in database (<platform_cache>/<product>)
  97. f"{str(platform)}/{product}".replace("\\", "/") for platform in platforms
  98. ]
  99. missing_dependencies = {}
  100. # Fetch missing dependencies for each platform for the target product
  101. for product_name in product_names:
  102. logger.info(f"Checking missing dependencies for '{product_name}'")
  103. missing_dependencies[product_name] = set() # Set() to not allow duplicates
  104. product_id = db_utils.get_product_id(self.asset_db, product_name)
  105. # Assert exactly one match
  106. assert product_id, f"Expected to find exactly one DB match for {product_name}, " \
  107. f"instead found {len(product_id)}"
  108. db_dependencies = db_utils.get_missing_dependencies(self.asset_db, product_id)
  109. for db_dep in db_dependencies:
  110. # Parse database entries to get ready for comparison #
  111. # UUID stored as binary; convert to hex string
  112. uuid = hex(int.from_bytes(db_dep[self.UUID_INDEX], byteorder="big", signed=False))
  113. # Sub ID stored in DB as unsigned 32-bit int (may appear negative in python); make it positive
  114. sub_id = int(db_dep[self.SUB_ID_INDEX]) & 0xFFFFFFFF
  115. # File String stored as a string (no funky conversion needed)
  116. file_str = db_dep[self.STR_INDEX]
  117. # Asset-ID is in format: "{<uuid>}:<sub_id>" (sub_id as hex string: trim off first 2 chars: "0x")
  118. asset_id = f"{{{self.uuid_format(uuid)}}}:{hex(sub_id)[2:]}"
  119. # Only add to missing dependencies if this tuple is in fact a missing dependency.
  120. # 'No missing dependencies' are represented as a specific "File String", "Asset-ID" pair
  121. if file_str != self.DB_NO_MISSING_DEP[0] and asset_id != self.DB_NO_MISSING_DEP[1]:
  122. logger.info(f"Found missing dependency '{file_str}' with asset ID '{asset_id}'")
  123. missing_dependencies[product_name].add((file_str, asset_id))
  124. return missing_dependencies
  125. def uuid_format(self, hex_string: str) -> str:
  126. """
  127. Converts the UUID hex string into a common format.
  128. ex: '0x785a05d2483e5b43a2b992acdae6e938}:[0' -> '785A05D2-483E-5B43-A2B9-92ACDAE6E938'
  129. """
  130. hex_string = hex_string.upper()
  131. # Remove unwanted text
  132. hex_string = hex_string.replace("-", "").replace("{", "").replace("}", "").replace("0X", "")
  133. hex_string = hex_string.split(":")[0] # Anything after ':' is the sub id; remove
  134. if len(hex_string) < 32:
  135. # If the hex value didn't fill 32 characters, pad with leading zeroes
  136. hex_string = ("0" * (32 - len(hex_string))) + hex_string
  137. # fmt:off
  138. # Add hyphen separators
  139. return hex_string[0:8] + "-" + hex_string[8:12] + "-" + hex_string[12:16] + \
  140. "-" + hex_string[16:20] + "-" + hex_string[20:]
  141. # fmt:on
  142. def validate_expected_dependencies(self, product: str, expected_dependencies: List[Tuple[str, str]],
  143. log_output: List[str or List[str]], platforms: List[str] = None) -> None:
  144. """
  145. Validates that the [product] has missing dependencies in both the AP Batch log and the asset database.
  146. :param product: The file to look for missing dependencies in.
  147. :param expected_dependencies: A list of tuples, where:
  148. tuple[0] - the asset string for a missing dependency
  149. tuple[1] - the asset id for a missing dependency
  150. ;param platforms: Platforms to validate in the DB. Checks the db for all platforms processed if None.
  151. :return: None
  152. """
  153. logger.info(f"Searching output for expected dependencies for product {product}")
  154. sorted_expected = sorted(expected_dependencies)
  155. # Check dependencies found either in the log or console output
  156. for product_name, missing_deps in self.extract_missing_dependencies_from_output(log_output).items():
  157. if product in product_name:
  158. sorted_missing = sorted(missing_deps)
  159. # fmt:off
  160. assert sorted_expected == sorted_missing, \
  161. f"Missing dependencies for '{product_name}' did not match expected. Expected: " \
  162. f"{sorted_expected}, Actual: {sorted_missing}"
  163. # fmt:on
  164. # Check dependencies found in Database
  165. for product_name, missing_deps in self.extract_missing_dependencies_from_database(product,
  166. platforms).items():
  167. if product.replace("\\", "/") in product_name:
  168. sorted_missing = sorted(missing_deps)
  169. # fmt:off
  170. assert sorted_expected == sorted_missing, \
  171. f"Product '{product_name}' expected missing dependencies: {sorted_expected}; " \
  172. f"actual missing dependencies {sorted_missing}"
  173. # fmt:on
  174. def __getitem__(self, item: str) -> object:
  175. """
  176. Get Item overload to use the object like a dictionary.
  177. Implemented so this fixture "looks and feels" like other AP fixtures that return dictionaries
  178. """
  179. return self.__dict__[str(item)]
  180. # End class MissingDependencyHelper
  181. md = MissingDependencyHelper()
  182. return md