utils.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. """
  2. All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. its licensors.
  4. For complete copyright and license terms please see the LICENSE at the root of this
  5. distribution (the "License"). All use of this software is governed by the License,
  6. or, if provided, by the license below or the license accompanying this file. Do not
  7. remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. """
  10. import os
  11. import time
  12. import math
  13. import azlmbr
  14. import azlmbr.legacy.general as general
  15. import azlmbr.debug
  16. import traceback
  17. class FailFast(Exception):
  18. """
  19. Raise to stop proceeding through test steps.
  20. """
  21. pass
  22. class TestHelper:
  23. @staticmethod
  24. def init_idle():
  25. general.idle_enable(True)
  26. # JIRA: SPEC-2880
  27. # general.idle_wait_frames(1)
  28. @staticmethod
  29. def open_level(directory, level):
  30. # type: (str, ) -> None
  31. """
  32. :param level: the name of the level folder in AutomatedTesting\\Physics\\
  33. :return: None
  34. """
  35. Report.info("Open level {}/{}".format(directory, level))
  36. success = general.open_level_no_prompt(os.path.join(directory, level))
  37. if not success:
  38. open_level_name = general.get_current_level_name()
  39. if open_level_name == level:
  40. Report.info("{} was already opened".format(level))
  41. else:
  42. assert False, "Failed to open level: {} does not exist or is invalid".format(level)
  43. # FIX-ME: Expose call for checking when has been finished loading and change this frame waiting
  44. # Jira: LY-113761
  45. general.idle_wait_frames(200)
  46. @staticmethod
  47. def enter_game_mode(msgtuple_success_fail):
  48. # type: (tuple) -> None
  49. """
  50. :param msgtuple_success_fail: The tuple with the expected/unexpected messages for entering game mode.
  51. :return: None
  52. """
  53. Report.info("Entering game mode")
  54. general.enter_game_mode()
  55. TestHelper.wait_for_condition(lambda : general.is_in_game_mode(), 1.0)
  56. Report.critical_result(msgtuple_success_fail, general.is_in_game_mode())
  57. @staticmethod
  58. def exit_game_mode(msgtuple_success_fail):
  59. # type: (tuple) -> None
  60. """
  61. :param msgtuple_success_fail: The tuple with the expected/unexpected messages for exiting game mode.
  62. :return: None
  63. """
  64. Report.info("Exiting game mode")
  65. general.exit_game_mode()
  66. TestHelper.wait_for_condition(lambda : not general.is_in_game_mode(), 1.0)
  67. Report.critical_result(msgtuple_success_fail, not general.is_in_game_mode())
  68. @staticmethod
  69. def close_editor():
  70. general.exit_no_prompt()
  71. @staticmethod
  72. def fail_fast(message=None):
  73. # type: (str) -> None
  74. """
  75. A state has been reached where progressing in the test is not viable.
  76. raises FailFast
  77. :return: None
  78. """
  79. Report.info("Failing fast. Raising an exception and shutting down the editor.")
  80. if message:
  81. Report.info("Fail fast message: {}".format(message))
  82. TestHelper.close_editor()
  83. raise FailFast()
  84. @staticmethod
  85. def wait_for_condition(function, timeout_in_seconds):
  86. # type: (function, float) -> bool
  87. """
  88. **** Will be replaced by a function of the same name exposed in the Engine*****
  89. a function to run until it returns True or timeout is reached
  90. the function can have no parameters and
  91. waiting idle__wait_* is handled here not in the function
  92. :param function: a function that returns a boolean indicating a desired condition is achieved
  93. :param timeout_in_seconds: when reached, function execution is abandoned and False is returned
  94. """
  95. with Timeout(timeout_in_seconds) as t:
  96. while True:
  97. try:
  98. general.idle_wait_frames(1)
  99. except:
  100. Report.info("WARNING: Couldn't wait for frame")
  101. if t.timed_out:
  102. return False
  103. ret = function()
  104. if not isinstance(ret, bool):
  105. raise TypeError("return value for wait_for_condition function must be a bool")
  106. if ret:
  107. return True
  108. class Timeout:
  109. # type: (float) -> None
  110. """
  111. contextual timeout
  112. :param seconds: float seconds to allow before timed_out is True
  113. """
  114. def __init__(self, seconds):
  115. self.seconds = seconds
  116. def __enter__(self):
  117. self.die_after = time.time() + self.seconds
  118. return self
  119. def __exit__(self, type, value, traceback):
  120. pass
  121. @property
  122. def timed_out(self):
  123. return time.time() > self.die_after
  124. class Report:
  125. _results = []
  126. _exception = None
  127. @staticmethod
  128. def start_test(test_function):
  129. try:
  130. test_function()
  131. except Exception as ex:
  132. Report._exception = traceback.format_exc()
  133. Report.report_results(test_function)
  134. @staticmethod
  135. def report_results(test_function):
  136. success = True
  137. report = f"Report for {test_function.__name__}:\n"
  138. for result in Report._results:
  139. passed, info = result
  140. success = success and passed
  141. if passed:
  142. report += f"[SUCCESS] {info}\n"
  143. else:
  144. report += f"[FAILED ] {info}\n"
  145. if Report._exception:
  146. report += "EXCEPTION raised:\n %s\n" % Report._exception[:-1].replace("\n", "\n ")
  147. success = False
  148. report += "Test result: "
  149. report += "SUCCESS" if success else "FAILURE"
  150. print(report)
  151. general.report_test_result(success, report)
  152. @staticmethod
  153. def info(msg):
  154. print("Info: {}".format(msg))
  155. @staticmethod
  156. def success(msgtuple_success_fail):
  157. outcome = "Success: {}".format(msgtuple_success_fail[0])
  158. print(outcome)
  159. Report._results.append((True, outcome))
  160. @staticmethod
  161. def failure(msgtuple_success_fail):
  162. outcome = "Failure: {}".format(msgtuple_success_fail[1])
  163. print(outcome)
  164. Report._results.append((False, outcome))
  165. @staticmethod
  166. def result(msgtuple_success_fail, condition):
  167. if not isinstance(condition, bool):
  168. raise TypeError("condition argument must be a bool")
  169. if condition:
  170. Report.success(msgtuple_success_fail)
  171. else:
  172. Report.failure(msgtuple_success_fail)
  173. return condition
  174. @staticmethod
  175. def critical_result(msgtuple_success_fail, condition, fast_fail_message=None):
  176. # type: (tuple, bool, str) -> None
  177. """
  178. if condition is False we will fail fast
  179. :param msgtuple_success_fail: messages to print based on the condition
  180. :param condition: success (True) or failure (False)
  181. :param fast_fail_message: [optional] message to include on fast fail
  182. """
  183. if not isinstance(condition, bool):
  184. raise TypeError("condition argument must be a bool")
  185. if not Report.result(msgtuple_success_fail, condition):
  186. TestHelper.fail_fast(fast_fail_message)
  187. # DEPRECATED: Use vector3_str()
  188. @staticmethod
  189. def info_vector3(vector3, label="", magnitude=None):
  190. # type: (azlmbr.math.Vector3, str, float) -> None
  191. """
  192. prints the vector to the Report.info log. If applied, label will print first,
  193. followed by the vector's values (x, y, z,) to 2 decimal places. Lastly if the
  194. magnitude is supplied, it will print on the third line.
  195. :param vector3: a azlmbr.math.Vector3 object to print
  196. prints in [x: , y: , z: ] format.
  197. :param label: [optional] A string to print before printing the vector3's contents
  198. :param magnitude: [optional] the vector's magnitude to print after the vector's contents
  199. :return: None
  200. """
  201. if label != "":
  202. Report.info(label)
  203. Report.info(" x: {:.2f}, y: {:.2f}, z: {:.2f}".format(vector3.x, vector3.y, vector3.z))
  204. if magnitude is not None:
  205. Report.info(" magnitude: {:.2f}".format(magnitude))
  206. '''
  207. Utility for scope tracing errors and warnings.
  208. Usage:
  209. ...
  210. with Tracer() as section_tracer:
  211. # section were we are interested in capturing errors/warnings/asserts
  212. ...
  213. Report.result(Tests.warnings_not_found_in_section, not section_tracer.has_warnings)
  214. '''
  215. class Tracer:
  216. def __init__(self):
  217. self.warnings = []
  218. self.errors = []
  219. self.asserts = []
  220. self.has_warnings = False
  221. self.has_errors = False
  222. self.has_asserts = False
  223. self.handler = None
  224. class WarningInfo:
  225. def __init__(self, args):
  226. self.window = args[0]
  227. self.filename = args[1]
  228. self.line = args[2]
  229. self.function = args[3]
  230. self.message = args[4]
  231. class ErrorInfo:
  232. def __init__(self, args):
  233. self.window = args[0]
  234. self.filename = args[1]
  235. self.line = args[2]
  236. self.function = args[3]
  237. self.message = args[4]
  238. class AssertInfo:
  239. def __init__(self, args):
  240. self.filename = args[0]
  241. self.line = args[1]
  242. self.function = args[2]
  243. self.message = args[3]
  244. def _on_warning(self, args):
  245. warningInfo = Tracer.WarningInfo(args)
  246. self.warnings.append(warningInfo)
  247. Report.info("Tracer caught Warning: %s" % warningInfo.message)
  248. self.has_warnings = True
  249. return False
  250. def _on_error(self, args):
  251. errorInfo = Tracer.ErrorInfo(args)
  252. self.errors.append(errorInfo)
  253. Report.info("Tracer caught Error: %s" % errorInfo.message)
  254. self.has_errors = True
  255. return False
  256. def _on_assert(self, args):
  257. assertInfo = Tracer.AssertInfo(args)
  258. self.asserts.append(assertInfo)
  259. Report.info("Tracer caught Assert: %s:%i[%s] \"%s\"" % (assertInfo.filename, assertInfo.line, assertInfo.function, assertInfo.message))
  260. self.has_asserts = True
  261. return False
  262. def __enter__(self):
  263. self.handler = azlmbr.debug.TraceMessageBusHandler()
  264. self.handler.connect(None)
  265. self.handler.add_callback("OnPreAssert", self._on_assert)
  266. self.handler.add_callback("OnPreWarning", self._on_warning)
  267. self.handler.add_callback("OnPreError", self._on_error)
  268. return self
  269. def __exit__(self, type, value, traceback):
  270. self.handler.disconnect()
  271. self.handler = None
  272. return False
  273. class AngleHelper:
  274. @staticmethod
  275. def is_angle_close(x_rad, y_rad, tolerance):
  276. # type: (float, float , float) -> bool
  277. """
  278. compare if 2 angles measured in radians are close
  279. :param x_rad: angle in radians
  280. :param y_rad: angle in radians
  281. :param tolerance: the tolerance to define close
  282. :return: bool
  283. """
  284. sinx_sub_siny = math.sin(x_rad) - math.sin(y_rad)
  285. cosx_sub_cosy = math.cos(x_rad) - math.cos(y_rad)
  286. r = sinx_sub_siny * sinx_sub_siny + cosx_sub_cosy * cosx_sub_cosy
  287. diff = math.acos((2.0 - r) / 2.0)
  288. return abs(diff) <= tolerance
  289. @staticmethod
  290. def is_angle_close_deg(x_deg, y_deg, tolerance):
  291. # type: (float, float , float) -> bool
  292. """
  293. compare if 2 angles measured in degrees are close
  294. :param x_deg: angle in degrees
  295. :param y_deg: angle in degrees
  296. :param tolerance: the tolerance to define close
  297. :return: bool
  298. """
  299. return AngleHelper.is_angle_close(math.radians(x_deg), math.radians(y_deg), tolerance)
  300. def vector3_str(vector3):
  301. return "(x: {:.2f}, y: {:.2f}, z: {:.2f})".format(vector3.x, vector3.y, vector3.z)
  302. def aabb_str(aabb):
  303. return "[Min: %s, Max: %s]" % (vector3_str(aabb.min), vector3_str(aabb.max))