You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/utils.py

367 lines
12 KiB
Python

"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import os
import time
import math
import azlmbr
import azlmbr.legacy.general as general
import azlmbr.debug
import traceback
class FailFast(Exception):
"""
Raise to stop proceeding through test steps.
"""
pass
class TestHelper:
@staticmethod
def init_idle():
general.idle_enable(True)
# JIRA: SPEC-2880
# general.idle_wait_frames(1)
@staticmethod
def open_level(directory, level):
# type: (str, ) -> None
"""
:param level: the name of the level folder in AutomatedTesting\\Physics\\
:return: None
"""
Report.info("Open level {}/{}".format(directory, level))
success = general.open_level_no_prompt(os.path.join(directory, level))
if not success:
open_level_name = general.get_current_level_name()
if open_level_name == level:
Report.info("{} was already opened".format(level))
else:
assert False, "Failed to open level: {} does not exist or is invalid".format(level)
# FIX-ME: Expose call for checking when has been finished loading and change this frame waiting
# Jira: LY-113761
general.idle_wait_frames(200)
@staticmethod
def enter_game_mode(msgtuple_success_fail):
# type: (tuple) -> None
"""
:param msgtuple_success_fail: The tuple with the expected/unexpected messages for entering game mode.
:return: None
"""
Report.info("Entering game mode")
general.enter_game_mode()
TestHelper.wait_for_condition(lambda : general.is_in_game_mode(), 1.0)
Report.critical_result(msgtuple_success_fail, general.is_in_game_mode())
@staticmethod
def exit_game_mode(msgtuple_success_fail):
# type: (tuple) -> None
"""
:param msgtuple_success_fail: The tuple with the expected/unexpected messages for exiting game mode.
:return: None
"""
Report.info("Exiting game mode")
general.exit_game_mode()
TestHelper.wait_for_condition(lambda : not general.is_in_game_mode(), 1.0)
Report.critical_result(msgtuple_success_fail, not general.is_in_game_mode())
@staticmethod
def close_editor():
general.exit_no_prompt()
@staticmethod
def fail_fast(message=None):
# type: (str) -> None
"""
A state has been reached where progressing in the test is not viable.
raises FailFast
:return: None
"""
Report.info("Failing fast. Raising an exception and shutting down the editor.")
if message:
Report.info("Fail fast message: {}".format(message))
TestHelper.close_editor()
raise FailFast()
@staticmethod
def wait_for_condition(function, timeout_in_seconds):
# type: (function, float) -> bool
"""
**** Will be replaced by a function of the same name exposed in the Engine*****
a function to run until it returns True or timeout is reached
the function can have no parameters and
waiting idle__wait_* is handled here not in the function
:param function: a function that returns a boolean indicating a desired condition is achieved
:param timeout_in_seconds: when reached, function execution is abandoned and False is returned
"""
with Timeout(timeout_in_seconds) as t:
while True:
try:
general.idle_wait_frames(1)
except:
Report.info("WARNING: Couldn't wait for frame")
if t.timed_out:
return False
ret = function()
if not isinstance(ret, bool):
raise TypeError("return value for wait_for_condition function must be a bool")
if ret:
return True
class Timeout:
# type: (float) -> None
"""
contextual timeout
:param seconds: float seconds to allow before timed_out is True
"""
def __init__(self, seconds):
self.seconds = seconds
def __enter__(self):
self.die_after = time.time() + self.seconds
return self
def __exit__(self, type, value, traceback):
pass
@property
def timed_out(self):
return time.time() > self.die_after
class Report:
_results = []
_exception = None
@staticmethod
def start_test(test_function):
try:
test_function()
except Exception as ex:
Report._exception = traceback.format_exc()
Report.report_results(test_function)
@staticmethod
def report_results(test_function):
success = True
report = f"Report for {test_function.__name__}:\n"
for result in Report._results:
passed, info = result
success = success and passed
if passed:
report += f"[SUCCESS] {info}\n"
else:
report += f"[FAILED ] {info}\n"
if Report._exception:
report += "EXCEPTION raised:\n %s\n" % Report._exception[:-1].replace("\n", "\n ")
success = False
report += "Test result: "
report += "SUCCESS" if success else "FAILURE"
print(report)
general.report_test_result(success, report)
@staticmethod
def info(msg):
print("Info: {}".format(msg))
@staticmethod
def success(msgtuple_success_fail):
outcome = "Success: {}".format(msgtuple_success_fail[0])
print(outcome)
Report._results.append((True, outcome))
@staticmethod
def failure(msgtuple_success_fail):
outcome = "Failure: {}".format(msgtuple_success_fail[1])
print(outcome)
Report._results.append((False, outcome))
@staticmethod
def result(msgtuple_success_fail, condition):
if not isinstance(condition, bool):
raise TypeError("condition argument must be a bool")
if condition:
Report.success(msgtuple_success_fail)
else:
Report.failure(msgtuple_success_fail)
return condition
@staticmethod
def critical_result(msgtuple_success_fail, condition, fast_fail_message=None):
# type: (tuple, bool, str) -> None
"""
if condition is False we will fail fast
:param msgtuple_success_fail: messages to print based on the condition
:param condition: success (True) or failure (False)
:param fast_fail_message: [optional] message to include on fast fail
"""
if not isinstance(condition, bool):
raise TypeError("condition argument must be a bool")
if not Report.result(msgtuple_success_fail, condition):
TestHelper.fail_fast(fast_fail_message)
# DEPRECATED: Use vector3_str()
@staticmethod
def info_vector3(vector3, label="", magnitude=None):
# type: (azlmbr.math.Vector3, str, float) -> None
"""
prints the vector to the Report.info log. If applied, label will print first,
followed by the vector's values (x, y, z,) to 2 decimal places. Lastly if the
magnitude is supplied, it will print on the third line.
:param vector3: a azlmbr.math.Vector3 object to print
prints in [x: , y: , z: ] format.
:param label: [optional] A string to print before printing the vector3's contents
:param magnitude: [optional] the vector's magnitude to print after the vector's contents
:return: None
"""
if label != "":
Report.info(label)
Report.info(" x: {:.2f}, y: {:.2f}, z: {:.2f}".format(vector3.x, vector3.y, vector3.z))
if magnitude is not None:
Report.info(" magnitude: {:.2f}".format(magnitude))
'''
Utility for scope tracing errors and warnings.
Usage:
...
with Tracer() as section_tracer:
# section were we are interested in capturing errors/warnings/asserts
...
Report.result(Tests.warnings_not_found_in_section, not section_tracer.has_warnings)
'''
class Tracer:
def __init__(self):
self.warnings = []
self.errors = []
self.asserts = []
self.has_warnings = False
self.has_errors = False
self.has_asserts = False
self.handler = None
class WarningInfo:
def __init__(self, args):
self.window = args[0]
self.filename = args[1]
self.line = args[2]
self.function = args[3]
self.message = args[4]
class ErrorInfo:
def __init__(self, args):
self.window = args[0]
self.filename = args[1]
self.line = args[2]
self.function = args[3]
self.message = args[4]
class AssertInfo:
def __init__(self, args):
self.filename = args[0]
self.line = args[1]
self.function = args[2]
self.message = args[3]
def _on_warning(self, args):
warningInfo = Tracer.WarningInfo(args)
self.warnings.append(warningInfo)
Report.info("Tracer caught Warning: %s" % warningInfo.message)
self.has_warnings = True
return False
def _on_error(self, args):
errorInfo = Tracer.ErrorInfo(args)
self.errors.append(errorInfo)
Report.info("Tracer caught Error: %s" % errorInfo.message)
self.has_errors = True
return False
def _on_assert(self, args):
assertInfo = Tracer.AssertInfo(args)
self.asserts.append(assertInfo)
Report.info("Tracer caught Assert: %s:%i[%s] \"%s\"" % (assertInfo.filename, assertInfo.line, assertInfo.function, assertInfo.message))
self.has_asserts = True
return False
def __enter__(self):
self.handler = azlmbr.debug.TraceMessageBusHandler()
self.handler.connect(None)
self.handler.add_callback("OnPreAssert", self._on_assert)
self.handler.add_callback("OnPreWarning", self._on_warning)
self.handler.add_callback("OnPreError", self._on_error)
return self
def __exit__(self, type, value, traceback):
self.handler.disconnect()
self.handler = None
return False
class AngleHelper:
@staticmethod
def is_angle_close(x_rad, y_rad, tolerance):
# type: (float, float , float) -> bool
"""
compare if 2 angles measured in radians are close
:param x_rad: angle in radians
:param y_rad: angle in radians
:param tolerance: the tolerance to define close
:return: bool
"""
sinx_sub_siny = math.sin(x_rad) - math.sin(y_rad)
cosx_sub_cosy = math.cos(x_rad) - math.cos(y_rad)
r = sinx_sub_siny * sinx_sub_siny + cosx_sub_cosy * cosx_sub_cosy
diff = math.acos((2.0 - r) / 2.0)
return abs(diff) <= tolerance
@staticmethod
def is_angle_close_deg(x_deg, y_deg, tolerance):
# type: (float, float , float) -> bool
"""
compare if 2 angles measured in degrees are close
:param x_deg: angle in degrees
:param y_deg: angle in degrees
:param tolerance: the tolerance to define close
:return: bool
"""
return AngleHelper.is_angle_close(math.radians(x_deg), math.radians(y_deg), tolerance)
def vector3_str(vector3):
return "(x: {:.2f}, y: {:.2f}, z: {:.2f})".format(vector3.x, vector3.y, vector3.z)
def aabb_str(aabb):
return "[Min: %s, Max: %s]" % (vector3_str(aabb.min), vector3_str(aabb.max))