driver-manager: P0 — claim-via-channel collapse, orphan-patch resolution, modern_tech/exec removal

P0-1: Collapse the device claim into pcid's channel open (ENOLCK
exclusivity) — the pcid-spawner model. The assumed /scheme/pci/<addr>/bind
endpoint never existed in pcid (orphaned P3 patches); every probe would
have defer-looped on ENOENT at runtime. probe() now does a single
PciFunctionHandle::connect_by_path: ENOLCK -> next candidate, then
enable_device + into_inner_fd -> PCID_CLIENT_CHANNEL. claim_pci_device
and open_pcid_channel deleted; SpawnedDriver stores the channel fd.

P0-2: Resolve orphaned patches per the decision tree:
P3-pcid-bind-scheme.patch -> legacy-superseded (design rejected —
channel ENOLCK is the claim); P3-pcid-uevent-format-fix.patch ->
legacy-superseded (0-byte uevent stub superseded by the accepted
polling model; AER content duplicates the retained aer-scheme patch);
P3-pcid-aer-scheme.patch retained as the P2-1 producer blueprint.
SUPERSEDED.md audit log added.

P0-3: Remove advisory theater and suppressed dead code:
- modern_tech.rs deleted (hardcoded C/P-state 'advisories' to JSON
  files nothing reads; msix proposal computed then discarded). The
  useful parts are now correctly wired as spawn env hints:
  REDBEAR_DRIVER_IOMMU_GROUP / REDBEAR_DRIVER_NUMA_NODE /
  REDBEAR_DRIVER_MSIX_VECTORS (same pattern as the quirk hints).
- redox-driver-core: CStateCoordinator/PStateCoordinator and their
  advisory-path helpers deleted (no consumers anywhere after the
  driver-manager removal); IOMMU/NUMA/MSI-X helpers retained.
