tiaf_persistent_storage.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import json
  9. import pathlib
  10. from abc import ABC, abstractmethod
  11. from tiaf_logger import get_logger
  12. logger = get_logger(__file__)
  13. # Abstraction for the persistent storage required by TIAF to store and retrieve the branch coverage data and other meta-data
  14. class PersistentStorage(ABC):
  15. COMMON_CONFIG_KEY = "common"
  16. WORKSPACE_KEY = "workspace"
  17. HISTORIC_SEQUENCES_KEY = "historic_sequences"
  18. ACTIVE_KEY = "active"
  19. ROOT_KEY = "root"
  20. RELATIVE_PATHS_KEY = "relative_paths"
  21. TEST_IMPACT_DATA_FILE_KEY = "test_impact_data_file"
  22. PREVIOUS_TEST_RUN_DATA_FILE_KEY = "previous_test_run_data_file"
  23. LAST_COMMIT_HASH_KEY = "last_commit_hash"
  24. COVERAGE_DATA_KEY = "coverage_data"
  25. PREVIOUS_TEST_RUNS_KEY = "previous_test_runs"
  26. RUNTIME_ARTIFACT_DIRECTORY = "RuntimeArtifacts"
  27. RUNTIME_COVERAGE_DIRECTORY = "RuntimeCoverage"
  28. def __init__(self, config: dict, suites_string: str, commit: str, active_workspace: str, unpacked_coverage_data_file_path: str, previous_test_run_data_file_path: str, temp_workspace: str):
  29. """
  30. Initializes the persistent storage into a state for which there is no historic data available.
  31. @param config: The runtime configuration to obtain the data file paths from.
  32. @param suites_string: The unique key to differentiate the different suite combinations from one another different for which the historic data will be obtained for.
  33. @param commit: The commit hash for this build.
  34. """
  35. # Work on the assumption that there is no historic meta-data (a valid state to be in, should none exist)
  36. self._suites_string = suites_string
  37. self._last_commit_hash = None
  38. self._has_historic_data = False
  39. self._has_previous_last_commit_hash = False
  40. self._this_commit_hash = commit
  41. self._this_commit_hash_last_commit_hash = None
  42. self._historic_data = None
  43. logger.info(f"Attempting to access persistent storage for the commit '{self._this_commit_hash}' for suites '{self._suites_string}'")
  44. self._temp_workspace = pathlib.Path(temp_workspace)
  45. self._active_workspace = pathlib.Path(active_workspace).joinpath(pathlib.Path(self._suites_string))
  46. self._unpacked_coverage_data_file = self._active_workspace.joinpath(unpacked_coverage_data_file_path)
  47. self._previous_test_run_data_file = self._active_workspace.joinpath(previous_test_run_data_file_path)
  48. def _unpack_historic_data(self, historic_data_json: str):
  49. """
  50. Unpacks the historic data into the appropriate memory and disk locations.
  51. @param historic_data_json: The historic data in JSON format.
  52. """
  53. self._has_historic_data = False
  54. self._has_previous_last_commit_hash = False
  55. try:
  56. self._historic_data = json.loads(historic_data_json)
  57. # Last commit hash for this branch
  58. self._last_commit_hash = self._historic_data[self.LAST_COMMIT_HASH_KEY]
  59. logger.info(f"Last commit hash '{self._last_commit_hash}' found.")
  60. # Last commit hash for the sequence that was run for this commit previously (if any)
  61. if self.HISTORIC_SEQUENCES_KEY in self._historic_data:
  62. if self._this_commit_hash in self._historic_data[self.HISTORIC_SEQUENCES_KEY]:
  63. # 'None' is a valid value for the previously used last commit hash if there was no coverage data at that time
  64. self._this_commit_hash_last_commit_hash = self._historic_data[self.HISTORIC_SEQUENCES_KEY][self._this_commit_hash]
  65. self._has_previous_last_commit_hash = self._this_commit_hash_last_commit_hash is not None
  66. if self._has_previous_last_commit_hash:
  67. logger.info(f"Last commit hash '{self._this_commit_hash_last_commit_hash}' was used previously for the commit '{self._last_commit_hash}'.")
  68. else:
  69. logger.info(f"Prior sequence data found for this commit but it is empty (there was no coverage data available at that time).")
  70. else:
  71. logger.info(f"No prior sequence data found for commit '{self._this_commit_hash}', this is the first sequence for this commit.")
  72. else:
  73. logger.info(f"No prior sequence data found for any commits.")
  74. # Test runs for the previous sequence associated with the last commit hash
  75. previous_test_runs = self._historic_data.get(self.PREVIOUS_TEST_RUNS_KEY, None)
  76. if previous_test_runs:
  77. logger.info(f"Previous test run data for a sequence of '{len(previous_test_runs)}' test targets found.")
  78. else:
  79. self._historic_data[self.PREVIOUS_TEST_RUNS_KEY] = {}
  80. logger.info("No previous test run data found.")
  81. # Create the active workspace directory for the unpacked historic data files so they are accessible by the runtime
  82. self._active_workspace.mkdir(exist_ok=True, parents=True)
  83. # Coverage file
  84. logger.info(f"Writing coverage data to '{self._unpacked_coverage_data_file}'.")
  85. with open(self._unpacked_coverage_data_file, "w", newline='\n') as coverage_data:
  86. coverage_data.write(self._historic_data[self.COVERAGE_DATA_KEY])
  87. # Previous test runs file
  88. logger.info(f"Writing previous test runs data to '{self._previous_test_run_data_file}'.")
  89. with open(self._previous_test_run_data_file, "w", newline='\n') as previous_test_runs_data:
  90. previous_test_runs_json = json.dumps(self._historic_data[self.PREVIOUS_TEST_RUNS_KEY])
  91. previous_test_runs_data.write(previous_test_runs_json)
  92. self._has_historic_data = True
  93. except json.JSONDecodeError:
  94. logger.error("The historic data does not contain valid JSON.")
  95. except KeyError as e:
  96. logger.error(f"The historic data does not contain the key {str(e)}.")
  97. except EnvironmentError as e:
  98. logger.error(f"There was a problem the coverage data file '{self._unpacked_coverage_data_file}': '{e}'.")
  99. def _pack_historic_data(self, test_runs: list):
  100. """
  101. Packs the current historic data into a JSON file for serializing.
  102. @param test_runs: The test runs for the sequence that just completed.
  103. @return: The packed historic data in JSON format.
  104. """
  105. try:
  106. # Attempt to read the existing coverage data
  107. if self._unpacked_coverage_data_file.is_file():
  108. if not self._historic_data:
  109. self._historic_data = {}
  110. # Last commit hash for this branch
  111. self._historic_data[self.LAST_COMMIT_HASH_KEY] = self._this_commit_hash
  112. # Last commit hash for this commit
  113. if not self.HISTORIC_SEQUENCES_KEY in self._historic_data:
  114. self._historic_data[self.HISTORIC_SEQUENCES_KEY] = {}
  115. self._historic_data[self.HISTORIC_SEQUENCES_KEY][self._this_commit_hash] = self._last_commit_hash
  116. # Test runs for this completed sequence
  117. self._historic_data[self.PREVIOUS_TEST_RUNS_KEY] = test_runs
  118. # Coverage data for this branch
  119. with open(self._unpacked_coverage_data_file, "r") as coverage_data:
  120. self._historic_data[self.COVERAGE_DATA_KEY] = coverage_data.read()
  121. return json.dumps(self._historic_data)
  122. else:
  123. logger.info(f"No coverage data exists at location '{self._unpacked_coverage_data_file}'.")
  124. except EnvironmentError as e:
  125. logger.error(f"There was a problem the coverage data file '{self._unpacked_coverage_data_file}': '{e}'.")
  126. except TypeError:
  127. logger.error("The historic data could not be serialized to valid JSON.")
  128. return None
  129. @abstractmethod
  130. def _store_historic_data(self, historic_data_json: str):
  131. """
  132. Stores the historic data in the designated persistent storage location.
  133. @param historic_data_json: The historic data (in JSON format) to be stored in persistent storage.
  134. """
  135. pass
  136. def update_and_store_historic_data(self, test_runs: list):
  137. """
  138. Updates the historic data and stores it in the designated persistent storage location.
  139. @param test_runs: The test runs for the sequence that just completed.
  140. """
  141. historic_data_json = self._pack_historic_data(test_runs)
  142. if historic_data_json:
  143. logger.info(f"Attempting to store historic data with new last commit hash '{self._this_commit_hash}'...")
  144. self._store_historic_data(historic_data_json)
  145. logger.info("The historic data was successfully stored.")
  146. else:
  147. logger.info("The historic data could not be successfully stored.")
  148. def store_artifacts(self, runtime_artifact_dir, runtime_coverage_dir):
  149. """
  150. Store the runtime artifacts and runtime coverage artifacts stored in the specified directories.
  151. @param runtime_artifact_dir: The directory containing the runtime artifacts to store.
  152. @param runtime_coverage_dir: The directory contianing the runtime coverage artifacts to store.
  153. """
  154. self._store_runtime_artifacts(runtime_artifact_dir)
  155. self._store_coverage_artifacts(runtime_coverage_dir)
  156. @abstractmethod
  157. def _store_runtime_artifacts(self, runtime_artifact_dir):
  158. """
  159. Store the runtime artifacts in the designated persistent storage location.
  160. @param runtime_artifact_dir: The directory containing the runtime artifacts to store.
  161. """
  162. pass
  163. @abstractmethod
  164. def _store_coverage_artifacts(self, runtime_coverage_dir):
  165. """
  166. Store the coverage artifacts in the designated persistent storage location.
  167. @param runtime_coverage_dir: The directory contianing the runtime coverage artifacts to store.
  168. """
  169. pass
  170. @property
  171. def has_historic_data(self):
  172. """
  173. Flag denoting that persistent storage was able to find relevant historic data for the requested branch.
  174. """
  175. return self._has_historic_data
  176. @property
  177. def last_commit_hash(self):
  178. """
  179. Hash of the last commit we processed, ingested from our historic data.
  180. """
  181. return self._last_commit_hash
  182. @property
  183. def is_last_commit_hash_equal_to_this_commit_hash(self):
  184. """
  185. Is the current commit that we are running TIAF on the same as the last commit we have in our historic data.
  186. This means that this is a repeat sequence.
  187. """
  188. return self._last_commit_hash == self._this_commit_hash
  189. @property
  190. def this_commit_last_commit_hash(self):
  191. """
  192. Hash of this commit. Is none if this commit hash was not found in our historic data.
  193. """
  194. return self._this_commit_hash_last_commit_hash
  195. @property
  196. def has_previous_last_commit_hash(self):
  197. """
  198. If the hash of the last commit was found in our historic data, then this flag will be set.
  199. """
  200. return self._has_previous_last_commit_hash