conftest.py 11 KB

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