atom_tools_utils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 pathlib
  8. import shutil
  9. import sys
  10. import time
  11. import azlmbr.asset as azasset
  12. import azlmbr.atom
  13. import azlmbr.atomtools
  14. import azlmbr.bus as bus
  15. import azlmbr.math as azmath
  16. import azlmbr.paths
  17. import ly_test_tools.environment.file_system as fs
  18. from Atom.atom_utils.atom_constants import (
  19. AtomToolsDocumentRequestBusEvents, AtomToolsDocumentSystemRequestBusEvents, AtomToolsMainWindowRequestBusEvents,
  20. EntityPreviewViewportSettingsRequestBusEvents)
  21. MATERIAL_TYPES_PATH = pathlib.PurePath(azlmbr.paths.resolve_path(
  22. "@gemroot:Atom_Feature_Common@/Assets/Materials/Types"))
  23. MATERIALCANVAS_GRAPH_PATH = pathlib.PurePath(azlmbr.paths.resolve_path(
  24. "@gemroot:MaterialCanvas@/Assets/MaterialCanvas/TestData"))
  25. TEST_DATA_MATERIALS_PATH = pathlib.PurePath(azlmbr.paths.resolve_path(
  26. "@gemroot:Atom_TestData@/Assets/TestData/Materials"))
  27. VIEWPORT_LIGHTING_PRESETS_PATH = pathlib.PurePath(azlmbr.paths.resolve_path(
  28. "@gemroot:MaterialEditor@/Assets/MaterialEditor/LightingPresets"))
  29. VIEWPORT_MODELS_PRESETS_PATH = pathlib.PurePath(azlmbr.paths.resolve_path(
  30. "@gemroot:MaterialEditor@/Assets/MaterialEditor/ViewportModels"))
  31. def is_close(
  32. actual: int or float or object, expected: int or float or object, buffer: float = sys.float_info.min) -> bool:
  33. """
  34. Obtains the absolute value using an actual value and expected value then compares it to the buffer.
  35. The result of this comparison is returned as a boolean.
  36. :param actual: actual value
  37. :param expected: expected value
  38. :param buffer: acceptable variation from expected
  39. :return: bool
  40. """
  41. return abs(actual - expected) < buffer
  42. def compare_colors(color1: azlmbr.math.Color, color2: azlmbr.math.Color, buffer: float = 0.00001) -> bool:
  43. """
  44. Compares the red, green and blue properties of a color allowing a slight variance of buffer.
  45. :param color1: azlmbr.math.Color first value to compare.
  46. :param color2: azlmbr.math.Color second value to compare.
  47. :param buffer: allowed variance in individual color value.
  48. :return: boolean representing whether the colors are close (True) or too different (False).
  49. """
  50. return (
  51. is_close(color1.r, color2.r, buffer)
  52. and is_close(color1.g, color2.g, buffer)
  53. and is_close(color1.b, color2.b, buffer)
  54. )
  55. def verify_one_material_document_opened(
  56. material_document_ids_list: [azlmbr.math.Uuid], opened_document_index: int) -> bool:
  57. """
  58. Validation helper to verify if the document at opened_document_index value in the material_document_ids list
  59. is the only opened document.
  60. Returns True on success, False on failure.
  61. :param material_document_ids_list: List of material document IDs used for making the document opened check.
  62. :param opened_document_index: Index number of the one material document that should be open
  63. :return: bool
  64. """
  65. if not is_document_open(material_document_ids_list[opened_document_index]):
  66. return False
  67. material_document_ids_verification_list = material_document_ids_list.copy()
  68. material_document_ids_verification_list.pop(opened_document_index)
  69. for closed_material_document_id in material_document_ids_verification_list:
  70. if is_document_open(closed_material_document_id):
  71. return False
  72. return True
  73. def open_document(file_path: str) -> azlmbr.math.Uuid:
  74. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  75. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.OPEN_DOCUMENT, file_path)
  76. def is_document_open(document_id: azlmbr.math.Uuid) -> bool:
  77. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  78. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.IS_DOCUMENT_OPEN, document_id)
  79. def save_document(document_id: azlmbr.math.Uuid) -> bool:
  80. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  81. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.SAVE_DOCUMENT, document_id)
  82. def save_document_as_copy(document_id: azlmbr.math.Uuid, target_path: str) -> bool:
  83. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  84. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.SAVE_DOCUMENT_AS_COPY, document_id, target_path)
  85. def save_document_as_child(document_id: azlmbr.math.Uuid, target_path: str) -> bool:
  86. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  87. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.SAVE_DOCUMENT_AS_CHILD, document_id, target_path)
  88. def save_all_documents() -> bool:
  89. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  90. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.SAVE_ALL_DOCUMENTS)
  91. def close_document(document_id: azlmbr.math.Uuid):
  92. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  93. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.CLOSE_DOCUMENT, document_id)
  94. def close_all_documents() -> bool:
  95. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  96. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.CLOSE_ALL_DOCUMENTS)
  97. def close_all_except_selected(document_id: azlmbr.math.Uuid) -> bool:
  98. return azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(
  99. bus.Broadcast, AtomToolsDocumentSystemRequestBusEvents.CLOSE_ALL_DOCUMENTS_EXCEPT, document_id)
  100. def is_pane_visible(pane_name: str) -> bool:
  101. return azlmbr.atomtools.AtomToolsMainWindowRequestBus(
  102. bus.Broadcast, AtomToolsMainWindowRequestBusEvents.IS_DOCK_WIDGET_VISIBLE, pane_name)
  103. def set_pane_visibility(pane_name: str, value: bool) -> None:
  104. azlmbr.atomtools.AtomToolsMainWindowRequestBus(
  105. bus.Broadcast, AtomToolsMainWindowRequestBusEvents.SET_DOCK_WIDGET_VISIBLE, pane_name, value)
  106. def load_lighting_preset_by_asset_id(asset_id: azlmbr.math.Uuid) -> bool:
  107. """
  108. Takes in an asset ID and attempts to select the lighting background in the viewporet dropdown using that ID.
  109. Returns True if it successfully changes it, False otherwise.
  110. """
  111. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  112. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.LOAD_LIGHTING_PRESET_BY_ASSET_ID, asset_id)
  113. def load_lighting_preset_by_path(asset_path: str) -> bool:
  114. """
  115. Takes in an asset path and attempts to select the lighting background in the viewport dropdown using that path.
  116. Returns True if it successfully changes it, False otherwise.
  117. """
  118. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  119. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.LOAD_LIGHTING_PRESET, asset_path)
  120. def get_last_lighting_preset_asset_id() -> azlmbr.math.Uuid:
  121. """
  122. Returns the asset ID of the currently selected viewport lighting background.
  123. Example return values observed when selecting different lighting backgrounds from the viewport dropdown:
  124. {B9AA460C-622D-5F38-A02C-C0BCB0B65D96}:0
  125. {C4978B46-D509-5B71-86A1-09D8CC051DC4}:0
  126. {AB7FA1BA-7207-5333-BDD6-69C3F5B7A410}:0
  127. """
  128. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  129. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.GET_LAST_LIGHTING_PRESET_ASSET_ID)
  130. def get_last_lighting_preset_path() -> str:
  131. """
  132. Returns the full path to the currently selected viewport lighting background.
  133. Example return values observed when selecting different lighting backgrounds from the viewport dropdown:
  134. "C:/git/o3de/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/LightingPresets/neutral_urban.lightingpreset.azasset"
  135. "C:/git/o3de/Gems/Atom/Feature/Common/Assets/LightingPresets/LowContrast/artist_workshop.lightingpreset.azasset"
  136. "@gemroot:Atom_TestData@/Assets/TestData/LightingPresets/beach_parking.lightingpreset.azasset"
  137. """
  138. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  139. azlmbr.bus.Broadcast,
  140. EntityPreviewViewportSettingsRequestBusEvents.GET_LAST_LIGHTING_PRESET_PATH_WITHOUT_ALIAS)
  141. def get_last_model_preset_path() -> str:
  142. """
  143. Returns the full path to the currently selected viewport model.
  144. Example return values observed when selecting different models from the vioewport dropdown:
  145. "C:/git/o3de/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/Shaderball.modelpreset.azasset"
  146. "C:/git/o3de/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/Cone.modelpreset.azasset"
  147. "C:/git/o3de/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/BeveledCone.modelpreset.azasset"
  148. """
  149. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  150. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.GET_LAST_MODEL_PRESET_PATH_WITHOUT_ALIAS)
  151. def get_last_model_preset_asset_id() -> azlmbr.math.Uuid:
  152. """
  153. Returns the asset ID of the currently selected viewport model.
  154. Example return values observed when selecting different models from the viewport dropdown:
  155. {9B61FAD7-2717-528C-9EBF-C1324D46ED56}:0
  156. {56C02199-7B4F-5896-A713-F70E6EBA0726}:0
  157. {46D4B53F-A900-591B-B4CD-75A79E47749B}:0
  158. """
  159. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  160. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.GET_LAST_MODEL_PRESET_ASSET_ID)
  161. def set_grid_enabled(value: bool) -> None:
  162. azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  163. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.SET_GRID_ENABLED, value)
  164. def get_grid_enabled() -> bool:
  165. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  166. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.GET_GRID_ENABLED)
  167. def set_shadow_catcher_enabled(value: bool) -> None:
  168. azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  169. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.SET_SHADOW_CATCHER_ENABLED, value)
  170. def get_shadow_catcher_enabled() -> bool:
  171. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  172. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.GET_SHADOW_CATCHER_ENABLED)
  173. def load_model_preset_by_asset_id(asset_id: azlmbr.math.Uuid) -> bool:
  174. """
  175. Takes in an asset ID and attempts to select the model in the viewport dropdown using that ID.
  176. Returns True if it successfully changes, False otherwise.
  177. """
  178. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  179. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.LOAD_MODEL_PRESET_BY_ASSET_ID, asset_id)
  180. def load_model_preset_by_path(asset_path: str) -> bool:
  181. """
  182. Takes in an asset path and attempts to select the model in the viewport dropdown using that path.
  183. Returns True if it successfully changes it, False otherwise.
  184. """
  185. return azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(
  186. azlmbr.bus.Broadcast, EntityPreviewViewportSettingsRequestBusEvents.LOAD_MODEL_PRESET, asset_path)
  187. def undo(document_id: azlmbr.math.Uuid) -> bool:
  188. return azlmbr.atomtools.AtomToolsDocumentRequestBus(bus.Event, AtomToolsDocumentRequestBusEvents.UNDO, document_id)
  189. def redo(document_id: azlmbr.math.Uuid) -> bool:
  190. return azlmbr.atomtools.AtomToolsDocumentRequestBus(bus.Event, AtomToolsDocumentRequestBusEvents.REDO, document_id)
  191. def begin_edit(document_id: azlmbr.math.Uuid) -> bool:
  192. return azlmbr.atomtools.AtomToolsDocumentRequestBus(
  193. azlmbr.bus.Event, AtomToolsDocumentRequestBusEvents.BEGIN_EDIT, document_id)
  194. def end_edit(document_id: azlmbr.math.Uuid) -> bool:
  195. return azlmbr.atomtools.AtomToolsDocumentRequestBus(
  196. azlmbr.bus.Event, AtomToolsDocumentRequestBusEvents.END_EDIT, document_id)
  197. def crash() -> None:
  198. azlmbr.atomtools.general.crash()
  199. def exit() -> None:
  200. """
  201. Closes the current Atom tools executable.
  202. :return: None
  203. """
  204. azlmbr.atomtools.general.exit()
  205. def disable_document_message_box_settings() -> None:
  206. """
  207. Modifies some registry settings to disable warning and error message boxes that block test progression.
  208. :return: None
  209. """
  210. azlmbr.atomtools.util.SetSettingsValue_bool("/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/DisplayErrorMessageDialogs", False)
  211. azlmbr.atomtools.util.SetSettingsValue_bool("/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/DisplayWarningMessageDialogs", False)
  212. def disable_graph_compiler_settings() -> None:
  213. """
  214. Modifies some registry settings to disable automatic graph compilation on open/edit/save.
  215. This is because some tests (i.e. main suite) need to avoid file writing/saving during the test run.
  216. :return: None
  217. """
  218. azlmbr.atomtools.util.SetSettingsValue_bool("/O3DE/AtomToolsFramework/GraphCompiler/CompileOnOpen", False)
  219. azlmbr.atomtools.util.SetSettingsValue_bool("/O3DE/AtomToolsFramework/GraphCompiler/CompileOnSave", False)
  220. azlmbr.atomtools.util.SetSettingsValue_bool("/O3DE/AtomToolsFramework/GraphCompiler/CompileOnEdit", False)
  221. def wait_for_condition(function: (), timeout_in_seconds: float = 1.0) -> bool or TypeError:
  222. """
  223. Function to run until it returns True or timeout is reached
  224. the function can have no parameters and
  225. waiting idle__wait_* is handled here not in the function
  226. :param function: a function that returns a boolean indicating a desired condition is achieved
  227. :param timeout_in_seconds: when reached, function execution is abandoned and False is returned
  228. :return: boolean representing success (True) or failure (False)
  229. """
  230. with Timeout(timeout_in_seconds) as t:
  231. while True:
  232. try:
  233. azlmbr.atomtools.general.idle_wait_frames(1)
  234. except Exception as e:
  235. print(f"WARNING: Couldn't wait for frame, got Exception : {e}")
  236. if t.timed_out:
  237. return False
  238. ret = function()
  239. if not isinstance(ret, bool):
  240. raise TypeError("return value for wait_for_condition function must be a bool")
  241. if ret:
  242. return True
  243. class ShaderAssetTestHelper:
  244. def copy_file(src_file, src_path, target_file, target_path):
  245. # type: (str, str, str, str) -> None
  246. """
  247. Copies the [src_file] located in [src_path] to the [target_file] located at [target_path].
  248. Leaves the [target_file] unlocked for reading and writing privileges
  249. :param src_file: The source file to copy (file name)
  250. :param src_path: The source file's path
  251. :param target_file: The target file to copy into (file name)
  252. :param target_path: The target file's path
  253. :return: None
  254. """
  255. target_file_path = os.path.join(target_path, target_file)
  256. src_file_path = os.path.join(src_path, src_file)
  257. if os.path.exists(target_file_path):
  258. fs.unlock_file(target_file_path)
  259. shutil.copyfile(src_file_path, target_file_path)
  260. def copy_tmp_files_in_order(src_directory, file_list, dst_directory, wait_time_in_between=0.0):
  261. import azlmbr.legacy.general as general
  262. # type: (str, list, str, float) -> None
  263. """
  264. This function assumes that for each file name listed in @file_list
  265. there's file named "@filename.txt" which the original source file
  266. but they will be copied with just the @filename (.txt removed).
  267. """
  268. for filename in file_list:
  269. src_name = f"{filename}.txt"
  270. ShaderAssetTestHelper.copy_file(src_name, src_directory, filename, dst_directory)
  271. if wait_time_in_between > 0.0:
  272. print(f"Created {filename} in {dst_directory}")
  273. general.idle_wait(wait_time_in_between)
  274. def remove_file(src_file, src_path):
  275. # type: (str, str) -> None
  276. """
  277. Removes the [src_file] located in [src_path].
  278. :param src_file: The source file to copy (file name)
  279. :param src_path: The source file's path
  280. :return: None
  281. """
  282. src_file_path = os.path.join(src_path, src_file)
  283. if os.path.exists(src_file_path):
  284. fs.unlock_file(src_file_path)
  285. os.remove(src_file_path)
  286. def remove_files(directory, file_list):
  287. for filename in file_list:
  288. ShaderAssetTestHelper.remove_file(filename, directory)
  289. def asset_exists(cache_relative_path):
  290. asset_id = azasset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", cache_relative_path, azmath.Uuid(), False)
  291. return asset_id.is_valid()
  292. class Timeout(object):
  293. """
  294. Contextual timeout
  295. """
  296. def __init__(self, seconds: float) -> None:
  297. """
  298. :param seconds: float seconds to allow before timed_out is True
  299. :return: None
  300. """
  301. self.seconds = seconds
  302. def __enter__(self) -> object:
  303. self.die_after = time.time() + self.seconds
  304. return self
  305. def __exit__(self, type: any, value: any, traceback: str) -> None:
  306. pass
  307. @property
  308. def timed_out(self) -> float:
  309. return time.time() > self.die_after
  310. class ScreenshotHelper(object):
  311. """
  312. A helper to capture screenshots and wait for them.
  313. """
  314. def __init__(self, idle_wait_frames_callback: ()) -> None:
  315. super().__init__()
  316. self.done = False
  317. self.captured_screenshot = False
  318. self.max_frames_to_wait = 60
  319. self.idle_wait_frames_callback = idle_wait_frames_callback
  320. def capture_screenshot_blocking(self, filename: str) -> bool:
  321. """
  322. Capture a screenshot and block the execution until the screenshot has been written to the disk.
  323. """
  324. self.done = False
  325. self.captured_screenshot = False
  326. outcome = azlmbr.atom.FrameCaptureRequestBus(azlmbr.bus.Broadcast, "CaptureScreenshot", filename)
  327. if outcome.IsSuccess():
  328. self.handler = azlmbr.atom.FrameCaptureNotificationBusHandler()
  329. self.handler.connect(outcome.GetValue())
  330. self.handler.add_callback("OnCaptureFinished", self.on_screenshot_captured)
  331. self.wait_until_screenshot()
  332. print("Screenshot taken.")
  333. else:
  334. print(f"Screenshot failed. {outcome.GetError().error_message}")
  335. return self.captured_screenshot
  336. def on_screenshot_captured(self, parameters: tuple[any, any]) -> None:
  337. """
  338. Sets the value for self.captured_screenshot based on the values in the parameters tuple.
  339. :param parameters: A tuple of 2 values that indicates if the capture was successful.
  340. """
  341. if parameters[0]:
  342. print(f"screenshot saved: {parameters[1]}")
  343. self.captured_screenshot = True
  344. else:
  345. print(f"screenshot failed: {parameters[1]}")
  346. self.done = True
  347. self.handler.disconnect()
  348. def wait_until_screenshot(self) -> None:
  349. frames_waited = 0
  350. while self.done is False:
  351. self.idle_wait_frames_callback(1)
  352. if frames_waited > self.max_frames_to_wait:
  353. print("timeout while waiting for the screenshot to be written")
  354. self.handler.disconnect()
  355. break
  356. else:
  357. frames_waited = frames_waited + 1
  358. print(f"(waited {frames_waited} frames)")
  359. def capture_screenshot(file_path: str) -> bool:
  360. return ScreenshotHelper(azlmbr.atomtools.general.idle_wait_frames).capture_screenshot_blocking(
  361. file_path
  362. )