TinyFW.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. """ Interface for test cases. """
  4. import functools
  5. import os
  6. import socket
  7. import time
  8. from datetime import datetime
  9. import junit_xml
  10. from . import DUT, App, Env, Utility
  11. from .Utility import format_case_id
  12. class TestCaseFailed(AssertionError):
  13. def __init__(self, *cases):
  14. """
  15. Raise this exception if one or more test cases fail in a 'normal' way (ie the test runs but fails, no unexpected exceptions)
  16. This will avoid dumping the Python stack trace, because the assumption is the junit error info and full job log already has
  17. enough information for a developer to debug.
  18. 'cases' argument is the names of one or more test cases
  19. """
  20. message = 'Test case{} failed: {}'.format('s' if len(cases) > 1 else '', ', '.join(str(c) for c in cases))
  21. super(TestCaseFailed, self).__init__(self, message)
  22. class DefaultEnvConfig(object):
  23. """
  24. default test configs. There're 3 places to set configs, priority is (high -> low):
  25. 1. overwrite set by caller of test method
  26. 2. values set by test_method decorator
  27. 3. default env config get from this class
  28. """
  29. DEFAULT_CONFIG = {
  30. 'app': App.BaseApp,
  31. 'dut': DUT.BaseDUT,
  32. 'env_tag': 'default',
  33. 'env_config_file': None,
  34. 'test_suite_name': None,
  35. }
  36. @classmethod
  37. def set_default_config(cls, **kwargs):
  38. """
  39. :param kwargs: configs need to be updated
  40. :return: None
  41. """
  42. cls.DEFAULT_CONFIG.update(kwargs)
  43. @classmethod
  44. def get_default_config(cls):
  45. """
  46. :return: current default config
  47. """
  48. return cls.DEFAULT_CONFIG.copy()
  49. set_default_config = DefaultEnvConfig.set_default_config
  50. get_default_config = DefaultEnvConfig.get_default_config
  51. MANDATORY_INFO = {
  52. 'execution_time': 1,
  53. 'env_tag': 'default',
  54. 'category': 'function',
  55. 'ignore': False,
  56. }
  57. class JunitReport(object):
  58. # wrapper for junit test report
  59. # TODO: JunitReport methods are not thread safe (although not likely to be used this way).
  60. JUNIT_FILE_NAME = 'XUNIT_RESULT.xml'
  61. JUNIT_DEFAULT_TEST_SUITE = 'test-suite'
  62. JUNIT_TEST_SUITE = junit_xml.TestSuite(JUNIT_DEFAULT_TEST_SUITE,
  63. hostname=socket.gethostname(),
  64. timestamp=datetime.utcnow().isoformat())
  65. JUNIT_CURRENT_TEST_CASE = None
  66. _TEST_CASE_CREATED_TS = 0
  67. @classmethod
  68. def output_report(cls, junit_file_path):
  69. """ Output current test result to file. """
  70. with open(os.path.join(junit_file_path, cls.JUNIT_FILE_NAME), 'w') as f:
  71. junit_xml.to_xml_report_file(f, [cls.JUNIT_TEST_SUITE], prettyprint=False)
  72. @classmethod
  73. def get_current_test_case(cls):
  74. """
  75. By default, the test framework will handle junit test report automatically.
  76. While some test case might want to update some info to test report.
  77. They can use this method to get current test case created by test framework.
  78. :return: current junit test case instance created by ``JunitTestReport.create_test_case``
  79. """
  80. return cls.JUNIT_CURRENT_TEST_CASE
  81. @classmethod
  82. def test_case_finish(cls, test_case):
  83. """
  84. Append the test case to test suite so it can be output to file.
  85. Execution time will be automatically updated (compared to ``create_test_case``).
  86. """
  87. test_case.elapsed_sec = time.time() - cls._TEST_CASE_CREATED_TS
  88. cls.JUNIT_TEST_SUITE.test_cases.append(test_case)
  89. @classmethod
  90. def create_test_case(cls, name):
  91. """
  92. Extend ``junit_xml.TestCase`` with:
  93. 1. save create test case so it can be get by ``get_current_test_case``
  94. 2. log create timestamp, so ``elapsed_sec`` can be auto updated in ``test_case_finish``.
  95. :param name: test case name
  96. :return: instance of ``junit_xml.TestCase``
  97. """
  98. # set stdout to empty string, so we can always append string to stdout.
  99. # It won't affect output logic. If stdout is empty, it won't be put to report.
  100. test_case = junit_xml.TestCase(name, stdout='')
  101. cls.JUNIT_CURRENT_TEST_CASE = test_case
  102. cls._TEST_CASE_CREATED_TS = time.time()
  103. return test_case
  104. @classmethod
  105. def update_performance(cls, performance_items):
  106. """
  107. Update performance results to ``stdout`` of current test case.
  108. :param performance_items: a list of performance items. each performance item is a key-value pair.
  109. """
  110. assert cls.JUNIT_CURRENT_TEST_CASE
  111. for item in performance_items:
  112. cls.JUNIT_CURRENT_TEST_CASE.stdout += '[Performance][{}]: {}\n'.format(item[0], item[1])
  113. def test_method(**kwargs):
  114. """
  115. decorator for test case function.
  116. The following keyword arguments are pre-defined.
  117. Any other keyword arguments will be regarded as filter for the test case,
  118. able to access them by ``case_info`` attribute of test method.
  119. :keyword app: class for test app. see :doc:`App <App>` for details
  120. :keyword dut: class for current dut. see :doc:`DUT <DUT>` for details
  121. :keyword env_tag: name for test environment, used to select configs from config file
  122. :keyword env_config_file: test env config file. usually will not set this keyword when define case
  123. :keyword test_suite_name: test suite name, used for generating log folder name and adding xunit format test result.
  124. usually will not set this keyword when define case
  125. :keyword junit_report_by_case: By default the test fw will handle junit report generation.
  126. In some cases, one test function might test many test cases.
  127. If this flag is set, test case can update junit report by its own.
  128. """
  129. def test(test_func):
  130. case_info = MANDATORY_INFO.copy()
  131. case_info['name'] = case_info['ID'] = test_func.__name__
  132. case_info['junit_report_by_case'] = False
  133. case_info.update(kwargs)
  134. @functools.wraps(test_func)
  135. def handle_test(extra_data=None, **overwrite):
  136. """
  137. create env, run test and record test results
  138. :param extra_data: extra data that runner or main passed to test case
  139. :param overwrite: args that runner or main want to overwrite
  140. :return: None
  141. """
  142. # create env instance
  143. env_config = DefaultEnvConfig.get_default_config()
  144. for key in kwargs:
  145. if key in env_config:
  146. env_config[key] = kwargs[key]
  147. env_config.update(overwrite)
  148. env_inst = Env.Env(**env_config)
  149. # prepare for xunit test results
  150. junit_file_path = env_inst.app_cls.get_log_folder(env_config['test_suite_name'])
  151. junit_test_case = JunitReport.create_test_case(format_case_id(case_info['ID'],
  152. target=env_inst.default_dut_cls.TARGET))
  153. result = False
  154. unexpected_error = False
  155. try:
  156. Utility.console_log('starting running test: ' + test_func.__name__, color='green')
  157. # execute test function
  158. test_func(env_inst, extra_data)
  159. # if finish without exception, test result is True
  160. result = True
  161. except TestCaseFailed as e:
  162. junit_test_case.add_failure_info(str(e))
  163. except Exception as e:
  164. Utility.handle_unexpected_exception(junit_test_case, e)
  165. unexpected_error = True
  166. finally:
  167. # do close all DUTs, if result is False then print DUT debug info
  168. close_errors = env_inst.close(dut_debug=(not result))
  169. # We have a hook in DUT close, allow DUT to raise error to fail test case.
  170. # For example, we don't allow DUT exception (reset) during test execution.
  171. # We don't want to implement in exception detection in test function logic,
  172. # as we need to add it to every test case.
  173. # We can implement it in DUT receive thread,
  174. # and raise exception in DUT close to fail test case if reset detected.
  175. if close_errors:
  176. for error in close_errors:
  177. junit_test_case.add_failure_info('env close error: {}'.format(error))
  178. result = False
  179. if not case_info['junit_report_by_case'] or unexpected_error:
  180. JunitReport.test_case_finish(junit_test_case)
  181. # end case and output result
  182. JunitReport.output_report(junit_file_path)
  183. if result:
  184. Utility.console_log('Test Succeed: ' + test_func.__name__, color='green')
  185. else:
  186. Utility.console_log(('Test Fail: ' + test_func.__name__), color='red')
  187. return result
  188. handle_test.case_info = case_info
  189. handle_test.test_method = True
  190. return handle_test
  191. return test