 c3e24cff2b
			
		
	
	
		c3e24cff2b
		
	
	
	
	
		
			
			Document the new functional testing framework. The text is originally based on the Avocado documentation, but heavily modified to match the new framework. Message-ID: <20240830133841.142644-45-thuth@redhat.com> Signed-off-by: Thomas Huth <thuth@redhat.com>
		
			
				
	
	
		
			339 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
| .. _checkfunctional-ref:
 | |
| 
 | |
| Functional testing with Python
 | |
| ==============================
 | |
| 
 | |
| The ``tests/functional`` directory hosts functional tests written in
 | |
| Python. They are usually higher level tests, and may interact with
 | |
| external resources and with various guest operating systems.
 | |
| The functional tests have initially evolved from the Avocado tests, so there
 | |
| is a lot of similarity to those tests here (see :ref:`checkavocado-ref` for
 | |
| details about the Avocado tests).
 | |
| 
 | |
| The tests should be written in the style of the Python `unittest`_ framework,
 | |
| using stdio for the TAP protocol. The folder ``tests/functional/qemu_test``
 | |
| provides classes (e.g. the ``QemuBaseTest``, ``QemuUserTest`` and the
 | |
| ``QemuSystemTest`` classes) and utility functions that help to get your test
 | |
| into the right shape, e.g. by replacing the 'stdout' python object to redirect
 | |
| the normal output of your test to stderr instead.
 | |
| 
 | |
| Note that if you don't use one of the QemuBaseTest based classes for your
 | |
| test, or if you spawn subprocesses from your test, you have to make sure
 | |
| that there is no TAP-incompatible output written to stdio, e.g. either by
 | |
| prefixing every line with a "# " to mark the output as a TAP comment, or
 | |
| e.g. by capturing the stdout output of subprocesses (redirecting it to
 | |
| stderr is OK).
 | |
| 
 | |
| Tests based on ``qemu_test.QemuSystemTest`` can easily:
 | |
| 
 | |
|  * Customize the command line arguments given to the convenience
 | |
|    ``self.vm`` attribute (a QEMUMachine instance)
 | |
| 
 | |
|  * Interact with the QEMU monitor, send QMP commands and check
 | |
|    their results
 | |
| 
 | |
|  * Interact with the guest OS, using the convenience console device
 | |
|    (which may be useful to assert the effectiveness and correctness of
 | |
|    command line arguments or QMP commands)
 | |
| 
 | |
|  * Download (and cache) remote data files, such as firmware and kernel
 | |
|    images
 | |
| 
 | |
| Running tests
 | |
| -------------
 | |
| 
 | |
| You can run the functional tests simply by executing:
 | |
| 
 | |
| .. code::
 | |
| 
 | |
|   make check-functional
 | |
| 
 | |
| It is also possible to run tests for a certain target only, for example
 | |
| the following line will only run the tests for the x86_64 target:
 | |
| 
 | |
| .. code::
 | |
| 
 | |
|   make check-functional-x86_64
 | |
| 
 | |
| To run a single test file without the meson test runner, you can also
 | |
| execute the file directly by specifying two environment variables first,
 | |
| the PYTHONPATH that has to include the python folder and the tests/functional
 | |
| folder of the source tree, and QEMU_TEST_QEMU_BINARY that has to point
 | |
| to the QEMU binary that should be used for the test, for example::
 | |
| 
 | |
|   $ export PYTHONPATH=../python:../tests/functional
 | |
|   $ export QEMU_TEST_QEMU_BINARY=$PWD/qemu-system-x86_64
 | |
|   $ python3 ../tests/functional/test_file.py
 | |
| 
 | |
| Overview
 | |
| --------
 | |
| 
 | |
| The ``tests/functional/qemu_test`` directory provides the ``qemu_test``
 | |
| Python module, containing the ``qemu_test.QemuSystemTest`` class.
 | |
| Here is a simple usage example:
 | |
| 
 | |
