Part 1
요약·해설
Pytest 기반 black-box test의 directory 규칙, importlib 실행과 CLI 대응, output file 검사, decorator·fixture 배치, 여러 run 사이의 capture와 logger 정리를 설명합니다.
CLI · output · files
decorator · fixture
capture · clear log
Part 2
접을 수 있는 영어 원문 전체
영어 원문 전체 펼치기
E2FD47FA33847DFDA9D7270613EED8C454600B75EBB4F08250F510B3B485E6C6
.. _twister_blackbox:
Twister blackbox tests
######################
This guide aims to explain the structure of a test file so the reader will be able
to understand existing files and create their own. All developers should fix any tests
they break and create new ones when introducing new features, so this knowledge is
important for any Twister developer.
Basics
******
Twister blackbox tests are written in python, using the ``pytest`` library.
Read up on it :ref:`here <integration_with_pytest>` .
Auxiliary test data follows whichever format it was in originally.
Tests and data are wholly contained in the :zephyr_file:`scripts/tests/twister_blackbox`
directory and prepended with ``test_``.
Blackbox tests should not be aware of the internal twister code. Instead, they should
call twister as user would and check the results.
Sample test file
****************
.. literalinclude:: ../../../../../develop/test/twister/sample_blackbox_test.py
:language: python
:linenos:
Comparison with CLI
*******************
Test above runs the command
.. code-block:: console
twister -i --outdir $OUTDIR -T $TEST_DATA/tests -y --level $LEVEL
--test-config $TEST_DATA/test_config.yaml -p qemu_x86 -p frdm_k64f
It presumes a CLI with the ``zephyr-env.sh`` or ``zephyr-env.cmd`` already run.
Such a test provides us with all the outputs we typically expect of a Twister run thanks to
``importlib`` 's ``exec_module()`` [#f1]_ .
We can easily set up all flags that we expect from a Twister call via ``args`` variable [#f2]_ .
We can check the standard output or stderr in ``out`` and ``err`` variables.
Beside the standard outputs, we can also investigate the file outputs, normally placed in
``twister-out`` directories. Most of the time, we will use the ``out_path`` fixture in conjunction
with ``--outdir`` flag (L52) to keep test-generated files in temporary directories.
Typical files read in blackbox tests are ``testplan.json`` , ``twister.xml`` and ``twister.log`` .
Other functionalities
*********************
Decorators
==========
* ``@pytest.mark.usefixtures('clear_log')``
- allows us to use ``clear_log`` fixture from ``conftest.py`` .
The fixture is to become ``autouse`` in the future.
After that, this decorator can be removed.
* ``@pytest.mark.parametrize('level, expected_tests', TESTDATA_X, ids=['smoke', 'acceptance'])``
- this is an example of ``pytest`` 's test parametrization.
Read up on it `here <https://docs.pytest.org/en/7.1.x/example/parametrize.html#different-options-for-test-ids>`__.
TESTDATAs are most often declared as class fields.
* ``@mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock)``
- this decorator allows us to use only tests defined in the ``test_data`` and
ignore the Zephyr testcases in the ``tests`` directory. **Note that all ``test_data``
tests use** ``test_data.yaml`` **as a filename, not** ``testcase.yaml`` **!**
Read up on the ``mock`` library
`here <https://docs.python.org/3/library/unittest.mock.html>`__.
Fixtures
========
Blackbox tests use ``pytest`` 's fixtures, further reading on which is available
`here <https://docs.pytest.org/en/6.2.x/fixture.html>`__.
If you would like to add your own fixtures,
consider whether they will be used in just one test file, or in many.
* If in many, create such a fixture in the
:zephyr_file:`scripts/tests/twister_blackbox/conftest.py` file.
- :zephyr_file:`scripts/tests/twister_blackbox/conftest.py` already contains some fixtures -
take a look there for an example.
* If in just one, declare it in that file.
- Consider using class fields instead - look at TESTDATAs for an example.
How do I...
***********
Call Twister multiple times in one test?
========================================
Sometimes we want to test something that requires prior Twister use. ``--test-only``
flag would be a typical example, as it is to be coupled with previous ``--build-only``
Twister call. How should we approach that?
If we just call the ``importlib`` 's ``exec_module`` two times, we will experience log
duplication. ``twister.log`` will duplicate every line (triplicate if we call it three times, etc.)
instead of overwriting the log or appending to the end of it.
It is caused by the use of logger module variables in the Twister files.
Thus us executing the module again causes the loggers to have multiple handles.
To overcome this, between the calls you ought to use
.. code:: python
capfd.readouterr() # To remove output from the buffer
# Note that if you want output from all runs after each other,
# skip this line.
clear_log_in_test() # To remove log duplication
------
.. rubric:: Footnotes
.. [#f1] Take note of the ``setup_class()`` class function, which allows us to run
``twister`` python file as if it were called directly
(bypassing the ``__name__ == '__main__'`` check).
.. [#f2] We advise you to keep the first section of ``args`` definition intact in almost all
of your tests, as it is used for the common test setup.
Part 3
한국어 전문 번역
Twister black-box test
이 guide는 기존 Twister black-box test file을 이해하고 새 test를 만들 수 있도록 file 구조를 설명합니다. 모든 developer는 자신이 깨뜨린 test를 고치고 새 기능을 도입할 때 test도 추가해야 하므로 Twister developer에게 필요한 지식입니다.
기본 사항
Twister black-box test는 Python과 pytest library로 작성합니다. Pytest 통합은 Pytest test framework와 통합 문서를 참조하십시오. 보조 test data는 원래 사용하던 format을 그대로 따릅니다.
Test와 data는 모두 scripts/tests/twister_blackbox directory 안에 있으며 file 이름은 test_로 시작합니다.
Black-box test는 Twister 내부 code를 알아서는 안 됩니다. 사용자가 하듯 Twister를 호출하고 외부에서 관찰 가능한 결과를 검사해야 합니다.
Sample test file
다음은 원문이 포함한 완전한 sample입니다. Twister module을 command처럼 실행하고 level별 결과 수를 검증합니다.
#!/usr/bin/env python3
# Copyright (c) 2024 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
import importlib
import mock
import os
import pytest
import sys
import json
from conftest import ZEPHYR_BASE, TEST_DATA, testsuite_filename_mock
from twisterlib.testplan import TestPlan
class TestDummy:
TESTDATA_X = [
("smoke", 5),
("acceptance", 6),
]
@classmethod
def setup_class(cls):
apath = os.path.join(ZEPHYR_BASE, "scripts", "twister")
cls.loader = importlib.machinery.SourceFileLoader("__main__", apath)
cls.spec = importlib.util.spec_from_loader(cls.loader.name, cls.loader)
cls.twister_module = importlib.util.module_from_spec(cls.spec)
@classmethod
def teardown_class(cls):
pass
@pytest.mark.parametrize(
"level, expected_tests", TESTDATA_X, ids=["smoke", "acceptance"]
)
@mock.patch.object(TestPlan, "TESTSUITE_FILENAME", testsuite_filename_mock)
def test_level(self, capfd, out_path, level, expected_tests):
# Select platforms used for the tests
test_platforms = ["qemu_x86", "frdm_k64f"]
# Select test root
path = os.path.join(TEST_DATA, "tests")
config_path = os.path.join(TEST_DATA, "test_config.yaml")
# Set flags for our Twister command as a list of strs
args = (
# Flags related to the generic test setup:
# * Control the level of detail in stdout/err
# * Establish the output directory
# * Select Zephyr tests to use
# * Control whether to only build or build and run aforementioned tests
["-i", "--outdir", out_path, "-T", path, "-y"]
# Flags under test
+ ["--level", level]
# Flags required for the test
+ ["--test-config", config_path]
# Flags related to platform selection
+ [
val
for pair in zip(["-p"] * len(test_platforms), test_platforms)
for val in pair
]
)
# First, provide the args variable as our Twister command line arguments.
# Then, catch the exit code in the sys_exit variable.
with mock.patch.object(sys, "argv", [sys.argv[0]] + args), pytest.raises(
SystemExit
) as sys_exit:
# Execute the Twister call itself.
self.loader.exec_module(self.twister_module)
# Check whether the Twister call succeeded
assert str(sys_exit.value) == "0"
# Access to the test file output
with open(os.path.join(out_path, "testplan.json")) as f:
j = json.load(f)
filtered_j = [
(ts["platform"], ts["name"], tc["identifier"])
for ts in j["testsuites"]
for tc in ts["testcases"]
if "reason" not in tc
]
# Read stdout and stderr to out and err variables respectively
out, err = capfd.readouterr()
# Rewrite the captured buffers to stdout and stderr so the user can still read them
sys.stdout.write(out)
sys.stderr.write(err)
# Test-relevant checks
assert expected_tests == len(filtered_j)CLI와 비교
위 test는 다음 command를 실행하는 것과 같습니다.
twister -i --outdir $OUTDIR -T $TEST_DATA/tests -y --level $LEVEL
--test-config $TEST_DATA/test_config.yaml -p qemu_x86 -p frdm_k64fzephyr-env.sh 또는 zephyr-env.cmd를 이미 실행한 CLI environment를 전제로 합니다.
importlib.exec_module()을 사용하므로 일반 Twister run에서 기대하는 output 전체를 얻을 수 있습니다. args variable에 Twister flag를 list로 구성하고 out·err에서 standard output과 standard error를 검사합니다.
Standard stream 외에도 보통 twister-out에 생성되는 file을 검사할 수 있습니다. 대개 out_path fixture와 --outdir를 함께 써서 test 생성 file을 temporary directory에 격리합니다. 자주 읽는 file은 testplan.json, twister.xml, twister.log입니다.
다른 기능
Decorator
@pytest.mark.usefixtures('clear_log'):conftest.py의clear_logfixture를 사용합니다. 향후autouse가 되면 decorator를 제거할 수 있습니다.@pytest.mark.parametrize('level, expected_tests', TESTDATA_X, ids=['smoke', 'acceptance']): Pytest parameterization 예이며 TESTDATA는 보통 class field로 선언합니다.@mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock): Zephyrtestsdirectory의 testcase를 무시하고test_data에 정의한 test만 사용합니다. 모든 test_data test file 이름은testcase.yaml이 아니라 반드시test_data.yaml입니다. 자세한 내용은 unittest.mock을 참조하십시오.
Fixture
Black-box test는 Pytest fixture를 사용합니다. 새 fixture를 추가하기 전에 여러 test file에서 공유할지 file 하나에서만 쓸지 판단합니다.
- 여러 file에서 쓰면
scripts/tests/twister_blackbox/conftest.py에 만듭니다. 기존 fixture를 구현 예제로 참고할 수 있습니다. - File 하나에서만 쓰면 그 file 안에 선언합니다. 단순 data라면 fixture 대신 TESTDATA 같은 class field가 더 적합한지도 검토합니다.
어떻게 하나?
Test 하나에서 Twister를 여러 번 호출하려면?
--test-only처럼 이전 --build-only run을 전제로 하는 기능은 test 하나에서 Twister를 여러 번 실행해야 합니다.
importlib.exec_module()을 단순히 두 번 호출하면 twister.log의 각 line이 두 번 기록되고 세 번 호출하면 세 번 기록되는 log duplication이 생깁니다. 덮어쓰기나 정상 append가 아닙니다.
Twister file의 logger module variable이 이전 handler를 유지한 상태에서 module을 다시 실행해 handler가 여러 개가 되기 때문입니다. 호출 사이에 다음 처리를 넣습니다.
capfd.readouterr() # To remove output from the buffer
# Note that if you want output from all runs after each other,
# skip this line.
clear_log_in_test() # To remove log duplicationcapfd.readouterr()는 capture buffer를 비웁니다. 여러 run의 output을 이어서 검사하려면 이 line을 생략합니다. clear_log_in_test()는 중복 logger handler를 제거합니다.
각주 1. setup_class()는 __name__ == '__main__' 검사를 우회해 twister Python file을 직접 호출한 것처럼 실행합니다.
각주 2. 대부분의 test에서 args definition 첫 section은 공통 test setup에 쓰이므로 그대로 유지하는 것이 좋습니다.
Source
출처
영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.