| .. _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 |