| .. code::
 | |
| 
 | |
|   #!/usr/bin/env python3
 | |
| 
 | |
|   from qemu_test import QemuSystemTest
 | |
| 
 | |
|   class Version(QemuSystemTest):
 | |
| 
 | |
|       def test_qmp_human_info_version(self):
 | |
|           self.vm.launch()
 | |
|           res = self.vm.cmd('human-monitor-command',
 | |
|                             command_line='info version')
 | |
|           self.assertRegex(res, r'^(\d+\.\d+\.\d)')
 | |
| 
 | |
|   if __name__ == '__main__':
 | |
|       QemuSystemTest.main()
 | |
| 
 | |
| By providing the "hash bang" line at the beginning of the script, marking
 | |
| the file as executable and by calling into QemuSystemTest.main(), the test
 | |
| can also be run stand-alone, without a test runner. OTOH when run via a test
 | |
| runner, the QemuSystemTest.main() function takes care of running the test
 | |
| functions in the right fassion (e.g. with TAP output that is required by the
 | |
| meson test runner).
 | |
| 
 | |
| The ``qemu_test.QemuSystemTest`` base test class
 | |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | |
| 
 | |
| The ``qemu_test.QemuSystemTest`` class has a number of characteristics
 | |
| that are worth being mentioned.
 | |
| 
 | |
| First of all, it attempts to give each test a ready to use QEMUMachine
 | |
| instance, available at ``self.vm``.  Because many tests will tweak the
 | |
| QEMU command line, launching the QEMUMachine (by using ``self.vm.launch()``)
 | |
| is left to the test writer.
 | |
| 
 | |
| The base test class has also support for tests with more than one
 | |
| QEMUMachine. The way to get machines is through the ``self.get_vm()``
 | |
| method which will return a QEMUMachine instance. The ``self.get_vm()``
 | |
| method accepts arguments that will be passed to the QEMUMachine creation
 | |
| and also an optional ``name`` attribute so you can identify a specific
 | |
| machine and get it more than once through the tests methods. A simple
 | |
| and hypothetical example follows:
 | |
| 
 | |
| .. code::
 | |
| 
 | |
|   from qemu_test import QemuSystemTest
 | |
| 
 | |
|   class MultipleMachines(QemuSystemTest):
 | |
|       def test_multiple_machines(self):
 | |
|           first_machine = self.get_vm()
 | |
|           second_machine = self.get_vm()
 | |
|           self.get_vm(name='third_machine').launch()
 | |
| 
 | |
|           first_machine.launch()
 | |
|           second_machine.launch()
 | |
| 
 | |
|           first_res = first_machine.cmd(
 | |
|               'human-monitor-command',
 | |
|               command_line='info version')
 | |
| 
 | |
|           second_res = second_machine.cmd(
 | |
|               'human-monitor-command',
 | |
|               command_line='info version')
 | |
| 
 | |
|           third_res = self.get_vm(name='third_machine').cmd(
 | |
|               'human-monitor-command',
 | |
|               command_line='info version')
 | |
| 
 | |
|           self.assertEqual(first_res, second_res, third_res)
 | |
| 
 | |
| At test "tear down", ``qemu_test.QemuSystemTest`` handles all the QEMUMachines
 | |
| shutdown.
 | |
| 
 | |
| QEMUMachine
 | |
| -----------
 | |
| 
 | |
| The QEMUMachine API is already widely used in the Python iotests,
 | |
| device-crash-test and other Python scripts.  It's a wrapper around the
 | |
| execution of a QEMU binary, giving its users:
 | |
| 
 | |
|  * the ability to set command line arguments to be given to the QEMU
 | |
|    binary
 | |
| 
 | |
|  * a ready to use QMP connection and interface, which can be used to
 | |
|    send commands and inspect its results, as well as asynchronous
 | |
|    events
 | |
| 
 | |
|  * convenience methods to set commonly used command line arguments in
 | |
|    a more succinct and intuitive way
 | |