- exec.rs deleted (dead spawn_driver with #[allow(dead_code)]).

88 tests pass (53 driver-manager + 30 redox-driver-core lib + 5 dynid);
repo cook driver-manager succeeds for x86_64-unknown-redox with zero
crate-local warnings; audit-no-stubs: 0 violations.
This commit is contained in:
2026-07-23 11:55:11 +09:00
parent 7e8d63ecc1
commit c6fb24ae28
8 changed files with 84 additions and 493 deletions
@@ -0,0 +1,43 @@
# Supersession Audit Log — pcid bind/uevent Patches
**Date:** 2026-07-22
**Source:** `local/patches/base/`
**Destination:** `local/patches/legacy-superseded-2026-07-22/base/`
**Trigger:** driver-manager v3.0 major assessment
(`local/docs/evidence/driver-manager/ASSESSMENT-2026-07-22.md`, finding B1/G1)
Per AGENTS.md § "Orphan-Patch Supersession Decision Tree": both patches
were orphans — their content exists nowhere in the `local/sources/base`
fork (zero matching commits), and because `base` is a `path =` fork
recipe, the cookbook never applied them. driver-manager v1.3v2.2 was
written against the *assumed* `/scheme/pci/<addr>/bind` endpoint from
`P3-pcid-bind-scheme.patch` and would have defer-looped forever at
runtime (every open → ENOENT → Deferred).
## Classification
### `P3-pcid-bind-scheme.patch` → SUPERSEDED (design rejected)
Bucket (a) — superseded by a better in-tree mechanism. The patch adds a
separate `/scheme/pci/<addr>/bind` endpoint with an `EALREADY` claim
map. pcid already provides atomic exclusivity: a second open of a
device's `channel` returns `ENOLCK`
(`local/sources/base/drivers/pcid/src/scheme.rs:396-398`), and
pcid-spawner has used channel-as-claim in production since the
beginning. driver-manager v3.0 (P0-1) collapses its claim into the
channel open, making the patch's endpoint redundant. The duplicate
exclusivity mechanism is rejected per the no-redundant-mechanisms rule.
### `P3-pcid-uevent-format-fix.patch` → SUPERSEDED (stub + duplication)
Two parts: (1) a `/scheme/pci/uevent` endpoint whose reads return 0
bytes — a placeholder, not an event stream; superseded by the accepted
250 ms hotplug enumeration-poll model. A real push model (fevent-driven)
is future work under plan P2 and will not reuse this stub. (2) AER
register content that duplicates `P3-pcid-aer-scheme.patch`, which is
retained.
### `P3-pcid-aer-scheme.patch` → RETAINED in `local/patches/base/`
Bucket (c) — genuine MISSING-UPSTREAM gap, kept as the blueprint for
plan P2-1 (per-device AER register files produced by pcid). Not moved.
@@ -1,10 +1,7 @@
//! Modern-technology-surface helper module for [`DeviceManager`].
//!
//! See migration plan § 5.1 D2.2 + D2.3. Provides concrete (non-stub)
//! See migration plan § 5.1 D2.3. Provides concrete (non-stub)
//! implementations of:
//! - C-state and P-state coordination: the manager publishes JSON
//! advisories to `/var/run/driver-manager-{cstate,pstate}.json` so
//! downstream `thermald` and `cpufreqd` can read them.
//! - IOMMU group registration: queries `/scheme/iommu/domain/N` and
//! assigns each device a group number.
//! - MSI-X vector proposal: counts available vectors and proposes an
@@ -12,185 +9,14 @@
//! - NUMA node lookup: queries `/scheme/numad` and reports the node id
//! for a given BDF.
//!
//! These helpers are real implementations, not stubs. If a downstream
//! `cpufreqd` / `thermald` / `iommu` / `numad` is not present at runtime
//! the helper reports a clean error and the manager logs+skips.
//! If the `iommu` / `numad` scheme is not present at runtime the helper
//! reports a clean error or a deterministic synthetic fallback and the
//! caller logs+skips.
extern crate alloc;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::device::DeviceId;
const CSTATE_PATH_DEFAULT: &str = "/var/run/driver-manager-cstate.json";
const PSTATE_PATH_DEFAULT: &str = "/var/run/driver-manager-pstate.json";
/// Where the C-state advisory JSON should be written. The default is
/// `/var/run/driver-manager-cstate.json`; tests override this to a
/// tmpfile path so they don't write to the live system.
pub fn cstate_advisory_path() -> PathBuf {
if let Ok(p) = std::env::var("DRIVER_MANAGER_CSTATE_PATH") {
return PathBuf::from(p);
}
PathBuf::from(CSTATE_PATH_DEFAULT)
}
/// Where the P-state advisory JSON should be written.
pub fn pstate_advisory_path() -> PathBuf {
if let Ok(p) = std::env::var("DRIVER_MANAGER_PSTATE_PATH") {
return PathBuf::from(p);
}
PathBuf::from(PSTATE_PATH_DEFAULT)
}
/// C-state coordinator. Records "C-state go-deeper" events (e.g. when a
/// device is unbound, the manager may want to nudge thermald toward a
/// deeper CPU idle state).
pub struct CStateCoordinator {
path: PathBuf,
history: Vec<CStateEvent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CStateEvent {
pub device: DeviceId,
/// Suggested new C-state level (`0` = C0 active, `1` = C1 halt, etc.).
pub suggested_level: u8,
/// Optional reason (free-form short string).
pub reason: String,
}
impl CStateCoordinator {
pub fn new(path: PathBuf) -> Self {
Self {
path,
history: Vec::new(),
}
}
pub fn default_at(path: PathBuf) -> Self {
Self::new(path)
}
/// Record a C-state event and flush the advisory to disk.
pub fn record(&mut self, event: CStateEvent) -> Result<(), String> {
self.history.push(event.clone());
self.write_advisory()
}
/// Write the cumulative advisory to disk.
pub fn write_advisory(&self) -> Result<(), String> {
let mut s = String::new();
s.push_str("{\"events\":[");
for (i, e) in self.history.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&format!(
"{{\"device\":\"{}\",\"level\":{},\"reason\":\"{}\"}}",
escape_json(&e.device.path),
e.suggested_level,
escape_json(&e.reason),
));
}
s.push_str("]}");
write_atomic(&self.path, s.as_bytes())
.map_err(|e| format!("cstate write failed: {}", e))
}
pub fn event_count(&self) -> usize {
self.history.len()
}
}
/// P-state coordinator. Records "P-state go-faster" or "P-state
/// go-slower" events for downstream `cpufreqd`.
pub struct PStateCoordinator {
path: PathBuf,
history: Vec<PStateEvent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PStateEvent {
pub device: DeviceId,
/// Suggested P-state index (`0` = lowest performance, `n-1` = max).
pub suggested_state: u8,
pub reason: String,
}
impl PStateCoordinator {
pub fn new(path: PathBuf) -> Self {
Self {
path,
history: Vec::new(),
}
}
pub fn default_at(path: PathBuf) -> Self {
Self::new(path)
}
pub fn record(&mut self, event: PStateEvent) -> Result<(), String> {
self.history.push(event.clone());
self.write_advisory()
}
pub fn write_advisory(&self) -> Result<(), String> {
let mut s = String::new();
s.push_str("{\"events\":[");
for (i, e) in self.history.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&format!(
"{{\"device\":\"{}\",\"state\":{},\"reason\":\"{}\"}}",
escape_json(&e.device.path),
e.suggested_state,
escape_json(&e.reason),
));
}
s.push_str("]}");
write_atomic(&self.path, s.as_bytes())
.map_err(|e| format!("pstate write failed: {}", e))
}
pub fn event_count(&self) -> usize {
self.history.len()
}
}
fn escape_json(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
let tmp = path.with_extension("tmp");
{
let mut f = fs::File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_data()?;
}
fs::rename(&tmp, path)
}
use std::path::Path;
/// IOMMU group for a PCI device. Reads `/scheme/iommu/domain/N` files
/// and returns the assigned group number. If the iommu daemon is not
@@ -339,59 +165,6 @@ pub fn numa_node_for(bdf: &str) -> NumaNode {
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn temp_path(label: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("rb-dm-{}-{}-{}", label, pid, nanos))
}
#[test]
fn cstate_coordinator_writes_valid_json() {
let p = temp_path("cstate.json");
let mut coord = CStateCoordinator::new(p.clone());
let dev = DeviceId {
bus: "pci".to_string(),
path: "0000:00:1f.2".to_string(),
};
coord
.record(CStateEvent {
device: dev,
suggested_level: 3,
reason: "storage unbound".to_string(),
})
.unwrap();
let body = fs::read_to_string(&p).unwrap();
assert!(body.contains("\"level\":3"));
assert!(body.contains("storage unbound"));
assert_eq!(coord.event_count(), 1);
}
#[test]
fn pstate_coordinator_writes_valid_json() {
let p = temp_path("pstate.json");
let mut coord = PStateCoordinator::new(p.clone());
let dev = DeviceId {
bus: "pci".to_string(),
path: "0000:01:00.0".to_string(),
};
coord
.record(PStateEvent {
device: dev,
suggested_state: 7,
reason: "GPU bind".to_string(),
})
.unwrap();
let body = fs::read_to_string(&p).unwrap();
assert!(body.contains("\"state\":7"));
}
#[test]
fn iommu_group_returns_synthetic_when_scheme_missing() {
@@ -422,12 +195,4 @@ mod tests {
let n = numa_node_for("0000:00:00.0");
assert_eq!(n.source, NumaSource::Synthetic);
}
#[test]
fn escape_json_handles_quotes_and_backslashes() {
let s = escape_json("a\"b\\c\n");
assert!(s.contains("\\\""));
assert!(s.contains("\\\\"));
assert!(s.contains("\\n"));
}
}
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::fs;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::Path;
use std::process::Command;
@@ -25,7 +25,7 @@ const POLL_INTERVAL_MS: u64 = 50;
#[derive(Debug)]
struct SpawnedDriver {
pid: u32,
bind_handle: File,
channel_fd: OwnedFd,
}
#[derive(Debug)]
@@ -518,41 +518,6 @@ fn pci_device_path(info: &DeviceInfo) -> String {
}
}
fn claim_pci_device(info: &DeviceInfo) -> Result<(String, File), ProbeResult> {
let device_path = pci_device_path(info);
let bind_path = format!("{}/bind", device_path);
match OpenOptions::new().read(true).write(true).open(&bind_path) {
Ok(bind_handle) => Ok((device_path, bind_handle)),
Err(err) => match err.raw_os_error() {
Some(code) if code == syscall::EALREADY as i32 || code == 114 => {
log::debug!("device {} already claimed via {}", info.id.path, bind_path);
Err(ProbeResult::NotSupported)
}
_ => Err(ProbeResult::Deferred {
reason: format!("bind {} failed: {}", bind_path, err),
}),
},
}
}
fn open_pcid_channel(device_path: &str) -> Result<OwnedFd, ProbeResult> {
let mut handle = match PciFunctionHandle::connect_by_path(Path::new(device_path)) {
Ok(handle) => handle,
Err(err) => {
return Err(ProbeResult::Deferred {
reason: format!("open channel for {} failed: {}", device_path, err),
});
}
};
handle.enable_device();
let channel_fd = handle.into_inner_fd();
let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) };
Ok(channel_fd)
}
fn check_scheme_available(name: &str) -> bool {
if std::path::Path::new(&format!("/scheme/{}", name)).exists() {
return true;
@@ -588,13 +553,23 @@ impl Driver for DriverConfig {
}
}
// Claim the device BEFORE checking exclusive_with. The claim is
// atomic (pcid's bind is exclusive) so a concurrent probe of the
// same device is handled correctly by pcid's lock. Once we own
// the bind handle, we check exclusive_with before spawning.
let (device_path, bind_handle) = match claim_pci_device(info) {
Ok(claimed) => claimed,
Err(result) => return result,
// Claim the device by opening its pcid channel. The channel is
// exclusive: pcid returns ENOLCK to a second opener, so a
// concurrent probe of the same device loses atomically. This is
// the same model pcid-spawner uses — the channel fd doubles as
// the claim and is later handed to the spawned child.
let device_path = pci_device_path(info);
let mut handle = match PciFunctionHandle::connect_by_path(Path::new(&device_path)) {
Ok(handle) => handle,
Err(err) => {
if err.raw_os_error() == Some(syscall::ENOLCK as i32) {
log::debug!("device {} already claimed (ENOLCK)", device_key);
return ProbeResult::NotSupported;
}
return ProbeResult::Deferred {
reason: format!("channel open for {} failed: {}", device_path, err),
};
}
};
// Mutual exclusion: if this driver is exclusive_with another driver
@@ -703,10 +678,9 @@ impl Driver for DriverConfig {
log::info!("probing {} with driver {}", device_key, self.name);
let channel_fd = match open_pcid_channel(&device_path) {
Ok(channel_fd) => channel_fd,
Err(result) => return result,
};
handle.enable_device();
let channel_fd = handle.into_inner_fd();
let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) };
let mut cmd = Command::new(&actual_path);
for arg in &self.command[1..] {
@@ -734,6 +708,16 @@ impl Driver for DriverConfig {
cmd.env("REDBEAR_DRIVER_DISABLE_ACCEL", "1");
}
let iommu_group = redox_driver_core::modern_technology::iommu_group_for(&info.id.path);
cmd.env(
"REDBEAR_DRIVER_IOMMU_GROUP",
iommu_group.into_inner().group.to_string(),
);
let numa_node = redox_driver_core::modern_technology::numa_node_for(&info.id.path);
cmd.env("REDBEAR_DRIVER_NUMA_NODE", numa_node.node.to_string());
let msix = redox_driver_core::modern_technology::propose_msix_vectors(&info.id.path, 1, 16);
cmd.env("REDBEAR_DRIVER_MSIX_VECTORS", msix.recommended.to_string());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
@@ -744,20 +728,10 @@ impl Driver for DriverConfig {
device_key
);
let mut spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
spawned.insert(device_key.clone(), SpawnedDriver { pid, bind_handle });
spawned.insert(device_key.clone(), SpawnedDriver { pid, channel_fd });
if let Ok(mut p2d) = self.pid_to_device.lock() {
p2d.insert(pid, device_key.clone());
}
if let Some(mt) = crate::modern_tech::modern_tech_for_path(&device_key) {
mt.on_bind(&info.id, "spawned");
let iommu_grp = mt.iommu_group(&info.id);
let numa_node = mt.numa_node(&info.id);
let msix_count = mt.msix_proposal(&info.id, 1, 16);
log::info!(
"spawn-modern-tech: device={} iommu_group={} numa_node={} msix_recommended={}",
device_key, iommu_grp, numa_node, msix_count
);
}
ProbeResult::Bound
}
Err(e) => ProbeResult::Fatal {
@@ -783,13 +757,13 @@ impl Driver for DriverConfig {
match binding {
Some(binding) => {
let bind_fd = binding.bind_handle.as_raw_fd();
let channel_fd = binding.channel_fd.as_raw_fd();
log::info!(
"unbind-request: device {} from driver {} (pid {}, bind fd {})",
"unbind-request: device {} from driver {} (pid {}, channel fd {})",
device_key,
self.name,
binding.pid,
bind_fd
channel_fd
);
match signal_then_collect(binding.pid, Duration::from_millis(GRACE_PERIOD_MS)) {
Ok(()) => {
@@ -799,9 +773,6 @@ impl Driver for DriverConfig {
self.name,
binding.pid
);
if let Some(mt) = crate::modern_tech::modern_tech_for_path(&device_key) {
mt.on_unbind(&info.id, "exited cleanly");
}
Ok(())
}
Err(why) => {
@@ -1,18 +0,0 @@
use std::process::Command;
#[allow(dead_code)]
pub fn spawn_driver(command: &[String]) -> Result<std::process::Child, std::io::Error> {
if command.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"empty command",
));
}
let mut cmd = Command::new(&command[0]);
for arg in &command[1..] {
cmd.arg(arg);
}
cmd.spawn()
}
@@ -2,13 +2,11 @@ mod aer;
mod config;
#[cfg(test)]
mod end_to_end_test;
mod exec;
mod heartbeat;
mod hotplug;
#[cfg(test)]
mod linux_loader;
mod modalias;
mod modern_tech;
mod pciehp;
mod policy;
mod quirks;
@@ -300,8 +298,6 @@ fn main() {
let heartbeat_handle = heartbeat.handle();
let _heartbeat_thread = heartbeat.spawn();
crate::modern_tech::init_modern_tech();
let policy_dir_str = policy_dir.to_string();
let policy_path = std::path::PathBuf::from(&policy_dir_str);
let shared_blacklist = std::sync::Arc::new(
@@ -1,166 +0,0 @@
//! Modern-technology wiring for driver-manager. Each bind / unbind
//! event triggers C-state and P-state advisories (JSON to
//! `/var/run/driver-manager-{cstate,pstate}.json`), plus IOMMU group
//! assignment and NUMA node lookup for the device. These are real
//! implementations, not stubs; the downstream `cpufreqd` and `thermald`
//! daemons can read the JSON when they grow the hook.
//!
//! See `redox-driver-core::modern_technology` for the helpers.
use redox_driver_core::device::DeviceId;
use redox_driver_core::modern_technology::{
cstate_advisory_path, iommu_group_for, numa_node_for, propose_msix_vectors,
pstate_advisory_path, CStateCoordinator, CStateEvent, PStateCoordinator, PStateEvent,
};
/// One coordinator per process. The `Mutex<...>` serialises access so
/// that hotplug + cold-bind can both write without conflicting on the
/// underlying file.
pub struct ModernTech {
cstate: std::sync::Mutex<CStateCoordinator>,
pstate: std::sync::Mutex<PStateCoordinator>,
}
/// Process-wide `ModernTech` instance. Initialized in `main()` and
/// consulted by `config::probe()` and `config::remove()` on bind/unbind.
/// A `None` value (e.g. in tests that never call `init`) is treated as
/// no-op (matches the behaviour of the missing-scheme path in
/// `redox-driver-core::modern_technology`).
pub static MODERN_TECH: std::sync::OnceLock<ModernTech> = std::sync::OnceLock::new();
pub fn init_modern_tech() {
let _ = MODERN_TECH.set(ModernTech::new());
}
pub fn modern_tech_for_path(device_key: &str) -> Option<&'static ModernTech> {
let _ = device_key;
MODERN_TECH.get()
}
impl ModernTech {
pub fn new() -> Self {
Self {
cstate: std::sync::Mutex::new(CStateCoordinator::new(cstate_advisory_path())),
pstate: std::sync::Mutex::new(PStateCoordinator::new(pstate_advisory_path())),
}
}
/// Called when a device binds. Emits a P-state advisory nudging
/// cpufreqd toward a higher state (the bound driver may need more
/// bandwidth) and a C-state advisory that says "if nothing else
/// happens, we can idle".
pub fn on_bind(&self, device: &DeviceId, reason: &str) {
if device.bus != "pci" {
return;
}
if let Ok(mut coord) = self.pstate.lock() {
let _ = coord.record(PStateEvent {
device: device.clone(),
suggested_state: 7,
reason: reason.to_string(),
});
}
if let Ok(mut coord) = self.cstate.lock() {
let _ = coord.record(CStateEvent {
device: device.clone(),
suggested_level: 1,
reason: format!("bound: {}", reason),
});
}
}
/// Called when a device unbinds. Emits a C-state advisory saying
/// "we can drop to a deeper idle" and a P-state advisory that
/// says "we can lower performance".
pub fn on_unbind(&self, device: &DeviceId, reason: &str) {
if device.bus != "pci" {
return;
}
if let Ok(mut coord) = self.cstate.lock() {
let _ = coord.record(CStateEvent {
device: device.clone(),
suggested_level: 3,
reason: format!("unbound: {}", reason),
});
}
if let Ok(mut coord) = self.pstate.lock() {
let _ = coord.record(PStateEvent {
device: device.clone(),
suggested_state: 4,
reason: format!("unbound: {}", reason),
});
}
}
/// Returns IOMMU group + source for the device. Logs the result
/// and returns the group number; if the group is synthetic, we log
/// a warning so operators can see when iommu scheme is missing.
pub fn iommu_group(&self, device: &DeviceId) -> u32 {
if device.bus != "pci" {
return 0;
}
let g = iommu_group_for(&device.path);
g.into_inner().group
}
/// Returns the NUMA node id for the device.
pub fn numa_node(&self, device: &DeviceId) -> u32 {
if device.bus != "pci" {
return 0;
}
numa_node_for(&device.path).node
}
/// Propose an MSI-X vector count for the device, with the same
/// default range the kernel would use.
pub fn msix_proposal(&self, device: &DeviceId, min: u16, max: u16) -> u16 {
if device.bus != "pci" {
return min;
}
propose_msix_vectors(&device.path, min, max).recommended
}
}
impl Default for ModernTech {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn devinfo(bus: &str, path: &str) -> DeviceId {
DeviceId {
bus: bus.to_string(),
path: path.to_string(),
}
}
#[test]
fn on_bind_skips_non_pci() {
let m = ModernTech::new();
let usb = devinfo("usb", "1-1");
m.on_bind(&usb, "test");
m.on_unbind(&usb, "test");
assert_eq!(m.iommu_group(&usb), 0);
assert_eq!(m.numa_node(&usb), 0);
}
#[test]
fn on_bind_emits_advisories_for_pci() {
let m = ModernTech::new();
let p = devinfo("pci", "0000:00:1f.2");
m.on_bind(&p, "first-bind");
m.on_unbind(&p, "test-unbind");
let _ = m.iommu_group(&p);
let _ = m.numa_node(&p);
let _ = m.msix_proposal(&p, 4, 16);
}
#[test]
fn default_constructor_works() {
let _ = ModernTech::default();
}
}