tests: replace CLI-dependent xfail/skipif conditions

This commit is contained in:
Fu Hanxi
2026-08-18 11:18:52 +02:00
parent 844a03718a
commit 289a17b3a2
4 changed files with 83 additions and 23 deletions

View File

@@ -27,15 +27,14 @@ def test_esp_event_ext_ram(dut: Dut) -> None:
@pytest.mark.qemu @pytest.mark.qemu
@pytest.mark.xfail('config.getvalue("target") == "esp32c3"', reason='Unstable on QEMU, needs investigation') @idf_parametrize(
@pytest.mark.parametrize( 'config,target,markers',
'config',
[ [
'defaults', ('defaults', 'esp32'),
('defaults', 'esp32c3', (pytest.mark.xfail(reason='Unstable on QEMU, needs investigation'),)),
], ],
indirect=True, indirect=['config', 'target'],
) )
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_esp_event_qemu(dut: Dut) -> None: def test_esp_event_qemu(dut: Dut) -> None:
for case in dut.test_menu: for case in dut.test_menu:
if 'qemu-ignore' not in case.groups and not case.is_ignored and case.type == 'normal': if 'qemu-ignore' not in case.groups and not case.is_ignored and case.type == 'normal':

View File

@@ -735,15 +735,35 @@ Sometimes, a test can consistently fail for the following reasons:
Now you may mark this test case with marker `xfail <https://docs.pytest.org/en/latest/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail>`__ with a user-friendly readable reason. Now you may mark this test case with marker `xfail <https://docs.pytest.org/en/latest/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail>`__ with a user-friendly readable reason.
.. attention::
Avoid using a string condition, e.g., ``@pytest.mark.xfail('config.getvalue("target") == "esp32s2"', ...)``. This condition is evaluated against the session-level CLI ``--target`` option, not against the actual target resolved for a given parametrized test instance, so it can silently mismatch when the same test is parametrized over multiple targets and produce unreliable results.
Instead, attach the ``xfail`` marker directly to the specific parametrized value passed to ``idf_parametrize`` (see :ref:`Same App With Different Running Environments <pytest-same-app-different-running-environments>`), so the marker is tied to that value at collection time.
This code example is taken from :idf_file:`pytest_panic.py <tools/test_apps/system/panic/panic_base/pytest_panic.py>` This code example is taken from :idf_file:`pytest_panic.py <tools/test_apps/system/panic/panic_base/pytest_panic.py>`
.. code-block:: python .. code-block:: python
@pytest.mark.xfail('config.getvalue("target") == "esp32s2"', reason='raised IllegalInstruction instead') @pytest.mark.generic
def test_cache_error(dut: PanicTestDut, config: str, test_func_name: str) -> None: @idf_parametrize(
'config,target,markers',
[
(
'memprot_esp32s2',
'esp32s2',
pytest.mark.xfail(reason='Incorrect panic reason may be observed', run=False),
),
('memprot_esp32c3', 'esp32c3'),
],
indirect=['config', 'target'],
)
def test_cache_error(dut: PanicTestDut, test_func_name: str) -> None:
This marker means that test is a known failure on the ESP32-S2. This marker means that test is a known failure on the ESP32-S2.
If a test is only ever parametrized with a single target, there is no ambiguity, and you can use an unconditional ``@pytest.mark.xfail(reason=..., run=False)`` instead.
Mark Nightly Run Test Cases Mark Nightly Run Test Cases
--------------------------- ---------------------------

View File

@@ -735,15 +735,35 @@ Pytest 使用技巧
可使用 `xfail <https://docs.pytest.org/en/latest/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail>`__ marker 来标记此测试用例,并写出原因。 可使用 `xfail <https://docs.pytest.org/en/latest/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail>`__ marker 来标记此测试用例,并写出原因。
.. attention::
请避免使用字符串条件,例如 ``@pytest.mark.xfail('config.getvalue("target") == "esp32s2"', ...)``。该条件是在运行时根据 session 级别的 CLI ``--target`` 参数求值的,而不是针对某个具体参数化测试实例实际解析出的 target 值,因此当同一测试针对多个 target 参数化时,该条件可能被错误匹配,导致结果不可靠。
推荐做法是通过 ``idf_parametrize`` 传入的具体参数化取值直接附加 ``xfail`` marker更复杂的示例请参阅 :ref:`在不同运行环境中运行相同的应用程序 <pytest-same-app-different-running-environments>` 小节,这样该 marker 在 collection 阶段就与该取值绑定。
以下代码来自 :idf_file:`pytest_panic.py <tools/test_apps/system/panic/panic_base/pytest_panic.py>` 以下代码来自 :idf_file:`pytest_panic.py <tools/test_apps/system/panic/panic_base/pytest_panic.py>`
.. code-block:: python .. code-block:: python
@pytest.mark.xfail('config.getvalue("target") == "esp32s2"', reason='raised IllegalInstruction instead') @pytest.mark.generic
def test_cache_error(dut: PanicTestDut, config: str, test_func_name: str) -> None: @idf_parametrize(
'config,target,markers',
[
(
'memprot_esp32s2',
'esp32s2',
pytest.mark.xfail(reason='Incorrect panic reason may be observed', run=False),
),
('memprot_esp32c3', 'esp32c3'),
],
indirect=['config', 'target'],
)
def test_cache_error(dut: PanicTestDut, test_func_name: str) -> None:
这一 marker 表示该测试在 ESP32-S2 上是一个已知失败。 这一 marker 表示该测试在 ESP32-S2 上是一个已知失败。
如果一个测试始终只针对单个 target 参数化,则不存在歧义,此时可直接使用无条件的 ``@pytest.mark.xfail(reason=..., run=False)``
标记夜间运行的测试用例 标记夜间运行的测试用例
--------------------------- ---------------------------

View File

