From nobody Thu Sep 3 07:05:38 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) client-ip=192.237.175.120; envelope-from=xen-devel-bounces@lists.xenproject.org; helo=lists.xenproject.org; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass(p=none dis=none) header.from=vates.tech ARC-Seal: i=1; a=rsa-sha256; t=1787823818; cv=none; d=zohomail.com; s=zohoarc; b=e+Cr2/xgd5H7udE2RcK2k7YbVaBFSdUb0GEDFda+9WPuod5EfwwQEeQ6YS6l7KkcdVhKn6NXYdpTmg3uFeq3EdGZRxiGUm3OYiB7XSgJRgpBEAQUO8d88h7RAMDSZAeJYYkaBCXLjKWPmmXIMWG0WvmdfTx8BE/nquiylQEs4lY= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1787823818; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=+fIlO9mf1bNb6w2Vy7RTkh4SsFCS7tMV++tpqFsgJCE=; b=cqEQroKjkTpIx+CvZFMTywp7+gjqeggUi/Ue6tF519NrELxEwS9Om+/KOOBxf2IwrxztRbGPwzHkDuzK0d/PS2a2fEByuquvgE/MOn62X4tonxotI7c85gXteUNxdLsdd/lqkNrA75m9FUr1HXbhWWz5IM61CkY8w0WC+M8ft9E= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 1787823818037464.74248229404975; Thu, 27 Aug 2026 02:43:38 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400479.1636132 (Exim 4.92) (envelope-from ) id 1wzWde-0005Tr-QF; Thu, 27 Aug 2026 09:43:14 +0000 Received: by outflank-mailman (output) from mailman id 1400479.1636132; Thu, 27 Aug 2026 09:43:14 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWde-0005Ti-L9; Thu, 27 Aug 2026 09:43:14 +0000 Received: by outflank-mailman (input) for mailman id 1400479; Thu, 27 Aug 2026 09:43:12 +0000 Received: from mx.expurgate.net ([195.190.135.10]) by lists.xenproject.org with esmtp (Exim 4.92) id 1wzWdc-0005TX-ST for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:12 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdc-00B8Qw-94 for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:12 +0200 Received: from [10.42.69.6] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006a5-8faa-0a2a0a5109dd-0a2a4506caa8-20 for ; Thu, 27 Aug 2026 11:43:12 +0200 Received: from [185.255.28.18] (helo=prod-mta-13.swg-srv.net) by tlsNG-16d1c6.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006af-195a-0a2a45060019-b9ff1c12a20f-3 for ; Thu, 27 Aug 2026 11:43:12 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a0db7000c4f3.004 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:07 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id 656DC81230; Thu, 27 Aug 2026 11:43:06 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=+fIlO9mf1bNb6w2Vy7RTkh4SsFCS7tMV++tpqFsgJCE=; h=from:subject:date:message-id:to:cc:mime-version:content-transfer-encoding:in-reply-to:references:feedback-id; b=FkPrl4dpV8RQmqZi2y9uisTYaUlsQ9UyRpJM+Fepfexh4sjknAVUjb86QPX1q1WYy/mDoZudC Z1oqeoT1rIG+gVszk3SnI8NjxBu9lNkTwnJsCe0OUYzszmypD3Pq/PEqEGVcgTtxWYyjXiSJKw0 +YPPLqVDi0NuMyyNL1yMP+e+JoaH5/WBWJj0N34GaxiIAoHmvN4elod4UkQv3sjeTT/VkOeoSXq qpLmgPotZlPnaYtFyDJ+vyS7b+Ejgm0CjOkofuxLgzuPxaZ8cZdt1k5xegybT/46M54hFW1Hm3k iD1GiKgQq4FuyCvaEdQ2yD/wx1r43SZ3t/fgLNTXBimA== X-Zone-Loop: ea63cb4949b7c6921f821a088449c34448928466fe93 x-campaign-type: default x-transaction-id: 049912a5-1f7d-4876-aba9-f756917953f3 x-swg-uid: 01-3f407e4b-4268-461f-97cb-edbe9143ad2a X-Mailer: Sweego Message-ID: <1787823787.8631fc262581453bbf619ec5b2062170.1a0429a0db7000c4f3@vates.tech> x-swg-bid: 1787823787.8631fc262581453bbf619ec5b2062170.1a0429a0db7000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Andrew Cooper , Anthony PERARD , Michal Orzel , Jan Beulich , Julien Grall , =?UTF-8?q?Roger=20Pau=20Monn=C3=A9?= , Stefano Stabellini Subject: [PATCH v2 1/6] Add a QTB container to run the Xen riscv64 tests Date: Thu, 27 Aug 2026 11:42:47 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823786578 X-purgate-ID: tlsNG-16d1c6/1787823792-FD80D77B-67A9A57B/10/63158204843 X-purgate-type: spam X-purgate-size: 2965 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823819726158500 Content-Type: text/plain; charset="utf-8" QTB (QEMU Test Bench) is a Python framework, developed as part of AMD's Xen safety initiative, that drives a live QEMU instance over qtest and QMP. It gives a test full access to the machine while the emulation runs: read and write the consoles, inspect the registers and the memory, inject interrupts. For now the riscv64 CI tests only use it to read the Xen console in the smoke test, since DomU is not supported yet. Once it is, the same framework will read and write the DomU consoles and inject IRQs, so the tests can assert an interrupt reaches the expected guest and vCPU. QTB is not upstream in QEMU yet, though it is planned to be, so get the framework from AMD's fork gitlab.com/xen-project/people/amd/qemu and pip-installs qemu.qtb from it. Signed-off-by: Baptiste Le Duc --- containerize | 1 + images/debian/13-qtb-riscv64.dockerfile | 45 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 images/debian/13-qtb-riscv64.dockerfile diff --git a/containerize b/containerize index dad9afa..477b670 100755 --- a/containerize +++ b/containerize @@ -31,6 +31,7 @@ case "_${CONTAINER}" in _alpine-3.24-arm64-base) CONTAINER=3D"${BASE}/alpine:3.24-arm64-base" = ;; _alpine-3.24-arm64-build) CONTAINER=3D"${BASE}/alpine:3.24-arm64-build= " ;; _alpine-3.24-x86_64-base) CONTAINER=3D"${BASE}/alpine:3.24-x86_64-base= " ;; + _debian-13-qtb-riscv64) CONTAINER=3D"${BASE}/debian:13-qtb-riscv64" ;; _alpine-3.24-x86_64-build|_) CONTAINER=3D"${BASE}/alpine:3.24-x86_64-b= uild" ;; esac =20 diff --git a/images/debian/13-qtb-riscv64.dockerfile b/images/debian/13-qtb= -riscv64.dockerfile new file mode 100644 index 0000000..86215d3 --- /dev/null +++ b/images/debian/13-qtb-riscv64.dockerfile @@ -0,0 +1,45 @@ +# syntax=3Ddocker/dockerfile:1 +FROM --platform=3Dlinux/amd64 debian:trixie-slim +LABEL maintainer.name=3D"The Xen Project" +LABEL maintainer.email=3D"xen-devel@lists.xenproject.org" + +ENV DEBIAN_FRONTEND=3Dnoninteractive + +ARG QEMU_REPO=3Dhttps://gitlab.com/xen-project/people/amd/qemu.git +ARG QEMU_COMMIT=3D24cd7e5c0d7e4db8e651f091d0b9da2e7c58dbab + +RUN < (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 1787823809974181.53966169270188; Thu, 27 Aug 2026 02:43:29 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400480.1636141 (Exim 4.92) (envelope-from ) id 1wzWdf-0005gE-Vj; Thu, 27 Aug 2026 09:43:15 +0000 Received: by outflank-mailman (output) from mailman id 1400480.1636141; Thu, 27 Aug 2026 09:43:15 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdf-0005g7-Rb; Thu, 27 Aug 2026 09:43:15 +0000 Received: by outflank-mailman (input) for mailman id 1400480; Thu, 27 Aug 2026 09:43:14 +0000 Received: from mx.expurgate.net ([195.190.135.10]) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWde-0005Td-FO for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:14 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdd-00B8Qw-S7 for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:13 +0200 Received: from [10.42.69.6] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006a5-8faa-0a2a0a5109dd-0a2a4506caa8-26 for ; Thu, 27 Aug 2026 11:43:13 +0200 Received: from [185.255.28.34] (helo=prod-mta-13-01.swg-srv.net) by tlsNG-16d1c6.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006b1-195a-0a2a45060019-b9ff1c2295af-3 for ; Thu, 27 Aug 2026 11:43:13 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13-01.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a1050000c4f3.003 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:08 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id 16CE283B2D; Thu, 27 Aug 2026 11:43:07 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=sbEkocC2EuCtg3d1VQNUGSyKIv15JnFJv8WCsyhEZIU=; h=from:subject:date:message-id:to:cc:mime-version:content-transfer-encoding:in-reply-to:references:feedback-id; b=t2DyFejTbgi9masF0k9lA2VrdJpmtdqwdkwebJSSyrbdrv5mT+Lj0o0IAShKPkfp8p/wFXkmT aonTBkB/GS12Q/6nWrjn7Mf9V+S7Pquru+HAHxzXCVcwr+kpsoC2a8NWCr1k/HTqsQo6VIkQHx1 HdGvadcW2gNnnhxTAa0jqSUBCJR//pmmdiiCzSO1WOIF5R94TsBxDoj+tzyxhI1BVsRs5NxGmz9 w37uRsjGxAuc1apmBgpnucy7VVL3yg7Z5QzLNO9cYkz5dwSXAvlFOGzptbdE/nCoX+fOiecT1eY f8zwRnI0WrufqBDpekvUBgVJD6Bjeec5KVxIvSOTJVtA== X-Zone-Loop: 52ebf5fdabd5f7c4a8336b4b7b4a14c75c1fc4a217c0 x-campaign-type: default x-transaction-id: 604569c2-b308-4713-b81c-144e4520ef14 x-swg-uid: 01-17b6331a-b3e4-4096-9bcc-a833dfc66e74 X-Mailer: Sweego Message-ID: <1787823788.8631fc262581453bbf619ec5b2062170.1a0429a1050000c4f3@vates.tech> x-swg-bid: 1787823788.8631fc262581453bbf619ec5b2062170.1a0429a1050000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Doug Goldstein , Stefano Stabellini Subject: [PATCH v2 2/6] automation/qtb: add jinja2 device trees for riscv64 smoke tests Date: Thu, 27 Aug 2026 11:42:48 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823787186 X-purgate-ID: tlsNG-16d1c6/1787823793-F440377B-4AF80189/0/0 X-purgate-type: clean X-purgate-size: 6917 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823814745158500 Content-Type: text/plain; charset="utf-8" The dom0less RISC-V smoke tests need a host device tree describing the platform (CPUs, APLIC/IMSIC, uart). It varies per machine (hart count, MMU type), so a single static .dts cannot cover the test matrix. Add dts/qemu-host.dts.j2, a template of the QEMU virt platform in aia=3Daplic-imsic mode: per-hart cpu/cpu-intc nodes, the M- and S-mode APLIC and IMSIC pairs, CLINT and the ns16550a uart. It takes ncpus, mmu_type and xen_bootargs as arguments. Values QEMU hardcodes are set as named constants matching their source symbols (QEMU_UART0_IRQ, QEMU_IRQCHIP_NUM_SOURCES, ...) rather than open-coded, so a QEMU-side change is easy to trace. The template is inert on its own: the generated dtb will be used in next patch. Assisted-by: Claude:claude-opus-5 Signed-off-by: Baptiste Le Duc --- .../scripts/qtb/riscv/dts/qemu-host.dts.j2 | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 automation/scripts/qtb/riscv/dts/qemu-host.dts.j2 diff --git a/automation/scripts/qtb/riscv/dts/qemu-host.dts.j2 b/automation= /scripts/qtb/riscv/dts/qemu-host.dts.j2 new file mode 100644 index 0000000000..13a8e983ce --- /dev/null +++ b/automation/scripts/qtb/riscv/dts/qemu-host.dts.j2 @@ -0,0 +1,160 @@ +/dts-v1/; + +{#- + * Jinja2 QEMU "virt" platform device tree for Xen RISC-V tests. + * + * Interrupt controller: APLIC in MSI mode + IMSIC + * (QEMU -M virt,aia=3Daplic-imsic). + * + * Rendered by xen_dt.py. + * + * Variables: + * ncpus - number of physical harts (int, >=3D 1) + * mmu_type - Xen host MMU type, e.g. "sv39" (string) + * xen_bootargs - Xen command line (string) + * + * Per-hart nodes are labelled cpu / cpu_intc and referenced with &l= abel. + * + * No `aia-guests=3DN`, so no VS-mode guest files: IMSIC reg size is + * ncpus * page size. +-#} +{#- Values QEMU hardcodes, need to be described to Xen -#} +{%- set QEMU_TIMEBASE_FREQUENCY =3D 10000000 %} {#- RISCV_ACLINT_DEFAULT= _TIMEBASE_FREQ -#} +{%- set QEMU_IRQCHIP_NUM_SOURCES =3D 96 %} {#- VIRT_IRQCHIP_NUM_SOU= RCES (virt.h) -#} +{%- set QEMU_IRQCHIP_NUM_MSIS =3D 255 %} {#- VIRT_IRQCHIP_NUM_MSI= S -#} +{%- set QEMU_UART_CLOCK_FREQUENCY =3D 3686400 %} {#- create_fdt_uart() -#} +{%- set QEMU_UART0_IRQ =3D 10 %} {#- UART0_IRQ -#} +{%- set QEMU_IMSIC_PAGE_SZ =3D 0x1000 %} {#- IMSIC_MMIO_PAGE_SZ -= #} + +{%- set IRQ_TYPE_LEVEL_HIGH =3D 4 %} +{%- set APLIC_IRQ_CELLS =3D 2 %} + +{%- set IRQ_M_SOFT =3D 3 %} +{%- set IRQ_M_TIMER =3D 7 %} +{%- set IRQ_S_EXT =3D 9 %} +{%- set IRQ_M_EXT =3D 11 %} + +/ { + #address-cells =3D <0x02>; + #size-cells =3D <0x02>; + compatible =3D "riscv-virtio"; + model =3D "riscv-virtio,qemu"; + + memory@80000000 { + device_type =3D "memory"; + reg =3D <0x00 0x80000000 0x00 0x80000000>; + }; + + cpus { + #address-cells =3D <0x01>; + #size-cells =3D <0x00>; + timebase-frequency =3D <{{ QEMU_TIMEBASE_FREQUENCY }}>; +{% for i in range(ncpus) %} + cpu{{ i }}: cpu@{{ i }} { + device_type =3D "cpu"; + reg =3D <0x{{ '%x' % i }}>; + status =3D "okay"; + compatible =3D "riscv"; + riscv,cbop-block-size =3D <0x40>; + riscv,cboz-block-size =3D <0x40>; + riscv,cbom-block-size =3D <0x40>; + riscv,isa =3D "rv64imafdch_zicntr_zicsr_zifencei_zihintpause_z= ihpm_zba_zbb_zbs_smstateen_svpbmt_smaia_ssaia"; + mmu-type =3D "riscv,{{ mmu_type }}"; + + cpu{{ i }}_intc: interrupt-controller@{{ i }} { + #interrupt-cells =3D <0x01>; + interrupt-controller; + compatible =3D "riscv,cpu-intc"; + }; + }; +{% endfor %} + cpu-map { + + cluster0 { +{% for i in range(ncpus) %} + core{{ i }} { + cpu =3D <&cpu{{ i }}>; + }; +{% endfor %} + }; + }; + }; + + soc { + #address-cells =3D <0x02>; + #size-cells =3D <0x02>; + compatible =3D "simple-bus"; + ranges; + + serial@10000000 { + interrupts =3D <{{ QEMU_UART0_IRQ }} {{ IRQ_TYPE_LEVEL_HIGH }}= >; + interrupt-parent =3D <&aplic_s>; + clock-frequency =3D <{{ QEMU_UART_CLOCK_FREQUENCY }}>; + reg =3D <0x00 0x10000000 0x00 0x100>; + compatible =3D "ns16550a"; + }; + + aplic_s: aplic@d000000 { + riscv,num-sources =3D <{{ QEMU_IRQCHIP_NUM_SOURCES }}>; + reg =3D <0x00 0xd000000 0x00 0x8000>; + msi-parent =3D <&imsic_s>; + interrupt-controller; + #interrupt-cells =3D <{{ APLIC_IRQ_CELLS }}>; + compatible =3D "riscv,aplic"; + }; + + aplic@c000000 { + riscv,delegate =3D <&aplic_s 0x01 {{ QEMU_IRQCHIP_NUM_SOURCES = }}>; + riscv,children =3D <&aplic_s>; + riscv,num-sources =3D <{{ QEMU_IRQCHIP_NUM_SOURCES }}>; + reg =3D <0x00 0xc000000 0x00 0x8000>; + msi-parent =3D <&imsic_m>; + interrupt-controller; + #interrupt-cells =3D <{{ APLIC_IRQ_CELLS }}>; + compatible =3D "riscv,aplic"; + }; + + imsic_s: imsics@28000000 { + riscv,num-ids =3D <{{ QEMU_IRQCHIP_NUM_MSIS }}>; + reg =3D <0x00 0x28000000 0x00 0x{{ '%x' % (ncpus * QEMU_IMSIC_= PAGE_SZ) }}>; + interrupts-extended =3D < + {%- for i in range(ncpus) %} + &cpu{{ i }}_intc {{ IRQ_S_EXT }} + {%- endfor %} + >; + msi-controller; + interrupt-controller; + #interrupt-cells =3D <0x00>; + compatible =3D "riscv,imsics"; + }; + + imsic_m: imsics@24000000 { + riscv,num-ids =3D <{{ QEMU_IRQCHIP_NUM_MSIS }}>; + reg =3D <0x00 0x24000000 0x00 0x{{ '%x' % (ncpus * QEMU_IMSIC_= PAGE_SZ) }}>; + interrupts-extended =3D < + {%- for i in range(ncpus) %} + &cpu{{ i }}_intc {{ IRQ_M_EXT }} + {%- endfor %} + >; + msi-controller; + interrupt-controller; + #interrupt-cells =3D <0x00>; + compatible =3D "riscv,imsics"; + }; + + clint@2000000 { + interrupts-extended =3D < + {%- for i in range(ncpus) %} + &cpu{{ i }}_intc {{ IRQ_M_SOFT }} &cpu{{ i }}_intc {{ = IRQ_M_TIMER }} + {%- endfor %} + >; + reg =3D <0x00 0x2000000 0x00 0x10000>; + compatible =3D "sifive,clint0", "riscv,clint0"; + }; + }; + + chosen { + stdout-path =3D "/soc/serial@10000000"; + xen,xen-bootargs =3D "{{ xen_bootargs }}"; + }; +}; From nobody Thu Sep 3 07:05:38 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) client-ip=192.237.175.120; envelope-from=xen-devel-bounces@lists.xenproject.org; helo=lists.xenproject.org; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass(p=none dis=none) header.from=vates.tech ARC-Seal: i=1; a=rsa-sha256; t=1787823822; cv=none; d=zohomail.com; s=zohoarc; b=ALVSh5PdGBzmuvh+4XY+wWnmJPo+kVVOZpBo8p1BvdbN/zRcpXMutaRl9iFz2kFi+VKeB11nzvJnvS/lkbDar+oGvsZdgE7fT0mYE6UIb6N6UY+UGq0D+DuHYsBi5bF2BaMwho0eexS89FT8kLcVsKurnOfEZvw9BJXfEbxhheU= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1787823822; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=gmD+/yUGFmr+Kw4z5rV1wyYCJPmGDuI5iAWOZKM9ipI=; b=AxVi9eC+nTp+Pbj3ldmCeaC44/pVVhJvCJNq0yebDL4d6FoCbOHMEVOq537H88WcVj6gmIVqjSR/zxxc2xjimIOCqT/hL/XPsXjf7p79fdPXqp53HPZTg46rwdnmucXdTARB+ln2Cw/fQoiblIUU5yDN3YuRQxsH6OpcWDDw/ww= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 1787823821720817.2378063905984; Thu, 27 Aug 2026 02:43:41 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400481.1636149 (Exim 4.92) (envelope-from ) id 1wzWdj-0005ul-4w; Thu, 27 Aug 2026 09:43:19 +0000 Received: by outflank-mailman (output) from mailman id 1400481.1636149; Thu, 27 Aug 2026 09:43:19 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdj-0005ue-2C; Thu, 27 Aug 2026 09:43:19 +0000 Received: by outflank-mailman (input) for mailman id 1400481; Thu, 27 Aug 2026 09:43:17 +0000 Received: from mx.expurgate.net ([195.190.135.10]) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdh-0005t3-52 for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:17 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdg-00Fwyk-I1 for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:16 +0200 Received: from [10.42.69.12] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006ab-2eae-0a2a0a5409dd-0a2a450c8740-14 for ; Thu, 27 Aug 2026 11:43:16 +0200 Received: from [185.255.28.35] (helo=prod-mta-13-02.swg-srv.net) by tlsNG-d25034.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006b4-f479-0a2a450c0019-b9ff1c23ac25-3 for ; Thu, 27 Aug 2026 11:43:16 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13-02.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a1248000c4f3.003 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:08 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id C8E9881230; Thu, 27 Aug 2026 11:43:07 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=gmD+/yUGFmr+Kw4z5rV1wyYCJPmGDuI5iAWOZKM9ipI=; h=from:subject:date:message-id:to:cc:mime-version:content-transfer-encoding:in-reply-to:references:feedback-id; b=lF//gPMh4IZ7JGz9ltPnd+NVQwLcrfDWrXXCsze08igi3HH/jOA9CeZ1HYTQXZmrAZTpZkzTd KhPz6gzf01HoqqafjMANLMI5ltC5/UUa1+tAfzuMPRP5ZKgUJM6BrZCJr/WtFHHAHvwC9KwXpVU ue/DWzmsNs8JDfalJd1R94TCOUoqAK5KOUva1olnZ4dsFZUNN2ubXlniLy37aDVVAGtIJhROqa7 zAablxk+AEhvgyUk+RGFi4SvThCaLt9lG0FAPFOZUFu6u1uVrX4T3CdJhpBvTrR+hDRbcRquJoy ZwZcJejbyaw5LBc8zsdb7YgVF7TQfh8MoNu4s72MkPBg== X-Zone-Loop: e0465787c52ba03e2322fc009b167e8a3ec347e8619e x-campaign-type: default x-transaction-id: ca1f5808-1918-4b1d-ab19-ccf96af2fa07 x-swg-uid: 01-ddb26cfa-a869-405e-a862-3831fc4020b1 X-Mailer: Sweego Message-ID: <1787823788.8631fc262581453bbf619ec5b2062170.1a0429a1248000c4f3@vates.tech> x-swg-bid: 1787823788.8631fc262581453bbf619ec5b2062170.1a0429a1248000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Doug Goldstein , Stefano Stabellini Subject: [PATCH v2 3/6] automation/qtb: add Python QTB framework with the console-test type Date: Thu, 27 Aug 2026 11:42:49 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823787917 X-purgate-ID: tlsNG-d25034/1787823796-028DDA5B-019E4CE5/0/0 X-purgate-type: clean X-purgate-size: 30584 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823823662158500 Content-Type: text/plain; charset="utf-8" Port qemu-smoke-riscv64.sh from shell to a Python QTB framework that drives QEMU over qtest/QMP and the console to run riscv64 smoke tests. The framework is based on AMD's QTB (QEMU Test Bench) framework, which is not yet upstream in QEMU but is planned to be soon. This is a riscv64 adaptation of it. The framework: - parses a test config (config.yaml) into typed machine descriptions (config.py). The host device tree of a machine is compiled on first use of MachineConfig.dt, so a run that never boots (`list`, or a config error) does not invoke dtc. - generates the Xen host device tree from a Jinja2 template and compiles it to a DTB with dtc (xen_dt.py, dt.py). - assembles the QEMU command line in RiscvTestMachine (machine.py), resolving artifact paths via paths.py. - defines an abstract RiscvQtbTest base shared by every test type (qtb_test.py). - wires it together behind a CLI: the test type is a leading positional with `list` and `run` subcommands (qemu_smoke_riscv64.py). console-test test type comes with it. It boots a machine from the shared catalog and asserts every expected string is printed on Xen's own console within the timeout. Its tests live in console-test.yaml, which maps console indices to the expected output strings. This makes it straightforward to add support for a domU console index once Xen provides it. Assisted-by: Claude:claude-opus-5 Signed-off-by: Baptiste Le Duc --- automation/scripts/qemu_smoke_riscv64.py | 122 +++++++++++++++ automation/scripts/qtb/__init__.py | 2 + automation/scripts/qtb/riscv/__init__.py | 9 ++ automation/scripts/qtb/riscv/config.py | 119 ++++++++++++++ automation/scripts/qtb/riscv/config.yaml | 17 ++ .../qtb/riscv/console_test/__init__.py | 4 + .../qtb/riscv/console_test/console-test.yaml | 18 +++ .../qtb/riscv/console_test/console_test.py | 145 ++++++++++++++++++ automation/scripts/qtb/riscv/dt.py | 57 +++++++ automation/scripts/qtb/riscv/machine.py | 57 +++++++ automation/scripts/qtb/riscv/paths.py | 60 ++++++++ automation/scripts/qtb/riscv/qtb_test.py | 53 +++++++ automation/scripts/qtb/riscv/xen_dt.py | 58 +++++++ 13 files changed, 721 insertions(+) create mode 100755 automation/scripts/qemu_smoke_riscv64.py create mode 100644 automation/scripts/qtb/__init__.py create mode 100644 automation/scripts/qtb/riscv/__init__.py create mode 100644 automation/scripts/qtb/riscv/config.py create mode 100644 automation/scripts/qtb/riscv/config.yaml create mode 100644 automation/scripts/qtb/riscv/console_test/__init__.py create mode 100644 automation/scripts/qtb/riscv/console_test/console-test.= yaml create mode 100644 automation/scripts/qtb/riscv/console_test/console_test.= py create mode 100644 automation/scripts/qtb/riscv/dt.py create mode 100644 automation/scripts/qtb/riscv/machine.py create mode 100644 automation/scripts/qtb/riscv/paths.py create mode 100644 automation/scripts/qtb/riscv/qtb_test.py create mode 100644 automation/scripts/qtb/riscv/xen_dt.py diff --git a/automation/scripts/qemu_smoke_riscv64.py b/automation/scripts/= qemu_smoke_riscv64.py new file mode 100755 index 0000000000..f338fafcc2 --- /dev/null +++ b/automation/scripts/qemu_smoke_riscv64.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""CLI launcher for the qtb riscv64 dom0less tests. + +The test type comes first (e.g. `console-test`), then a command. Each type +reads its own config file, which ships with the type. + +Commands: + list Print every test the type defines in the config, then exit. + run Boot one test's machine under QEMU and drive it to a pass/fail + verdict. + +Usage: + ./qemu_smoke_riscv64.py console-test list + ./qemu_smoke_riscv64.py console-test run dom0less-1smp-0domu-1vcpu-apl= ic-imsic-null +""" + +from __future__ import annotations + +import argparse +import logging +import sys +from collections.abc import Sequence +from traceback import extract_tb, format_exc + +from qtb.riscv import RiscvQtbTest, TEST_TYPES, RiscvTestMachine, cleanup_= temp_dir + +logger =3D logging.getLogger(__name__) + + +def _run_test(test: RiscvQtbTest, log_dir: str | None) -> int: + """Compile the machine's device trees, boot it, and run the test.""" + vm =3D RiscvTestMachine(test.machine, timeout=3Dtest.timeout, log_dir= =3Dlog_dir) + try: + with vm: + vm.launch() + test.run(vm) + except Exception as exc: + print( + f"FAIL: {test.name}: {type(exc).__name__}: {exc}", + file=3Dsys.stderr, + flush=3DTrue, + ) + return 1 + + print(f"PASS: {test.name}", flush=3DTrue) + return 0 + + +def _cmd_list(ns: argparse.Namespace) -> int: + """` list`: print every test the type defines.""" + for name in ns.cls.list_tests(ns.cls.config_file): + print(name) + return 0 + + +def _cmd_run(ns: argparse.Namespace) -> int: + """` run`: build the named test and drive it to a verdict.""" + test =3D ns.cls.from_config(ns.cls.config_file, ns.test) + return _run_test(test, ns.log_dir) + + +def setup_parser() -> argparse.ArgumentParser: + parser =3D argparse.ArgumentParser( + description=3D"Launch a qtb riscv64 dom0less test: " + "qemu_smoke_riscv64.py .", + ) + common_args =3D argparse.ArgumentParser(add_help=3DFalse) + common_args.add_argument( + "-v", "--verbose", action=3D"store_true", help=3D"Print debug outp= ut" + ) + run_args =3D argparse.ArgumentParser(add_help=3DFalse) + run_args.add_argument("test", help=3D"Name of the test to run.") + run_args.add_argument( + "--log-dir", + default=3DNone, + metavar=3D"DIR", + help=3D"Directory for all logs (QEMU process log, qtest, and the " + "consoles as con.log). When unset, no logs are written.", + ) + + # qemu_smoke_riscv64.py + types =3D parser.add_subparsers(dest=3D"type", required=3DTrue) + for cls in TEST_TYPES: + desc =3D cls.description + cmds =3D types.add_parser( + cls.type_id, help=3Ddesc, description=3Ddesc + ).add_subparsers(dest=3D"command", required=3DTrue) + cmds.add_parser( + "list", + parents=3D[common_args], + description=3Ddesc, + help=3D"List the tests for the type and exit.", + ).set_defaults(func=3D_cmd_list, cls=3Dcls) + cmds.add_parser( + "run", + parents=3D[common_args, run_args], + description=3Ddesc, + help=3D"Run one test.", + ).set_defaults(func=3D_cmd_run, cls=3Dcls) + + return parser + + +def main(argv: Sequence[str] | None =3D None) -> int: + ns =3D setup_parser().parse_args(argv) + logging.basicConfig( + level=3Dlogging.DEBUG if ns.verbose else logging.WARNING, format= =3D"%(message)s" + ) + try: + return ns.func(ns) + except Exception as exc: + logger.debug(format_exc()) + frame =3D extract_tb(exc.__traceback__)[-1] + print(f"{frame.filename}:{frame.lineno}: {exc}") + return 2 + finally: + cleanup_temp_dir() + + +if __name__ =3D=3D "__main__": + sys.exit(main()) diff --git a/automation/scripts/qtb/__init__.py b/automation/scripts/qtb/__= init__.py new file mode 100644 index 0000000000..a0e6e76cb2 --- /dev/null +++ b/automation/scripts/qtb/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""QTB (QEMU Test Bench) test frameworks.""" diff --git a/automation/scripts/qtb/riscv/__init__.py b/automation/scripts/= qtb/riscv/__init__.py new file mode 100644 index 0000000000..6a6c48be32 --- /dev/null +++ b/automation/scripts/qtb/riscv/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""QTB riscv64 test framework package.""" + +from .qtb_test import RiscvQtbTest +from .console_test import ConsoleTest +from .machine import RiscvTestMachine +from .paths import cleanup_temp_dir + +TEST_TYPES: tuple[type[RiscvQtbTest], ...] =3D (ConsoleTest,) diff --git a/automation/scripts/qtb/riscv/config.py b/automation/scripts/qt= b/riscv/config.py new file mode 100644 index 0000000000..b5068d6bfb --- /dev/null +++ b/automation/scripts/qtb/riscv/config.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""YAML config parser for qtb-based Xen riscv64 tests. + +Parsing steps: + - validate the YAML + - build the dataclasses: the host device tree is compiled on first use + of MachineConfig.dt +""" + +from __future__ import annotations + +import functools +import inspect +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path + +import yaml + +from .paths import resolve_binary, resolve_path +from .xen_dt import DeviceTree, build_xen_device_tree + +XEN_MMU_TYPE_DEFAULT: str =3D "sv48" +XEN_BOOTARGS_DEFAULT: str =3D "" + +MACHINE_MEMORY: int =3D 2048 +MACHINE_INTERRUPT_CONTROLLER: str =3D "aplic-imsic" + + +def required_keys(argument_name: str, keys: set, label: str =3D ""): + """Validate the dict passed as `argument_name` has every key in `keys`= .""" + keys =3D set(keys) + + def decorate(func): + sig =3D inspect.signature(func) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + arg =3D sig.bind(*args, **kwargs).arguments[argument_name] + missing =3D keys - arg.keys() + if missing: + raise ValueError( + f"{label or func.__name__} missing keys: {sorted(missi= ng)}" + ) + return func(*args, **kwargs) + + return wrapper + + return decorate + + +@dataclass +class BinariesConfig: + xen: Path + + +@required_keys("raw", {"xen"}, label=3D"binaries config") +def _parse_binaries(raw: dict) -> BinariesConfig: + return BinariesConfig(xen=3Dresolve_binary(raw["xen"])) + + +@required_keys("raw", {"pcpu"}, label=3D"machine config") +def _parse_machine( + raw: dict, + binaries: BinariesConfig, + machine: str, +) -> MachineConfig: + return MachineConfig( + name=3Dmachine, + pcpu=3Draw["pcpu"], + binaries=3Dbinaries, + mmu_type=3Draw.get("mmu_type", XEN_MMU_TYPE_DEFAULT), + xen_bootargs=3Draw.get("xen_bootargs", XEN_BOOTARGS_DEFAULT), + ) + + +@dataclass(frozen=3DTrue) +class MachineConfig: + """One named machine: the test-agnostic description of what to boot. + + A machine is reusable across test types; a test (see RiscvQtbTest + subclasses) picks a machine by name and layers its own parameters on t= op. + """ + + name: str + pcpu: int + binaries: BinariesConfig + mmu_type: str # Xen (host) MMU type, injected into the host dts cpus. + xen_bootargs: str + + @classmethod + def from_config(cls, file_name: str, machine: str) -> MachineConfig: + """Build only the single named machine from the catalog at `path`. + + A test run boots one machine, so there is no need to construct the + whole catalog: parse the YAML, validate the shared binaries, and + build just the requested entry. + """ + fpath: Path =3D resolve_path(file_name) + raw: dict =3D yaml.safe_load(fpath.read_text()) + + required =3D ("binaries", "machines") + missing =3D [k for k in required if k not in raw] + if missing: + raise ValueError(f"Global config {file_name} missing keys: {mi= ssing}") + + binaries: BinariesConfig =3D _parse_binaries(raw["binaries"]) + + machines =3D raw["machines"] + if machine not in machines: + known =3D ", ".join(sorted(machines)) or "(none)" + raise ValueError(f"unknown machine {machine!r}; known machines= : {known}") + + return _parse_machine(machines[machine], binaries, machine) + + @cached_property + def dt(self) -> DeviceTree: + """Host device tree, compiled on first use.""" + return build_xen_device_tree(self) diff --git a/automation/scripts/qtb/riscv/config.yaml b/automation/scripts/= qtb/riscv/config.yaml new file mode 100644 index 0000000000..94897827b7 --- /dev/null +++ b/automation/scripts/qtb/riscv/config.yaml @@ -0,0 +1,17 @@ +# Shared config for the qtb riscv64 tests +# +# A machine is the test-agnostic description of what to boot (cpus, Xen co= mmand +# line). Test YAMLs (e.g. console-test.yaml) pick a machine by name and la= yer +# their own parameters on top. +# +# Path resolution (see paths.py): `binaries:` entries resolve against +# $QTB_BINARIES_DIR env var if defined else `binaries`. Absolute paths used +# as-is. + +binaries: + xen: xen + +machines: + dom0less-1smp-0domu-1vcpu-aplic-imsic-null: + xen_bootargs: "sched=3Dnull" + pcpu: 1 diff --git a/automation/scripts/qtb/riscv/console_test/__init__.py b/automa= tion/scripts/qtb/riscv/console_test/__init__.py new file mode 100644 index 0000000000..5db5569965 --- /dev/null +++ b/automation/scripts/qtb/riscv/console_test/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""console-test type package.""" + +from .console_test import ConsoleTest diff --git a/automation/scripts/qtb/riscv/console_test/console-test.yaml b/= automation/scripts/qtb/riscv/console_test/console-test.yaml new file mode 100644 index 0000000000..cec0edc510 --- /dev/null +++ b/automation/scripts/qtb/riscv/console_test/console-test.yaml @@ -0,0 +1,18 @@ +# Console string expectation test (run with: qemu_smoke_riscv64.py console= -test run ). +# +# Each test uses a machine from config.yaml and maps a console index to th= e list +# of string(s) expected on that console: 0 is Xen's own console. The runne= r boots +# the machine and asserts each string is printed within the timeout. Nothi= ng is +# injected. +# +# Test options: +# timeout: int # timeout between each string match (in seconds) +# attempts: int # number of tries for a wait before failing. Default 3 (= min =3D 1) + +machine_catalog: config.yaml + +tests: + dom0less-1smp-0domu-1vcpu-aplic-imsic-null: + machine: dom0less-1smp-0domu-1vcpu-aplic-imsic-null + expect: + 0: ["All set up"] diff --git a/automation/scripts/qtb/riscv/console_test/console_test.py b/au= tomation/scripts/qtb/riscv/console_test/console_test.py new file mode 100644 index 0000000000..95540fd388 --- /dev/null +++ b/automation/scripts/qtb/riscv/console_test/console_test.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Console string expectation test (`type: console-test`). + +Per console: assert each expected string is printed within the timeout. +Nothing is injected; this only watches console output. + +YAML (console-test.yaml): each test names the machine it boots (tests may +share one) and maps a console index to the string(s) expected on it: 0 is +Xen's own console. + + machine_catalog: config.yaml + tests: + dom0less-1smp-0domu-1vcpu-aplic-imsic-null: # test name (run positi= onal) + machine: dom0less-1smp-0domu-1vcpu-aplic-imsic-null # from config= .yaml + expect: # console index -> stri= ng(s) + 0: [All set up] +""" + +from __future__ import annotations + +import logging +from typing import ClassVar + +import pexpect +import yaml + +from ..paths import resolve_path +from ..config import MachineConfig, required_keys +from ..qtb_test import TIMEOUT_DEFAULT, RiscvQtbTest +from ..machine import RiscvTestMachine + +logger =3D logging.getLogger(__name__) + +TYPE_ID: str =3D "console-test" + +CONFIG_FILE_DEFAULT: str =3D "console_test/console-test.yaml" +DESCRIPTION_DEFAULT: str =3D "Assert expected string(s) are printed on the= Xen console" +ATTEMPTS_DEFAULT: int =3D 3 + +XEN_CONS_IDX: int =3D 0 + + +class ConsoleTest(RiscvQtbTest): + type_id: ClassVar[str] =3D TYPE_ID + description: ClassVar[str] =3D DESCRIPTION_DEFAULT + config_file: ClassVar[str] =3D CONFIG_FILE_DEFAULT + + def __init__(self, raw: dict, test_name: str) -> None: + self.name, self.data, self.machine =3D self._parse_test_cfg(raw, t= est_name) + + self.expect =3D self.data["expect"] # expected console string + + self.timeout =3D int(self.data.get("timeout", TIMEOUT_DEFAULT)) + if self.timeout < 1: + raise ValueError("timeout < 1, must be at least 1") + + self.attempts =3D int(self.data.get("attempts", ATTEMPTS_DEFAULT)) + if self.attempts < 1: + raise ValueError("attempts < 1, must be at least 1") + + @staticmethod + def _load_yaml(path) -> dict: + return yaml.safe_load(resolve_path(path).read_text()) + + @classmethod + def from_config(cls, config_file: str, test_name: str) -> ConsoleTest: + return cls(cls._load_yaml(config_file), test_name) + + @staticmethod + @required_keys("test_data", {"machine", "expect"}) + def _parse_test_data( + machine_catalog: str, test_data: dict, test_name: str + ) -> tuple[str, dict, MachineConfig]: + """Parse test data dictionary""" + test_machine =3D MachineConfig.from_config(machine_catalog, test_d= ata["machine"]) + + def invalid(why: str) -> ValueError: + return ValueError( + f"test {test_name!r}: {why}; expected " + f"{{{XEN_CONS_IDX}: ['str1', 'str2', ...]}}" + ) + + expect =3D test_data["expect"] + if not isinstance(expect, dict) or expect.keys() !=3D {XEN_CONS_ID= X}: + raise invalid( + f"expect must map console index {XEN_CONS_IDX} (Xen's own " + f"console, the only one) and nothing else, got {expect!r}" + ) + strings =3D expect[XEN_CONS_IDX] + if not isinstance(strings, list): + raise invalid( + f"{ConsoleTest.config_file} expects a list of string(s), " + f"got {type(strings).__name__}" + ) + if not strings: + raise invalid("Xen has no expected string") + if not all(isinstance(s, str) and s for s in strings): + raise invalid(f"Xen expects non-empty strings, got {strings!r}= ") + return (test_name, test_data, test_machine) + + @staticmethod + @required_keys("raw", {"machine_catalog", "tests"}) + def _parse_test_cfg(raw: dict, test_name: str) -> tuple[str, dict, Mac= hineConfig]: + """Read the config: return the named test dict and its machine.""" + + tests =3D raw["tests"] + if test_name not in tests: + known =3D ", ".join(sorted(tests)) or "(none)" + raise ValueError(f"unknown test {test_name!r}; known tests: {k= nown}") + + test_data =3D tests[test_name] + return ConsoleTest._parse_test_data( + raw["machine_catalog"], test_data, test_name + ) + + @staticmethod + def list_tests(config_file: str) -> list[str]: + tests =3D ConsoleTest._load_yaml(config_file).get("tests") + if not tests: + logger.warning("no 'tests' key found in %s", config_file) + return [] + return list(tests) + + @staticmethod + def _console(vm: RiscvTestMachine): + """Xen's own console (con0).""" + if vm.console is None: + raise RuntimeError("Xen console not wired up, machine not laun= ched?") + return vm.console + + def run(self, vm: RiscvTestMachine) -> None: + cons =3D self._console(vm) + for strings in self.expect.values(): + for s in strings: + self._expect_string(cons, s) + + def _expect_string(self, cons, expected: str) -> None: + """Wait for `expected` on the console, retrying on timeout.""" + for attempt in range(self.attempts): + try: + cons.expect_exact(expected, timeout=3Dself.timeout) + return + except pexpect.TIMEOUT: + if attempt =3D=3D self.attempts - 1: + raise diff --git a/automation/scripts/qtb/riscv/dt.py b/automation/scripts/qtb/ri= scv/dt.py new file mode 100644 index 0000000000..f0376979e5 --- /dev/null +++ b/automation/scripts/qtb/riscv/dt.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Device tree (DT) handling: compile a .dts source into a .dtb""" + +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path + +logger =3D logging.getLogger(__name__) + + +def _compile_dts(src: Path, out: Path): + """Run dtc to compile .dts `src` into the .dtb file at `out`""" + try: + p =3D subprocess.run( + ["dtc", "-I", "dts", "-O", "dtb", "-o", str(out), str(src)], + check=3DTrue, + capture_output=3DTrue, + text=3DTrue, + ) + logger.debug("dtc %s: stdout: %s stderr: %s", src, p.stdout, p.std= err) + + except FileNotFoundError as e: + raise RuntimeError("dtc not found in PATH; install device-tree-com= piler") from e + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"dtc failed on {str(src)!r} (exit {e.returncode}):\n" + f" stdout: {e.stdout}\n stderr: {e.stderr}" + ) from e + + +def compile_to_dtb(src: Path, out: Path) -> Path: + """ + Compile a .dts source to a .dtb under out dir and return the .dtb path. + + Raises FileNotFoundError if `src` or `out` don't exist. + """ + if not src.exists(): + raise FileNotFoundError( + f"Device tree source {str(src)!r} not found" + ) + + if not out.exists(): + raise FileNotFoundError(f"Device Tree output dir: {out} doesn't ex= ist") + + dtb =3D out / (src.stem + ".dtb") + _compile_dts(src, dtb) + return dtb + + +def write_dts(text: str, name: str, out_dir: Path) -> Path: + """Write generated .dts text to /.dts; return its path.= """ + src =3D out_dir / f"{name}.dts" + src.parent.mkdir(parents=3DTrue, exist_ok=3DTrue) + src.write_text(text) + return src diff --git a/automation/scripts/qtb/riscv/machine.py b/automation/scripts/q= tb/riscv/machine.py new file mode 100644 index 0000000000..74a05dfc82 --- /dev/null +++ b/automation/scripts/qtb/riscv/machine.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""QtbMachine for riscv64.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from qemu.qtb import QtbMachine + +from .config import MACHINE_INTERRUPT_CONTROLLER, MACHINE_MEMORY, MachineC= onfig +from .paths import resolve_from_path + + +class RiscvTestMachine(QtbMachine): + arch_name =3D "riscv64" + gdb_arch =3D "riscv:rv64" + + qemu_bin =3D "qemu-system-riscv64" + + def __init__( + self, + mc: MachineConfig, + *, + timeout: int, + log_dir: str | None =3D None, + ) -> None: + self.machine_conf =3D mc + super().__init__( + memory=3DMACHINE_MEMORY, + cpus=3Dmc.pcpu, + mirror_console=3DFalse, + timeout=3Dtimeout, + log_dir=3Dlog_dir, + ) + + def _machine_args(self, memory: int, cpus: int) -> Sequence[str]: + machine =3D self.machine_conf + machine_opt =3D f"virt,aclint=3Doff,aia=3D{MACHINE_INTERRUPT_CONTR= OLLER}" + # Xen has no sstc support yet. + cpu_opt =3D "rv64,svpbmt=3Don,smstateen=3Don,sstc=3Doff" + return [ + "-dtb", + str(machine.dt.dtb), + "-M", + machine_opt, + "-cpu", + cpu_opt, + "-smp", + str(cpus), + "-m", + str(memory), + "-kernel", + str(machine.binaries.xen), + ] + + def _resolve_binary(self) -> str: + return str(resolve_from_path(self.qemu_bin)) diff --git a/automation/scripts/qtb/riscv/paths.py b/automation/scripts/qtb= /riscv/paths.py new file mode 100644 index 0000000000..237f1bf502 --- /dev/null +++ b/automation/scripts/qtb/riscv/paths.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Path helpers: resolve pkg-relative paths.""" + +from __future__ import annotations + +import os +import shutil +import tempfile +from functools import lru_cache +from pathlib import Path + +# Every relative path in this module is resolved against this base. +_BASE =3D Path(__file__).resolve().parent + +# Build artifacts, overridable for CI. +_BINARIES_BASE =3D Path(os.environ.get("QTB_BINARIES_DIR") or _BASE / "bin= aries") + + +@lru_cache(maxsize=3D1) +def _temp_dir_handle() -> tempfile.TemporaryDirectory: + return tempfile.TemporaryDirectory(prefix=3D"qtb-") + + +def temp_dir() -> Path: + """Process-wide scratch dir for generated/compiled artifacts (singleto= n).""" + return Path(_temp_dir_handle().name) + + +def cleanup_temp_dir() -> None: + """Remove the scratch dir, if one was created, and clear the cache.""" + if _temp_dir_handle.cache_info().currsize: + _temp_dir_handle().cleanup() + _temp_dir_handle.cache_clear() + + +def resolve_path(file_name: str) -> Path: + """Resolve `file_name` against _BASE, absolute paths pass through.""" + return _resolve_under(file_name, _BASE) + + +def resolve_binary(file_name: str) -> Path: + """Resolve a build artifact against _BINARIES_BASE, absolute paths pas= s through.""" + return _resolve_under(file_name, _BINARIES_BASE) + + +def resolve_from_path(file_name: str) -> Path: + """Resolve an executable on $PATH, absolute paths pass through.""" + path =3D shutil.which(file_name) + if not path: + raise FileNotFoundError(f"cannot resolve {file_name!r}: not an exe= cutable on $PATH") + return Path(path) + + +def _resolve_under(file_name: str, base: Path) -> Path: + """Resolve `file_name` against `base`, absolute paths pass through.""" + p =3D Path(file_name) + path =3D p if p.is_absolute() else base / p + if not path.exists(): + raise FileNotFoundError(f"cannot resolve {str(path)!r}: does not e= xist") + return path diff --git a/automation/scripts/qtb/riscv/qtb_test.py b/automation/scripts/= qtb/riscv/qtb_test.py new file mode 100644 index 0000000000..872fbc2fa9 --- /dev/null +++ b/automation/scripts/qtb/riscv/qtb_test.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Abstract base for the qtb riscv64 test types. + +A test type is a RiscvQtbTest subclass owning a config file that describes= its +tests, each bound to a machine from the shared catalog. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import ClassVar + +from .config import MachineConfig +from .machine import RiscvTestMachine + +# Default per-test timeout (seconds) +TIMEOUT_DEFAULT: int =3D 120 + + +class RiscvQtbTest(ABC): + """One runnable test bound to the machine it boots. + + A subclass sets `type_id`, `description`, and `config_file`, and imple= ments + `from_config` to parse its config file, `list_tests` to enumerate the = tests + it declares, and `run` to drive the test logic. + """ + + # Set by each concrete subclass. + type_id: ClassVar[str] =3D "" + # One-line summary of what the type does, shown in the CLI help. + description: ClassVar[str] =3D "" + # Config file the type reads its tests from, resolved pkg-relative. + config_file: ClassVar[str] =3D "" + + # Set by the subclass parser. + name: str + data: dict + machine: MachineConfig + timeout: int =3D TIMEOUT_DEFAULT + + @classmethod + @abstractmethod + def from_config(cls, config_file: str, test_name: str) -> RiscvQtbTest: + """Build the test named `test_name` from `config_file`.""" + + @staticmethod + @abstractmethod + def list_tests(config_file: str) -> list[str]: + """Return the names of every test declared in the config file.""" + + @abstractmethod + def run(self, vm: RiscvTestMachine) -> None: + """Drive the running machine and assert the expected result.""" diff --git a/automation/scripts/qtb/riscv/xen_dt.py b/automation/scripts/qt= b/riscv/xen_dt.py new file mode 100644 index 0000000000..8881b01b36 --- /dev/null +++ b/automation/scripts/qtb/riscv/xen_dt.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Build the Xen host device tree for a MachineConfig. + +The tree is rendered from its Jinja2 template (dts/qemu-host.dts.j2), which +takes the hart count, the Xen MMU type and the Xen command line, then comp= iled +to a DTB with dtc. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING +from jinja2 import Environment, FileSystemLoader + +from .paths import resolve_path, temp_dir +from .dt import compile_to_dtb, write_dts + +if TYPE_CHECKING: # config imports this module, so only import it for typ= ing. + from .config import MachineConfig + +# Directory holding the Jinja2 platform device tree templates. +_DTS_DIR =3D "dts" + + +@dataclass(frozen=3DTrue) +class DeviceTree: + """Compiled device trees for one machine launch.""" + + dts: Path + dtb: Path + + +@lru_cache(maxsize=3D1) +def _env() -> Environment: + return Environment( + loader=3DFileSystemLoader(resolve_path(_DTS_DIR)), + keep_trailing_newline=3DTrue, + ) + + +def _render_xen_dts(machine: MachineConfig) -> str: + """Render the Xen host device tree source text for `machine`.""" + tmpl =3D _env().get_template("qemu-host.dts.j2") + return tmpl.render( + ncpus=3Dmachine.pcpu, + mmu_type=3Dmachine.mmu_type, + xen_bootargs=3Dmachine.xen_bootargs, + ) + + +def build_xen_device_tree(machine: MachineConfig) -> DeviceTree: + """Compile `machine` device tree into the shared scratch dir.""" + out =3D temp_dir() + dts: Path =3D write_dts(_render_xen_dts(machine), machine.name, out) + dtb: Path =3D compile_to_dtb(dts, out) + return DeviceTree(dts=3Ddts, dtb=3Ddtb) From nobody Thu Sep 3 07:05:38 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) client-ip=192.237.175.120; envelope-from=xen-devel-bounces@lists.xenproject.org; helo=lists.xenproject.org; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass(p=none dis=none) header.from=vates.tech ARC-Seal: i=1; a=rsa-sha256; t=1787823816; cv=none; d=zohomail.com; s=zohoarc; b=jHZwcCLUw2khZ1VfWNNuIFS5TK0epa1W+++pvOUQoJDwfd+r2WBBraA2knhEWXb76bBNqRAsBxi+K4mhLEmTIA2snA0/6YkccdX7FoCsinKag66MrSlWPc6lKAg8MZhA5b77E3iljAq7DaNSdLypscckbLnm4whyB0f0XhmCgAg= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1787823816; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=6mJ8pRhovm2+57Ktp8ucrhFg2lizqcmzFInUehED0eY=; b=iZpGkpGrWUgxJB3RESDgzEiSrGA71Mx+DngQ8n4uU3In9lCxeYrbkWM+YmFumo0T/5Qu+Eol+lpBK86fNaPTbaZArQfRgadIlrJ8Rp3AxoU4cZCIxoUTbTdHYLBS1cFz3dHGK4dpnSKPTy5q97Xw5NAwAfEMm16NrNNsZFdf7zc= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 17878238165657.72395875675511; Thu, 27 Aug 2026 02:43:36 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400482.1636154 (Exim 4.92) (envelope-from ) id 1wzWdj-0005xi-F4; Thu, 27 Aug 2026 09:43:19 +0000 Received: by outflank-mailman (output) from mailman id 1400482.1636154; Thu, 27 Aug 2026 09:43:19 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdj-0005xO-97; Thu, 27 Aug 2026 09:43:19 +0000 Received: by outflank-mailman (input) for mailman id 1400482; Thu, 27 Aug 2026 09:43:17 +0000 Received: from mx.expurgate.net ([195.190.135.10]) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdh-0005t6-Fg for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:17 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdg-00Fwyk-Sf for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:16 +0200 Received: from [10.42.69.12] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006ab-2eae-0a2a0a5409dd-0a2a450c8740-18 for ; Thu, 27 Aug 2026 11:43:16 +0200 Received: from [185.255.28.35] (helo=prod-mta-13-02.swg-srv.net) by tlsNG-d25034.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006b4-f479-0a2a450c0019-b9ff1c23ac25-4 for ; Thu, 27 Aug 2026 11:43:16 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13-02.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a13b8000c4f3.003 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:08 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id 4AE4483B2D; Thu, 27 Aug 2026 11:43:08 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=6mJ8pRhovm2+57Ktp8ucrhFg2lizqcmzFInUehED0eY=; h=from:subject:date:message-id:to:cc:mime-version:content-transfer-encoding:in-reply-to:references:feedback-id; b=ntsIHfGb7RobZHhoTvpUvXEdWsN0TRDx0g5GdBbPfCEwb3ZOL7/1dRdhZdw9Y7zpQf9v6JeNG SIQHeAQI1b8SbBQO21UeAUDtapmexb+1oscdZR34Ux08Ego/CeI1aYDGwTMmuLvPguO6BcpIlAX Iiwtn412nN5k8umuzKqNmr2SmT3YkagWsmCxtBh4ykP0+CWt4rEitm8EOk1YuSYa9zDQN6ZMeze V7u9TncLzlIvXHYgtaTpYH9U4BgkPv4tYxxAPicT/hAwhZgSOaAWjaiIUXbluz04fMbyw+C+eFy wIeVB9NEENP4eDY2CW+l/CM8W28xujnoanN3tl+o60Bg== X-Zone-Loop: f549f6904b0c164413a1efaa9959808997ab1665a6b7 x-campaign-type: default x-transaction-id: 616ba5a6-bfce-46ac-a600-21a132984cd4 x-swg-uid: 01-2acc789e-fd0d-4ed6-8569-fac68cd69c8a X-Mailer: Sweego Message-ID: <1787823789.8631fc262581453bbf619ec5b2062170.1a0429a13b8000c4f3@vates.tech> x-swg-bid: 1787823789.8631fc262581453bbf619ec5b2062170.1a0429a13b8000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Doug Goldstein , Stefano Stabellini Subject: [PATCH v2 4/6] automation/qtb: add unit tests for the QTB framework Date: Thu, 27 Aug 2026 11:42:50 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823788399 X-purgate-ID: tlsNG-d25034/1787823796-01AC4A5B-345BE6C6/0/0 X-purgate-type: clean X-purgate-size: 24135 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823817818158500 Content-Type: text/plain; charset="utf-8" The QTB framework is meant to be extended: new test types, new machines and new device trees aim to be added by other people. Add pytest coverage of the framework's functions, and of the console-test type's config validation and expect/retry loop, so that such changes get immediate feedback and existing behaviour does not silently regress. The suite covers 100% of the framework's statements, so a new code path added without a test shows up as a coverage drop. The tests are meant to be run locally, from the Xen tree root: python3 -m pytest automation/scripts/qtb/riscv/unit/ and, with pytest-cov installed, the coverage report is: python3 -m pytest --cov=3Dautomation/scripts/qtb \ automation/scripts/qtb/riscv/unit/ The tests drive fakes rather than QEMU, so they need no artifacts to run. Assisted-by: Claude:claude-opus-5 Signed-off-by: Baptiste Le Duc --- automation/scripts/qtb/riscv/unit/__init__.py | 2 + automation/scripts/qtb/riscv/unit/conftest.py | 42 ++++ .../scripts/qtb/riscv/unit/test_config.py | 121 ++++++++++ .../qtb/riscv/unit/test_console_test.py | 217 ++++++++++++++++++ automation/scripts/qtb/riscv/unit/test_dt.py | 99 ++++++++ .../scripts/qtb/riscv/unit/test_machine.py | 60 +++++ .../scripts/qtb/riscv/unit/test_temp_dir.py | 42 ++++ .../scripts/qtb/riscv/unit/test_xen_dt.py | 46 ++++ 8 files changed, 629 insertions(+) create mode 100644 automation/scripts/qtb/riscv/unit/__init__.py create mode 100644 automation/scripts/qtb/riscv/unit/conftest.py create mode 100644 automation/scripts/qtb/riscv/unit/test_config.py create mode 100644 automation/scripts/qtb/riscv/unit/test_console_test.py create mode 100644 automation/scripts/qtb/riscv/unit/test_dt.py create mode 100644 automation/scripts/qtb/riscv/unit/test_machine.py create mode 100644 automation/scripts/qtb/riscv/unit/test_temp_dir.py create mode 100644 automation/scripts/qtb/riscv/unit/test_xen_dt.py diff --git a/automation/scripts/qtb/riscv/unit/__init__.py b/automation/scr= ipts/qtb/riscv/unit/__init__.py new file mode 100644 index 0000000000..b234dc5303 --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""pytest tests of the framework's own logic.""" diff --git a/automation/scripts/qtb/riscv/unit/conftest.py b/automation/scr= ipts/qtb/riscv/unit/conftest.py new file mode 100644 index 0000000000..576eaf13b1 --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/conftest.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Shared pytest fixtures for the qtb unit tests.""" + +from __future__ import annotations + +import pytest + +from ..config import MachineConfig + + +@pytest.fixture +def make_file(tmp_path): + """Return a factory creating a file of `size` bytes, yielding its path= .""" + + def _make(name: str, size: int =3D 16) -> str: + p =3D tmp_path / name + p.write_bytes(b"\0" * size) + return str(p) + + return _make + + +@pytest.fixture +def make_machine(): + """Return a factory building a MachineConfig for tests.""" + + def _make( + *, + name=3D"m", + binaries=3DNone, + mmu=3D"sv48", + xen_bootargs=3D"", + ) -> MachineConfig: + return MachineConfig( + name=3Dname, + pcpu=3D4, + binaries=3Dbinaries, + mmu_type=3Dmmu, + xen_bootargs=3Dxen_bootargs, + ) + + return _make diff --git a/automation/scripts/qtb/riscv/unit/test_config.py b/automation/= scripts/qtb/riscv/unit/test_config.py new file mode 100644 index 0000000000..422ba2a5ea --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_config.py @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for the YAML machine-catalog parser.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +from ..config import ( + XEN_BOOTARGS_DEFAULT, + XEN_MMU_TYPE_DEFAULT, + MachineConfig, + _parse_binaries, +) + + +@pytest.fixture +def binaries(make_file): + """Raw binaries dict pointing at an existing file.""" + return {"xen": make_file("xen")} + + +# ---- _parse_binaries ---- + + +def test_parse_binaries_resolves_existing_paths(binaries): + assert _parse_binaries(binaries).xen =3D=3D Path(binaries["xen"]) + + +def test_parse_binaries_missing_key_raises(binaries): + del binaries["xen"] + with pytest.raises(ValueError, match=3D"missing keys"): + _parse_binaries(binaries) + + +def test_parse_binaries_missing_file_raises(binaries, tmp_path): + binaries["xen"] =3D str(tmp_path / "absent") + with pytest.raises(FileNotFoundError): + _parse_binaries(binaries) + + +# ---- MachineConfig.from_config ---- + + +def _write_yaml(tmp_path, binaries, name=3D"machine-a", **machine_override= s): + machine =3D { + "pcpu": 1, + "xen_bootargs": "com1=3Dpoll sched=3Dnull", + } + machine.update(machine_overrides) + doc =3D {"binaries": binaries, "machines": {name: machine}} + path =3D tmp_path / "config.yaml" + path.write_text(yaml.safe_dump(doc)) + return str(path) + + +def test_from_config_builds_machineconfig(tmp_path, binaries): + path =3D _write_yaml(tmp_path, binaries) + + mc =3D MachineConfig.from_config(path, "machine-a") + + assert mc.name =3D=3D "machine-a" + assert mc.pcpu =3D=3D 1 + assert mc.xen_bootargs =3D=3D "com1=3Dpoll sched=3Dnull" + assert mc.binaries.xen =3D=3D Path(binaries["xen"]) + + +def test_from_config_applies_optional_defaults(tmp_path, binaries): + # A machine with only the required keys falls back to the module defau= lts. + path =3D _write_yaml( + tmp_path, + binaries, + name=3D"bare", + mmu_type=3DNone, + xen_bootargs=3DNone, + ) + # Drop the keys set to None so the parser sees them as absent. + doc =3D yaml.safe_load(Path(path).read_text()) + for k in ("mmu_type", "xen_bootargs"): + doc["machines"]["bare"].pop(k, None) + Path(path).write_text(yaml.safe_dump(doc)) + + mc =3D MachineConfig.from_config(path, "bare") + + assert mc.mmu_type =3D=3D XEN_MMU_TYPE_DEFAULT + assert mc.xen_bootargs =3D=3D XEN_BOOTARGS_DEFAULT + + +def test_from_config_missing_machine_key_raises(tmp_path, binaries): + path =3D _write_yaml(tmp_path, binaries, name=3D"bare") + doc =3D yaml.safe_load(Path(path).read_text()) + del doc["machines"]["bare"]["pcpu"] + Path(path).write_text(yaml.safe_dump(doc)) + + with pytest.raises(ValueError, match=3D"machine config missing keys"): + MachineConfig.from_config(path, "bare") + + +def test_from_config_missing_top_level_key_raises(tmp_path, binaries): + path =3D _write_yaml(tmp_path, binaries) + doc =3D yaml.safe_load(Path(path).read_text()) + del doc["binaries"] + Path(path).write_text(yaml.safe_dump(doc)) + + with pytest.raises(ValueError, match=3D"missing keys: \\['binaries'\\]= "): + MachineConfig.from_config(path, "machine-a") + + +def test_from_config_unknown_machine_raises(tmp_path, binaries): + path =3D _write_yaml(tmp_path, binaries) + with pytest.raises(ValueError, match=3D"unknown machine 'nope'"): + MachineConfig.from_config(path, "nope") + + +def test_from_config_missing_binary_raises(tmp_path, binaries): + binaries["xen"] =3D str(tmp_path / "gone") + path =3D _write_yaml(tmp_path, binaries) + with pytest.raises(FileNotFoundError): + MachineConfig.from_config(path, "machine-a") diff --git a/automation/scripts/qtb/riscv/unit/test_console_test.py b/autom= ation/scripts/qtb/riscv/unit/test_console_test.py new file mode 100644 index 0000000000..cf15ed3b1b --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_console_test.py @@ -0,0 +1,217 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for the console-test test type (console_test.py).""" + +from __future__ import annotations + +from itertools import chain, repeat +from unittest import mock + +import pexpect +import pytest + +from ..console_test.console_test import ConsoleTest +from ..config import MachineConfig + + +# ---- helpers ---- + + +def _parse_test_data(machine, expect, name=3D"dummy"): + """Validate `expect` against `machine`, without reading a catalog file= .""" + test_data =3D {"machine": "box", "expect": expect} + with mock.patch.object(MachineConfig, "from_config", return_value=3Dma= chine): + return ConsoleTest._parse_test_data("config.yaml", test_data, name) + + +def _console(fail_times: int =3D 0, matches: int =3D 1) -> mock.Mock: + """Stand in for a pexpect spawn: `fail_times` timeouts, then `matches`= hits. + + Every wait past `matches` times out, so a test that waits more often t= han it + should fails instead of silently passing. + """ + cons =3D mock.Mock() + cons.expect_exact.side_effect =3D chain( + [pexpect.TIMEOUT("nope")] * fail_times, + [None] * matches, + repeat(pexpect.TIMEOUT("nope")), + ) + return cons + + +def _asked(cons: mock.Mock) -> list[str]: + """The strings waited for on `cons`, one entry per attempt (matched or= not).""" + return [call.args[0] for call in cons.expect_exact.call_args_list] + + +def _test(expect, machine, **opts): + """Build a ConsoleTest bound to `machine`, skipping the YAML read.""" + raw =3D { + "machine_catalog": "config.yaml", + "tests": {"dummy": {"machine": "box", "expect": expect, **opts}}, + } + with mock.patch.object(MachineConfig, "from_config", return_value=3Dma= chine): + return ConsoleTest(raw, "dummy") + + +# ---- _parse_test_data ---- + + +def test_parse_test_data_accepts_a_list_of_strings(make_machine): + machine =3D make_machine() + name, data, got =3D _parse_test_data(machine, {0: ["Hello", "All set u= p"]}) + assert (name, got) =3D=3D ("dummy", machine) + assert data["expect"] =3D=3D {0: ["Hello", "All set up"]} + + +def test_parse_test_data_bare_string_raises(make_machine): + # A bare string is refused, not wrapped: the YAML must spell out the l= ist. + machine =3D make_machine() + with pytest.raises(ValueError, match=3D"expects a list of string"): + _parse_test_data(machine, {0: "All set up"}) + + +@pytest.mark.parametrize( + "expect", + [ + {-1: ["All set up"]}, # console index below Xen's + {1: ["All set up"]}, # console index above Xen's + {0: ["All set up"], 1: ["More"]}, # Xen's + another unknown + ["All set up"], # no console index + None, + "All set up", # not a map at all + ], +) +def test_parse_test_data_not_the_xen_console_map_raises(expect, make_machi= ne): + machine =3D make_machine() + with pytest.raises(ValueError, match=3D"must map console index 0"): + _parse_test_data(machine, expect) + + +def test_parse_test_data_empty_list_raises(make_machine): + machine =3D make_machine() + with pytest.raises(ValueError, match=3D"Xen has no expected string"): + _parse_test_data(machine, {0: []}) + + +def test_parse_test_data_empty_string_in_list_raises(make_machine): + machine =3D make_machine() + with pytest.raises(ValueError, match=3D"Xen expects non-empty strings"= ): + _parse_test_data(machine, {0: ["All set up", ""]}) + + +def test_parse_test_data_missing_expect_raises(): + with pytest.raises(ValueError, match=3D"missing keys"): + ConsoleTest._parse_test_data("config.yaml", {"machine": "box"}, "d= ummy") + + +# ---- _parse_test_cfg ---- + + +def test_parse_test_cfg_missing_machine_catalog_raises(): + with pytest.raises(ValueError, match=3D"missing keys"): + ConsoleTest._parse_test_cfg({"tests": {}}, "dummy") + + +def test_parse_test_cfg_unknown_test_raises(): + raw =3D {"machine_catalog": "config.yaml", "tests": {"a": {}}} + with pytest.raises(ValueError, match=3D"unknown test 'dummy'"): + ConsoleTest._parse_test_cfg(raw, "dummy") + + +# ---- __init__ ---- + + +def test_timeout_and_attempts_are_read(make_machine): + test =3D _test({0: ["All set up"]}, make_machine(), timeout=3D7, attem= pts=3D2) + assert (test.timeout, test.attempts) =3D=3D (7, 2) + + +def test_timeout_below_one_raises(make_machine): + with pytest.raises(ValueError, match=3D"timeout < 1"): + _test({0: ["All set up"]}, make_machine(), timeout=3D0) + + +def test_attempts_below_one_raises(make_machine): + with pytest.raises(ValueError, match=3D"attempts < 1"): + _test({0: ["All set up"]}, make_machine(), attempts=3D0) + + +# ---- run ---- + + +def test_run_expects_each_string_in_order_on_con0(make_machine): + test =3D _test({0: ["first", "then"]}, make_machine()) + vm =3D mock.Mock(console=3D_console(matches=3D2)) + + test.run(vm) + + assert _asked(vm.console) =3D=3D ["first", "then"] + + +def test_run_xen_console_not_wired_raises(make_machine): + test =3D _test({0: ["All set up"]}, make_machine()) + vm =3D mock.Mock(console=3DNone) + + with pytest.raises(RuntimeError, match=3D"not launched"): + test.run(vm) + + +# ---- _expect_string ---- + + +def test_expect_string_retries_after_a_timeout(make_machine): + test =3D _test({0: ["All set up"]}, make_machine(), attempts=3D3) + cons =3D _console(fail_times=3D2) + + test._expect_string(cons, "All set up") + + assert _asked(cons) =3D=3D ["All set up"] * 3 + + +def test_expect_string_raises_once_attempts_are_spent(make_machine): + test =3D _test({0: ["All set up"]}, make_machine(), attempts=3D2) + cons =3D _console(matches=3D0) + + with pytest.raises(pexpect.TIMEOUT): + test._expect_string(cons, "All set up") + + assert _asked(cons) =3D=3D ["All set up"] * 2 + + +# ---- config IO ---- + + +def test_list_tests_reads_the_type_yaml(): + names =3D ConsoleTest.list_tests("console_test/console-test.yaml") + assert "dom0less-1smp-0domu-1vcpu-aplic-imsic-null" in names + + +def test_list_tests_without_a_tests_key_returns_empty(): + with mock.patch.object(ConsoleTest, "_load_yaml", return_value=3D{}): + assert ConsoleTest.list_tests("console-test.yaml") =3D=3D [] + + +def test_from_config_builds_instance(make_machine): + machine =3D make_machine() + + raw =3D { + "machine_catalog": "config.yaml", + "tests": {"dummy": {"machine": "box", "expect": {0: ["All set up"]= }}}, + } + # Mock the YAML read and the catalog lookup: only the build logic is u= nder test. + with ( + mock.patch.object(ConsoleTest, "_load_yaml", return_value=3Draw), + mock.patch.object(MachineConfig, "from_config", return_value=3Dmac= hine), + ): + test =3D ConsoleTest.from_config("console-test.yaml", "dummy") + + assert test.name =3D=3D "dummy" + assert test.type_id =3D=3D "console-test" + assert test.machine is machine + assert test.expect =3D=3D {0: ["All set up"]} + + +def test_from_config_unknown_test_raises(): + # Also pins from_config's argument order (config file, then test name). + with pytest.raises(ValueError, match=3D"unknown test 'nope'"): + ConsoleTest.from_config("console_test/console-test.yaml", "nope") diff --git a/automation/scripts/qtb/riscv/unit/test_dt.py b/automation/scri= pts/qtb/riscv/unit/test_dt.py new file mode 100644 index 0000000000..2201a8651f --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_dt.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for the device-tree compile path (dt.py).""" + +from __future__ import annotations + +import shutil +from unittest import mock + +import pytest + +from .. import dt + +_HAS_DTC =3D shutil.which("dtc") is not None +_MINIMAL_DTS =3D "/dts-v1/;\n/ { };\n" + + +# ---- _compile_dts (dtc wrapper) ---- + + +def test_compile_dts_invokes_dtc(tmp_path): + src =3D tmp_path / "in.dts" + src.write_text(_MINIMAL_DTS) + dtb =3D tmp_path / "in.dtb" + + with mock.patch.object(dt.subprocess, "run") as run: + dt._compile_dts(src, dtb) + + run.assert_called_once() + argv =3D run.call_args.args[0] + assert argv =3D=3D ["dtc", "-I", "dts", "-O", "dtb", "-o", str(dtb), s= tr(src)] + assert run.call_args.kwargs["check"] is True + assert run.call_args.kwargs["capture_output"] is True + + +def test_compile_dts_missing_dtc_raises_runtimeerror(tmp_path): + src, dtb =3D tmp_path / "a.dts", tmp_path / "a.dtb" + src.write_text(_MINIMAL_DTS) + + with mock.patch.object(dt.subprocess, "run", side_effect=3DFileNotFoun= dError): + with pytest.raises(RuntimeError, match=3D"dtc not found"): + dt._compile_dts(src, dtb) + + +def test_compile_dts_dtc_failure_raises_runtimeerror(tmp_path): + src, dtb =3D tmp_path / "a.dts", tmp_path / "a.dtb" + src.write_text(_MINIMAL_DTS) + err =3D dt.subprocess.CalledProcessError(1, "dtc", output=3D"out", std= err=3D"syntax error") + + with mock.patch.object(dt.subprocess, "run", side_effect=3Derr): + with pytest.raises(RuntimeError, match=3D"syntax error"): + dt._compile_dts(src, dtb) + + +# ---- compile_to_dtb path handling ---- + + +def test_compile_to_dtb_missing_source_raises_filenotfound(tmp_path): + with pytest.raises(FileNotFoundError, match=3D"not found"): + dt.compile_to_dtb(tmp_path / "nope.dts", tmp_path) + + +def test_compile_to_dtb_missing_out_dir_raises_filenotfound(tmp_path): + src =3D tmp_path / "a.dts" + src.write_text(_MINIMAL_DTS) + + with pytest.raises(FileNotFoundError, match=3D"doesn't exist"): + dt.compile_to_dtb(src, tmp_path / "absent") + + +def test_compile_to_dtb_source_goes_to_out_dir_with_dtb_suffix(tmp_path): + src =3D tmp_path / "host-1smp.dts" + src.write_text(_MINIMAL_DTS) + out =3D tmp_path / "binaries" + out.mkdir() + + with mock.patch.object(dt, "_compile_dts") as compile_mock: + result =3D dt.compile_to_dtb(src, out) + + assert result =3D=3D out / "host-1smp.dtb" + compile_mock.assert_called_once_with(src, out / "host-1smp.dtb") + + +# ---- write_dts ---- + + +def test_write_dts_writes_source_under_out_dir(tmp_path): + src =3D dt.write_dts(_MINIMAL_DTS, name=3D"unit", out_dir=3Dtmp_path) + + assert src =3D=3D tmp_path / "unit.dts" + assert src.read_text() =3D=3D _MINIMAL_DTS + + +@pytest.mark.skipif(not _HAS_DTC, reason=3D"dtc not installed") +def test_write_then_compile_produces_dtb(tmp_path): + src =3D dt.write_dts(_MINIMAL_DTS, name=3D"unit", out_dir=3Dtmp_path) + dtb =3D dt.compile_to_dtb(src, tmp_path) + + assert dtb.is_file() + assert dtb.suffix =3D=3D ".dtb" diff --git a/automation/scripts/qtb/riscv/unit/test_machine.py b/automation= /scripts/qtb/riscv/unit/test_machine.py new file mode 100644 index 0000000000..68a1564072 --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_machine.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for RiscvTestMachine QEMU argument assembly.""" + +from __future__ import annotations + +from stat import S_IEXEC +from unittest import mock + +from ..machine import RiscvTestMachine +from ..config import MACHINE_MEMORY, BinariesConfig + + +def _binaries(tmp_path): + xen =3D tmp_path / "xen" + xen.write_bytes(b"") + return BinariesConfig(xen=3Dxen) + + +def _machine(mc) -> RiscvTestMachine: + """Build the machine with QtbMachine.__init__ stubbed out (it spawns Q= EMU).""" + with mock.patch("qemu.qtb.QtbMachine.__init__", return_value=3DNone): + return RiscvTestMachine(mc, timeout=3D30) + + +def test_init_forwards_cpus_to_qtbmachine(tmp_path, make_machine): + mc =3D make_machine(name=3D"machine", binaries=3D_binaries(tmp_path)) + + with mock.patch("qemu.qtb.QtbMachine.__init__", return_value=3DNone) a= s base_init: + vm =3D RiscvTestMachine(mc, timeout=3D30, log_dir=3D"/logs") + + assert vm.machine_conf is mc + assert base_init.call_args.kwargs =3D=3D { + "memory": MACHINE_MEMORY, + "cpus": mc.pcpu, + "mirror_console": False, + "timeout": 30, + "log_dir": "/logs", + } + + +def test_resolve_qemu_on_path(tmp_path, make_machine, monkeypatch): + qemu =3D tmp_path / "qemu-system-riscv64" + qemu.write_bytes(b"") + qemu.chmod(S_IEXEC) + monkeypatch.setenv("PATH", str(tmp_path)) + mc =3D make_machine(name=3D"machine", binaries=3D_binaries(tmp_path)) + + assert _machine(mc)._resolve_binary() =3D=3D str(qemu) + + + +def test_machine_args_wires_kernel_and_dtb_but_no_bios(tmp_path, make_mach= ine): + mc =3D make_machine(name=3D"machine", binaries=3D_binaries(tmp_path)) + + args =3D list(_machine(mc)._machine_args(memory=3D2048, cpus=3D4)) + + assert args[args.index("-kernel") + 1] =3D=3D str(mc.binaries.xen) + assert args[args.index("-dtb") + 1] =3D=3D str(mc.dt.dtb) + assert args[args.index("-m") + 1] =3D=3D "2048" + assert args[args.index("-smp") + 1] =3D=3D "4" diff --git a/automation/scripts/qtb/riscv/unit/test_temp_dir.py b/automatio= n/scripts/qtb/riscv/unit/test_temp_dir.py new file mode 100644 index 0000000000..d595d824d7 --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_temp_dir.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for the temp_dir scratch-directory singleton.""" + +from __future__ import annotations + +import pytest + +from .. import paths + + +@pytest.fixture(autouse=3DTrue) +def _reset_singleton(): + # Force each test to begin with fresh temp dir + yield + paths.cleanup_temp_dir() + + +def test_temp_dir_exists_and_prefixed(): + d =3D paths.temp_dir() + assert d.is_dir() + assert d.name.startswith("qtb-") + + +def test_temp_dir_is_singleton(): + assert paths.temp_dir() =3D=3D paths.temp_dir() + + +def test_temp_dir_handle_kept_alive(): + h =3D paths._temp_dir_handle() + assert h is paths._temp_dir_handle() + assert h.name =3D=3D str(paths.temp_dir()) + + +def test_cleanup_temp_dir_removes_and_resets(): + d =3D paths.temp_dir() + assert d.is_dir() + paths.cleanup_temp_dir() + assert not d.exists() + # Cache reset: next call builds a fresh, existing dir, not the gone on= e. + fresh =3D paths.temp_dir() + assert fresh.is_dir() + assert fresh !=3D d diff --git a/automation/scripts/qtb/riscv/unit/test_xen_dt.py b/automation/= scripts/qtb/riscv/unit/test_xen_dt.py new file mode 100644 index 0000000000..37c4055b31 --- /dev/null +++ b/automation/scripts/qtb/riscv/unit/test_xen_dt.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Unit tests for xen_dt device-tree generation.""" + +from __future__ import annotations + +from unittest import mock + +from .. import xen_dt + + +# ---- _render_xen_dts ---- + + +def test_render_injects_bootargs(make_machine): + machine =3D make_machine(xen_bootargs=3D"com1=3Dpoll sched=3Dnull") + out =3D xen_dt._render_xen_dts(machine) + + assert 'xen,xen-bootargs =3D "com1=3Dpoll sched=3Dnull";' in out + + +def test_render_injects_xen_mmu_type(make_machine): + machine =3D make_machine(mmu=3D"sv39") + out =3D xen_dt._render_xen_dts(machine) + + assert 'mmu-type =3D "riscv,sv39";' in out + + +# ---- build_xen_device_tree ---- + + +def test_build_xen_device_tree_compiles(tmp_path, make_machine): + machine =3D make_machine(name=3D"unit-test") + dts =3D tmp_path / "unit-test.dts" + dtb =3D tmp_path / "unit-test.dtb" + with ( + mock.patch.object(xen_dt, "temp_dir", return_value=3Dtmp_path), + mock.patch.object(xen_dt, "write_dts", return_value=3Ddts) as writ= e_dts, + mock.patch.object(xen_dt, "compile_to_dtb", return_value=3Ddtb) as= compile_to_dtb, + ): + result =3D xen_dt.build_xen_device_tree(machine) + + write_dts.assert_called_once() + assert write_dts.call_args.args[1] =3D=3D machine.name + compile_to_dtb.assert_called_once_with(dts, tmp_path) + assert result.dts =3D=3D dts + assert result.dtb =3D=3D dtb From nobody Thu Sep 3 07:05:38 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) client-ip=192.237.175.120; envelope-from=xen-devel-bounces@lists.xenproject.org; helo=lists.xenproject.org; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass(p=none dis=none) header.from=vates.tech ARC-Seal: i=1; a=rsa-sha256; t=1787823822; cv=none; d=zohomail.com; s=zohoarc; b=VzfFfR0GNN3TZBEuiLz16e1/WTqGAvmhku3ErDDXaiCbdAC2ApDsWTqx7cEvyuK8QrUgtRiTn9v8+smMMwTaSkgwMey0jJF5n+0GQSavCTlXu3rRgywTN1uhq95mXcu5fv5t954ryk/UDJms2DF29oV8KxVJLS1bHMWMaCsG88Y= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1787823822; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=7bSuJNzprWfjbzLQMqWGQPycYoufJkdjbgVndy19/bs=; b=irSWdmV6SmFBymIrHgx1zLAB5wNVUsBZttBd0mDz6abdkeVPf/QjlTqfjoxI3I0sxgUO3T1/g0IMaIkSpCvZ1rd7oe7ynK+2WWdozXsbX8qTB+4iq1ktpYiZiYOjpagZVVc+Yg7yd0y6rPt2Gs7NMXvRptgjs4NUDdcdZBABn20= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 1787823822828447.21081176834696; Thu, 27 Aug 2026 02:43:42 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400484.1636173 (Exim 4.92) (envelope-from ) id 1wzWdp-0006Uq-8c; Thu, 27 Aug 2026 09:43:25 +0000 Received: by outflank-mailman (output) from mailman id 1400484.1636173; Thu, 27 Aug 2026 09:43:25 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdp-0006UT-1X; Thu, 27 Aug 2026 09:43:25 +0000 Received: by outflank-mailman (input) for mailman id 1400484; Thu, 27 Aug 2026 09:43:23 +0000 Received: from mx.expurgate.net ([195.190.135.20]) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdn-0006OC-Gw for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:23 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdm-00AoWH-Ep for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:22 +0200 Received: from [10.42.69.8] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006ad-e002-0a2a0a5209dd-0a2a4508ba2a-42 for ; Thu, 27 Aug 2026 11:43:22 +0200 Received: from [185.255.28.18] (helo=prod-mta-13.swg-srv.net) by tlsNG-c1860d.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006b8-f659-0a2a45080019-b9ff1c12af39-3 for ; Thu, 27 Aug 2026 11:43:20 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a1484000c4f3.003 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:09 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id 9413D83B32; Thu, 27 Aug 2026 11:43:08 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=7bSuJNzprWfjbzLQMqWGQPycYoufJkdjbgVndy19/bs=; h=from:subject:date:message-id:to:cc:mime-version:content-type:content-transfer-encoding:in-reply-to:references:feedback-id; b=MsNcnMmp4UIMF8WiJYhRETCynXGqhLHnSXz5lL4IWWjZvhFoNocv+q3uIPWe0psQW534iwfyy PgVe1GOLeaENEzjxVE6IBtVKzzG05PtZ8lTcyabogWbf8fsLrzp0IM4HKt6RUcrC6HO7B+yHYOr YLHFKJFK+JqRlCj9ogLx+DQJcLkIPSeIJSJsfwPi2GYJHl36ok9KgfLHX2ojrlr29k1bxPGJIFW +FKOAbf7DqPTrO8M6gwj1+n9F5l0MLEpiB1ubwzUKRY6Smrd2gr2oroWS0TFJizWemr+PeEgj4D vcsUmIY8/BQ8WXWx926ZFCWjN1B32mzltuwCXASOIoVw== X-Zone-Loop: 6ba272c28740d1c7ee60548b8ae2d6428574eae73383 x-campaign-type: default x-transaction-id: 61ee72db-108d-4708-9d11-c2b3ecd9acdc x-swg-uid: 01-d2edfc05-4cf1-4eef-a57c-8b8064b8cc43 X-Mailer: Sweego Message-ID: <1787823789.8631fc262581453bbf619ec5b2062170.1a0429a1484000c4f3@vates.tech> x-swg-bid: 1787823789.8631fc262581453bbf619ec5b2062170.1a0429a1484000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Doug Goldstein , Stefano Stabellini Subject: [PATCH v2 5/6] automation/qtb: add QTB framework README Date: Thu, 27 Aug 2026 11:42:51 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823788688 X-purgate-ID: tlsNG-c1860d/1787823801-CCB7187B-6472FC02/0/0 X-purgate-type: clean X-purgate-size: 7495 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823823612158500 Document the qtb riscv64 test framework in a README. It covers: - the core concepts (machine, test type, test) and how they map to files - the source files layout - the CLI: `qemu_smoke_riscv64.py `, with "console-test" as the type - the config files: the machine catalog and a type's own `.yaml` - the Jinja2 device-tree templates under dts/ - how to add a test (config-only) and how to add a new test type. Assisted-by: Claude:claude-opus-5 Signed-off-by: Baptiste Le Duc --- automation/scripts/qtb/riscv/README.md | 182 +++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 automation/scripts/qtb/riscv/README.md diff --git a/automation/scripts/qtb/riscv/README.md b/automation/scripts/qt= b/riscv/README.md new file mode 100644 index 0000000000..c173714d75 --- /dev/null +++ b/automation/scripts/qtb/riscv/README.md @@ -0,0 +1,182 @@ +qtb riscv64 test framework +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D + +A small framework that boots Xen under QEMU on riscv64 and drives it to a +pass/fail verdict automatically from its console output. + +It is built on QEMU's qtb (QEMU Test Bench) Python package, which gives +programmatic control of a QEMU process over QMP and qtest, plus access to = the +consoles. + +What it does +------------ + +1. Reads a machine description (what to boot: cpus, Xen command line) and a + test description (what to assert). +2. Generates the host device tree. +3. Launches QEMU with Xen. +4. Reads the console and checks what Xen printed. + +Core concepts +------------- + +- Machine: test-agnostic description of what to boot. Reusable across test + types. `config.yaml` -> `MachineConfig`. +- Test type: a `RiscvQtbTest` subclass implementing the logic of a kind of= test + (e.g. `console-test`). Identified by `type_id`. See `console_test/`. +- Test: one named, runnable instance of a type: a machine plus the type's + parameters. Lives in the type's `.yaml`. + +A test type owns a config file describing its tests, each test names a mac= hine +from the shared catalog (`config.yaml`) and layers its own parameters on t= op. + +Layout +------ + +``` +qemu_smoke_riscv64.py CLI entry point ( run | list) + +qtb/riscv/ This framework + __init__.py Public API + qtb_test.py RiscvQtbTest ABC every test type derives from + config.py Machine catalog parser -> MachineConfig + xen_dt.py Generates the host device tree from its Jinja= 2 template + dt.py Compile .dts -> .dtb with dtc + paths.py Path resolution (pkg-relative) + machine.py RiscvTestMachine: assembles the QEMU command = line + + config.yaml The machine catalog (shared across test types) + dts/ Jinja2 device-tree templates (host, common) + + console_test/ The console-test type + __init__.py + console_test.py ConsoleTest implementation + console-test.yaml Its tests + + unit/ pytest unit tests of the framework logic itse= lf +``` + +How a type is selected +---------------------- + +The test type is the first positional argument (`qemu_smoke_riscv64.py con= sole-test run +...`). The CLI builds one subcommand per entry of `TEST_TYPES` (`__init__.= py`), +named after the type's `type_id`. + +Each type declares the `config_file` it reads its tests from. + +Prerequisites +------------- + +- `qemu.qtb`, QEMU's Python package (`python/` in the QEMU tree) +- `jinja2`, `pyyaml`, `pexpect` +- `dtc` (device-tree-compiler) +- `qemu-system-riscv64` +- the `xen` binary a machine boots + +CLI usage +--------- + +Run from `automation/scripts/`, or give the full path from the Xen tree ro= ot +(`./automation/scripts/qemu_smoke_riscv64.py ...`), which is what CI does. + +``` +# List every test the type defines in its config: +./qemu_smoke_riscv64.py console-test list + +# Run one test (drives it to PASS/FAIL, exit 0/1): +./qemu_smoke_riscv64.py console-test run dom0less-1smp-0domu-1vcpu-aplic-i= msic-null \ + --log-dir qtb-logs +``` + +`--log-dir` (run only) collects the QEMU process log, the qtest log, and e= ach +console as `con.log`: `con0.log` is Xen's own console, the only one wir= ed +up today. Omit it to write no logs. `-v/--verbose` raises the +log level to debug. + +Config files +------------ + +`config.yaml` is the machine catalog. `binaries:` are build artifacts reso= lved +under `binaries/` (overridable with `$QTB_BINARIES_DIR`); absolute paths p= ass +through. + +Machine entries omit any optional field left at its default. +Here are the parameters: + +- `pcpu` (required): host physical cpus. +- `mmu_type` (default `sv48`): Xen host MMU type. +- `xen_bootargs` (default `""`): Xen command line. + +`.yaml` describes the tests of that type. + +`console-test` +-------------- + +A test names a machine and maps a console index to the string(s) expected = on +that console: index 0 is Xen's own console (`con0`, logged as `con0.log`). + +``` +machine_catalog: config.yaml # the catalog to resolve machine names aga= inst +tests: + dom0less-1smp-0domu-1vcpu-aplic-imsic-null: + machine: dom0less-1smp-0domu-1vcpu-aplic-imsic-null # a name in conf= ig.yaml + expect: + 0: [All set up] # Xen itself must print "All set up" +``` + +Logic, per console: + +1. read the console +2. wait for each expected string in turn, in the order listed +3. bound each wait by `timeout` seconds, retrying a timed-out wait up to + `attempts` times + +The map itself must not be empty, otherwise the test would pass without +asserting anything. + +Device trees (dts/) +------------------- + +Jinja2 template, generated per machine and compiled with dtc: + +- `qemu-host.dts.j2` - the Xen host tree: the hart count, the host MMU typ= e and + the Xen command line. + +Adding a test +------------- + +To add a test to an existing type (e.g. `console-test`): + +1. Pick a machine from `config.yaml`, or add a new one under `machines:` (= set + `pcpu`, and any optional field that differs from its default =E2=80=94 = see the + field list above). +2. Add a test entry under `tests:` in the type's `.yaml`, naming that + machine and supplying the type's own parameters (for `console-test`, one + `expect` list per console). +3. Run it: `./qemu_smoke_riscv64.py console-test run `. + +No code change is needed, a test is pure config. + +Adding a new test type +---------------------- + +1. Create `mytype/` with `mytype.py` defining a `RiscvQtbTest` subclass: s= et + `type_id`, `description`, and `config_file`, and implement `from_config= `, + `list_tests`, and `run(vm)`. +2. Add `mytype/__init__.py` that does `from .mytype import MyType`. +3. Add `MyType` to `TEST_TYPES` in `__init__.py` so the CLI exposes it. + +Unit tests +---------- + +The `unit/` directory holds pytest tests of the framework's own logic (con= fig +parsing, device-tree rendering, QEMU arg assembly). They do not boot QEMU = and +are independent of the CI smoke tests, but they import the framework, so t= hey +need the prerequisites above plus `pytest`. + +Run from the Xen tree root: + +``` +python3 -m pytest automation/scripts/qtb/riscv/unit/ +``` From nobody Thu Sep 3 07:05:38 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) client-ip=192.237.175.120; envelope-from=xen-devel-bounces@lists.xenproject.org; helo=lists.xenproject.org; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass(p=none dis=none) header.from=vates.tech ARC-Seal: i=1; a=rsa-sha256; t=1787823824; cv=none; d=zohomail.com; s=zohoarc; b=lFQfMiee2bwN2JFCda5W7TqOvNtCwLPc0VS+LqYMiyQTfgJTsTd8buDJfypp68OUKkSo01U9d3a2Fpbi1EMzHyjkdgTD0cDp3aIOKC+PuC7ynPRoG2O43JBwd34wVBUHINUAYaOioWYkMgwRMSg+/OWa5LhqHrsdvC+ZPxCU5GE= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1787823824; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=euv1GKoVBRcVfOMKB9vvDBqdsaalOO1hGV5PMr23tAI=; b=GVvW2rbuCjnBe4a05a7DAY1qqB3BIHPX95ahI2WH/u12satLmkrG9NN1WT6fgx5l/jGsO98RkRlEbuXWMaabTwbtfXo8vs7q278eb/cpOAhR5Exx5McsgdP0TZg+OuiGDcY4v44LYtj0mASqB9Lqo3UI/t07IbHxKmNkhGm3BM0= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of lists.xenproject.org designates 192.237.175.120 as permitted sender) smtp.mailfrom=xen-devel-bounces@lists.xenproject.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.xenproject.org (lists.xenproject.org [192.237.175.120]) by mx.zohomail.com with SMTPS id 1787823824040920.1698695988078; Thu, 27 Aug 2026 02:43:44 -0700 (PDT) Received: from list by lists.xenproject.org with outflank-mailman.1400483.1636168 (Exim 4.92) (envelope-from ) id 1wzWdo-0006RS-Sd; Thu, 27 Aug 2026 09:43:24 +0000 Received: by outflank-mailman (output) from mailman id 1400483.1636168; Thu, 27 Aug 2026 09:43:24 +0000 Received: from localhost ([127.0.0.1] helo=lists.xenproject.org) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdo-0006RI-OR; Thu, 27 Aug 2026 09:43:24 +0000 Received: by outflank-mailman (input) for mailman id 1400483; Thu, 27 Aug 2026 09:43:23 +0000 Received: from mx.expurgate.net ([195.190.135.20]) by lists.xenproject.org with esmtp (Exim 4.92) (envelope-from ) id 1wzWdn-0006Oz-Hz for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 09:43:23 +0000 Received: from mx.expurgate.net (helo=localhost) by mx.expurgate.net with esmtp id 1wzWdm-00AoWH-Uk for xen-devel@lists.xenproject.org; Thu, 27 Aug 2026 11:43:22 +0200 Received: from [10.42.69.8] (helo=localhost) by localhost with ESMTP (eXpurgate MTA 0.9.1) (envelope-from ) id 6a9006ad-e002-0a2a0a5209dd-0a2a4508ba2a-48 for ; Thu, 27 Aug 2026 11:43:22 +0200 Received: from [185.255.28.34] (helo=prod-mta-13-01.swg-srv.net) by tlsNG-c1860d.mxtls.expurgate.net with ESMTPS (eXpurgate 4.57.1) (envelope-from ) id 6a9006ba-f659-0a2a45080019-b9ff1c22aa53-3 for ; Thu, 27 Aug 2026 11:43:22 +0200 Received: from mail2.vates.fr ([37.26.189.201] mail2.vates.fr) (Authenticated sender: 8631fc262581453bbf619ec5b2062170/smtp/7773de5a-2839-4720-82ee-e06722ae1d3e) by prod-mta-13-01.swg-srv.net (ZoneMTA - prod-mta-13) with ESMTPSA id 1a0429a155d000c4f3.003 for (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Thu, 27 Aug 2026 09:43:09 +0000 Received: from leducb.home (areims-651-1-80-194.w90-18.abo.wanadoo.fr [90.18.187.194]) (Authenticated sender: baptiste.le-duc) by mail2.vates.fr (Postfix) with ESMTPSA id D77DC81230; Thu, 27 Aug 2026 11:43:08 +0200 (CEST) X-Outflank-Mailman: Message body and most headers restored to incoming version X-BeenThere: xen-devel@lists.xenproject.org List-Id: Xen developer discussion List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Errors-To: xen-devel-bounces@lists.xenproject.org Precedence: list Sender: "Xen-devel" Authentication-Results: eu.smtp.expurgate.cloud; dkim=pass header.s=selector1 header.d=vates.tech header.i="@vates.tech" header.h="From:Subject:Date:Message-ID:To:Cc:MIME-Version:Content-Transfer-Encoding:In-Reply-To:References:Feedback-ID" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vates.tech; q=dns/txt; s=selector1; bh=euv1GKoVBRcVfOMKB9vvDBqdsaalOO1hGV5PMr23tAI=; h=from:subject:date:message-id:to:cc:mime-version:content-transfer-encoding:in-reply-to:references:feedback-id; b=S1WARVRDzC3L9J/lVdNZ9KNGpcxecckW0Mi0iXhWSBGlPtmgIkvMRv59WML/ehuBa11xHCNl7 GRjVD8PNvreQYoZqUcxD8LIGiUqIp/8UdhloSOXsmSGAXyyCWSN/ebNq0NmXhT7EH2SU6F8d1BL GHgheKikl4be4OWStsO3p78ZeKLviBsW1HvafHRMZ6QsLNP6sxltYoSf6AfFDVV2Bn9DhXlIRvY wdan5sdvQEIhGOmcQBLWWu/guIhS7lL/hMVB+94NX2dA4MK1Q5V1l1gy6kyhOoISw+IGfegctRs zfdFykhN1T2zDs7TVKguKiqmeCcu5ZRWh3qIYHD8uFdA== X-Zone-Loop: 979fe070d2e3795f14efb2f7b7383ec99d3be8436984 x-campaign-type: default x-transaction-id: da2da8be-e678-48f8-be08-ab76297a3d86 x-swg-uid: 01-344139b8-3069-4c6a-a68e-f1ac67380296 X-Mailer: Sweego Message-ID: <1787823789.8631fc262581453bbf619ec5b2062170.1a0429a155d000c4f3@vates.tech> x-swg-bid: 1787823789.8631fc262581453bbf619ec5b2062170.1a0429a155d000c4f3 Feedback-ID: default:8631fc262581453bbf619ec5b2062170:Sweego x-campaign-id: default x-client-id: 8631fc262581453bbf619ec5b2062170 X-Originating-IP: [37.26.189.201] From: Baptiste Le Duc To: xen-devel@lists.xenproject.org Cc: Baptiste Le Duc , Doug Goldstein , Stefano Stabellini Subject: [PATCH v2 6/6] CI: run the riscv64 smoke test via QTB framework console-test Date: Thu, 27 Aug 2026 11:42:52 +0200 In-Reply-To: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> References: <1787823334.8631fc262581453bbf619ec5b2062170.1a04293255c000c4f3@vates.tech> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 4ffbd6c1-ee69-4e1b-aabd-f977039bd3e2 X-Bm-Transport-Timestamp: 1787823788970 X-purgate-ID: tlsNG-c1860d/1787823802-CD34D87B-C4E01DE3/0/0 X-purgate-type: clean X-purgate-size: 4129 X-ZohoMail-DKIM: pass (identity @vates.tech) X-ZM-MESSAGEID: 1787823825604158501 Content-Type: text/plain; charset="utf-8" qemu-smoke-riscv64-gcc drove QEMU through automation/scripts/qemu-smoke-riscv64.sh, an expect wrapper whose machine description (cpus, memory, device tree, console wiring) lived in the script itself. The QTB framework now owns all of that: machines come from the shared catalog, expectations from the test type's own YAML. Turn .qemu-riscv64 into a template running qemu_smoke_riscv64.py run in the qtb-riscv64 container, machine and test picked per job through QTB_TEST_TYPE/QTB_TEST. The container comes from the test-artifacts registry, hence the new ARTIFACTS_REGISTRY next to the existing ARTIFACTS_REPO/ARTIFACTS_BRANCH. QTB_BINARIES_DIR points at the artifacts of the job (Xen only currently, but aims to have initrd and linux images when dom0less will be supported). QTB_LOG_DIR collects the per-console logs, kept on failure and on success. Point qemu-smoke-riscv64-gcc at that template, running the console-test type on dom0less-1smp-0domu-1vcpu-aplic-imsic-null: a Xen-only machine, so the smoke check is Xen's own "All set up" on console 0, the same string the expect script waited for. Drop automation/scripts/qemu-smoke-riscv64.sh as it has no caller left in the CI after this patch and drop smoke.serial from the .qemu-riscv64 artifacts since no riscv64 job uses it anymore, the logs are now kept under QTB_LOG_DIR. Assisted-by: Claude:claude-opus-5 Signed-off-by: Baptiste Le Duc --- .gitlab-ci.yml | 3 +++ automation/gitlab-ci/test.yaml | 20 ++++++++++++++------ automation/scripts/qemu-smoke-riscv64.sh | 19 ------------------- 3 files changed, 17 insertions(+), 25 deletions(-) delete mode 100755 automation/scripts/qemu-smoke-riscv64.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f42a9abeaa..15f93b8634 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,9 @@ variables: ARTIFACTS_BRANCH: description: "Branch in test-artifacts to use" value: master + ARTIFACTS_REGISTRY: + description: "Registry holding the test-artifacts containers" + value: registry.gitlab.com/xen-project/hardware/test-artifacts LINUX_JOB_X86_64: description: "Job name in test-artifacts to use for Linux x86_64" value: linux-6.6.56-x86_64 diff --git a/automation/gitlab-ci/test.yaml b/automation/gitlab-ci/test.yaml index 61adc1baff..e9dd147380 100644 --- a/automation/gitlab-ci/test.yaml +++ b/automation/gitlab-ci/test.yaml @@ -72,14 +72,21 @@ TEST_TIMEOUT_OVERRIDE: 120 =20 .qemu-riscv64: + image: ${ARTIFACTS_REGISTRY}/${CONTAINER} extends: .test-jobs-common variables: - CONTAINER: debian:13-riscv64 - LOGFILE: qemu-smoke-riscv64.log + CONTAINER: debian:13-qtb-riscv64 + QTB_LOG_DIR: qtb-logs + QTB_BINARIES_DIR: ${CI_PROJECT_DIR}/binaries + script: + - ./automation/scripts/qemu_smoke_riscv64.py + ${QTB_TEST_TYPE} + run + ${QTB_TEST} + --log-dir ${QTB_LOG_DIR} artifacts: paths: - - smoke.serial - - '*.log' + - ${QTB_LOG_DIR} when: always tags: - x86_64 @@ -779,8 +786,9 @@ qemu-xtf-argo-x86_64-gcc-debug: =20 qemu-smoke-riscv64-gcc: extends: .qemu-riscv64 - script: - - ./automation/scripts/qemu-smoke-riscv64.sh 2>&1 | tee ${LOGFILE} + variables: + QTB_TEST_TYPE: console-test + QTB_TEST: dom0less-1smp-0domu-1vcpu-aplic-imsic-null needs: - debian-13-riscv64-gcc-debug =20 diff --git a/automation/scripts/qemu-smoke-riscv64.sh b/automation/scripts/= qemu-smoke-riscv64.sh deleted file mode 100755 index c0b1082a08..0000000000 --- a/automation/scripts/qemu-smoke-riscv64.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -set -ex -o pipefail - -# Run the test -rm -f smoke.serial - -export TEST_CMD=3D"qemu-system-riscv64 \ - -M virt,aia=3Daplic-imsic \ - -cpu rv64,svpbmt=3Don \ - -smp 1 \ - -nographic \ - -m 2g \ - -kernel binaries/xen" - -export TEST_LOG=3D"smoke.serial" -export PASSED=3D"All set up" - -./automation/scripts/console.exp |& sed 's/\r\+$//'