conftest.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # SPDX-FileCopyrightText: 2021-2022 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 logging
  14. import os
  15. import sys
  16. import xml.etree.ElementTree as ET
  17. from typing import Callable, List, Optional
  18. import pytest
  19. from _pytest.config import Config
  20. from _pytest.fixtures import FixtureRequest
  21. from _pytest.nodes import Item
  22. from _pytest.python import Function
  23. from pytest_embedded.plugin import parse_configuration
  24. from pytest_embedded.utils import find_by_suffix
  25. SUPPORTED_TARGETS = ['esp32', 'esp32s2', 'esp32c3', 'esp32s3']
  26. PREVIEW_TARGETS = ['linux', 'esp32h2', 'esp32c2']
  27. DEFAULT_SDKCONFIG = 'default'
  28. ##################
  29. # Help Functions #
  30. ##################
  31. def is_target_marker(marker: str) -> bool:
  32. if marker.startswith('esp32'):
  33. return True
  34. if marker.startswith('esp8'):
  35. return True
  36. return False
  37. def format_case_id(target: Optional[str], config: Optional[str], case: str) -> str:
  38. return f'{target}.{config}.{case}'
  39. def item_marker_names(item: Item) -> List[str]:
  40. return [marker.name for marker in item.iter_markers()]
  41. ############
  42. # Fixtures #
  43. ############
  44. @pytest.fixture
  45. def config(request: FixtureRequest) -> str:
  46. return getattr(request, 'param', None) or DEFAULT_SDKCONFIG
  47. @pytest.fixture
  48. def test_func_name(request: FixtureRequest) -> str:
  49. return request.node.function.__name__ # type: ignore
  50. @pytest.fixture
  51. def test_case_name(request: FixtureRequest, target: str, config: str) -> str:
  52. return format_case_id(target, config, request.node.originalname)
  53. @pytest.fixture
  54. @parse_configuration
  55. def build_dir(
  56. request: FixtureRequest, app_path: str, target: Optional[str], config: Optional[str]
  57. ) -> str:
  58. """
  59. Check local build dir with the following priority:
  60. 1. build_<target>_<config>
  61. 2. build_<target>
  62. 3. build_<config>
  63. 4. build
  64. Args:
  65. request: pytest fixture
  66. app_path: app path
  67. target: target
  68. config: config
  69. Returns:
  70. valid build directory
  71. """
  72. param_or_cli: str = getattr(
  73. request, 'param', None
  74. ) or request.config.option.__dict__.get('build_dir')
  75. if param_or_cli is not None: # respect the param and the cli
  76. return param_or_cli
  77. check_dirs = []
  78. if target is not None and config is not None:
  79. check_dirs.append(f'build_{target}_{config}')
  80. if target is not None:
  81. check_dirs.append(f'build_{target}')
  82. if config is not None:
  83. check_dirs.append(f'build_{config}')
  84. check_dirs.append('build')
  85. for check_dir in check_dirs:
  86. binary_path = os.path.join(app_path, check_dir)
  87. if os.path.isdir(binary_path):
  88. logging.info(f'find valid binary path: {binary_path}')
  89. return check_dir
  90. logging.warning(
  91. 'checking binary path: %s... missing... try another place', binary_path
  92. )
  93. recommend_place = check_dirs[0]
  94. logging.error(
  95. f'no build dir valid. Please build the binary via "idf.py -B {recommend_place} build" and run pytest again'
  96. )
  97. sys.exit(1)
  98. @pytest.fixture(autouse=True)
  99. def junit_properties(
  100. test_case_name: str, record_xml_attribute: Callable[[str, object], None]
  101. ) -> None:
  102. """
  103. This fixture is autoused and will modify the junit report test case name to <target>.<config>.<case_name>
  104. """
  105. record_xml_attribute('name', test_case_name)
  106. ##################
  107. # Hook functions #
  108. ##################
  109. def pytest_addoption(parser: pytest.Parser) -> None:
  110. base_group = parser.getgroup('idf')
  111. base_group.addoption(
  112. '--sdkconfig',
  113. help='sdkconfig postfix, like sdkconfig.ci.<config>. (Default: None, which would build all found apps)',
  114. )
  115. @pytest.hookimpl(tryfirst=True)
  116. def pytest_collection_modifyitems(config: Config, items: List[Function]) -> None:
  117. target = config.getoption('target', None) # use the `build` dir
  118. if not target:
  119. return
  120. # sort by file path and callspec.config
  121. # implement like this since this is a limitation of pytest, couldn't get fixture values while collecting
  122. # https://github.com/pytest-dev/pytest/discussions/9689
  123. def _get_param_config(_item: Function) -> str:
  124. if hasattr(_item, 'callspec'):
  125. return _item.callspec.params.get('config', DEFAULT_SDKCONFIG) # type: ignore
  126. return DEFAULT_SDKCONFIG
  127. items.sort(key=lambda x: (os.path.dirname(x.path), _get_param_config(x)))
  128. # add markers for special markers
  129. for item in items:
  130. if 'supported_targets' in item_marker_names(item):
  131. for _target in SUPPORTED_TARGETS:
  132. item.add_marker(_target)
  133. if 'preview_targets' in item_marker_names(item):
  134. for _target in PREVIEW_TARGETS:
  135. item.add_marker(_target)
  136. if 'all_targets' in item_marker_names(item):
  137. for _target in [*SUPPORTED_TARGETS, *PREVIEW_TARGETS]:
  138. item.add_marker(_target)
  139. # filter all the test cases with "--target"
  140. items[:] = [item for item in items if target in item_marker_names(item)]
  141. # filter all the test cases with cli option "config"
  142. if config.getoption('sdkconfig'):
  143. items[:] = [
  144. item
  145. for item in items
  146. if _get_param_config(item) == config.getoption('sdkconfig')
  147. ]
  148. @pytest.hookimpl(trylast=True)
  149. def pytest_runtest_teardown(item: Function) -> None:
  150. """
  151. Format the test case generated junit reports
  152. """
  153. tempdir = item.funcargs.get('test_case_tempdir')
  154. if not tempdir:
  155. return
  156. junits = find_by_suffix('.xml', tempdir)
  157. if not junits:
  158. return
  159. target = item.funcargs['target']
  160. config = item.funcargs['config']
  161. for junit in junits:
  162. xml = ET.parse(junit)
  163. testcases = xml.findall('.//testcase')
  164. for case in testcases:
  165. case.attrib['name'] = format_case_id(target, config, case.attrib['name'])
  166. if 'file' in case.attrib:
  167. case.attrib['file'] = case.attrib['file'].replace(
  168. '/IDF/', ''
  169. ) # our unity test framework
  170. xml.write(junit)