@@ -35,6 +35,15 @@ def configs_for_app(app_path: str, configs: Sequence[str]) -> list[tuple[str, st
return [(app_path, config) for config in configs] return [(app_path, config) for config in configs]
def configs_with_esp32s2_xfail(
configs: Sequence[tuple[str, str] | tuple[str, str, Any]], reason: str
) -> list[tuple[str, str] | tuple[str, str, Any]]:
return [
(entry[0], entry[1], pytest.mark.xfail(reason=reason, run=False)) if entry[1] == 'esp32s2' else entry
for entry in configs
]
CONFIGS = configs_for_app(PANIC_APP, ['gdbstub', 'panic']) CONFIGS = configs_for_app(PANIC_APP, ['gdbstub', 'panic'])
CONFIGS_UBSAN = configs_for_app(PANIC_APP, ['gdbstub', 'panic']) CONFIGS_UBSAN = configs_for_app(PANIC_APP, ['gdbstub', 'panic'])
@@ -748,7 +757,7 @@ def test_dcache_read_violation(dut: PanicTestDut, test_func_name: str) -> None:
# TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs # TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@pytest.mark.generic @pytest.mark.generic
@pytest.mark.xfail(targets=['esp32s2'], reason='Incorrect panic reason may be observed', run=False) @pytest.mark.xfail(reason='Incorrect panic reason may be observed', run=False)
@idf_parametrize('config, target', CONFIGS_MEMPROT_DCACHE, indirect=['config', 'target']) @idf_parametrize('config, target', CONFIGS_MEMPROT_DCACHE, indirect=['config', 'target'])
def test_dcache_write_violation(dut: PanicTestDut, test_func_name: str) -> None: def test_dcache_write_violation(dut: PanicTestDut, test_func_name: str) -> None:
dut.run_test_func(test_func_name) dut.run_test_func(test_func_name)
@@ -888,11 +897,14 @@ def iram_reg4_write_violation(dut: PanicTestDut, test_func_name: str) -> None:
dut.expect_cpu_reset() dut.expect_cpu_reset()
# TODO: IDF-6820: ESP32-S2 -> Fix incorrect panic reason: Unhandled debug exception
@pytest.mark.generic @pytest.mark.generic
@pytest.mark.xfail(targets=['esp32s2'], reason='Incorrect panic reason may be observed', run=False)
@pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners') @pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners')
@idf_parametrize('config, target', CONFIGS_MEMPROT_IDRAM, indirect=['config', 'target']) # TODO: IDF-6820: ESP32-S2 -> Fix incorrect panic reason: Unhandled debug exception
@idf_parametrize(
'config,target,markers',
configs_with_esp32s2_xfail(CONFIGS_MEMPROT_IDRAM, 'Incorrect panic reason may be observed'),
indirect=['config', 'target'],
)
def test_iram_reg4_write_violation(dut: PanicTestDut, test_func_name: str) -> None: def test_iram_reg4_write_violation(dut: PanicTestDut, test_func_name: str) -> None:
iram_reg_write_violation(dut, test_func_name) iram_reg_write_violation(dut, test_func_name)
@@ -920,11 +932,14 @@ def dram_reg1_execute_violation(dut: PanicTestDut, test_func_name: str) -> None:
dut.expect_cpu_reset() dut.expect_cpu_reset()
# TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@pytest.mark.generic @pytest.mark.generic
@pytest.mark.xfail(targets=['esp32s2'], reason='Multiple panic reasons for the same test may surface', run=False)
@pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners') @pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners')
@idf_parametrize('config, target', CONFIGS_MEMPROT_IDRAM, indirect=['config', 'target']) # TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@idf_parametrize(
'config,target,markers',
configs_with_esp32s2_xfail(CONFIGS_MEMPROT_IDRAM, 'Multiple panic reasons for the same test may surface'),
indirect=['config', 'target'],
)
def test_dram_reg1_execute_violation(dut: PanicTestDut, test_func_name: str) -> None: def test_dram_reg1_execute_violation(dut: PanicTestDut, test_func_name: str) -> None:
dram_reg1_execute_violation(dut, test_func_name) dram_reg1_execute_violation(dut, test_func_name)
@@ -951,11 +966,14 @@ def dram_reg2_execute_violation(dut: PanicTestDut, test_func_name: str) -> None:
dut.expect_cpu_reset() dut.expect_cpu_reset()
# TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@pytest.mark.generic @pytest.mark.generic
@pytest.mark.xfail(targets=['esp32s2'], reason='Multiple panic reasons for the same test may surface', run=False)
@pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners') @pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners')
@idf_parametrize('config, target', CONFIGS_MEMPROT_IDRAM, indirect=['config', 'target']) # TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@idf_parametrize(
'config,target,markers',
configs_with_esp32s2_xfail(CONFIGS_MEMPROT_IDRAM, 'Multiple panic reasons for the same test may surface'),
indirect=['config', 'target'],
)
def test_dram_reg2_execute_violation(dut: PanicTestDut, test_func_name: str) -> None: def test_dram_reg2_execute_violation(dut: PanicTestDut, test_func_name: str) -> None:
dram_reg2_execute_violation(dut, test_func_name) dram_reg2_execute_violation(dut, test_func_name)
@@ -1000,11 +1018,14 @@ def test_rtc_fast_reg2_execute_violation(dut: PanicTestDut, test_func_name: str)
dut.expect_cpu_reset() dut.expect_cpu_reset()
# TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@pytest.mark.generic @pytest.mark.generic
@pytest.mark.xfail(targets=['esp32s2'], reason='Multiple panic reasons for the same test may surface', run=False)
@pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners') @pytest.mark.temp_skip_ci(targets=['esp32h21'], reason='lack of runners')
@idf_parametrize('config, target', CONFIGS_MEMPROT_RTC_FAST_MEM, indirect=['config', 'target']) # TODO: IDF-6820: ESP32-S2 -> Fix multiple panic reasons in different runs
@idf_parametrize(
'config,target,markers',
configs_with_esp32s2_xfail(CONFIGS_MEMPROT_RTC_FAST_MEM, 'Multiple panic reasons for the same test may surface'),
indirect=['config', 'target'],
)
def test_rtc_fast_reg3_execute_violation(dut: PanicTestDut, test_func_name: str) -> None: def test_rtc_fast_reg3_execute_violation(dut: PanicTestDut, test_func_name: str) -> None:
dut.run_test_func(test_func_name) dut.run_test_func(test_func_name)