From 3240d2d6958ca7d63eda0675e985788a8c822034 Mon Sep 17 00:00:00 2001 From: zhangshuxian Date: Mon, 20 May 2024 17:43:00 +0800 Subject: [PATCH] docs: update CN trans for esp-idf-tests-with-pytest.rst --- .../contribute/esp-idf-tests-with-pytest.rst | 416 +++---- .../contribute/esp-idf-tests-with-pytest.rst | 1003 +++++++++-------- 2 files changed, 727 insertions(+), 692 deletions(-) diff --git a/docs/en/contribute/esp-idf-tests-with-pytest.rst b/docs/en/contribute/esp-idf-tests-with-pytest.rst index e668dc8b8a..30210f694b 100644 --- a/docs/en/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/en/contribute/esp-idf-tests-with-pytest.rst @@ -18,33 +18,33 @@ On the host side, ESP-IDF employs the pytest framework (alongside certain pytest .. note:: - In ESP-IDF, we use the following pytest plugins by default: + In ESP-IDF, we use the following pytest plugins by default: - - `pytest-embedded `__ with default services ``esp,idf`` - - `pytest-rerunfailures `__ - - `pytest-ignore-test-results `__ + - `pytest-embedded `__ with default services ``esp,idf`` + - `pytest-rerunfailures `__ + - `pytest-ignore-test-results `__ - All the concepts and usages introduced in this guide are based on the default behavior of these plugins, thus may not be available in vanilla pytest. + All the concepts and usages introduced in this guide are based on the default behavior of these plugins, thus may not be available in vanilla pytest. .. important:: - This guide specifically targets ESP-IDF contributors. Some of the concepts, like the custom markers, may not be directly applicable to personal projects using the ESP-IDF SDK. For running pytest-embedded in personal projects, please refer to `pytest-embedded documentation `__, and explore the `provided examples `__. + This guide specifically targets ESP-IDF contributors. Some of the concepts, like the custom markers, may not be directly applicable to personal projects using the ESP-IDF SDK. For running pytest-embedded in personal projects, please refer to `pytest-embedded documentation `__, and explore the `provided examples `__. Installation ============ -All dependencies could be installed by running the install script with the ``--enable-pytest`` argument: +All dependencies could be installed by running the ESP-IDF install script with the ``--enable-pytest`` argument: .. code-block:: bash - $ install.sh --enable-pytest + $ install.sh --enable-pytest -We have implemented several mechanisms to ensure the successful execution of all installation processes. If you encounter any issues during the installation, please submit an issue report to our `GitHub issue tracker `__. +Several mechanisms have been implemented to ensure the successful execution of the installation processes. If you encounter any issues during installation, please submit an issue report to our `GitHub issue tracker `__. Common Concepts =============== -A **test app** is a set of binaries which is being built from an IDF project that is used to test a particular feature of your project. Test apps are usually located under ``${IDF_PATH}/examples``, ``${IDF_PATH}/tools/test_apps``, and ``${IDF_PATH}/components//test_apps``. +A **test app** is a set of binaries which are built from an IDF project that is used to test a particular feature of your project. Test apps are usually located under ``${IDF_PATH}/examples``, ``${IDF_PATH}/tools/test_apps``, and ``${IDF_PATH}/components//test_apps``. A **Device under test (DUT)** is a set of ESP chip(s) which connect to a host (e.g., a PC). The host is responsible for flashing the apps to the DUT, triggering the test cases, and inspecting the test results. @@ -52,27 +52,27 @@ A typical ESP-IDF project that contains a pytest script will have the following .. code-block:: text - . - └── my_app/ - ├── main/ - │ └── ... - ├── CMakeLists.txt - └── pytest_foo.py + . + └── my_app/ + ├── main/ + │ └── ... + ├── CMakeLists.txt + └── pytest_foo.py Sometimes, for some multi-dut tests, one test case requires multiple test apps. In this case, the test app folder structure would be like this: .. code-block:: text - . - ├── my_app_foo/ - │ ├── main/ - │ │ └── ... - │ └── CMakeLists.txt - ├── my_app_bar/ - │ ├── main/ - │ │ └── ... - │ └── CMakeLists.txt - └── pytest_foo_bar.py + . + ├── my_app_foo/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + ├── my_app_bar/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + └── pytest_foo_bar.py pytest in ESP-IDF ================= @@ -85,13 +85,13 @@ Getting Started .. code-block:: python - @pytest.mark.esp32 - @pytest.mark.esp32s2 - @pytest.mark.generic - def test_hello_world(dut) -> None: - dut.expect('Hello world!') + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.generic + def test_hello_world(dut) -> None: + dut.expect('Hello world!') -This is a simple test script that could run with our getting-started example :example:`get-started/hello_world`. +This is a simple test script that could run with the ESP-IDF getting-started example :example:`get-started/hello_world`. First two lines are the target markers: @@ -100,9 +100,9 @@ First two lines are the target markers: .. note:: - If the test case can be run on all targets officially supported by ESP-IDF (call ``idf.py --list-targets`` for more details), you can use a special marker ``supported_targets`` to apply all of them in one line. + If the test case can be run on all targets officially supported by ESP-IDF (call ``idf.py --list-targets`` for more details), you can use a special marker ``supported_targets`` to apply all of them in one line. - We also supports ``preview_targets`` and ``all_targets`` as special target markers (call ``idf.py --list-targets --preview`` for a full targets list including preview targets). + We also supports ``preview_targets`` and ``all_targets`` as special target markers (call ``idf.py --list-targets --preview`` for a full targets list including preview targets). Next, we have the environment marker: @@ -110,11 +110,11 @@ Next, we have the environment marker: .. note:: - For the detailed explanation of the environment markers, please refer to :idf_file:`ENV_MARKERS definition ` + For the detailed explanation of the environment markers, please refer to :idf_file:`ENV_MARKERS definition ` Finally, we have the test function. With a ``dut`` fixture. In single-dut test cases, the ``dut`` fixture is an instance of ``IdfDut`` class, for multi-dut test cases, it is a tuple of ``IdfDut`` instances. For more details regarding the ``IdfDut`` class, please refer to `pytest-embedded IdfDut API reference `__. -Same App with Different sdkconfig Files +Same App With Different sdkconfig Files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For some test cases, you may need to run the same app with different sdkconfig files. For detailed documentation regarding sdkconfig related concepts, please refer to `idf-build-apps Documentation `__. @@ -123,37 +123,37 @@ Here's a simple example that demonstrates how to run the same app with different .. code-block:: text - . - └── my_app/ - ├── main/ - │ └── ... - ├── CMakeLists.txt - ├── sdkconfig.ci.foo - ├── sdkconfig.ci.bar - └── pytest_foo.py + . + └── my_app/ + ├── main/ + │ └── ... + ├── CMakeLists.txt + ├── sdkconfig.ci.foo + ├── sdkconfig.ci.bar + └── pytest_foo.py If the test case needs to run all supported targets with these two sdkconfig files, you can use the following code: .. code-block:: python - @pytest.mark.esp32 - @pytest.mark.esp32s2 - @pytest.mark.parametrize('config', [ # <-- parameterize the sdkconfig file - 'foo', # <-- run with sdkconfig.ci.foo - 'bar', # <-- run with sdkconfig.ci.bar - ], indirect=True) # <-- `indirect=True` is required, indicates this param is pre-calculated before other fixtures - def test_foo_bar(dut, config) -> None: - if config == 'foo': - dut.expect('This is from sdkconfig.ci.foo') - elif config == 'bar': - dut.expect('This is from sdkconfig.ci.bar') + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.parametrize('config', [ # <-- parameterize the sdkconfig file + 'foo', # <-- run with sdkconfig.ci.foo + 'bar', # <-- run with sdkconfig.ci.bar + ], indirect=True) # <-- `indirect=True` is required, indicates this param is pre-calculated before other fixtures + def test_foo_bar(dut, config) -> None: + if config == 'foo': + dut.expect('This is from sdkconfig.ci.foo') + elif config == 'bar': + dut.expect('This is from sdkconfig.ci.bar') All markers will impact the test case simultaneously. Overall, this test function would be replicated to 4 test cases: -- ``test_foo_bar``, with esp32 target, and sdkconfig.ci.foo as the sdkconfig file -- ``test_foo_bar``, with esp32 target, and sdkconfig.ci.bar as the sdkconfig file -- ``test_foo_bar``, with esp32s2 target, and sdkconfig.ci.foo as the sdkconfig file -- ``test_foo_bar``, with esp32s2 target, and sdkconfig.ci.bar as the sdkconfig file +- ``test_foo_bar``, with esp32 target, and ``sdkconfig.ci.foo`` as the sdkconfig file +- ``test_foo_bar``, with esp32 target, and ``sdkconfig.ci.bar`` as the sdkconfig file +- ``test_foo_bar``, with esp32s2 target, and ``sdkconfig.ci.foo`` as the sdkconfig file +- ``test_foo_bar``, with esp32s2 target, and ``sdkconfig.ci.bar`` as the sdkconfig file Sometimes in the test script or the log file, you may see the following format: @@ -172,30 +172,30 @@ The test case ID is used to identify the test case in the JUnit report. .. note:: - Nearly all the CLI options of pytest-embedded supports parameterization. To see all supported CLI options, you may run ``pytest --help`` and check the ``embedded-...`` sections for vanilla pytest-embedded ones, and the ``idf`` sections for ESP-IDF specific ones. + Nearly all the CLI options of pytest-embedded supports parameterization. To see all supported CLI options, you may run ``pytest --help`` and check the ``embedded-...`` sections for vanilla pytest-embedded ones, and the ``idf`` sections for ESP-IDF specific ones. .. note:: - The target markers, like ``@pytest.mark.esp32`` and ``@pytest.mark.esp32s2``, are actually syntactic sugar for parameterization. In fact they are defined as: + The target markers, like ``@pytest.mark.esp32`` and ``@pytest.mark.esp32s2``, are actually syntactic sugar for parameterization. In fact they are defined as: - .. code-block:: python + .. code-block:: python - @pytest.mark.parametrize('target', [ - 'esp32', - 'esp32s2', - ], indirect=True) + @pytest.mark.parametrize('target', [ + 'esp32', + 'esp32s2', + ], indirect=True) -Same App with Different sdkconfig Files, Different Targets +Same App With Different sdkconfig Files, Different Targets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For some test cases, you may need to run the same app with different sdkconfig files. These sdkconfig files supports different targets. We may use ``pytest.param`` to achieve this. Let's use the same folder structure as above. .. code-block:: python - @pytest.mark.parametrize('config', [ - pytest.param('foo', marks=[pytest.mark.esp32]), - pytest.param('bar', marks=[pytest.mark.esp32s2]), - ], indirect=True) + @pytest.mark.parametrize('config', [ + pytest.param('foo', marks=[pytest.mark.esp32]), + pytest.param('bar', marks=[pytest.mark.esp32s2]), + ], indirect=True) Now this test function would be replicated to 2 test cases (represented as test case IDs): @@ -209,9 +209,9 @@ To ensure that test has executed successfully on target, the test script can tes .. code-block:: python - def test_hello_world(dut) -> None: - dut.expect('\d+') # <-- `expect`ing from a regex - dut.expect_exact('Hello world!') # <-- `expect_exact`ly the string + def test_hello_world(dut) -> None: + dut.expect('\d+') # <-- `expect`ing from a regex + dut.expect_exact('Hello world!') # <-- `expect_exact`ly the string The ``dut.expect(...)`` will first compile the expected string into regex, which in turn is then used to seek through the serial output until the compiled regex is matched, or until a timeout occurs. @@ -225,22 +225,22 @@ Multi-DUT Test Cases Multi-Target Tests with the Same App ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In some cases a test may involve multiple targets running the same test app. Parametrize ``count`` to the number of DUTs you want to test with. +In some cases a test may involve multiple targets running the same test app. Parameterize ``count`` to the number of DUTs you want to test with. .. code-block:: python - @pytest.mark.parametrize('count', [ - 2, - ], indirect=True) - @pytest.mark.parametrize('target', [ - 'esp32|esp32s2', - 'esp32s3', - ], indirect=True) - def test_hello_world(dut) -> None: - dut[0].expect('Hello world!') - dut[1].expect('Hello world!') + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + @pytest.mark.parametrize('target', [ + 'esp32|esp32s2', + 'esp32s3', + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') -The ``|`` symbol in all parametrized items is used for separating the settings for each DUT. In this example, the test case would be tested with: +The ``|`` symbol in all parameterized items is used for separating the settings for each DUT. In this example, the test case would be tested with: * esp32, esp32s2 * esp32s3, esp32s3 @@ -249,45 +249,45 @@ After setting the param ``count`` to 2, all the fixtures are changed into tuples .. important:: - ``count`` is mandatory for multi-DUT tests. + ``count`` is mandatory for multi-DUT tests. .. note:: - For detailed multi-dut parametrization documentation, please refer to `pytest-embedded Multi-DUT documentation `__. + For detailed multi-dut parametrization documentation, please refer to `pytest-embedded Multi-DUT documentation `__. .. warning:: - In some test scripts, you may see target markers like ``@pytest.mark.esp32`` and ``@pytest.mark.esp32s2`` used together with multi-DUT test cases. This is deprecated and should be replaced with the ``target`` parametrization. + In some test scripts, you may see target markers like ``@pytest.mark.esp32`` and ``@pytest.mark.esp32s2`` used together with multi-DUT test cases. This is deprecated and should be replaced with the ``target`` parametrization. - For example, + For example, - .. code-block:: python + .. code-block:: python - @pytest.mark.esp32 - @pytest.mark.esp32s2 - @pytest.mark.parametrize('count', [ - 2, - ], indirect=True) - def test_hello_world(dut) -> None: - dut[0].expect('Hello world!') - dut[1].expect('Hello world!') + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') - should be replaced with: + should be replaced with: - .. code-block:: python + .. code-block:: python - @pytest.mark.parametrize('count', [ - 2, - ], indirect=True) - @pytest.mark.parametrize('target', [ - 'esp32', - 'esp32s2', - ], indirect=True) - def test_hello_world(dut) -> None: - dut[0].expect('Hello world!') - dut[1].expect('Hello world!') + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + @pytest.mark.parametrize('target', [ + 'esp32', + 'esp32s2', + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') - This could help avoid the ambiguity of the target markers when multi-DUT test cases are using different type of targets. + This could help avoid the ambiguity of the target markers when multi-DUT test cases are using different type of targets. Multi-Target Tests with Different Apps ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -296,39 +296,39 @@ In some cases, a test may involve multiple targets running different test apps ( .. code-block:: text - . - ├── master/ - │ ├── main/ - │ │ └── ... - │ └── CMakeLists.txt - ├── slave/ - │ ├── main/ - │ │ └── ... - │ └── CMakeLists.txt - └── pytest_master_slave.py + . + ├── master/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + ├── slave/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + └── pytest_master_slave.py -In this case, we can parametrize the ``app_path`` to the path of the test apps you want to test with. +In this case, we can parameterize the ``app_path`` to the path of the test apps you want to test with. .. code-block:: python - @pytest.mark.multi_dut_generic - @pytest.mark.parametrize('count', [ - 2, - ], indirect=True) - @pytest.mark.parametrize('app_path, target', [ - (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32|esp32s2'), - (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32s2|esp32'), - ], indirect=True) - def test_master_slave(dut) -> None: - master = dut[0] - slave = dut[1] + @pytest.mark.multi_dut_generic + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + @pytest.mark.parametrize('app_path, target', [ + (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32|esp32s2'), + (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32s2|esp32'), + ], indirect=True) + def test_master_slave(dut) -> None: + master = dut[0] + slave = dut[1] - master.write('Hello world!') - slave.expect_exact('Hello world!') + master.write('Hello world!') + slave.expect_exact('Hello world!') .. note:: - When parametrizing two items, like ``app_path, target`` here, make sure you're passing a list of tuples to the ``parametrize`` decorator. Each tuple should contain the values for each item. + When parametrizing two items, like ``app_path, target`` here, make sure you're passing a list of tuples to the ``parametrize`` decorator. Each tuple should contain the values for each item. The test case here will be replicated to 2 test cases: @@ -338,7 +338,7 @@ The test case here will be replicated to 2 test cases: Test Cases with Unity Test Framework ------------------------------------ -We use `Unity test framework `__ in our unit tests. Overall, we have three types of test cases (`Unity test framework `__): +We use the `Unity test framework `__ in our unit tests. Overall, we have three types of test cases (`Unity test framework `__): * Normal test cases (single DUT) * Multi-stage test cases (single DUT) @@ -348,8 +348,8 @@ All single-DUT test cases (including normal test cases and multi-stage test case .. code-block:: python - def test_unity_single_dut(dut: IdfDut): - dut.run_all_single_board_cases() + def test_unity_single_dut(dut: IdfDut): + dut.run_all_single_board_cases() Using this command will skip all the test cases containing the ``[ignore]`` tag. @@ -357,30 +357,30 @@ If you need to run a group of test cases, you may run: .. code-block:: python - def test_unity_single_dut(dut: IdfDut): - dut.run_all_single_board_cases(group='psram') + def test_unity_single_dut(dut: IdfDut): + dut.run_all_single_board_cases(group='psram') It would trigger all test cases with the ``[psram]`` tag. .. warning:: - You may also see that there are some test scripts with the following statements, which are deprecated. Please use the suggested one as above. + You may also see that there are some test scripts with the following statements, which are deprecated. Please use the suggested one as above. - .. code-block:: python + .. code-block:: python - def test_unity_single_dut(dut: IdfDut): - dut.expect_exact('Press ENTER to see the list of tests') - dut.write('*') - dut.expect_unity_test_output() + def test_unity_single_dut(dut: IdfDut): + dut.expect_exact('Press ENTER to see the list of tests') + dut.write('*') + dut.expect_unity_test_output() We also provide a fixture ``case_tester`` to trigger all kinds of test cases easier. For example: .. code-block:: python - def test_unity_single_dut(case_tester): - case_tester.run_all_normal_cases() # to run all normal test cases - case_tester.run_all_multi_dev_cases() # to run all multi-device test cases - case_tester.run_all_multi_stage_cases() # to run all multi-stage test cases + def test_unity_single_dut(case_tester): + case_tester.run_all_normal_cases() # to run all normal test cases + case_tester.run_all_multi_dev_cases() # to run all multi-device test cases + case_tester.run_all_multi_stage_cases() # to run all multi-stage test cases For a full list of the available functions, please refer to `pytest-embedded case_tester API reference `__. @@ -432,19 +432,19 @@ In CI, all ESP-IDF projects under ``components``, ``examples``, and ``tools/test .. code-block:: text - . - ├── build_esp32_history/ - │ └── ... - ├── build_esp32_nohistory/ - │ └── ... - ├── build_esp32s2_history/ - │ └── ... - ├── ... - ├── main/ - ├── CMakeLists.txt - ├── sdkconfig.ci.history - ├── sdkconfig.ci.nohistory - └── ... + . + ├── build_esp32_history/ + │ └── ... + ├── build_esp32_nohistory/ + │ └── ... + ├── build_esp32s2_history/ + │ └── ... + ├── ... + ├── main/ + ├── CMakeLists.txt + ├── sdkconfig.ci.history + ├── sdkconfig.ci.nohistory + └── ... There are two types of build jobs, ``build_test_related_apps`` and ``build_non_test_related_apps``. @@ -469,9 +469,9 @@ First you need to install ESP-IDF with additional Python requirements: .. code-block:: shell - $ cd $IDF_PATH - $ bash install.sh --enable-ci --enable-pytest - $ . ./export.sh + $ cd $IDF_PATH + $ bash install.sh --enable-ci --enable-pytest + $ . ./export.sh Build Directories ----------------- @@ -484,32 +484,32 @@ By default, each test case looks for the required binary files in the following - ``build_`` - ``build`` -As long as one of the above directories exists, the test case uses that directory to flash the binaries. If non of the above directories exists, the test case fails with an error. +As long as one of the above directories exists, the test case uses that directory to flash the binaries. If none of the above directories exists, the test case fails with an error. Test Your Test Script --------------------- -Single-DUT Test Cases with ``sdkconfig.defaults`` +Single-DUT Test Cases With ``sdkconfig.defaults`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is the simplest use case. Let's take :project:`examples/get-started/hello_world` as an example. Assume we're testing with a ESP32 board. .. code-block:: shell - $ cd $IDF_PATH/examples/get-started/hello_world - $ idf.py set-target esp32 build - $ pytest --target esp32 + $ cd $IDF_PATH/examples/get-started/hello_world + $ idf.py set-target esp32 build + $ pytest --target esp32 -Single-DUT Test Cases with ``sdkconfig.ci.xxx`` +Single-DUT Test Cases With ``sdkconfig.ci.xxx`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some test cases may need to run with different sdkconfig files. Let's take :project:`examples/system/console/basic` as an example. Assume we're testing with a ESP32 board, and test with ``sdkconfig.ci.history``. .. code-block:: shell - $ cd $IDF_PATH/examples/system/console/basic - $ idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.defaults;sdkconfig.ci.history' -B build_esp32_history set-target esp32 build - $ pytest --target esp32 -k "not nohistory" + $ cd $IDF_PATH/examples/system/console/basic + $ idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.defaults;sdkconfig.ci.history' -B build_esp32_history set-target esp32 build + $ pytest --target esp32 -k "not nohistory" .. note:: @@ -519,9 +519,9 @@ If you want to build and test with all sdkconfig files at the same time, you sho .. code-block:: shell - $ cd $IDF_PATH/examples/system/console/basic - $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target esp32 -v --pytest-apps - $ pytest --target esp32 + $ cd $IDF_PATH/examples/system/console/basic + $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target esp32 -v --pytest-apps + $ pytest --target esp32 The app with ``sdkconfig.ci.history`` will be built in ``build_esp32_history``, and the app with ``sdkconfig.ci.nohistory`` will be built in ``build_esp32_nohistory``. ``pytest --target esp32`` will run tests on both apps. @@ -555,9 +555,9 @@ Of course we can build the required binaries manually, but we can also use our C .. code-block:: shell - $ cd $IDF_PATH/examples/openthread - $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target all -v --pytest-apps -k test_thread_connect - $ pytest --target esp32c6,esp32h2,esp32s3 -k test_thread_connect + $ cd $IDF_PATH/examples/openthread + $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target all -v --pytest-apps -k test_thread_connect + $ pytest --target esp32c6,esp32h2,esp32s3 -k test_thread_connect .. important:: @@ -566,14 +566,14 @@ Of course we can build the required binaries manually, but we can also use our C Debug CI Test Cases ------------------- -Sometimes you can't reprocude the CI test case failure locally. In this case, you may need to debug the test case with the binaries built in CI. +Sometimes you can't reproduce the CI test case failure locally. In this case, you may need to debug the test case with the binaries built in CI. Run pytest with ``--pipeline-id `` to force pytest to download the binaries from CI. For example: .. code-block:: shell - $ cd $IDF_PATH/examples/get-started/hello_world - $ pytest --target esp32 --pipeline-id 123456 + $ cd $IDF_PATH/examples/get-started/hello_world + $ pytest --target esp32 --pipeline-id 123456 Even if you have ``build_esp32_default``, or ``build`` directory locally, pytest would still download the binaries from pipeline 123456 and place the binaries in ``build_esp32_default``. Then run the test case with this binary. @@ -581,8 +581,8 @@ Even if you have ``build_esp32_default``, or ``build`` directory locally, pytest should be the parent pipeline id. You can copy it in your MR page. -Pytest Tips and Tricks -====================== +Pytest Tips & Tricks +==================== Custom Classes -------------- @@ -596,19 +596,19 @@ This code example is taken from :idf_file:`panic/conftest.py MonkeyPatch: - mp = MonkeyPatch() - request.addfinalizer(mp.undo) - return mp + @pytest.fixture(scope='module') + def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: + mp = MonkeyPatch() + request.addfinalizer(mp.undo) + return mp - @pytest.fixture(scope='module', autouse=True) - def replace_dut_class(monkeypatch_module: MonkeyPatch) -> None: - monkeypatch_module.setattr('pytest_embedded_idf.dut.IdfDut', PanicTestDut) + @pytest.fixture(scope='module', autouse=True) + def replace_dut_class(monkeypatch_module: MonkeyPatch) -> None: + monkeypatch_module.setattr('pytest_embedded_idf.dut.IdfDut', PanicTestDut) ``monkeypatch_module`` provides a `module-scoped `__ `monkeypatch `__ fixture. @@ -623,9 +623,9 @@ This code example is taken from :idf_file:`pytest_esp_eth.py None: - ... + @pytest.mark.flaky(reruns=3, reruns_delay=5) + def test_esp_eth_ip101(dut: IdfDut) -> None: + ... This flaky marker means that if the test function failed, the test case would rerun for a maximum of 3 times with 5 seconds delay. @@ -643,8 +643,8 @@ This code example is taken from :idf_file:`pytest_panic.py None: + @pytest.mark.xfail('config.getvalue("target") == "esp32s2"', reason='raised IllegalInstruction instead') + def test_cache_error(dut: PanicTestDut, config: str, test_func_name: str) -> None: This marker means that test is a known failure on the ESP32-S2. @@ -655,7 +655,7 @@ Some test cases are only triggered in nightly run pipelines due to a lack of run .. code-block:: python - @pytest.mark.nightly_run + @pytest.mark.nightly_run This marker means that the test case would only be run with env var ``NIGHTLY_RUN`` or ``INCLUDE_NIGHTLY_RUN``. @@ -666,7 +666,7 @@ Some test cases which can pass locally may need to be temporarily disabled in CI .. code-block:: python - @pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2'], reason='lack of runners') + @pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2'], reason='lack of runners') This marker means that the test case could still be run locally with ``pytest --target esp32``, but will not run in CI. @@ -725,7 +725,7 @@ The above example would log the performance item with pre-defined format: ``[per ``check_performance`` ^^^^^^^^^^^^^^^^^^^^^ -We provide C macros ``TEST_PERFORMANCE_LESS_THAN`` and ``TEST_PERFORMANCE_GREATER_THAN`` to log the performance item and check if the value is in the valid range. Sometimes the performance item value could not be measured in C code, so we also provide a Python function for the same purpose. Please note that using C macros is the preferred approach, since the Python function could not recognize the threshold values of the same performance item under different ifdef blocks well. +We provide C macros ``TEST_PERFORMANCE_LESS_THAN`` and ``TEST_PERFORMANCE_GREATER_THAN`` to log the performance item and check if the value is in the valid range. Sometimes the performance item value could not be measured in C code, so we also provide a Python function for the same purpose. Please note that using C macros is the preferred approach, since the Python function could not recognize the threshold values of the same performance item under different ``#ifdef`` blocks well. .. code-block:: python @@ -743,5 +743,5 @@ Let us assume the value of ``IDF_PERFORMANCE_MAX_RSA_2048KEY_PUBLIC_OP`` is 1900 Further Readings ================ -- pytest documentation: https://docs.pytest.org/en/latest/contents.html -- pytest-embedded documentation: https://docs.espressif.com/projects/pytest-embedded/en/latest/ +- `pytest documentation `_ +- `pytest-embedded documentation `_ diff --git a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst index 593bced4ea..7b93b22e3e 100644 --- a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst @@ -4,13 +4,13 @@ ESP-IDF pytest 指南 :link_to_translation:`en:[English]` -ESP-IDF 有多种类型的测试需在 ESP 芯片上执行(即 **目标测试**)。目标测试通常作为 IDF 测试项目(即 **测试应用程序**)的一部分进行编译,在这个过程中,测试应用程序和其他标准 IDF 项目遵循同样的构建、烧写和监控流程。 +ESP-IDF 有多种类型的测试需在 ESP 芯片上执行(即 **目标测试**)。目标测试通常作为 IDF 测试项目(即 **测试应用程序**)的一部分进行编译,在这个过程中,测试应用程序和其他标准 IDF 项目遵循同样的构建、烧录和监控流程。 通常,目标测试需要连接一台主机(如个人电脑),负责触发特定的测试用例、提供测试数据、检查测试结果。 ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自动进行目标测试。本文档介绍 ESP-IDF 中的 pytest,并介绍以下内容: -1. ESP-IDF 中不同类型的测试应用程序。 +1. ESP-IDF 目标测试的常见概念。 2. 将 pytest 框架应用于 Python 测试脚本,进行自动化目标测试。 3. ESP-IDF CI (Continuous Integration) 板载测试流程。 4. 使用 pytest 在本地执行目标测试。 @@ -18,421 +18,619 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自 .. note:: - ESP-IDF 默认使用以下插件: + ESP-IDF 默认使用以下插件: - - `pytest-embedded `__ 和默认服务 ``esp,idf`` - - `pytest-rerunfailures `__ + - `pytest-embedded `__ 和默认服务 ``esp,idf`` + - `pytest-rerunfailures `__ + - `pytest-ignore-test-results `__ - 本文档介绍的所有概念和用法都基于 ESP-IDF 的默认配置,并非都适用于原生 pytest。 + 本文档介绍的所有概念和用法都基于 ESP-IDF 的默认配置,并非都适用于原生 pytest。 + +.. important:: + + 本指南专门面向 ESP-IDF 贡献者。一些概念(如自定义标记)可能不直接适用于使用 ESP-IDF SDK 的个人项目。要在个人项目中运行 pytest-embedded,请参阅 `pytest-embedded 文档 `__ 和 `提供的示例 `__。 安装 ============ -所有依赖项都可以通过执行安装脚本的 ``--enable-pytest`` 进行安装: +所有依赖项都可以通过执行 ESP-IDF 安装脚本 ``--enable-pytest`` 进行安装: .. code-block:: bash - $ install.sh --enable-pytest + $ install.sh --enable-pytest +上面的脚本已预先实现了一些机制,以确保所有安装过程顺利进行。如果您在安装过程中遇到任何问题,请在 `GitHub Issue 版块 `__ 上提交问题说明。 -安装过程常见问题 ----------------------------- +常见概念 +=============== -No Package 'dbus-1' Found -^^^^^^^^^^^^^^^^^^^^^^^^^ +**测试应用程序** 是一组二进制文件,从一个 IDF 项目构建,用于测试项目的特定功能。测试应用程序通常位于 ``${IDF_PATH}/examples``,``${IDF_PATH}/tools/test_apps``,和 ``${IDF_PATH}/components//test_apps``。 -.. code:: text +**测试设备 (DUT)** 是指一组连接到主机(例如 PC)的 ESP 芯片。主机负责将应用程序烧录到 DUT 上,触发测试用例,并检查测试结果。 - configure: error: Package requirements (dbus-1 >= 1.8) were not met: +一个包含 pytest 脚本的典型 ESP-IDF 项目通常具有以下结构: - No package 'dbus-1' found +.. code-block:: text - Consider adjusting the PKG_CONFIG_PATH environment variable if you - installed software in a non-standard prefix. - -如果遇到上述错误信息,可能需要安装一些缺失的软件包。 - -如果使用 Ubuntu 系统,可能需要执行: - -.. code:: shell - - sudo apt-get install libdbus-glib-1-dev - -或 - -.. code:: shell - - sudo apt-get install libdbus-1-dev - -如使用 Linux 其他发行版本,请在搜索引擎中查询上述错误信息,并查找对应发行版需安装哪些缺失的软件包。 - -Invalid command 'bdist_wheel' -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code:: text - - error: invalid command 'bdist_wheel' - -如果遇到上述错误信息,可能需要安装一些缺失的 Python 包,例如: - -.. code:: shell - - python -m pip install -U pip - -或 - -.. code:: shell - - python -m pip install wheel - -.. note:: - - 执行 pip 命令前,请确保使用的环境为 IDF Python 虚拟环境。 - - -测试应用程序 -================== - -ESP-IDF 包含不同类型的测试应用程序,可用 pytest 自动完成。 - -组件测试 ----------- - -ESP-IDF 组件通常包含针对特定组件的测试应用程序,执行针对特定组件的单元测试。推荐通过组件测试应用程序来测试组件。所有测试应用程序都应位于 ``${IDF_PATH}/components//test_apps`` 下,例如: - -.. code:: text - - components/ - └── my_component/ - ├── include/ + . + └── my_app/ + ├── main/ │ └── ... - ├── test_apps/ - │ ├── test_app_1 - │ │ ├── main/ - │ │ │ └── ... - │ │ ├── CMakeLists.txt - │ │ └── pytest_my_component_app_1.py - │ ├── test_app_2 - │ │ ├── ... - │ │ └── pytest_my_component_app_2.py - │ └── parent_folder - │ ├── test_app_3 - │ │ ├── ... - │ │ └── pytest_my_component_app_3.py - │ └── ... - ├── my_component.c - └── CMakeLists.txt + ├── CMakeLists.txt + └── pytest_foo.py -例程测试 -------------- +有时,对于一些多 DUT 测试,一个测试用例需要多个测试应用程序。在这种情况下,测试应用程序的文件夹结构如下所示: -例程测试是为了向用户展示 ESP-IDF 的部分功能(要了解更多信息,请参考 :idf_file:`Examples Readme `)。 +.. code-block:: text -但是,要确保这些例程正确运行,可将例程看作测试应用,并用 pytest 自动执行。所有例程都应和已测试的例程,包括 Python 测试脚本一起放在 ``${IDF_PATH}/examples`` 路径下,例如: - -.. code:: text - - examples/ - └── parent_folder/ - └── example_1/ - ├── main/ - │ └── ... - ├── CMakeLists.txt - └── pytest_example_1.py - -自定义测试 --------------- - -自定义测试是为了测试 ESP-IDF 的一些任意功能,这些测试不是为了向用户展示 ESP-IDF 的功能。 - -所有自定义测试应用都位于 ``${IDF_PATH}/tools/test_apps`` 路径下。要了解更多信息,请参考 :idf_file:`Custom Test Readme `。 + . + ├── my_app_foo/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + ├── my_app_bar/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + └── pytest_foo_bar.py 在 ESP-IDF 中使用 pytest ============================ -.. _pytest-execution-process: +单个 DUT 测试用例 +------------------ -pytest 执行步骤 ---------------------- +入门教程 +^^^^^^^^^^^^^^^ -1. 引导阶段 +.. code-block:: python -创建会话缓存: + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.generic + def test_hello_world(dut) -> None: + dut.expect('Hello world!') - - 端口目标缓存 - - 端口应用缓存 +这是一个简单的测试脚本,可以与入门示例 :example:`get-started/hello_world` 一起运行。 -2. 数据获取阶段 +前两行是目标标记: - A. 获取所有前缀为 ``pytest_`` 的 Python 文件。 - B. 获取所有前缀为 ``test_`` 的测试函数。 - C. 应用 `params `__,并复制测试函数。 - D. 利用 CLI 选项筛选测试用例。详细用法请参考 :ref:`filter-the-test-cases`。 - -3. 运行阶段 - - A. 创建 `fixture `__。在 ESP-IDF 中,常见 fixture 的初始化顺序如下: - - a. ``pexpect_proc``: `pexpect `__ 实例 - - b. ``app``: `IdfApp `__ 实例 - - 此阶段会解析测试应用程序的相关信息,如 sdkconfig、flash_files、partition_table 等。 - - c. ``serial``: `IdfSerial `__ 实例 - - 此阶段会自动检测目标芯片所连接的主机端口。考虑到主机可能连接了多个目标芯片,应用程序会解析测试目标芯片的类型。测试程序的二进制文件会自动烧写到测试目标芯片上。 - - d. ``dut``: `IdfDut `__ 实例 - - B. 运行测试函数。 - - C. 析构 fixture。析构顺序如下: - - a. ``dut`` - - i. 关闭 ``serial`` 端口。 - ii. (仅适用于使用了 `Unity 测试框架 `__ 的应用程序)生成 Unity 测试用例的 JUnit 报告。 - - b. ``serial`` - c. ``app`` - d. ``pexpect_proc``:关闭文件描述符 - - D.(仅适用于使用了 `Unity 测试框架 `__ 的应用程序) - - 如果调用了 ``dut.expect_from_unity_output()``,那么检测到 Unity 测试失败时会触发 ``AssertionError``。 - -4. 报告阶段 - - A. 为测试函数生成 Junit 报告。 - B. 将 JUnit 报告中的测试用例名修改为 ESP-IDF 测试用例 ID 格式: ``..``。 - -5. 完成阶段(仅适用于使用了 `Unity 测试框架 `__ 的应用程序) - - 如果生成了 Unity 测试用例的 JUnit 报告,这些报告会被合并。 - -入门示例 -------------- - -以下 Python 测试脚本示例来自 :idf_file:`pytest_console_basic.py `。 - -.. code:: python - - @pytest.mark.esp32 - @pytest.mark.esp32c3 - @pytest.mark.generic - @pytest.mark.parametrize('config', [ - 'history', - 'nohistory', - ], indirect=True) - def test_console_advanced(config: str, dut: IdfDut) -> None: - if config == 'history': - dut.expect('Command history enabled') - elif config == 'nohistory': - dut.expect('Command history disabled') - -下面的小节对这个简单的测试脚本进行了逐行讲解,以说明 pytest 在 ESP-IDF 测试脚本中的典型使用方法。 - -目标芯片 marker -^^^^^^^^^^^^^^^^^^ - -使用 Pytest marker 可以指出某个特定测试用例应在哪个目标芯片(即 ESP 芯片)上运行。例如: - -.. code:: python - - @pytest.mark.esp32 # <-- support esp32 - @pytest.mark.esp32c3 # <-- support esp32c3 - @pytest.mark.generic # <-- test env "generic" - -上例表明,某一测试用例可以在 ESP32 和 ESP32-C3 上运行。此外,目标芯片的类型应为 ``generic``。要了解有关 ``generic`` 类型,运行 ``pytest --markers`` 以获取所有 marker 的详细信息。 +* ``@pytest.mark.esp32`` 是一个标记,表示此测试用例应在 ESP32 上运行。 +* ``@pytest.mark.esp32s2`` 是一个标记,表示此测试用例应在 ESP32-S2 上运行。 .. note:: - 如果测试用例可以在 ESP-IDF 官方支持的所有目标芯片上运行(调用 ``idf.py --list-targets`` 了解详情),则可以使用特殊 marker ``supported_targets`` 指定所有目标芯片。 + 如果测试用例可以在 ESP-IDF 官方支持的所有目标芯片上运行,调用 ``idf.py --list-targets`` 获取更多详情,可以使用特殊的标记 ``supported_targets`` 来在一行中应用所有目标。 -参数化 marker -^^^^^^^^^^^^^^^^ + 也支持 ``preview_targets`` 和 ``all_targets`` 作为特殊的目标标记,调用 ``idf.py --list-targets --preview`` 获取包括预览目标的完整目标列表。 -可使用 ``pytest.mark.parametrize`` 和 ``config`` 参数对包含不同 sdkconfig 文件的不同应用程序进行相同的测试。如需了解关于 ``sdkconfig.ci.xxx`` 文件的更多信息,请参考 :idf_file:`readme ` 下的 Configuration Files 章节。 +环境标记: -.. code:: python +* ``@pytest.mark.generic`` 标记表示此测试用例应在 generic 板类型上运行。 - @pytest.mark.parametrize('config', [ - 'history', # <-- run with app built by sdkconfig.ci.history - 'nohistory', # <-- run with app built by sdkconfig.ci.nohistory - ], indirect=True) # <-- `indirect=True` is required +.. note:: -总体而言,这一测试函数会复制为 4 个测试用例: + 有关环境标记的详细解释,请参阅 :idf_file:`ENV_MARKERS 定义 `。 -- ``esp32.history.test_console_advanced`` -- ``esp32.nohistory.test_console_advanced`` -- ``esp32c3.history.test_console_advanced`` -- ``esp32c3.nohistory.test_console_advanced`` +关于测试函数,使用了一个 ``dut`` fixture。在单一 DUT 测试用例中,``dut`` fixture 是 ``IdfDut`` 类的一个实例,对于多个 DUT 测试用例,它是 ``IdfDut`` 实例的一个元组。有关 ``IdfDut`` 类的更多详细信息,请参阅 `pytest-embedded IdfDut API 参考 `__。 + +使用不同的 sdkconfig 文件运行相同的应用程序 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +某些测试用例可能需要使用不同的 sdkconfig 文件运行相同的应用程序。与 sdkconfig 相关概念的详细文档,请参阅 `idf-build-apps 文档 `__。 + +以下是一个简单的示例,演示了如何使用不同的 sdkconfig 文件运行相同的应用程序。假设我们有以下文件夹结构: + +.. code-block:: text + + . + └── my_app/ + ├── main/ + │ └── ... + ├── CMakeLists.txt + ├── sdkconfig.ci.foo + ├── sdkconfig.ci.bar + └── pytest_foo.py + +如果测试用例需要使用这两个 sdkconfig 文件运行所有支持的目标芯片,您可以使用以下代码: + +.. code-block:: python + + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.parametrize('config', [ # <-- parameterize the sdkconfig file + 'foo', # <-- run with sdkconfig.ci.foo + 'bar', # <-- run with sdkconfig.ci.bar + ], indirect=True) # <-- `indirect=True` is required, indicates this param is pre-calculated before other fixtures + def test_foo_bar(dut, config) -> None: + if config == 'foo': + dut.expect('This is from sdkconfig.ci.foo') + elif config == 'bar': + dut.expect('This is from sdkconfig.ci.bar') + +所有标记将一并影响测试用例。总体而言,此测试函数将被复制为 4 个测试用例: + +- ``test_foo_bar`` 使用 esp32 目标芯片,将 sdkconfig.ci.foo 作为 sdkconfig 文件 +- ``test_foo_bar`` 使用 esp32 目标芯片,将 sdkconfig.ci.bar 作为 sdkconfig 文件 +- ``test_foo_bar`` 使用 esp32s2 目标芯片,将 sdkconfig.ci.foo 作为 sdkconfig 文件 +- ``test_foo_bar`` 使用 esp32s2 目标芯片,将 sdkconfig.ci.bar 作为 sdkconfig 文件 + +有时在测试脚本或日志文件中,可能会看到以下格式: + +- ``esp32.foo.test_foo_bar`` +- ``esp32.bar.test_foo_bar`` +- ``esp32s2.foo.test_foo_bar`` +- ``esp32s2.bar.test_foo_bar`` + +这种格式为 **测试用例 ID**。测试用例 ID 应被视为测试用例的唯一标识符。它由以下部分组成: + +- ``esp32``:目标名称 +- ``foo``:配置名称 +- ``test_foo_bar``:测试函数名称 + +测试用例 ID 用于在 JUnit 报告中标识测试用例。 + +.. note:: + + 几乎所有 pytest-embedded 的 CLI 选项都支持参数化。要查看所有支持的 CLI 选项,您可以运行 ``pytest --help`` 命令,并检查 ``embedded-...`` 部分以查看普通 pytest-embedded 选项,以及 ``idf`` 部分以查看 ESP-IDF 特定选项。 + +.. note:: + + 目标标记,例如 ``@pytest.mark.esp32`` 和 ``@pytest.mark.esp32s2``,是参数化的一种语法糖。它们被定义为: + + .. code-block:: python + + @pytest.mark.parametrize('target', [ + 'esp32', + 'esp32s2', + ], indirect=True) + +使用不同的 sdkconfig 文件运行相同的应用程序,支持不同的目标芯片 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +对于某些测试用例,可能需要使用不同的 sdkconfig 文件运行相同的应用程序。这些 sdkconfig 文件支持不同的目标芯片。可以使用 ``pytest.param`` 来实现。使用与上文相同的文件夹结构。 + +.. code-block:: python + + @pytest.mark.parametrize('config', [ + pytest.param('foo', marks=[pytest.mark.esp32]), + pytest.param('bar', marks=[pytest.mark.esp32s2]), + ], indirect=True) + +此时,这个测试函数将被复制为 2 个测试用例(测试用例 ID): + +* ``esp32.foo.test_foo_bar`` +* ``esp32s2.bar.test_foo_bar`` 测试串行输出 ^^^^^^^^^^^^^^^^ 为确保测试在目标芯片上顺利执行,测试脚本可使用 ``dut.expect()`` 函数来测试目标芯片上的串行输出: -.. code:: python +.. code-block:: python - def test_console_advanced(config: str, dut: IdfDut) -> None: # The value of argument ``config`` is assigned by the parameterization. - if config == 'history': - dut.expect('Command history enabled') - elif config == 'nohistory': - dut.expect('Command history disabled') + def test_hello_world(dut) -> None: + dut.expect('\d+') # <-- `expect`ing from a regex + dut.expect_exact('Hello world!') # <-- `expect_exact`ly the string 在执行 ``dut.expect(...)`` 时,首先会将预期字符串编译成正则表达式用于搜索串行输出结果,直到找到与该编译后的正则表达式匹配的结果或运行超时。 如果预期字符串中包含正则表达式关键字(如括号或方括号),则需格外注意。或者,也可以使用 ``dut.expect_exact(...)``,它会尝试直接匹配字符串,而不将其转换为正则表达式。 -如需了解关于 ``expect`` 函数类型的更多信息,请参考 `pytest-embedded Expecting documentation `__。 +如需了解关于 ``expect`` 函数类型的更多信息,请参考 `pytest-embedded 辅助文档 `__。 -进阶示例 ----------------- +多个 DUT 的测试用例 +------------------------------ 用同一应用程序进行多个 DUT 测试 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -有时,一个测试可能涉及多个运行同一测试程序的目标芯片。在这种情况下,可以使用 ``parameterize`` 将多个 DUT 实例化,例如: +有时,一个测试可能涉及多个目标芯片运行同一测试程序。在这种情况下,可以使用 ``count`` 将想要进行测试的 DUT 数量参数化。 -.. code:: python +.. code-block:: python - @pytest.mark.esp32s2 - @pytest.mark.esp32s3 - @pytest.mark.usb_host @pytest.mark.parametrize('count', [ 2, ], indirect=True) - def test_usb_host(dut: Tuple[IdfDut, IdfDut]) -> None: - device = dut[0] # <-- assume the first dut is the device - host = dut[1] # <-- and the second dut is the host - ... + @pytest.mark.parametrize('target', [ + 'esp32|esp32s2', + 'esp32s3', + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') + +所有参数化项中的 ``|`` 符号用于分隔每个 DUT 的设置。在这个例子中,以下芯片将用于测试: + +* esp32, esp32s2 +* esp32s3, esp32s3 将参数 ``count`` 设置为 2 后,所有 fixture 都会改为元组。 -用不同应用程序进行多个 DUT 测试 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. important:: -有时(特别是协议测试),一个测试可能涉及多个运行不同测试程序的目标芯片(例如不同目标芯片作为主机和从机)。在这种情况下,可以使用 ``parameterize`` 将针对不同测试应用程序的多个 DUT 实例化。 - -以下代码示例来自 :idf_file:`pytest_wifi_getting_started.py `。 - -.. code:: python - - @pytest.mark.esp32 - @pytest.mark.multi_dut_generic - @pytest.mark.parametrize( - 'count, app_path', [ - (2, - f'{os.path.join(os.path.dirname(__file__), "softAP")}|{os.path.join(os.path.dirname(__file__), "station")}'), - ], indirect=True - ) - def test_wifi_getting_started(dut: Tuple[IdfDut, IdfDut]) -> None: - softap = dut[0] - station = dut[1] - ... - -以上示例中,第一个 DUT 用 :idf_file:`softAP ` 应用程序烧录,第二个 DUT 用 :idf_file:`station ` 应用程序烧录。 + ``count`` 对于多个 DUT 测试是必需的。 .. note:: - 这里的 ``app_path`` 应设置为绝对路径。Python 中的 ``__file__`` 宏会返回测试脚本自身的绝对路径。 + 有关详细的多个 DUT 参数化文档,请参阅 `pytest-embedded Multi-DUT 文档 `__。 + +.. warning:: + + 在一些测试脚本中,您可能会看到目标标记,如 ``@pytest.mark.esp32`` 和 ``@pytest.mark.esp32s2`` 用于多个 DUT 测试用例。这些脚本已被弃用,应该替换为 ``target`` 参数化。 + + 例如, + + .. code-block:: python + + @pytest.mark.esp32 + @pytest.mark.esp32s2 + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') + + 应该改为: + + .. code-block:: python + + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + @pytest.mark.parametrize('target', [ + 'esp32', + 'esp32s2', + ], indirect=True) + def test_hello_world(dut) -> None: + dut[0].expect('Hello world!') + dut[1].expect('Hello world!') + + 这有助于避免多个 DUT 测试用例在运行不同目标芯片时造成歧义。 用不同应用程序和目标芯片进行多目标测试 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +在某些情况下,一个测试可能涉及多个目标芯片运行不同的测试应用程序(例如,将不同的目标用作主节点和从节点)。通常在 ESP-IDF 中,文件夹结构会是这样的: + +.. code-block:: text + + . + ├── master/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + ├── slave/ + │ ├── main/ + │ │ └── ... + │ └── CMakeLists.txt + └── pytest_master_slave.py + +在这种情况下,可以将测试应用程序的路径 ``app_path`` 作为参数提供给测试用例。 + +.. code-block:: python + + @pytest.mark.multi_dut_generic + @pytest.mark.parametrize('count', [ + 2, + ], indirect=True) + @pytest.mark.parametrize('app_path, target', [ + (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32|esp32s2'), + (f'{os.path.join(os.path.dirname(__file__), "master")}|{os.path.join(os.path.dirname(__file__), "slave")}', 'esp32s2|esp32'), + ], indirect=True) + def test_master_slave(dut) -> None: + master = dut[0] + slave = dut[1] + + master.write('Hello world!') + slave.expect_exact('Hello world!') + +.. note:: + + 当两个项作为参数时,比如 ``app_path, target`` 项,应确保将一个元组列表传递给 ``parametrize`` 装饰器。每个元组应包含每个项的值。 + +此测试用例会被复制为 2 个测试用例: + +* dut-0, ESP32 运行 ``master`` 应用程序, dut-1, ESP32-S2 运行 ``slave`` 应用程序 +* dut-0, ESP32-S2 运行 ``master`` 应用程序, dut-1, ESP32运行 ``slave`` 应用程序 + +运行 Unity 测试用例 +----------------------- + +使用 `Unity 测试框架 `__ 进行单元测试。共有三种测试用例( `Unity 测试框架 `__): + +* 普通测试用例(单个 DUT) +* 多阶段测试用例(单个 DUT) +* 多设备测试用例(多个 DUT) + +以下代码即可执行所有的单个 DUT 测试用例,包括普通测试用例和多阶段测试用例: + +.. code-block:: python + + def test_unity_single_dut(dut: IdfDut): + dut.run_all_single_board_cases() + +此代码将跳过所有 tag 为 ``[ignore]`` 的测试用例。 + +如需按组执行测试用例,可运行: + +.. code-block:: python + + def test_unity_single_dut(dut: IdfDut): + dut.run_all_single_board_cases(group='psram') + +此代码会触发模块包含 ``[psram]`` tag 的所有测试用例。 + +.. warning:: + + 你可能还会看到一些包含以下语句的测试脚本,这些脚本已被弃用。请使用上述建议的方法。 + + .. code-block:: python + + def test_unity_single_dut(dut: IdfDut): + dut.expect_exact('Press ENTER to see the list of tests') + dut.write('*') + dut.expect_unity_test_output() + +我们的 ``case_tester`` 夹具让执行各种测试用例更加简便。例如: + +.. code-block:: python + + def test_unity_single_dut(case_tester): + case_tester.run_all_normal_cases() # to run all normal test cases + case_tester.run_all_multi_dev_cases() # to run all multi-device test cases + case_tester.run_all_multi_stage_cases() # to run all multi-stage test cases + +有关可用函数的完整列表,请参阅 `pytest-embedded case_tester API 参考 `__。 + +在 CI 中执行板载测试 +====================== + +CI 的工作流程如下所示: + +.. blockdiag:: + :caption: 目标测试子流水线工作流程 + :align: center + + blockdiag child-pipeline-workflow { + default_group_color = lightgray; + + group { + label = "build" + + build_test_related_apps; build_non_test_related_apps; + } + + group { + label = "assign_test" + + build_job_report; generate_pytest_child_pipeline; + } + + group { + label = "target_test" + + "特定目标测试任务"; + } + + group { + label = ".post" + + target_test_report; + } + + build_test_related_apps, build_non_test_related_apps -> generate_pytest_child_pipeline, build_job_report -> "特定目标测试任务" -> target_test_report; + } + +所有编译和目标测试都是由我们的 CI 脚本 :project:`tools/ci/dynamic_pipelines` 自动生成。 + +编译 +----------- + +在 CI 中,所有位于 ``components``、``examples`` 和 ``tools/test_apps`` 下的 ESP-IDF 项目都会使用所有支持的目标芯片和 sdkconfig 文件进行编译。二进制文件将编译在 ``build__`` 下。例如: + +.. code-block:: text + + . + ├── build_esp32_history/ + │ └── ... + ├── build_esp32_nohistory/ + │ └── ... + ├── build_esp32s2_history/ + │ └── ... + ├── ... + ├── main/ + ├── CMakeLists.txt + ├── sdkconfig.ci.history + ├── sdkconfig.ci.nohistory + └── ... + +有两种类型的编译任务,``build_test_related_apps`` 和 ``build_non_test_related_apps``。 + +对于 ``build_test_related_apps``,所有编译的二进制文件将上传到内部 MinIO 服务器。下载链接可以在内部 MR 中发布的编译报告中获取。 + +对于 ``build_non_test_related_apps``,在编译完成后,所有编译的二进制文件将被删除。只有编译日志文件将上传到内部 MinIO 服务器。下载链接可以在内部 MR 中发布的编译报告中获取。 + +板载测试任务 +---------------- + +在CI中,所有板载测试任务都以 " - " 格式命名。例如,单个 DUT 测试任务 ``esp32 - generic`` 或多个 DUT 测试任务 ``esp32,esp32 - multi_dut_generic``。 + +板载测试任务中的二进制文件是从内部 MinIO 服务器下载的。对于大多数测试用例,仅下载烧录所需的文件(如 .bin 文件、flash_args 文件等)。对于某些测试用例,如 jtag 测试用例,还会下载 .elf 文件。 + +本地测试 +========== + +安装 +------- + +首先,你需为 ESP-IDF 安装 Python 依赖: + +.. code-block:: shell + + $ cd $IDF_PATH + $ bash install.sh --enable-ci --enable-pytest + $ . ./export.sh + +编译目录 +------------ + +默认情况下,pytest 脚本会按照以下顺序查找编译目录: + +- 由 ``--build-dir`` 命令行参数设置的目录(当指定时)。 +- ``build__`` +- ``build_`` +- ``build_`` +- ``build`` + +上述目录中如有任一个存在,测试用例就会使用该目录来烧录二进制文件。如果都不存在,测试用例将因错误而失败。 + +测试脚本 +------------- + +包含 ``sdkconfig.defaults`` 的单个 DUT 测试用例 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -以下代码示例来自 :idf_file:`pytest_wifi_getting_started.py `。如注释所述,该代码目前尚未在 ESP-IDF CI 中运行。 +这是最简单的用例。以 :project:`examples/get-started/hello_world` 为例。假设使用 ESP32 板进行测试。 -.. code:: python +.. code-block:: shell + + $ cd $IDF_PATH/examples/get-started/hello_world + $ idf.py set-target esp32 build + $ pytest --target esp32 + +包含 ``sdkconfig.ci.xxx`` 的单个 DUT 测试用例 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +一些测试用例可能需要运行不同的 sdkconfig 文件。以 :project:`examples/system/console/basic` 为例。假设使用 ESP32 板进行测试,并使用 ``sdkconfig.ci.history`` 进行测试。 + +.. code-block:: shell + + $ cd $IDF_PATH/examples/system/console/basic + $ idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.defaults;sdkconfig.ci.history' -B build_esp32_history set-target esp32 build + $ pytest --target esp32 -k "not nohistory" + +.. note:: + + 在这里,如果使用 ``pytest --target esp32 -k history``,两个测试用例都会被选中,因为 ``pytest -k`` 会使用字符串匹配来过滤测试用例。 + +如果你想同时编译测试所有 sdkconfig 文件,则需运行我们的 CI 脚本作为辅助脚本: + +.. code-block:: shell + + $ cd $IDF_PATH/examples/system/console/basic + $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target esp32 -v --pytest-apps + $ pytest --target esp32 + +包含 ``sdkconfig.ci.history`` 配置的应用程序会编译到 ``build_esp32_history`` 中,而包含 ``sdkconfig.ci.nohistory`` 配置的应用程序会编译到 ``build_esp32_nohistory`` 中。 ``pytest --target esp32`` 命令会在这两个应用程序上运行测试。 + +多个 DUT 测试用例 +^^^^^^^^^^^^^^^^^ + +一些测试用例可能需要运行多个 DUT。以 :project:`examples/openthread` 为例,测试用例函数如下所示: + +.. code-block:: python @pytest.mark.parametrize( - 'count, app_path, target', [ - (2, - f'{os.path.join(os.path.dirname(__file__), "softAP")}|{os.path.join(os.path.dirname(__file__), "station")}', - 'esp32|esp32s2'), - (2, - f'{os.path.join(os.path.dirname(__file__), "softAP")}|{os.path.join(os.path.dirname(__file__), "station")}', - 'esp32s2|esp32'), + 'config, count, app_path, target', [ + ('rcp|cli_h2|br', 3, + f'{os.path.join(os.path.dirname(__file__), "ot_rcp")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_cli")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_br")}', + 'esp32c6|esp32h2|esp32s3'), ], indirect=True, ) - def test_wifi_getting_started(dut: Tuple[IdfDut, IdfDut]) -> None: - softap = dut[0] - station = dut[1] + def test_thread_connect(dut:Tuple[IdfDut, IdfDut, IdfDut]) -> None: ... -总体而言,此测试函数会被复制为 2 个测试用例: +测试用例将使用以下芯片运行: -- 在 ESP32 上烧录 softAP,在 ESP32-S2 上烧录 station -- 在 ESP32-S2 上烧录 softAP,在 ESP32 上烧录 station +- 使用 ``ot_rcp`` 烧录的 ESP32-C6 +- 使用 ``ot_cli`` 烧录的 ESP32-H2 +- 使用 ``ot_br`` 烧录的 ESP32-S3 -支持对不同 sdkconfig 文件及目标芯片的组合测试 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +当然,我们可以手动编译所需的二进制文件,或者使用我们的 CI 脚本作为辅助脚本: -以下进阶代码示例来自 :idf_file:`pytest_panic.py `。 +.. code-block:: shell -.. code:: python + $ cd $IDF_PATH/examples/openthread + $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target all -v --pytest-apps -k test_thread_connect + $ pytest --target esp32c6,esp32h2,esp32s3 -k test_thread_connect - CONFIGS = [ - pytest.param('coredump_flash_bin_crc', marks=[pytest.mark.esp32, pytest.mark.esp32s2]), - pytest.param('coredump_flash_elf_sha', marks=[pytest.mark.esp32]), # sha256 only supported on esp32 - pytest.param('coredump_uart_bin_crc', marks=[pytest.mark.esp32, pytest.mark.esp32s2]), - pytest.param('coredump_uart_elf_crc', marks=[pytest.mark.esp32, pytest.mark.esp32s2]), - pytest.param('gdbstub', marks=[pytest.mark.esp32, pytest.mark.esp32s2]), - pytest.param('panic', marks=[pytest.mark.esp32, pytest.mark.esp32s2]), - ] +.. important:: - @pytest.mark.parametrize('config', CONFIGS, indirect=True) - ... + 多个 DUT 的测试用例,必须列出所有目标芯片。否则,测试用例将因错误而失败。 + +调试 CI 测试用例 +----------------------- + +有时无法在本地重现 CI 测试用例的失败。在这种情况下,可能需要借助 CI 中编译后的文件来调试测试用例。 + +运行带有 ``--pipeline-id `` 的 pytest,命令 pytest 从 CI 下载二进制文件。例如: + +.. code-block:: shell + + $ cd $IDF_PATH/examples/get-started/hello_world + $ pytest --target esp32 --pipeline-id 123456 + +即使你在本地有 ``build_esp32_default`` 或 ``build`` 目录,pytest 仍会从流水线 123456 下载二进制文件,并将这些二进制文件放置在 ``build_esp32_default`` 目录中,然后使用该二进制文件运行测试用例。 + +.. note:: + + 应该是父流水线 ID。你可以在你的 MR 页面上复制它。 + +Pytest 使用技巧 +======================= 自定义类 -^^^^^^^^^^^^ +------------ 通常,可能会在下列情况下编写自定义类: 1. 向一定数量的 DUT 添加更多可复用功能。 -2. 为不同阶段添加自定义的前置和后置函数,请参考章节 :ref:`pytest-execution-process`。 +2. 为不同阶段添加自定义的前置和后置函数。 以下代码示例来自 :idf_file:`panic/conftest.py `。 -.. code:: python +.. code-block:: python - class PanicTestDut(IdfDut): - ... + class PanicTestDut(IdfDut): + ... - @pytest.fixture(scope='module') - def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: - mp = MonkeyPatch() - request.addfinalizer(mp.undo) - return mp + @pytest.fixture(scope='module') + def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: + mp = MonkeyPatch() + request.addfinalizer(mp.undo) + return mp - @pytest.fixture(scope='module', autouse=True) - def replace_dut_class(monkeypatch_module: MonkeyPatch) -> None: - monkeypatch_module.setattr('pytest_embedded_idf.dut.IdfDut', PanicTestDut) + @pytest.fixture(scope='module', autouse=True) + def replace_dut_class(monkeypatch_module: MonkeyPatch) -> None: + monkeypatch_module.setattr('pytest_embedded_idf.dut.IdfDut', PanicTestDut) ``monkeypatch_module`` 提供了一个 `基于模块 `__ 的 `monkeypatch `__ fixture。 ``replace_dut_class`` 是一个 `基于模块 `__ 的 `自动执行 `__ fixture。 该函数会用你的自定义类替换 ``IdfDut`` 类。 标记不稳定测试 -^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------ 某些测试用例基于以太网或 Wi-Fi。然而由于网络问题,测试可能会不稳定。此时,可以将某个测试用例标记为不稳定的测试用例。 以下代码示例来自 :idf_file:`pytest_esp_eth.py `。 -.. code:: python +.. code-block:: python - @pytest.mark.flaky(reruns=3, reruns_delay=5) - def test_esp_eth_ip101(dut: IdfDut) -> None: - ... + @pytest.mark.flaky(reruns=3, reruns_delay=5) + def test_esp_eth_ip101(dut: IdfDut) -> None: + ... 这一 marker 表示,如果该测试函数失败,其测试用例会每隔 5 秒钟再运行一次,最多运行三次。 标记已知失败 -^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------ 有时,测试会因以下原因而持续失败: @@ -443,204 +641,41 @@ pytest 执行步骤 以下代码来自 :idf_file:`pytest_panic.py `。 -.. code:: python +.. code-block:: python - @pytest.mark.xfail('config.getvalue("target") == "esp32s2"', reason='raised IllegalInstruction instead') - def test_cache_error(dut: PanicTestDut, config: str, test_func_name: str) -> None: + @pytest.mark.xfail('config.getvalue("target") == "esp32s2"', reason='raised IllegalInstruction instead') + def test_cache_error(dut: PanicTestDut, config: str, test_func_name: str) -> None: 这一 marker 表示该测试在 ESP32-S2 上是一个已知失败。 标记夜间运行的测试用例 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +--------------------------- 在缺少 runner 时,一些测试用例仅在夜间运行的管道中触发。 -.. code:: python +.. code-block:: python @pytest.mark.nightly_run 这一 marker 表示,此测试用例仅在环境变量为 ``NIGHTLY_RUN`` 或 ``INCLUDE_NIGHTLY_RUN`` 时运行。 标记在 CI 中暂时禁用的测试用例 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +----------------------------------------------- 在缺少 runner 时,可以在 CI 中禁用一些本地能够通过测试的测试用例。 -.. code:: python +.. code-block:: python - @pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2'], reason='lack of runners') + @pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2'], reason='lack of runners') 这一 marker 表明,此测试用例仍可以在本地用 ``pytest --target esp32`` 执行,但不会在 CI 中执行。 -运行 Unity 测试用例 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -对基于组件的单元测试应用程序,以下代码即可执行所有的单板测试用例,包括普通测试用例和多阶段测试用例: - -.. code:: python - - def test_component_ut(dut: IdfDut): - dut.run_all_single_board_cases() - -此代码还会跳过所有 tag 为 ``[ignore]`` 的测试用例。 - -如需按组执行测试用例,可运行: - -.. code:: python - - def test_component_ut(dut: IdfDut): - dut.run_all_single_board_cases(group='psram') - -此代码会触发模块包含 ``[psram]`` tag 的所有测试用例。 - -你可能还会看到一些包含以下语句的测试脚本,这些脚本已被弃用。请使用上述建议的方法。 - -.. code:: python - - def test_component_ut(dut: IdfDut): - dut.expect_exact('Press ENTER to see the list of tests') - dut.write('*') - dut.expect_unity_test_output() - -如需了解关于 ESP-IDF 单元测试的更多内容,请参考 :doc:`../api-guides/unit-tests`。 - -在 CI 中执行测试 -====================== - -CI 的工作流程很简单,即 编译任务 -> 板载测试任务。 - -编译任务 --------------- - -编译任务命名 -^^^^^^^^^^^^^^^^^ - -- 基于组件的单元测试: ``build_pytest_components_`` -- 例程测试: ``build_pytest_examples_`` -- 自定义测试: ``build_pytest_test_apps_`` - -编译任务命令 -^^^^^^^^^^^^^^^^^ - -CI 用于创建所有相关测试的命令为: ``python $IDF_PATH/tools/ci/ci_build_apps.py --target -vv --pytest-apps`` - -所有支持指定目标芯片的应用程序都使用 ``build__`` 下支持的 sdkconfig 文件创建。 - -例如,如果运行 ``python $IDF_PATH/tools/ci/ci_build_apps.py $IDF_PATH/examples/system/console/basic --target esp32 --pytest-apps`` 指令,文件夹结构将如下所示: - -.. code:: text - - basic - ├── build_esp32_history/ - │ └── ... - ├── build_esp32_nohistory/ - │ └── ... - ├── main/ - ├── CMakeLists.txt - ├── pytest_console_basic.py - └── ... - -所有编译文件的文件夹都会上传到同一路径。 - -板载测试任务 ----------------- - -板载测试任务命名 -^^^^^^^^^^^^^^^^^^^ - -- 基于部件的单元测试: ``component_ut_pytest__`` -- 例程测试: ``example_test_pytest__`` -- 自定义测试: ``test_app_test_pytest__`` - -板载测试任务命令 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -CI 用于执行所有相关测试的命令为: ``pytest --target -m `` - -这一命令将执行父文件夹下所有具有指定目标芯片 marker 和测试环境 marker 的测试用例。 - -板载测试任务中的二进制文件是从编译任务中下载的,相应文件会放在同一路径下。 - -本地测试 -========== - -首先,你需为 ESP-IDF 安装 Python 依赖: - -.. code-block:: shell - - $ cd $IDF_PATH - $ bash install.sh --enable-pytest - $ . ./export.sh - -默认情况下,pytest 脚本会按照以下顺序查找编译目录: - -- 由 ``--build-dir`` 命令行参数设置的目录。(当指定时) -- ``build__`` -- ``build_`` -- ``build_`` -- ``build`` - -因此,运行 pytest 最简单的方式是调用 ``idf.py build``。 - -例如,如果你要执行 ``$IDF_PATH/examples/get-started/hello_world`` 文件夹下的所有 ESP32 测试,你可执行: - -.. code-block:: shell - - $ cd examples/get-started/hello_world - $ idf.py build - $ pytest --target esp32 - -如果你的测试应用程序中有多个 sdkconfig 文件,例如那些 ``sdkconfig.ci.*`` 文件, 仅使用 ``idf.py build`` 命令并不能调用这些额外的 sdkconfig 文件。下文以 ``$IDF_PATH/examples/system/console/basic`` 为例进行说明。 - -如果要用 ``history`` 配置测试此应用程序,并用 ``idf.py build`` 进行编译,你需运行: - -.. code-block:: shell - - $ cd examples/system/console/basic - $ idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.history" build - $ pytest --target esp32 --sdkconfig history - -如果你想同时编译测试所有 sdkconfig 文件,则需运行我们的 CI 脚本 (ci_build_apps.py) 作为辅助脚本: - -.. code-block:: shell - - $ cd examples/system/console/basic - $ python $IDF_PATH/tools/ci/ci_build_apps.py . --target esp32 -vv --pytest-apps - $ pytest --target esp32 - -包含 ``sdkconfig.ci.history`` 配置的应用程序会编译到 ``build_esp32_history`` 中,而包含 ``sdkconfig.ci.nohistory`` 配置的应用程序会编译到 ``build_esp32_nohistory`` 中。 ``pytest --target esp32`` 命令会在这两个应用程序上运行测试。 - -使用技巧 -============ - -.. _filter-the-test-cases: - -筛选测试用例 ---------------------- - -- 根据目标芯片筛选: ``pytest --target `` - - pytest 会执行所有支持指定目标芯片的测试用例。 - -- 根据 sdkconfig 文件筛选: ``pytest --sdkconfig `` - - 如果 ```` 为 ``default``,pytest 会执行所有 sdkconfig 文件包含 ``sdkconfig.defaults`` 的测试用例。 - - 如果是其他情况,pytest 会执行所有 sdkconfig 文件包含 ``sdkconfig.ci.`` 的测试用例。 - -- 使用 ``pytest -k `` 按测试用例名称筛选,可以运行单个测试用例,例如 ``pytest -k test_int_wdt_cache_disabled``。 - 添加新 marker ---------------- 我们目前使用两种自定义 marker。target marker 是指测试用例支持此目标芯片,env marker 是指测试用例应分配到 CI 中具有相应 tag 的 runner 上。 -你可以在 ``${IDF_PATH}/conftest.py`` 文件后添加一行新的 marker。如果该 marker 是 target marker,应将其添加到 ``TARGET_MARKERS`` 中。如果该 marker 指定了一类测试环境,应将其添加到 ``ENV_MARKERS`` 中。自定义 marker 格式: ``: ``。 - -生成 JUnit 报告 ---------------------- - -你可调用 pytest 执行 ``--junitxml `` 生成 JUnit 报告。在 ESP-IDF 中,测试用例命名会统一为 ``..``。 +你可以在 :idf_file:`conftest.py` 文件后添加一行新的 marker。如果该 marker 是 target marker,应将其添加到 ``TARGET_MARKERS`` 中。如果该 marker 指定了一类测试环境,应将其添加到 ``ENV_MARKERS`` 中。自定义 marker 格式:``: ``。 跳过自动烧录二进制文件 ------------------------------------- @@ -663,13 +698,12 @@ CI 用于执行所有相关测试的命令为: ``pytest --target 这可通过使用 `Python 日志模块 `__ 实现。 -其他日志函数(作为 fixture) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +以下是其他日志函数(作为 fixture) ``log_performance`` -""""""""""""""""""" +^^^^^^^^^^^^^^^^^^^ -.. code:: python +.. code-block:: python def test_hello_world( dut: IdfDut, @@ -677,6 +711,7 @@ CI 用于执行所有相关测试的命令为: ``pytest --target ) -> None: log_performance('test', 1) + 以上示例可实现用预定义格式 ``[performance][test]: 1`` 记录性能数据,并在指定 ``--junitxml `` 的情况下将其记录在 JUnit 报告的 ``properties`` tag 下。相应的 JUnit 测试用例节点如下所示: .. code:: html @@ -688,11 +723,11 @@ CI 用于执行所有相关测试的命令为: ``pytest --target ``check_performance`` -""""""""""""""""""""" +^^^^^^^^^^^^^^^^^^^^^ 我们提供了 ``TEST_PERFORMANCE_LESS_THAN`` 和 ``TEST_PERFORMANCE_GREATER_THAN`` 宏来记录性能项,并检测性能项的数值是否在有效范围内。有时 C 宏无法检测一些性能项的值,为此,我们提供了 Python 函数实现相同的目的。注意,由于该 Python 函数不能很好地识别不同的 ifdef 块下同一性能项的阈值,请尽量使用 C 宏。 -.. code:: python +.. code-block:: python def test_hello_world( dut: IdfDut, @@ -703,10 +738,10 @@ CI 用于执行所有相关测试的命令为: ``pytest --target 以上示例会首先从 :idf_file:`components/idf_test/include/idf_performance.h` 和指定目标芯片的 :idf_file:`components/idf_test/include/esp32/idf_performance_target.h` 头文件中获取性能项 ``RSA_2048KEY_PUBLIC_OP`` 的阈值,然后检查该值是否达到了最小值或超过了最大值。 -例如,假设 ``IDF_PERFORMANCE_MAX_RSA_2048KEY_PUBLIC_OP`` 的值为 19000,则上例中第一行 ``check_performance`` 会通过测试,第二行会失败并警告: ``[Performance] RSA_2048KEY_PUBLIC_OP value is 19001, doesn\'t meet pass standard 19000.0``。 +例如,假设 ``IDF_PERFORMANCE_MAX_RSA_2048KEY_PUBLIC_OP`` 的值为 19000,则上例中第一行 ``check_performance`` 会通过测试,第二行会失败并警告:``[Performance] RSA_2048KEY_PUBLIC_OP value is 19001, doesn\'t meet pass standard 19000.0``。 扩展阅读 ============= -- pytest:https://docs.pytest.org/en/latest/contents.html -- pytest-embedded:https://docs.espressif.com/projects/pytest-embedded/en/latest/ +- `pytest 文档 `_ +- `pytest-embedded 文档 `_