| .. _qgraph: |
| |
| Qtest Driver Framework |
| ====================== |
| |
| In order to test a specific driver, plain libqos tests need to |
| take care of booting QEMU with the right machine and devices. |
| This makes each test "hardcoded" for a specific configuration, reducing |
| the possible coverage that it can reach. |
| |
| For example, the sdhci device is supported on both x86_64 and ARM boards, |
| therefore a generic sdhci test should test all machines and drivers that |
| support that device. |
| Using only libqos APIs, the test has to manually take care of |
| covering all the setups, and build the correct command line. |
| |
| This also introduces backward compatibility issues: if a device/driver command |
| line name is changed, all tests that use that will not work |
| properly anymore and need to be adjusted. |
| |
| The aim of qgraph is to create a graph of drivers, machines and tests such that |
| a test aimed to a certain driver does not have to care of |
| booting the right QEMU machine, pick the right device, build the command line |
| and so on. Instead, it only defines what type of device it is testing |
| (interface in qgraph terms) and the framework takes care of |
| covering all supported types of devices and machine architectures. |
| |
| Following the above example, an interface would be ``sdhci``, |
| so the sdhci-test should only care of linking its qgraph node with |
| that interface. In this way, if the command line of a sdhci driver |
| is changed, only the respective qgraph driver node has to be adjusted. |
| |
| QGraph concepts |
| --------------- |
| |
| The graph is composed by nodes that represent machines, drivers, tests |
| and edges that define the relationships between them (``CONSUMES``, ``PRODUCES``, and |
| ``CONTAINS``). |
| |
| Nodes |
| ~~~~~ |
| |
| A node can be of four types: |
| |
| - **QNODE_MACHINE**: for example ``arm/raspi2b`` |
| - **QNODE_DRIVER**: for example ``generic-sdhci`` |
| - **QNODE_INTERFACE**: for example ``sdhci`` (interface for all ``-sdhci`` |
| drivers). |
| An interface is not explicitly created, it will be automatically |
| instantiated when a node consumes or produces it. |
| An interface is simply a struct that abstracts the various drivers |
| for the same type of device, and offers an API to the nodes that |
| use it ("consume" relation in qgraph terms) that is implemented/backed up by the drivers that implement it ("produce" relation in qgraph terms). |
| - **QNODE_TEST**: for example ``sdhci-test``. A test consumes an interface |
| and tests the functions provided by it. |
| |
| Notes for the nodes: |
| |
| - QNODE_MACHINE: each machine struct must have a ``QGuestAllocator`` and |
| implement ``get_driver()`` to return the allocator mapped to the interface |
| "memory". The function can also return ``NULL`` if the allocator |
| is not set. |
| - QNODE_DRIVER: driver names must be unique, and machines and nodes |
| planned to be "consumed" by other nodes must match QEMU |
| drivers name, otherwise they won't be discovered |
| |
| Edges |
| ~~~~~ |
| |
| An edge relation between two nodes (drivers or machines) ``X`` and ``Y`` can be: |
| |
| - ``X CONSUMES Y``: ``Y`` can be plugged into ``X`` |
| - ``X PRODUCES Y``: ``X`` provides the interface ``Y`` |
| - ``X CONTAINS Y``: ``Y`` is part of ``X`` component |
| |
| Execution steps |
| ~~~~~~~~~~~~~~~ |
| |
| The basic framework steps are the following: |
| |
| - All nodes and edges are created in their respective |
| machine/driver/test files |
| - The framework starts QEMU and asks for a list of available devices |
| and machines (note that only machines and "consumed" nodes are mapped |
| 1:1 with QEMU devices) |
| - The framework walks the graph starting from the available machines and |
| performs a Depth First Search for tests |
| - Once a test is found, the path is walked again and all drivers are |
| allocated accordingly and the final interface is passed to the test |
| - The test is executed |
| - Unused objects are cleaned and the path discovery is continued |
| |
| Depending on the QEMU binary used, only some drivers/machines will be |
| available and only test that are reached by them will be executed. |
| |
| Command line |
| ~~~~~~~~~~~~ |
| |
| Command line is built by using node names and optional arguments |
| passed by the user when building the edges. |
| |
| There are three types of command line arguments: |
| |
| - ``in node`` : created from the node name. For example, machines will |
| have ``-M <machine>`` to its command line, while devices |
| ``-device <device>``. It is automatically done by the framework. |
| - ``after node`` : added as additional argument to the node name. |
| This argument is added optionally when creating edges, |
| by setting the parameter ``after_cmd_line`` and |
| ``extra_edge_opts`` in ``QOSGraphEdgeOptions``. |
| The framework automatically adds |
| a comma before ``extra_edge_opts``, |
| because it is going to add attributes |
| after the destination node pointed by |
| the edge containing these options, and automatically |
| adds a space before ``after_cmd_line``, because it |
| adds an additional device, not an attribute. |
| - ``before node`` : added as additional argument to the node name. |
| This argument is added optionally when creating edges, |
| by setting the parameter ``before_cmd_line`` in |
| ``QOSGraphEdgeOptions``. This attribute |
| is going to add attributes before the destination node |
| pointed by the edge containing these options. It is |
| helpful to commands that are not node-representable, |
| such as ``-fdsev`` or ``-netdev``. |
| |
| While adding command line in edges is always used, not all nodes names are |
| used in every path walk: this is because the contained or produced ones |
| are already added by QEMU, so only nodes that "consumes" will be used to |
| build the command line. Also, nodes that will have ``{ "abstract" : true }`` |
| as QMP attribute will loose their command line, since they are not proper |
| devices to be added in QEMU. |
| |
| Example:: |
| |
| QOSGraphEdgeOptions opts = { |
| .before_cmd_line = "-drive id=drv0,if=none,file=null-co://," |
| "file.read-zeroes=on,format=raw", |
| .after_cmd_line = "-device scsi-hd,bus=vs0.0,drive=drv0", |
| |
| opts.extra_device_opts = "id=vs0"; |
| }; |
| |
| qos_node_create_driver("virtio-scsi-device", |
| virtio_scsi_device_create); |
| qos_node_consumes("virtio-scsi-device", "virtio-bus", &opts); |
| |
| Will produce the following command line: |
| ``-drive id=drv0,if=none,file=null-co://, -device virtio-scsi-device,id=vs0 -device scsi-hd,bus=vs0.0,drive=drv0`` |
| |
| Troubleshooting unavailable tests |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| |
| If there is no path from an available machine to a test then that test will be |
| unavailable and won't execute. This can happen if a test or driver did not set |
| up its qgraph node correctly. It can also happen if the necessary machine type |
| or device is missing from the QEMU binary because it was compiled out or |
| otherwise. |
| |
| It is possible to troubleshoot unavailable tests by running:: |
| |
| $ QTEST_QEMU_BINARY=build/qemu-system-x86_64 build/tests/qtest/qos-test --verbose |
| # ALL QGRAPH EDGES: { |
| # src='virtio-net' |
| # |-> dest='virtio-net-tests/vhost-user/multiqueue' type=2 (node=0x559142109e30) |
| # |-> dest='virtio-net-tests/vhost-user/migrate' type=2 (node=0x559142109d00) |
| # src='virtio-net-pci' |
| # |-> dest='virtio-net' type=1 (node=0x55914210d740) |
| # src='pci-bus' |
| # |-> dest='virtio-net-pci' type=2 (node=0x55914210d880) |
| # src='pci-bus-pc' |
| # |-> dest='pci-bus' type=1 (node=0x559142103f40) |
| # src='i440FX-pcihost' |
| # |-> dest='pci-bus-pc' type=0 (node=0x55914210ac70) |
| # src='x86_64/pc' |
| # |-> dest='i440FX-pcihost' type=0 (node=0x5591421117f0) |
| # src='' |
| # |-> dest='x86_64/pc' type=0 (node=0x559142111600) |
| # |-> dest='arm/raspi2b' type=0 (node=0x559142110740) |
| ... |
| # } |
| # ALL QGRAPH NODES: { |
| # name='virtio-net-tests/announce-self' type=3 cmd_line='(null)' [available] |
| # name='arm/raspi2b' type=0 cmd_line='-M raspi2b ' [UNAVAILABLE] |
| ... |
| # } |
| |
| The ``virtio-net-tests/announce-self`` test is listed as "available" in the |
| "ALL QGRAPH NODES" output. This means the test will execute. We can follow the |
| qgraph path in the "ALL QGRAPH EDGES" output as follows: '' -> 'x86_64/pc' -> |
| 'i440FX-pcihost' -> 'pci-bus-pc' -> 'pci-bus' -> 'virtio-net-pci' -> |
| 'virtio-net'. The root of the qgraph is '' and the depth first search begins |
| there. |
| |
| The ``arm/raspi2b`` machine node is listed as "UNAVAILABLE". Although it is |
| reachable from the root via '' -> 'arm/raspi2b' the node is unavailable because |
| the QEMU binary did not list it when queried by the framework. This is expected |
| because we used the ``qemu-system-x86_64`` binary which does not support ARM |
| machine types. |
| |
| If a test is unexpectedly listed as "UNAVAILABLE", first check that the "ALL |
| QGRAPH EDGES" output reports edge connectivity from the root ('') to the test. |
| If there is no connectivity then the qgraph nodes were not set up correctly and |
| the driver or test code is incorrect. If there is connectivity, check the |
| availability of each node in the path in the "ALL QGRAPH NODES" output. The |
| first unavailable node in the path is the reason why the test is unavailable. |
| Typically this is because the QEMU binary lacks support for the necessary |
| machine type or device. |
| |
| Creating a new driver and its interface |
| --------------------------------------- |
| |
| Here we continue the ``sdhci`` use case, with the following scenario: |
| |
| - ``sdhci-test`` aims to test the ``read[q,w], writeq`` functions |
| offered by the ``sdhci`` drivers. |
| - The current ``sdhci`` device is supported by both ``x86_64/pc`` and ``ARM`` |
| (in this example we focus on the ``arm-raspi2b``) machines. |
| - QEMU offers 2 types of drivers: ``QSDHCI_MemoryMapped`` for ``ARM`` and |
| ``QSDHCI_PCI`` for ``x86_64/pc``. Both implement the |
| ``read[q,w], writeq`` functions. |
| |
| In order to implement such scenario in qgraph, the test developer needs to: |
| |
| - Create the ``x86_64/pc`` machine node. This machine uses the |
| ``pci-bus`` architecture so it ``contains`` a PCI driver, |
| ``pci-bus-pc``. The actual path is |
| |
| ``x86_64/pc --contains--> 1440FX-pcihost --contains--> |
| pci-bus-pc --produces--> pci-bus``. |
| |
| For the sake of this example, |
| we do not focus on the PCI interface implementation. |
| - Create the ``sdhci-pci`` driver node, representing ``QSDHCI_PCI``. |
| The driver uses the PCI bus (and its API), |
| so it must ``consume`` the ``pci-bus`` generic interface (which abstracts |
| all the pci drivers available) |
| |
| ``sdhci-pci --consumes--> pci-bus`` |
| - Create an ``arm/raspi2b`` machine node. This machine ``contains`` |
| a ``generic-sdhci`` memory mapped ``sdhci`` driver node, representing |
| ``QSDHCI_MemoryMapped``. |
| |
| ``arm/raspi2b --contains--> generic-sdhci`` |
| - Create the ``sdhci`` interface node. This interface offers the |
| functions that are shared by all ``sdhci`` devices. |
| The interface is produced by ``sdhci-pci`` and ``generic-sdhci``, |
| the available architecture-specific drivers. |
| |
| ``sdhci-pci --produces--> sdhci`` |
| |
| ``generic-sdhci --produces--> sdhci`` |
| - Create the ``sdhci-test`` test node. The test ``consumes`` the |
| ``sdhci`` interface, using its API. It doesn't need to look at |
| the supported machines or drivers. |
| |
| ``sdhci-test --consumes--> sdhci`` |
| |
| ``arm-raspi2b`` machine, simplified from |
| ``tests/qtest/libqos/arm-raspi2-machine.c``:: |
| |
| #include "qgraph.h" |
| |
| struct QRaspi2Machine { |
| QOSGraphObject obj; |
| QGuestAllocator alloc; |
| QSDHCI_MemoryMapped sdhci; |
| }; |
| |
| static void *raspi2_get_driver(void *object, const char *interface) |
| { |
| QRaspi2Machine *machine = object; |
| if (!g_strcmp0(interface, "memory")) { |
| return &machine->alloc; |
| } |
| |
| fprintf(stderr, "%s not present in arm/raspi2b\n", interface); |
| g_assert_not_reached(); |
| } |
| |
| static QOSGraphObject *raspi2_get_device(void *obj, |
| const char *device) |
| { |
| QRaspi2Machine *machine = obj; |
| if (!g_strcmp0(device, "generic-sdhci")) { |
| return &machine->sdhci.obj; |
| } |
| |
| fprintf(stderr, "%s not present in arm/raspi2b\n", device); |
| g_assert_not_reached(); |
| } |
| |
| static void *qos_create_machine_arm_raspi2(QTestState *qts) |
| { |
| QRaspi2Machine *machine = g_new0(QRaspi2Machine, 1); |
| |
| alloc_init(&machine->alloc, ...); |
| |
| /* Get node(s) contained inside (CONTAINS) */ |
| machine->obj.get_device = raspi2_get_device; |
| |
| /* Get node(s) produced (PRODUCES) */ |
| machine->obj.get_driver = raspi2_get_driver; |
| |
| /* free the object */ |
| machine->obj.destructor = raspi2_destructor; |
| qos_init_sdhci_mm(&machine->sdhci, ...); |
| return &machine->obj; |
| } |
| |
| static void raspi2_register_nodes(void) |
| { |
| /* arm/raspi2b --contains--> generic-sdhci */ |
| qos_node_create_machine("arm/raspi2b", |
| qos_create_machine_arm_raspi2); |
| qos_node_contains("arm/raspi2b", "generic-sdhci", NULL); |
| } |
| |
| libqos_init(raspi2_register_nodes); |
| |
| ``x86_64/pc`` machine, simplified from |
| ``tests/qtest/libqos/x86_64_pc-machine.c``:: |
| |
| #include "qgraph.h" |
| |
| struct i440FX_pcihost { |
| QOSGraphObject obj; |
| QPCIBusPC pci; |
| }; |
| |
| struct QX86PCMachine { |
| QOSGraphObject obj; |
| QGuestAllocator alloc; |
| i440FX_pcihost bridge; |
| }; |
| |
| /* i440FX_pcihost */ |
| |
| static QOSGraphObject *i440FX_host_get_device(void *obj, |
| const char *device) |
| { |
| i440FX_pcihost *host = obj; |
| if (!g_strcmp0(device, "pci-bus-pc")) { |
| return &host->pci.obj; |
| } |
| fprintf(stderr, "%s not present in i440FX-pcihost\n", device); |
| g_assert_not_reached(); |
| } |
| |
| /* x86_64/pc machine */ |
| |
| static void *pc_get_driver(void *object, const char *interface) |
| { |
| QX86PCMachine *machine = object; |
| if (!g_strcmp0(interface, "memory")) { |
| return &machine->alloc; |
| } |
| |
| fprintf(stderr, "%s not present in x86_64/pc\n", interface); |
| g_assert_not_reached(); |
| } |
| |
| static QOSGraphObject *pc_get_device(void *obj, const char *device) |
| { |
| QX86PCMachine *machine = obj; |
| if (!g_strcmp0(device, "i440FX-pcihost")) { |
| return &machine->bridge.obj; |
| } |
| |
| fprintf(stderr, "%s not present in x86_64/pc\n", device); |
| g_assert_not_reached(); |
| } |
| |
| static void *qos_create_machine_pc(QTestState *qts) |
| { |
| QX86PCMachine *machine = g_new0(QX86PCMachine, 1); |
| |
| /* Get node(s) contained inside (CONTAINS) */ |
| machine->obj.get_device = pc_get_device; |
| |
| /* Get node(s) produced (PRODUCES) */ |
| machine->obj.get_driver = pc_get_driver; |
| |
| /* free the object */ |
| machine->obj.destructor = pc_destructor; |
| pc_alloc_init(&machine->alloc, qts, ALLOC_NO_FLAGS); |
| |
| /* Get node(s) contained inside (CONTAINS) */ |
| machine->bridge.obj.get_device = i440FX_host_get_device; |
| |
| return &machine->obj; |
| } |
| |
| static void pc_machine_register_nodes(void) |
| { |
| /* x86_64/pc --contains--> 1440FX-pcihost --contains--> |
| * pci-bus-pc [--produces--> pci-bus (in pci.h)] */ |
| qos_node_create_machine("x86_64/pc", qos_create_machine_pc); |
| qos_node_contains("x86_64/pc", "i440FX-pcihost", NULL); |
| |
| /* contained drivers don't need a constructor, |
| * they will be init by the parent */ |
| qos_node_create_driver("i440FX-pcihost", NULL); |
| qos_node_contains("i440FX-pcihost", "pci-bus-pc", NULL); |
| } |
| |
| libqos_init(pc_machine_register_nodes); |
| |
| ``sdhci`` taken from ``tests/qtest/libqos/sdhci.c``:: |
| |
| /* Interface node, offers the sdhci API */ |
| struct QSDHCI { |
| uint16_t (*readw)(QSDHCI *s, uint32_t reg); |
| uint64_t (*readq)(QSDHCI *s, uint32_t reg); |
| void (*writeq)(QSDHCI *s, uint32_t reg, uint64_t val); |
| /* other fields */ |
| }; |
| |
| /* Memory Mapped implementation of QSDHCI */ |
| struct QSDHCI_MemoryMapped { |
| QOSGraphObject obj; |
| QSDHCI sdhci; |
| /* other driver-specific fields */ |
| }; |
| |
| /* PCI implementation of QSDHCI */ |
| struct QSDHCI_PCI { |
| QOSGraphObject obj; |
| QSDHCI sdhci; |
| /* other driver-specific fields */ |
| }; |
| |
| /* Memory mapped implementation of QSDHCI */ |
| |
| static void *sdhci_mm_get_driver(void *obj, const char *interface) |
| { |
| QSDHCI_MemoryMapped *smm = obj; |
| if (!g_strcmp0(interface, "sdhci")) { |
| return &smm->sdhci; |
| } |
| fprintf(stderr, "%s not present in generic-sdhci\n", interface); |
| g_assert_not_reached(); |
| } |
| |
| void qos_init_sdhci_mm(QSDHCI_MemoryMapped *sdhci, QTestState *qts, |
| uint32_t addr, QSDHCIProperties *common) |
| { |
| /* Get node contained inside (CONTAINS) */ |
| sdhci->obj.get_driver = sdhci_mm_get_driver; |
| |
| /* SDHCI interface API */ |
| sdhci->sdhci.readw = sdhci_mm_readw; |
| sdhci->sdhci.readq = sdhci_mm_readq; |
| sdhci->sdhci.writeq = sdhci_mm_writeq; |
| sdhci->qts = qts; |
| } |
| |
| /* PCI implementation of QSDHCI */ |
| |
| static void *sdhci_pci_get_driver(void *object, |
| const char *interface) |
| { |
| QSDHCI_PCI *spci = object; |
| if (!g_strcmp0(interface, "sdhci")) { |
| return &spci->sdhci; |
| } |
| |
| fprintf(stderr, "%s not present in sdhci-pci\n", interface); |
| g_assert_not_reached(); |
| } |
| |
| static void *sdhci_pci_create(void *pci_bus, |
| QGuestAllocator *alloc, |
| void *addr) |
| { |
| QSDHCI_PCI *spci = g_new0(QSDHCI_PCI, 1); |
| QPCIBus *bus = pci_bus; |
| uint64_t barsize; |
| |
| qpci_device_init(&spci->dev, bus, addr); |
| |
| /* SDHCI interface API */ |
| spci->sdhci.readw = sdhci_pci_readw; |
| spci->sdhci.readq = sdhci_pci_readq; |
| spci->sdhci.writeq = sdhci_pci_writeq; |
| |
| /* Get node(s) produced (PRODUCES) */ |
| spci->obj.get_driver = sdhci_pci_get_driver; |
| |
| spci->obj.start_hw = sdhci_pci_start_hw; |
| spci->obj.destructor = sdhci_destructor; |
| return &spci->obj; |
| } |
| |
| static void qsdhci_register_nodes(void) |
| { |
| QOSGraphEdgeOptions opts = { |
| .extra_device_opts = "addr=04.0", |
| }; |
| |
| /* generic-sdhci */ |
| /* generic-sdhci --produces--> sdhci */ |
| qos_node_create_driver("generic-sdhci", NULL); |
| qos_node_produces("generic-sdhci", "sdhci"); |
| |
| /* sdhci-pci */ |
| /* sdhci-pci --produces--> sdhci |
| * sdhci-pci --consumes--> pci-bus */ |
| qos_node_create_driver("sdhci-pci", sdhci_pci_create); |
| qos_node_produces("sdhci-pci", "sdhci"); |
| qos_node_consumes("sdhci-pci", "pci-bus", &opts); |
| } |
| |
| libqos_init(qsdhci_register_nodes); |
| |
| In the above example, all possible types of relations are created:: |
| |
| x86_64/pc --contains--> 1440FX-pcihost --contains--> pci-bus-pc |
| | |
| sdhci-pci --consumes--> pci-bus <--produces--+ |
| | |
| +--produces--+ |
| | |
| v |
| sdhci |
| ^ |
| | |
| +--produces-- + |
| | |
| arm/raspi2b --contains--> generic-sdhci |
| |
| or inverting the consumes edge in consumed_by:: |
| |
| x86_64/pc --contains--> 1440FX-pcihost --contains--> pci-bus-pc |
| | |
| sdhci-pci <--consumed by-- pci-bus <--produces--+ |
| | |
| +--produces--+ |
| | |
| v |
| sdhci |
| ^ |
| | |
| +--produces-- + |
| | |
| arm/raspi2b --contains--> generic-sdhci |
| |
| Adding a new test |
| ----------------- |
| |
| Given the above setup, adding a new test is very simple. |
| ``sdhci-test``, taken from ``tests/qtest/sdhci-test.c``:: |
| |
| static void check_capab_sdma(QSDHCI *s, bool supported) |
| { |
| uint64_t capab, capab_sdma; |
| |
| capab = s->readq(s, SDHC_CAPAB); |
| capab_sdma = FIELD_EX64(capab, SDHC_CAPAB, SDMA); |
| g_assert_cmpuint(capab_sdma, ==, supported); |
| } |
| |
| static void test_registers(void *obj, void *data, |
| QGuestAllocator *alloc) |
| { |
| QSDHCI *s = obj; |
| |
| /* example test */ |
| check_capab_sdma(s, s->props.capab.sdma); |
| } |
| |
| static void register_sdhci_test(void) |
| { |
| /* sdhci-test --consumes--> sdhci */ |
| qos_add_test("registers", "sdhci", test_registers, NULL); |
| } |
| |
| libqos_init(register_sdhci_test); |
| |
| Here a new test is created, consuming ``sdhci`` interface node |
| and creating a valid path from both machines to a test. |
| Final graph will be like this:: |
| |
| x86_64/pc --contains--> 1440FX-pcihost --contains--> pci-bus-pc |
| | |
| sdhci-pci --consumes--> pci-bus <--produces--+ |
| | |
| +--produces--+ |
| | |
| v |
| sdhci <--consumes-- sdhci-test |
| ^ |
| | |
| +--produces-- + |
| | |
| arm/raspi2b --contains--> generic-sdhci |
| |
| or inverting the consumes edge in consumed_by:: |
| |
| x86_64/pc --contains--> 1440FX-pcihost --contains--> pci-bus-pc |
| | |
| sdhci-pci <--consumed by-- pci-bus <--produces--+ |
| | |
| +--produces--+ |
| | |
| v |
| sdhci --consumed by--> sdhci-test |
| ^ |
| | |
| +--produces-- + |
| | |
| arm/raspi2b --contains--> generic-sdhci |
| |
| Assuming there the binary is |
| ``QTEST_QEMU_BINARY=./qemu-system-x86_64`` |
| a valid test path will be: |
| ``/x86_64/pc/1440FX-pcihost/pci-bus-pc/pci-bus/sdhci-pc/sdhci/sdhci-test`` |
| |
| and for the binary ``QTEST_QEMU_BINARY=./qemu-system-arm``: |
| |
| ``/arm/raspi2b/generic-sdhci/sdhci/sdhci-test`` |
| |
| Additional examples are also in ``test-qgraph.c`` |
| |
| Qgraph API reference |
| -------------------- |
| |
| .. kernel-doc:: tests/qtest/libqos/qgraph.h |