NVIDIA GPU driver infrastructure
 help / color / mirror / Atom feed
* [PATCH drm-test] drm-test: run the tests with libtest
@ 2026-10-22 20:45 Edwin Peer
  0 siblings, 0 replies; only message in thread
From: Edwin Peer @ 2026-10-22 20:45 UTC (permalink / raw)
  To: Danilo Krummrich
  Cc: Edwin Peer, Alistair Popple, Alice Ryhl, John Hubbard, nova-gpu,
	dri-devel, rust-for-linux

Replace the hand rolled test runner with libtest, the harness behind the
Cargo #[test] attribute. Tests are now discovered from their annotations
rather than registered by hand - adding one no longer means implementing
the Test trait.

Switching to libtest brings a number of benefits:

  - Tests are independent. Each one runs with panics caught, so a failed
    assertion reports just that test as failed instead of aborting the
    process and leaving the remaining tests unreported. Pass --fail-fast
    (nightly, with -Z unstable-options) to stop at the first failure
    instead.

  - Results are reported per test, with a summary and a non-zero exit
    status when any test fails, which is easier to consume from CI than
    a single verdict for the whole suite.

  - Tests are selected by name, which replaces the driver dispatch that
    keyed off argv[1]. Tests are namespaced by their module path, so the
    previous invocation still selects the nova tests:

        drm-test nova

    while individual tests can now be run on their own:

        drm-test nova::test::test_gem_size

    and --list enumerates the tests without touching the hardware. See
    --exact and --skip for the remaining filtering options.

  - Test output is captured and printed only for failing tests, so that
    passing runs stay quiet without losing diagnostics when they are
    needed. Pass --show-output to print the output of successful tests
    too, or --no-capture to print it as it happens, as before.

Note that libtest runs tests in parallel; pass --test-threads=1 to run
them one at a time.

libtest is linked into the test binary, so cargo is only required to
build the tests, not to run them. The binary is built from the --tests
target, which cargo keeps in an internal location that differs between
releases, so add build.sh to place it in the target directory, where the
binary was found before.

Signed-off-by: Edwin Peer <epeer@nvidia.com>
---
This patch is for drm-test, the userspace test suite for the nova uAPI. It
lives outside the kernel tree, at:

  https://gitlab.freedesktop.org/dakr/drm-test

It is posted here for wider review, and is also open as a merge request
against that repository:

  https://gitlab.freedesktop.org/dakr/drm-test/-/merge_requests/3

The motivation is running these tests from CI. The current runner reports a
single verdict for the whole suite, and a failed assertion aborts the process,
so the tests after it go unreported. libtest reports a result per test and,
with --format json, machine readable output.

Tested on Turing (TU104), Ampere (GA104) and Blackwell (GB102), against
drm-rust-next.

 build.sh         |  49 ++++++++++++++
 src/drm/mod.rs   |   5 --
 src/lib.rs       |   2 +
 src/main.rs      |  18 ------
 src/nova/mod.rs  |   4 +-
 src/nova/test.rs | 163 +++++++++++++++++++++--------------------------
 6 files changed, 124 insertions(+), 117 deletions(-)
 create mode 100755 build.sh
 delete mode 100644 src/main.rs

diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..0134846
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# Build drm-test and place the binary in the target directory, where a plain
+# `cargo build` would have left it, e.g. target/debug/drm-test.
+#
+# The tests are run by libtest, so the executable is built from the --tests
+# target rather than from a plain `cargo build`. Cargo keeps test binaries in
+# an internal location that differs between releases, so ask it where the
+# binary was written and copy it next to the profile directory.
+#
+# Arguments are passed through to cargo, for example:
+#
+#     ./build.sh --release
+#     ./build.sh --target x86_64-unknown-linux-musl
+#
+set -eu
+
+cd "$(dirname "$0")"
+
+# Only the test target reports a non-null "executable". Diagnostics are
+# rendered to stderr as usual, leaving just artifact messages on stdout.
+bin=$(cargo build --tests --message-format=json-render-diagnostics "$@" \
+	| grep -o '"executable":"[^"]*"' \
+	| head -n1 \
+	| sed 's/^"executable":"//; s/"$//')
+
+if [ -z "${bin}" ]; then
+	echo "error: cargo produced no test binary" >&2
+	exit 1
+fi
+
+# Which directory cargo stages the binary in depends on the release: stable
+# uses <profile>/deps, while nightly uses <profile>/build/<pkg>/<hash>/out.
+# Climb out of whichever it used to reach the profile directory.
+dir="${bin%/*}"
+while :; do
+	case "${dir}" in
+	*/deps | */build) break ;;
+	*/*) dir="${dir%/*}" ;;
+	*)
+		echo "error: cannot locate the cargo profile directory" >&2
+		exit 1
+		;;
+	esac
+done
+
+dest="${dir%/*}/drm-test"
+cp -f "${bin}" "${dest}"
+echo "built: ${dest}"
diff --git a/src/drm/mod.rs b/src/drm/mod.rs
index c767dbe..5d6f280 100644
--- a/src/drm/mod.rs
+++ b/src/drm/mod.rs
@@ -85,8 +85,3 @@ impl Drm {
         Ok(())
     }
 }
-
-/// Represents the test functions all drivers must provide.
-pub trait Test {
-    fn all(&self);
-}
diff --git a/src/lib.rs b/src/lib.rs
index 663f484..5f73c55 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ mod bindings;
 pub mod drm;
 pub mod nova;
 
+#[cfg(test)]
 macro_rules! assert_ge {
     ($left:expr, $right:expr $(,)?) => {{
         match (&$left, &$right) {
@@ -16,4 +17,5 @@ macro_rules! assert_ge {
         }
     }};
 }
+#[cfg(test)]
 pub(crate) use assert_ge;
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index 2034285..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use drm_test::{drm, nova};
-use std::{env, process::exit};
-
-fn main() {
-    let args: Vec<String> = env::args().collect();
-
-    if args.len() < 2 {
-        println!("Usage: {} {{ nova }}", args[0]);
-        exit(1);
-    }
-
-    let test: Box<dyn drm::Test> = match args[1].as_str() {
-        "nova" => Box::new(nova::Test::open()),
-        _ => todo!(),
-    };
-
-    test.all();
-}
diff --git a/src/nova/mod.rs b/src/nova/mod.rs
index 7b53758..1d03335 100644
--- a/src/nova/mod.rs
+++ b/src/nova/mod.rs
@@ -1,8 +1,8 @@
 pub mod gpu;
 pub mod nova;
-pub mod test;
+#[cfg(test)]
+mod test;
 
 pub use self::nova::Nova;
-pub use self::test::Test;
 
 pub use self::gpu::{Architecture, Chipset};
diff --git a/src/nova/test.rs b/src/nova/test.rs
index 75c92d8..a02e449 100644
--- a/src/nova/test.rs
+++ b/src/nova/test.rs
@@ -1,106 +1,85 @@
 use std::{ffi::CStr, io, mem::size_of};
 
 use crate::{
-    assert_ge, bindings, drm,
+    assert_ge, bindings,
     nova::{Architecture, Chipset, Nova},
 };
 
-pub struct Test(Nova);
+/// Test whether GPU information is reported correctly.
+#[test]
+fn test_gpu_info() {
+    let nova = Nova::open().unwrap();
+    let info = nova.gpu_info().unwrap();
+    let gpu_name =
+        CStr::from_bytes_until_nul(&info.gpu_name).expect("GPU name must be NUL-terminated");
+    let gpu_short_name = CStr::from_bytes_until_nul(&info.gpu_short_name)
+        .expect("GPU short name must be NUL-terminated");
+    let architecture = Architecture::from_repr(info.architecture)
+        .map(|arch| arch.to_string())
+        .unwrap_or_else(|| "unknown".into());
+    let chipset = Chipset::from_repr(info.chipid)
+        .map(|chip| chip.to_string())
+        .unwrap_or_else(|| "unknown".into());
 
-impl Test {
-    /// Create a new `Test` instance.
-    pub fn open() -> Self {
-        let nova = Nova::open().unwrap();
-
-        Self(nova)
-    }
-
-    /// Test whether GPU information is reported correctly.
-    pub fn test_gpu_info(&self) {
-        let info = self.0.gpu_info().unwrap();
-        let gpu_name =
-            CStr::from_bytes_until_nul(&info.gpu_name).expect("GPU name must be NUL-terminated");
-        let gpu_short_name = CStr::from_bytes_until_nul(&info.gpu_short_name)
-            .expect("GPU short name must be NUL-terminated");
-        let architecture = Architecture::from_repr(info.architecture)
-            .map(|arch| arch.to_string())
-            .unwrap_or_else(|| "unknown".into());
-        let chipset = Chipset::from_repr(info.chipid)
-            .map(|chip| chip.to_string())
-            .unwrap_or_else(|| "unknown".into());
-
-        assert_ne!(info.architecture, 0);
-        println!("NOVA GPU information:");
-        println!(
-            "  Architecture: {} ({:#x})",
-            architecture, info.architecture
-        );
-        println!("  Chipset: {} ({:#x})", chipset, info.chipid);
-        println!(
-            "  VRAM size: {} bytes ({:.2} GiB)",
-            info.vram_size,
-            info.vram_size as f64 / (1u64 << 30) as f64
-        );
-        println!(
-            "  VRAM BAR size: {} bytes ({:.2} GiB)",
-            info.vram_bar_size,
-            info.vram_bar_size as f64 / (1u64 << 30) as f64
-        );
-        println!("  GPU name: {}", gpu_name.to_string_lossy());
-        println!("  GPU short name: {}", gpu_short_name.to_string_lossy());
-        println!(
-            "  GPU GID: {}",
-            [
-                &info.gpu_gid[..4],
-                &info.gpu_gid[4..6],
-                &info.gpu_gid[6..8],
-                &info.gpu_gid[8..10],
-                &info.gpu_gid[10..],
-            ]
-            .into_iter()
-            .map(|chunk| chunk
-                .iter()
-                .map(|byte| format!("{byte:02x}"))
-                .collect::<String>())
-            .collect::<Vec<_>>()
-            .join("-")
-        );
-    }
-
-    /// Test whether a NULL data pointer reports the required GPU information size.
-    pub fn test_gpu_info_size(&self) {
-        let size = self.0.gpu_info_size().unwrap();
-
-        assert_ge!(size as usize, size_of::<bindings::drm_nova_info_gpu>());
-    }
+    assert_ne!(info.architecture, 0);
+    println!("NOVA GPU information:");
+    println!(
+        "  Architecture: {} ({:#x})",
+        architecture, info.architecture
+    );
+    println!("  Chipset: {} ({:#x})", chipset, info.chipid);
+    println!(
+        "  VRAM size: {} bytes ({:.2} GiB)",
+        info.vram_size,
+        info.vram_size as f64 / (1u64 << 30) as f64
+    );
+    println!(
+        "  VRAM BAR size: {} bytes ({:.2} GiB)",
+        info.vram_bar_size,
+        info.vram_bar_size as f64 / (1u64 << 30) as f64
+    );
+    println!("  GPU name: {}", gpu_name.to_string_lossy());
+    println!("  GPU short name: {}", gpu_short_name.to_string_lossy());
+    println!(
+        "  GPU GID: {}",
+        [
+            &info.gpu_gid[..4],
+            &info.gpu_gid[4..6],
+            &info.gpu_gid[6..8],
+            &info.gpu_gid[8..10],
+            &info.gpu_gid[10..],
+        ]
+        .into_iter()
+        .map(|chunk| chunk
+            .iter()
+            .map(|byte| format!("{byte:02x}"))
+            .collect::<String>())
+        .collect::<Vec<_>>()
+        .join("-")
+    );
+}
 
-    fn gem_create(&self, size: u64) -> u32 {
-        self.0.gem_create(size).unwrap()
-    }
+/// Test whether a NULL data pointer reports the required GPU information size.
+#[test]
+fn test_gpu_info_size() {
+    let nova = Nova::open().unwrap();
+    let size = nova.gpu_info_size().unwrap();
 
-    fn gem_size(&self, handle: u32) -> u64 {
-        self.0.gem_size(handle).unwrap()
-    }
+    assert_ge!(size as usize, size_of::<bindings::drm_nova_info_gpu>());
+}
 
-    /// Test whether GEM objects are created with the expected size.
-    pub fn test_gem_size(&self) {
-        let ret = self.0.gem_create(0x0);
-        let err = ret.expect_err("GEM object must not be zero-sized!");
-        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
+/// Test whether GEM objects are created with the expected size.
+#[test]
+fn test_gem_size() {
+    let nova = Nova::open().unwrap();
 
-        let handle = self.gem_create(0x1000 + 0x1);
-        assert_eq!(self.gem_size(handle), 0x2000);
+    let ret = nova.gem_create(0x0);
+    let err = ret.expect_err("GEM object must not be zero-sized!");
+    assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
 
-        let handle = self.gem_create(0x2000);
-        assert_eq!(self.gem_size(handle), 0x2000);
-    }
-}
+    let handle = nova.gem_create(0x1000 + 0x1).unwrap();
+    assert_eq!(nova.gem_size(handle).unwrap(), 0x2000);
 
-impl drm::Test for Test {
-    // Run all tests.
-    fn all(&self) {
-        self.test_gpu_info();
-        self.test_gpu_info_size();
-        self.test_gem_size();
-    }
+    let handle = nova.gem_create(0x2000).unwrap();
+    assert_eq!(nova.gem_size(handle).unwrap(), 0x2000);
 }
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-22 20:46 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-22 20:45 [PATCH drm-test] drm-test: run the tests with libtest Edwin Peer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox