TinyFW.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http:#www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """ Interface for test cases. """
  15. import json
  16. import logging
  17. import os
  18. import time
  19. import traceback
  20. import functools
  21. import socket
  22. from datetime import datetime
  23. import junit_xml
  24. from . import Env
  25. from . import DUT
  26. from . import App
  27. from . import Utility
  28. class DefaultEnvConfig(object):
  29. """
  30. default test configs. There're 3 places to set configs, priority is (high -> low):
  31. 1. overwrite set by caller of test method
  32. 2. values set by test_method decorator
  33. 3. default env config get from this class
  34. """
  35. DEFAULT_CONFIG = {
  36. "app": App.BaseApp,
  37. "dut": DUT.BaseDUT,
  38. "env_tag": "default",
  39. "env_config_file": None,
  40. "test_suite_name": None,
  41. }
  42. @classmethod
  43. def set_default_config(cls, **kwargs):
  44. """
  45. :param kwargs: configs need to be updated
  46. :return: None
  47. """
  48. cls.DEFAULT_CONFIG.update(kwargs)
  49. @classmethod
  50. def get_default_config(cls):
  51. """
  52. :return: current default config
  53. """
  54. return cls.DEFAULT_CONFIG.copy()
  55. set_default_config = DefaultEnvConfig.set_default_config
  56. get_default_config = DefaultEnvConfig.get_default_config
  57. MANDATORY_INFO = {
  58. "execution_time": 1,
  59. "env_tag": "default",
  60. "category": "function",
  61. "ignore": False,
  62. }
  63. class JunitReport(object):
  64. # wrapper for junit test report
  65. # TODO: JunitReport methods are not thread safe (although not likely to be used this way).
  66. JUNIT_FILE_NAME = "XUNIT_RESULT.xml"
  67. JUNIT_DEFAULT_TEST_SUITE = "test-suite"
  68. JUNIT_TEST_SUITE = junit_xml.TestSuite(JUNIT_DEFAULT_TEST_SUITE,
  69. hostname=socket.gethostname(),
  70. timestamp=datetime.utcnow().isoformat())
  71. JUNIT_CURRENT_TEST_CASE = None
  72. _TEST_CASE_CREATED_TS = 0
  73. @classmethod
  74. def output_report(cls, junit_file_path):
  75. """ Output current test result to file. """
  76. with open(os.path.join(junit_file_path, cls.JUNIT_FILE_NAME), "w") as f:
  77. cls.JUNIT_TEST_SUITE.to_file(f, [cls.JUNIT_TEST_SUITE], prettyprint=False)
  78. @classmethod
  79. def get_current_test_case(cls):
  80. """
  81. By default, the test framework will handle junit test report automatically.
  82. While some test case might want to update some info to test report.
  83. They can use this method to get current test case created by test framework.
  84. :return: current junit test case instance created by ``JunitTestReport.create_test_case``
  85. """
  86. return cls.JUNIT_CURRENT_TEST_CASE
  87. @classmethod
  88. def test_case_finish(cls, test_case):
  89. """
  90. Append the test case to test suite so it can be output to file.
  91. Execution time will be automatically updated (compared to ``create_test_case``).
  92. """
  93. test_case.elapsed_sec = time.time() - cls._TEST_CASE_CREATED_TS
  94. cls.JUNIT_TEST_SUITE.test_cases.append(test_case)
  95. @classmethod
  96. def create_test_case(cls, name):
  97. """
  98. Extend ``junit_xml.TestCase`` with:
  99. 1. save create test case so it can be get by ``get_current_test_case``
  100. 2. log create timestamp, so ``elapsed_sec`` can be auto updated in ``test_case_finish``.
  101. :param name: test case name
  102. :return: instance of ``junit_xml.TestCase``
  103. """
  104. # set stdout to empty string, so we can always append string to stdout.
  105. # It won't affect output logic. If stdout is empty, it won't be put to report.
  106. test_case = junit_xml.TestCase(name, stdout="")
  107. cls.JUNIT_CURRENT_TEST_CASE = test_case
  108. cls._TEST_CASE_CREATED_TS = time.time()
  109. return test_case
  110. @classmethod
  111. def update_performance(cls, performance_items):
  112. """
  113. Update performance results to ``stdout`` of current test case.
  114. :param performance_items: a list of performance items. each performance item is a key-value pair.
  115. """
  116. assert cls.JUNIT_CURRENT_TEST_CASE
  117. for item in performance_items:
  118. cls.JUNIT_CURRENT_TEST_CASE.stdout += "[{}]: {}\n".format(item[0], item[1])
  119. def test_method(**kwargs):
  120. """
  121. decorator for test case function.
  122. The following keyword arguments are pre-defined.
  123. Any other keyword arguments will be regarded as filter for the test case,
  124. able to access them by ``case_info`` attribute of test method.
  125. :keyword app: class for test app. see :doc:`App <App>` for details
  126. :keyword dut: class for current dut. see :doc:`DUT <DUT>` for details
  127. :keyword env_tag: name for test environment, used to select configs from config file
  128. :keyword env_config_file: test env config file. usually will not set this keyword when define case
  129. :keyword test_suite_name: test suite name, used for generating log folder name and adding xunit format test result.
  130. usually will not set this keyword when define case
  131. :keyword junit_report_by_case: By default the test fw will handle junit report generation.
  132. In some cases, one test function might test many test cases.
  133. If this flag is set, test case can update junit report by its own.
  134. """
  135. def test(test_func):
  136. case_info = MANDATORY_INFO.copy()
  137. case_info["name"] = case_info["ID"] = test_func.__name__
  138. case_info["junit_report_by_case"] = False
  139. def _filter_ci_target(target, ci_target):
  140. if not ci_target:
  141. return target
  142. if isinstance(target, str):
  143. if isinstance(ci_target, str) and target == ci_target:
  144. return ci_target
  145. else:
  146. if isinstance(ci_target, str) and ci_target in target:
  147. return ci_target
  148. elif isinstance(ci_target, list) and set(ci_target).issubset(set(target)):
  149. return ci_target
  150. raise ValueError('ci_target must be a subset of target')
  151. if os.getenv('CI_JOB_NAME') and 'ci_target' in kwargs:
  152. kwargs['target'] = _filter_ci_target(kwargs['target'], kwargs['ci_target'])
  153. case_info.update(kwargs)
  154. @functools.wraps(test_func)
  155. def handle_test(extra_data=None, **overwrite):
  156. """
  157. create env, run test and record test results
  158. :param extra_data: extra data that runner or main passed to test case
  159. :param overwrite: args that runner or main want to overwrite
  160. :return: None
  161. """
  162. # create env instance
  163. env_config = DefaultEnvConfig.get_default_config()
  164. for key in kwargs:
  165. if key in env_config:
  166. env_config[key] = kwargs[key]
  167. # Runner.py should overwrite target with the current target.
  168. env_config.update(overwrite)
  169. # if target not in the default_config or the overwrite, then take it from kwargs passed from the decorator
  170. target = env_config['target'] if 'target' in env_config else kwargs['target']
  171. # This code block is used to do run local test script target set check
  172. if not os.getenv('CI_JOB_NAME'):
  173. idf_target = 'ESP32' # default if sdkconfig not found or not readable
  174. expected_json_path = os.path.join('build', 'config', 'sdkconfig.json')
  175. if os.path.exists(expected_json_path):
  176. sdkconfig = json.load(open(expected_json_path))
  177. try:
  178. idf_target = sdkconfig['IDF_TARGET'].upper()
  179. except KeyError:
  180. pass
  181. else:
  182. logging.info('IDF_TARGET: {}'.format(idf_target))
  183. else:
  184. logging.warning('{} not found. IDF_TARGET set to esp32'.format(os.path.abspath(expected_json_path)))
  185. if isinstance(target, list):
  186. if idf_target in target:
  187. target = idf_target
  188. else:
  189. raise ValueError('IDF_TARGET set to {}, not in decorator target value'.format(idf_target))
  190. else:
  191. if idf_target != target:
  192. raise ValueError('IDF_TARGET set to {}, not equal to decorator target value'.format(idf_target))
  193. dut_dict = kwargs['dut_dict']
  194. if target not in dut_dict:
  195. raise Exception('target can only be {%s}' % ', '.join(dut_dict.keys()))
  196. dut = dut_dict[target]
  197. try:
  198. # try to config the default behavior of erase nvs
  199. dut.ERASE_NVS = kwargs['erase_nvs']
  200. except AttributeError:
  201. pass
  202. env_config['dut'] = dut
  203. env_inst = Env.Env(**env_config)
  204. # prepare for xunit test results
  205. junit_file_path = env_inst.app_cls.get_log_folder(env_config["test_suite_name"])
  206. junit_test_case = JunitReport.create_test_case(case_info["ID"])
  207. result = False
  208. try:
  209. Utility.console_log("starting running test: " + test_func.__name__, color="green")
  210. # execute test function
  211. test_func(env_inst, extra_data)
  212. # if finish without exception, test result is True
  213. result = True
  214. except Exception as e:
  215. # handle all the exceptions here
  216. traceback.print_exc()
  217. # log failure
  218. junit_test_case.add_failure_info(str(e) + ":\r\n" + traceback.format_exc())
  219. finally:
  220. # do close all DUTs, if result is False then print DUT debug info
  221. close_errors = env_inst.close(dut_debug=(not result))
  222. # We have a hook in DUT close, allow DUT to raise error to fail test case.
  223. # For example, we don't allow DUT exception (reset) during test execution.
  224. # We don't want to implement in exception detection in test function logic,
  225. # as we need to add it to every test case.
  226. # We can implement it in DUT receive thread,
  227. # and raise exception in DUT close to fail test case if reset detected.
  228. if close_errors:
  229. for error in close_errors:
  230. junit_test_case.add_failure_info(str(error))
  231. result = False
  232. if not case_info["junit_report_by_case"]:
  233. JunitReport.test_case_finish(junit_test_case)
  234. # end case and output result
  235. JunitReport.output_report(junit_file_path)
  236. if result:
  237. Utility.console_log("Test Succeed: " + test_func.__name__, color="green")
  238. else:
  239. Utility.console_log(("Test Fail: " + test_func.__name__), color="red")
  240. return result
  241. handle_test.case_info = case_info
  242. handle_test.test_method = True
  243. return handle_test
  244. return test