conftest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. # SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. # pylint: disable=W0621 # redefined-outer-name
  4. # This file is a pytest root configuration file and provide the following functionalities:
  5. # 1. Defines a few fixtures that could be used under the whole project.
  6. # 2. Defines a few hook functions.
  7. #
  8. # IDF is using [pytest](https://github.com/pytest-dev/pytest) and
  9. # [pytest-embedded plugin](https://github.com/espressif/pytest-embedded) as its example test framework.
  10. #
  11. # This is an experimental feature, and if you found any bug or have any question, please report to
  12. # https://github.com/espressif/pytest-embedded/issues
  13. import glob
  14. import json
  15. import logging
  16. import os
  17. import re
  18. import sys
  19. from copy import deepcopy
  20. from datetime import datetime
  21. from typing import Callable, Optional
  22. import pytest
  23. from _pytest.config import Config
  24. from _pytest.fixtures import FixtureRequest
  25. from pytest_embedded.plugin import multi_dut_argument, multi_dut_fixture
  26. from pytest_embedded_idf.dut import IdfDut
  27. try:
  28. from idf_ci_utils import IDF_PATH
  29. from idf_pytest.constants import DEFAULT_SDKCONFIG, ENV_MARKERS, SPECIAL_MARKERS, TARGET_MARKERS
  30. from idf_pytest.plugin import IDF_PYTEST_EMBEDDED_KEY, IdfPytestEmbedded
  31. from idf_pytest.utils import format_case_id, get_target_marker_from_expr
  32. from idf_unity_tester import CaseTester
  33. except ImportError:
  34. sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci'))
  35. from idf_ci_utils import IDF_PATH
  36. from idf_pytest.constants import DEFAULT_SDKCONFIG, ENV_MARKERS, SPECIAL_MARKERS, TARGET_MARKERS
  37. from idf_pytest.plugin import IDF_PYTEST_EMBEDDED_KEY, IdfPytestEmbedded
  38. from idf_pytest.utils import format_case_id, get_target_marker_from_expr
  39. from idf_unity_tester import CaseTester
  40. try:
  41. import common_test_methods # noqa: F401
  42. except ImportError:
  43. sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci', 'python_packages'))
  44. import common_test_methods # noqa: F401
  45. ############
  46. # Fixtures #
  47. ############
  48. @pytest.fixture(scope='session')
  49. def idf_path() -> str:
  50. return os.path.dirname(__file__)
  51. @pytest.fixture(scope='session', autouse=True)
  52. def session_tempdir() -> str:
  53. _tmpdir = os.path.join(
  54. os.path.dirname(__file__),
  55. 'pytest_embedded_log',
  56. datetime.now().strftime('%Y-%m-%d_%H-%M-%S'),
  57. )
  58. os.makedirs(_tmpdir, exist_ok=True)
  59. return _tmpdir
  60. @pytest.fixture
  61. def case_tester(dut: IdfDut, **kwargs): # type: ignore
  62. yield CaseTester(dut, **kwargs)
  63. @pytest.fixture
  64. @multi_dut_argument
  65. def config(request: FixtureRequest) -> str:
  66. return getattr(request, 'param', None) or DEFAULT_SDKCONFIG # type: ignore
  67. @pytest.fixture
  68. def test_func_name(request: FixtureRequest) -> str:
  69. return request.node.function.__name__ # type: ignore
  70. @pytest.fixture
  71. def test_case_name(request: FixtureRequest, target: str, config: str) -> str:
  72. is_qemu = request._pyfuncitem.get_closest_marker('qemu') is not None
  73. if hasattr(request._pyfuncitem, 'callspec'):
  74. params = deepcopy(request._pyfuncitem.callspec.params) # type: ignore
  75. else:
  76. params = {}
  77. filtered_params = {}
  78. for k, v in params.items():
  79. if k not in request.session._fixturemanager._arg2fixturedefs: # type: ignore
  80. filtered_params[k] = v # not fixture ones
  81. return format_case_id(target, config, request.node.originalname, is_qemu=is_qemu, params=filtered_params) # type: ignore
  82. @pytest.fixture
  83. @multi_dut_fixture
  84. def build_dir(app_path: str, target: Optional[str], config: Optional[str]) -> str:
  85. """
  86. Check local build dir with the following priority:
  87. 1. build_<target>_<config>
  88. 2. build_<target>
  89. 3. build_<config>
  90. 4. build
  91. Returns:
  92. valid build directory
  93. """
  94. check_dirs = []
  95. if target is not None and config is not None:
  96. check_dirs.append(f'build_{target}_{config}')
  97. if target is not None:
  98. check_dirs.append(f'build_{target}')
  99. if config is not None:
  100. check_dirs.append(f'build_{config}')
  101. check_dirs.append('build')
  102. for check_dir in check_dirs:
  103. binary_path = os.path.join(app_path, check_dir)
  104. if os.path.isdir(binary_path):
  105. logging.info(f'found valid binary path: {binary_path}')
  106. return check_dir
  107. logging.warning('checking binary path: %s... missing... try another place', binary_path)
  108. raise ValueError(
  109. f'no build dir valid. Please build the binary via "idf.py -B {check_dirs[0]} build" and run pytest again'
  110. )
  111. @pytest.fixture(autouse=True)
  112. @multi_dut_fixture
  113. def junit_properties(test_case_name: str, record_xml_attribute: Callable[[str, object], None]) -> None:
  114. """
  115. This fixture is autoused and will modify the junit report test case name to <target>.<config>.<case_name>
  116. """
  117. record_xml_attribute('name', test_case_name)
  118. @pytest.fixture(autouse=True)
  119. def set_test_case_name(request: FixtureRequest, test_case_name: str) -> None:
  120. request.node.funcargs['test_case_name'] = test_case_name
  121. ######################
  122. # Log Util Functions #
  123. ######################
  124. @pytest.fixture
  125. def log_performance(record_property: Callable[[str, object], None]) -> Callable[[str, str], None]:
  126. """
  127. log performance item with pre-defined format to the console
  128. and record it under the ``properties`` tag in the junit report if available.
  129. """
  130. def real_func(item: str, value: str) -> None:
  131. """
  132. :param item: performance item name
  133. :param value: performance value
  134. """
  135. logging.info('[Performance][%s]: %s', item, value)
  136. record_property(item, value)
  137. return real_func
  138. @pytest.fixture
  139. def check_performance(idf_path: str) -> Callable[[str, float, str], None]:
  140. """
  141. check if the given performance item meets the passing standard or not
  142. """
  143. def real_func(item: str, value: float, target: str) -> None:
  144. """
  145. :param item: performance item name
  146. :param value: performance item value
  147. :param target: target chip
  148. :raise: AssertionError: if check fails
  149. """
  150. def _find_perf_item(operator: str, path: str) -> float:
  151. with open(path, 'r') as f:
  152. data = f.read()
  153. match = re.search(r'#define\s+IDF_PERFORMANCE_{}_{}\s+([\d.]+)'.format(operator, item.upper()), data)
  154. return float(match.group(1)) # type: ignore
  155. def _check_perf(operator: str, standard_value: float) -> None:
  156. if operator == 'MAX':
  157. ret = value <= standard_value
  158. else:
  159. ret = value >= standard_value
  160. if not ret:
  161. raise AssertionError(
  162. "[Performance] {} value is {}, doesn't meet pass standard {}".format(item, value, standard_value)
  163. )
  164. path_prefix = os.path.join(idf_path, 'components', 'idf_test', 'include')
  165. performance_files = (
  166. os.path.join(path_prefix, target, 'idf_performance_target.h'),
  167. os.path.join(path_prefix, 'idf_performance.h'),
  168. )
  169. found_item = False
  170. for op in ['MIN', 'MAX']:
  171. for performance_file in performance_files:
  172. try:
  173. standard = _find_perf_item(op, performance_file)
  174. except (IOError, AttributeError):
  175. # performance file doesn't exist or match is not found in it
  176. continue
  177. _check_perf(op, standard)
  178. found_item = True
  179. break
  180. if not found_item:
  181. raise AssertionError('Failed to get performance standard for {}'.format(item))
  182. return real_func
  183. @pytest.fixture
  184. def log_minimum_free_heap_size(dut: IdfDut, config: str) -> Callable[..., None]:
  185. def real_func() -> None:
  186. res = dut.expect(r'Minimum free heap size: (\d+) bytes')
  187. logging.info(
  188. '\n------ heap size info ------\n'
  189. '[app_name] {}\n'
  190. '[config_name] {}\n'
  191. '[target] {}\n'
  192. '[minimum_free_heap_size] {} Bytes\n'
  193. '------ heap size end ------'.format(
  194. os.path.basename(dut.app.app_path),
  195. config,
  196. dut.target,
  197. res.group(1).decode('utf8'),
  198. )
  199. )
  200. return real_func
  201. @pytest.fixture
  202. def dev_password(request: FixtureRequest) -> str:
  203. return request.config.getoption('dev_passwd') or ''
  204. @pytest.fixture
  205. def dev_user(request: FixtureRequest) -> str:
  206. return request.config.getoption('dev_user') or ''
  207. ##################
  208. # Hook functions #
  209. ##################
  210. def pytest_addoption(parser: pytest.Parser) -> None:
  211. idf_group = parser.getgroup('idf')
  212. idf_group.addoption(
  213. '--sdkconfig',
  214. help='sdkconfig postfix, like sdkconfig.ci.<config>. (Default: None, which would build all found apps)',
  215. )
  216. idf_group.addoption(
  217. '--dev-user',
  218. help='user name associated with some specific device/service used during the test execution',
  219. )
  220. idf_group.addoption(
  221. '--dev-passwd',
  222. help='password associated with some specific device/service used during the test execution',
  223. )
  224. idf_group.addoption(
  225. '--app-info-basedir',
  226. default=IDF_PATH,
  227. help='app info base directory. specify this value when you\'re building under a '
  228. 'different IDF_PATH. (Default: $IDF_PATH)',
  229. )
  230. idf_group.addoption(
  231. '--app-info-filepattern',
  232. help='glob pattern to specify the files that include built app info generated by '
  233. '`idf-build-apps --collect-app-info ...`. will not raise ValueError when binary '
  234. 'paths not exist in local file system if not listed recorded in the app info.',
  235. )
  236. def pytest_configure(config: Config) -> None:
  237. # cli option "--target"
  238. target = config.getoption('target') or ''
  239. help_commands = ['--help', '--fixtures', '--markers', '--version']
  240. for cmd in help_commands:
  241. if cmd in config.invocation_params.args:
  242. target = 'unneeded'
  243. break
  244. if not target: # also could specify through markexpr via "-m"
  245. target = get_target_marker_from_expr(config.getoption('markexpr') or '')
  246. apps_list = None
  247. app_info_basedir = config.getoption('app_info_basedir')
  248. app_info_filepattern = config.getoption('app_info_filepattern')
  249. if app_info_filepattern:
  250. apps_list = []
  251. for file in glob.glob(os.path.join(IDF_PATH, app_info_filepattern)):
  252. with open(file) as fr:
  253. for line in fr.readlines():
  254. if not line.strip():
  255. continue
  256. # each line is a valid json
  257. app_info = json.loads(line.strip())
  258. if app_info_basedir and app_info['app_dir'].startswith(app_info_basedir):
  259. relative_app_dir = os.path.relpath(app_info['app_dir'], app_info_basedir)
  260. apps_list.append(os.path.join(IDF_PATH, os.path.join(relative_app_dir, app_info['build_dir'])))
  261. print('Detected app: ', apps_list[-1])
  262. else:
  263. print(
  264. f'WARNING: app_info base dir {app_info_basedir} not recognizable in {app_info["app_dir"]}, skipping...'
  265. )
  266. continue
  267. config.stash[IDF_PYTEST_EMBEDDED_KEY] = IdfPytestEmbedded(
  268. target=target,
  269. sdkconfig=config.getoption('sdkconfig'),
  270. apps_list=apps_list,
  271. )
  272. config.pluginmanager.register(config.stash[IDF_PYTEST_EMBEDDED_KEY])
  273. for name, description in {**TARGET_MARKERS, **ENV_MARKERS, **SPECIAL_MARKERS}.items():
  274. config.addinivalue_line('markers', f'{name}: {description}')
  275. def pytest_unconfigure(config: Config) -> None:
  276. _pytest_embedded = config.stash.get(IDF_PYTEST_EMBEDDED_KEY, None)
  277. if _pytest_embedded:
  278. del config.stash[IDF_PYTEST_EMBEDDED_KEY]
  279. config.pluginmanager.unregister(_pytest_embedded)