| 
 | |
| QEMU binary selection
 | |
| ^^^^^^^^^^^^^^^^^^^^^
 | |
| 
 | |
| The QEMU binary used for the ``self.vm`` QEMUMachine instance will
 | |
| primarily depend on the value of the ``qemu_bin`` class attribute.
 | |
| If it is not explicitly set by the test code, its default value will
 | |
| be the result the QEMU_TEST_QEMU_BINARY environment variable.
 | |
| 
 | |
| Attribute reference
 | |
| -------------------
 | |
| 
 | |
| QemuBaseTest
 | |
| ^^^^^^^^^^^^
 | |
| 
 | |
| The following attributes are available on any ``qemu_test.QemuBaseTest``
 | |
| instance.
 | |
| 
 | |
| arch
 | |
| """"
 | |
| 
 | |
| The target architecture of the QEMU binary.
 | |
| 
 | |
| Tests are also free to use this attribute value, for their own needs.
 | |
| A test may, for instance, use this value when selecting the architecture
 | |
| of a kernel or disk image to boot a VM with.
 | |
| 
 | |
| qemu_bin
 | |
| """"""""
 | |
| 
 | |
| The preserved value of the ``QEMU_TEST_QEMU_BINARY`` environment
 | |
| variable.
 | |
| 
 | |
| QemuUserTest
 | |
| ^^^^^^^^^^^^
 | |
| 
 | |
| The QemuUserTest class can be used for running an executable via the
 | |
| usermode emulation binaries.
 | |
| 
 | |
| QemuSystemTest
 | |
| ^^^^^^^^^^^^^^
 | |
| 
 | |
| The QemuSystemTest class can be used for running tests via one of the
 | |
| qemu-system-* binaries.
 | |
| 
 | |
| vm
 | |
| ""
 | |
| 
 | |
| A QEMUMachine instance, initially configured according to the given
 | |
| ``qemu_bin`` parameter.
 | |
| 
 | |
| cpu
 | |
| """
 | |
| 
 | |
| The cpu model that will be set to all QEMUMachine instances created
 | |
| by the test.
 | |
| 
 | |
| machine
 | |
| """""""
 | |
| 
 | |
| The machine type that will be set to all QEMUMachine instances created
 | |
| by the test. By using the set_machine() function of the QemuSystemTest
 | |
| class to set this attribute, you can automatically check whether the
 | |
| machine is available to skip the test in case it is not built into the
 | |
| QEMU binary.
 | |
| 
 | |
| Asset handling
 | |
| --------------
 | |
| 
 | |
| Many functional tests download assets (e.g. Linux kernels, initrds,
 | |
| firmware images, etc.) from the internet to be able to run tests with
 | |
| them. This imposes additional challenges to the test framework.
 | |
| 
 | |
| First there is the the problem that some people might not have an
 | |
| unconstrained internet connection, so such tests should not be run by
 | |
| default when running ``make check``. To accomplish this situation,
 | |
| the tests that download files should only be added to the "thorough"
 | |
| speed mode in the meson.build file, while the "quick" speed mode is
 | |
| fine for functional tests that can be run without downloading files.
 | |
| ``make check`` then only runs the quick functional tests along with
 | |
| the other quick tests from the other test suites. If you choose to
 | |
| run only run ``make check-functional``, the "thorough" tests will be
 | |
| executed, too. And to run all functional tests along with the others,
 | |
| you can use something like::
 | |
| 
 | |
|   make -j$(nproc) check SPEED=thorough
 | |
| 
 | |
| The second problem with downloading files from the internet are time
 | |
| constraints. The time for downloading files should not be taken into
 | |
| account when the test is running and the timeout of the test is ticking
 | |
| (since downloading can be very slow, depending on the network bandwidth).
 | |
| This problem is solved by downloading the assets ahead of time, before
 | |
| the tests are run. This pre-caching is done with the qemu_test.Asset
 | |
| class. To use it in your test, declare an asset in your test class with
 | |
| its URL and SHA256 checksum like this::
 | |
| 
 | |
|     ASSET_somename = (
 | |
|         ('https://www.qemu.org/assets/images/qemu_head_200.png'),
 | |
|         '34b74cad46ea28a2966c1d04e102510daf1fd73e6582b6b74523940d5da029dd')
 | |
| 
 | |
| In your test function, you can then get the file name of the cached
 | |
| asset like this::
 | |
| 
 | |
|     def test_function(self):
 | |
|         file_path = self.ASSET_somename.fetch()
 | |
| 
 | |
| The pre-caching will be done automatically when running
 | |
| ``make check-functional`` (but not when running e.g.
 | |
| ``make check-functional-<target>``). In case you just want to download
 | |
| the assets without running the tests, you can do so by running::
 | |
| 
 | |
|     make precache-functional
 | |
| 
 | |
| The cache is populated in the ``~/.cache/qemu/download`` directory by
 | |
| default, but the location can be changed by setting the
 | |
| ``QEMU_TEST_CACHE_DIR`` environment variable.
 | |
| 
 | |
| Skipping tests
 | |
| --------------
 | |
| 
 | |
| Since the test framework is based on the common Python unittest framework,
 | |
| you can use the usual Python decorators which allow for easily skipping
 | |
| tests running under certain conditions, for example, on the lack of a binary
 | |
| on the test system or when the running environment is a CI system. For further
 | |
| information about those decorators, please refer to:
 | |
| 
 | |
|   https://docs.python.org/3/library/unittest.html#skipping-tests-and-expected-failures
 | |
| 
 | |
| While the conditions for skipping tests are often specifics of each one, there
 | |
| are recurring scenarios identified by the QEMU developers and the use of
 | |
| environment variables became a kind of standard way to enable/disable tests.
 | |
| 
 | |
| Here is a list of the most used variables:
 | |
| 
 | |
| QEMU_TEST_ALLOW_LARGE_STORAGE
 | |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | |
| Tests which are going to fetch or produce assets considered *large* are not
 | |
| going to run unless that ``QEMU_TEST_ALLOW_LARGE_STORAGE=1`` is exported on
 | |
| the environment.
 | |
| 
 | |
| The definition of *large* is a bit arbitrary here, but it usually means an
 | |
| asset which occupies at least 1GB of size on disk when uncompressed.
 | |
| 
 | |
| QEMU_TEST_ALLOW_UNTRUSTED_CODE
 | |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | |
| There are tests which will boot a kernel image or firmware that can be
 | |
| considered not safe to run on the developer's workstation, thus they are
 | |
| skipped by default. The definition of *not safe* is also arbitrary but
 | |
| usually it means a blob which either its source or build process aren't
 | |
| public available.
 | |
| 
 | |
| You should export ``QEMU_TEST_ALLOW_UNTRUSTED_CODE=1`` on the environment in
 | |
| order to allow tests which make use of those kind of assets.
 | |
| 
 | |
| QEMU_TEST_FLAKY_TESTS
 | |
| ^^^^^^^^^^^^^^^^^^^^^
 | |
| Some tests are not working reliably and thus are disabled by default.
 | |
| This includes tests that don't run reliably on GitLab's CI which
 | |
| usually expose real issues that are rarely seen on developer machines
 | |
| due to the constraints of the CI environment. If you encounter a
 | |
| similar situation then raise a bug and then mark the test as shown on
 | |
| the code snippet below:
 | |
| 
 | |
| .. code::
 | |
| 
 | |
|   # See https://gitlab.com/qemu-project/qemu/-/issues/nnnn
 | |
|   @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
 | |
|   def test(self):
 | |
|       do_something()
 | |
| 
 | |
| Tests should not live in this state forever and should either be fixed
 | |
| or eventually removed.
 | |
| 
 | |
| 
 | |
| .. _unittest: https://docs.python.org/3/library/unittest.html
 |