Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
pub const SOURCE_METAL: &str = "metal";
|
||||
pub const SOURCE_DXGI: &str = "dxgi";
|
||||
pub const SOURCE_LINUX_SYSFS: &str = "linux-sysfs";
|
||||
|
||||
#[napi(object)]
|
||||
pub struct PlatformGpuDeviceInfo {
|
||||
pub active: bool,
|
||||
#[napi(js_name = "vendorId")]
|
||||
pub vendor_id: u32,
|
||||
#[napi(js_name = "deviceId")]
|
||||
pub device_id: u32,
|
||||
#[napi(js_name = "vendorName")]
|
||||
pub vendor_name: Option<String>,
|
||||
#[napi(js_name = "deviceString")]
|
||||
pub device_string: Option<String>,
|
||||
#[napi(js_name = "driverVendor")]
|
||||
pub driver_vendor: Option<String>,
|
||||
#[napi(js_name = "driverVersion")]
|
||||
pub driver_version: Option<String>,
|
||||
#[napi(js_name = "dedicatedVideoMemory")]
|
||||
pub dedicated_video_memory: Option<f64>,
|
||||
#[napi(js_name = "sharedSystemMemory")]
|
||||
pub shared_system_memory: Option<f64>,
|
||||
#[napi(js_name = "subsystemVendorId")]
|
||||
pub subsystem_vendor_id: Option<u32>,
|
||||
#[napi(js_name = "subsystemDeviceId")]
|
||||
pub subsystem_device_id: Option<u32>,
|
||||
#[napi(js_name = "registryId")]
|
||||
pub registry_id: Option<String>,
|
||||
#[napi(js_name = "adapterLuid")]
|
||||
pub adapter_luid: Option<String>,
|
||||
#[napi(js_name = "pciPath")]
|
||||
pub pci_path: Option<String>,
|
||||
pub integrated: Option<bool>,
|
||||
pub removable: Option<bool>,
|
||||
pub headless: Option<bool>,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct PlatformGpuInfo {
|
||||
pub devices: Vec<PlatformGpuDeviceInfo>,
|
||||
pub source: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[napi(js_name = "getGpuInfo")]
|
||||
pub fn get_gpu_info() -> PlatformGpuInfo {
|
||||
platform::gpu_info()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn base_device(
|
||||
active: bool,
|
||||
vendor_id: u32,
|
||||
device_id: u32,
|
||||
source: &str,
|
||||
) -> PlatformGpuDeviceInfo {
|
||||
PlatformGpuDeviceInfo {
|
||||
active,
|
||||
vendor_id,
|
||||
device_id,
|
||||
vendor_name: fluxer_desktop_native::platform_info::probe_helpers::vendor_name(vendor_id)
|
||||
.map(str::to_owned),
|
||||
device_string: None,
|
||||
driver_vendor: None,
|
||||
driver_version: None,
|
||||
dedicated_video_memory: None,
|
||||
shared_system_memory: None,
|
||||
subsystem_vendor_id: None,
|
||||
subsystem_device_id: None,
|
||||
registry_id: None,
|
||||
adapter_luid: None,
|
||||
pci_path: None,
|
||||
integrated: None,
|
||||
removable: None,
|
||||
headless: None,
|
||||
source: source.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn empty_info(source: &str) -> PlatformGpuInfo {
|
||||
PlatformGpuInfo {
|
||||
devices: Vec::new(),
|
||||
source: source.to_owned(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod platform {
|
||||
use fluxer_desktop_native::platform_info::probe_helpers::{
|
||||
vendor_id_from_name, vendor_name, write_hex_u64,
|
||||
};
|
||||
use objc2::exception::catch;
|
||||
use objc2::rc::autoreleasepool;
|
||||
use objc2_metal::{MTLCopyAllDevices, MTLCreateSystemDefaultDevice, MTLDevice};
|
||||
|
||||
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_METAL, empty_info};
|
||||
|
||||
pub(super) fn gpu_info() -> PlatformGpuInfo {
|
||||
match catch(|| autoreleasepool(|_| gpu_info_inner())) {
|
||||
Ok(info) => info,
|
||||
Err(exception) => {
|
||||
let mut info = empty_info(SOURCE_METAL);
|
||||
info.error = Some(exception.map_or_else(
|
||||
|| "macOS GPU probe failed".to_owned(),
|
||||
|exception| exception.to_string(),
|
||||
));
|
||||
info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gpu_info_inner() -> PlatformGpuInfo {
|
||||
let default_device = MTLCreateSystemDefaultDevice();
|
||||
let default_registry_id = default_device.as_deref().map(MTLDevice::registryID);
|
||||
let all_devices = MTLCopyAllDevices();
|
||||
let mut devices = Vec::new();
|
||||
|
||||
for index in 0..all_devices.count() {
|
||||
let device = all_devices.objectAtIndex(index);
|
||||
let active =
|
||||
default_registry_id.is_some_and(|registry_id| registry_id == device.registryID());
|
||||
devices.push(mac_device(&device, active));
|
||||
}
|
||||
|
||||
if devices.is_empty() {
|
||||
if let Some(device) = default_device {
|
||||
devices.push(mac_device(&device, true));
|
||||
}
|
||||
} else if !devices.iter().any(|device| device.active) {
|
||||
devices[0].active = true;
|
||||
}
|
||||
|
||||
PlatformGpuInfo {
|
||||
devices,
|
||||
source: SOURCE_METAL.to_owned(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mac_device(
|
||||
device: &objc2::runtime::ProtocolObject<dyn MTLDevice>,
|
||||
active: bool,
|
||||
) -> PlatformGpuDeviceInfo {
|
||||
let name = device.name().to_string();
|
||||
let vendor_id = vendor_id_from_name(&name);
|
||||
PlatformGpuDeviceInfo {
|
||||
active,
|
||||
vendor_id,
|
||||
device_id: 0,
|
||||
vendor_name: vendor_name(vendor_id).map(str::to_owned),
|
||||
device_string: Some(if name.is_empty() {
|
||||
"Metal GPU".to_owned()
|
||||
} else {
|
||||
name
|
||||
}),
|
||||
driver_vendor: None,
|
||||
driver_version: None,
|
||||
dedicated_video_memory: None,
|
||||
shared_system_memory: None,
|
||||
subsystem_vendor_id: None,
|
||||
subsystem_device_id: None,
|
||||
registry_id: Some(write_hex_u64(device.registryID())),
|
||||
adapter_luid: None,
|
||||
pci_path: None,
|
||||
integrated: Some(device.isLowPower() || vendor_id == 0x106b),
|
||||
removable: Some(device.isRemovable()),
|
||||
headless: Some(device.isHeadless()),
|
||||
source: SOURCE_METAL.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod dxgi_helpers {
|
||||
pub fn utf16_description(raw: &[u16; 128]) -> String {
|
||||
let end = raw.iter().position(|ch| *ch == 0).unwrap_or(raw.len());
|
||||
let text = String::from_utf16_lossy(&raw[..end]);
|
||||
if text.is_empty() {
|
||||
"DXGI adapter".to_owned()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
pub fn luid_string(high_part: i32, low_part: u32) -> String {
|
||||
format!("{:08x}:{low_part:08x}", high_part as u32)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn luid_format_matches_legacy_contract() {
|
||||
assert_eq!("ffffffff:1234abcd", luid_string(-1, 0x1234_abcd));
|
||||
assert_eq!("00000002:00000001", luid_string(2, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_description_trims_at_first_nul() {
|
||||
let mut raw = [0u16; 128];
|
||||
raw[0] = 'G' as u16;
|
||||
raw[1] = 'P' as u16;
|
||||
raw[2] = 'U' as u16;
|
||||
raw[3] = 0;
|
||||
raw[4] = 'X' as u16;
|
||||
assert_eq!("GPU", utf16_description(&raw));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod platform {
|
||||
use super::dxgi_helpers::{luid_string, utf16_description};
|
||||
use fluxer_desktop_native::platform_info::probe_helpers::vendor_name;
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory1, DXGI_ADAPTER_DESC1, DXGI_ADAPTER_FLAG_SOFTWARE,
|
||||
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IDXGIAdapter1, IDXGIFactory1, IDXGIFactory6,
|
||||
};
|
||||
|
||||
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_DXGI, empty_info};
|
||||
|
||||
pub(super) fn gpu_info() -> PlatformGpuInfo {
|
||||
let mut info = empty_info(SOURCE_DXGI);
|
||||
info.devices = enumerate_dxgi_adapters();
|
||||
info
|
||||
}
|
||||
|
||||
fn enumerate_dxgi_adapters() -> Vec<PlatformGpuDeviceInfo> {
|
||||
unsafe {
|
||||
if let Ok(factory) = CreateDXGIFactory1::<IDXGIFactory6>() {
|
||||
return enumerate_factory6(&factory);
|
||||
}
|
||||
if let Ok(factory) = CreateDXGIFactory1::<IDXGIFactory1>() {
|
||||
return enumerate_factory1(&factory);
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
unsafe fn enumerate_factory6(factory: &IDXGIFactory6) -> Vec<PlatformGpuDeviceInfo> {
|
||||
let mut devices = Vec::new();
|
||||
let mut ordered_index = 0;
|
||||
loop {
|
||||
let Ok(adapter) = (unsafe {
|
||||
factory.EnumAdapterByGpuPreference::<IDXGIAdapter1>(
|
||||
ordered_index,
|
||||
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
|
||||
)
|
||||
}) else {
|
||||
break;
|
||||
};
|
||||
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
|
||||
push_dxgi_desc(&mut devices, &desc, ordered_index);
|
||||
}
|
||||
ordered_index += 1;
|
||||
}
|
||||
devices
|
||||
}
|
||||
|
||||
unsafe fn enumerate_factory1(factory: &IDXGIFactory1) -> Vec<PlatformGpuDeviceInfo> {
|
||||
let mut devices = Vec::new();
|
||||
let mut ordered_index = 0;
|
||||
loop {
|
||||
let Ok(adapter) = (unsafe { factory.EnumAdapters1(ordered_index) }) else {
|
||||
break;
|
||||
};
|
||||
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
|
||||
push_dxgi_desc(&mut devices, &desc, ordered_index);
|
||||
}
|
||||
ordered_index += 1;
|
||||
}
|
||||
devices
|
||||
}
|
||||
|
||||
fn push_dxgi_desc(
|
||||
devices: &mut Vec<PlatformGpuDeviceInfo>,
|
||||
desc: &DXGI_ADAPTER_DESC1,
|
||||
ordered_index: u32,
|
||||
) {
|
||||
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0 {
|
||||
return;
|
||||
}
|
||||
let mut device = PlatformGpuDeviceInfo {
|
||||
active: ordered_index == 0,
|
||||
vendor_id: desc.VendorId,
|
||||
device_id: desc.DeviceId,
|
||||
vendor_name: vendor_name(desc.VendorId).map(str::to_owned),
|
||||
device_string: Some(utf16_description(&desc.Description)),
|
||||
driver_vendor: None,
|
||||
driver_version: None,
|
||||
dedicated_video_memory: Some(desc.DedicatedVideoMemory as f64),
|
||||
shared_system_memory: Some(desc.SharedSystemMemory as f64),
|
||||
subsystem_vendor_id: Some(desc.SubSysId >> 16),
|
||||
subsystem_device_id: Some(desc.SubSysId & 0xffff),
|
||||
registry_id: None,
|
||||
adapter_luid: None,
|
||||
pci_path: None,
|
||||
integrated: None,
|
||||
removable: None,
|
||||
headless: None,
|
||||
source: SOURCE_DXGI.to_owned(),
|
||||
};
|
||||
device.adapter_luid = Some(luid_string(
|
||||
desc.AdapterLuid.HighPart,
|
||||
desc.AdapterLuid.LowPart,
|
||||
));
|
||||
devices.push(device);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod platform {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use fluxer_desktop_native::platform_info::probe_helpers::{
|
||||
basename, is_drm_card_name, parse_hex_id, vendor_name,
|
||||
};
|
||||
|
||||
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_LINUX_SYSFS, base_device};
|
||||
|
||||
pub(super) fn gpu_info() -> PlatformGpuInfo {
|
||||
linux_gpu_info_from_drm_root(Path::new("/sys/class/drm"))
|
||||
}
|
||||
|
||||
fn linux_gpu_info_from_drm_root(root: &Path) -> PlatformGpuInfo {
|
||||
let mut info = PlatformGpuInfo {
|
||||
devices: Vec::new(),
|
||||
source: SOURCE_LINUX_SYSFS.to_owned(),
|
||||
error: None,
|
||||
};
|
||||
let Ok(entries) = fs::read_dir(root) else {
|
||||
return info;
|
||||
};
|
||||
|
||||
let mut card_names = entries
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.filter(|name| is_drm_card_name(name))
|
||||
.collect::<Vec<_>>();
|
||||
card_names.sort_unstable_by_key(|name| card_sort_key(name));
|
||||
|
||||
for card_name in card_names {
|
||||
if let Some(device) = linux_card(root, &card_name, info.devices.is_empty()) {
|
||||
info.devices.push(device);
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
fn linux_card(root: &Path, card_name: &str, active: bool) -> Option<PlatformGpuDeviceInfo> {
|
||||
let base = root.join(card_name).join("device");
|
||||
let vendor_id = read_hex_file(base.join("vendor"))?;
|
||||
let device_id = read_hex_file(base.join("device")).unwrap_or(0);
|
||||
|
||||
let mut device = base_device(active, vendor_id, device_id, SOURCE_LINUX_SYSFS);
|
||||
device.subsystem_vendor_id =
|
||||
Some(read_hex_file(base.join("subsystem_vendor")).unwrap_or(0));
|
||||
device.subsystem_device_id =
|
||||
Some(read_hex_file(base.join("subsystem_device")).unwrap_or(0));
|
||||
device.pci_path = read_link_basename(&base);
|
||||
device.driver_vendor = read_link_basename(base.join("driver"));
|
||||
|
||||
if let Some(uevent) = read_trimmed_file(base.join("uevent")) {
|
||||
if let Some(driver) = uevent_value(&uevent, "DRIVER") {
|
||||
device.driver_vendor = Some(driver.to_owned());
|
||||
}
|
||||
if let Some(pci_id) = uevent_value(&uevent, "PCI_ID") {
|
||||
device.device_string = Some(match vendor_name(vendor_id) {
|
||||
Some(vendor) => format!("{vendor} GPU ({pci_id})"),
|
||||
None => format!("GPU ({pci_id})"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(vram) = read_trimmed_file(base.join("mem_info_vram_total"))
|
||||
&& let Ok(bytes) = vram.parse::<u64>()
|
||||
{
|
||||
device.dedicated_video_memory = Some(bytes as f64);
|
||||
}
|
||||
|
||||
Some(device)
|
||||
}
|
||||
|
||||
fn card_sort_key(name: &str) -> u32 {
|
||||
name.strip_prefix("card")
|
||||
.and_then(|rest| rest.parse::<u32>().ok())
|
||||
.unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
fn read_hex_file(path: impl AsRef<Path>) -> Option<u32> {
|
||||
let raw = fs::read_to_string(path).ok()?;
|
||||
parse_hex_id(&raw).ok()
|
||||
}
|
||||
|
||||
fn read_trimmed_file(path: impl AsRef<Path>) -> Option<String> {
|
||||
let raw = fs::read_to_string(path).ok()?;
|
||||
Some(raw.trim_matches([' ', '\t', '\r', '\n']).to_owned())
|
||||
}
|
||||
|
||||
fn read_link_basename(path: impl AsRef<Path>) -> Option<String> {
|
||||
let target = fs::read_link(path).ok()?;
|
||||
Some(path_basename(&target))
|
||||
}
|
||||
|
||||
fn path_basename(path: &Path) -> String {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| basename(&path.to_string_lossy()).to_owned())
|
||||
}
|
||||
|
||||
fn uevent_value<'a>(blob: &'a str, key: &str) -> Option<&'a str> {
|
||||
blob.lines().find_map(|line| {
|
||||
let (line_key, value) = line.split_once('=')?;
|
||||
if line_key == key {
|
||||
Some(value.trim_matches([' ', '\t', '\r', '\n']))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{create_dir_all, write};
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn uevent_value_matches_exact_keys() {
|
||||
let blob = "PCI_ID=8086:46A6\nDRIVER=i915\nNOT_DRIVER=bad\n";
|
||||
assert_eq!(Some("i915"), uevent_value(blob, "DRIVER"));
|
||||
assert_eq!(Some("8086:46A6"), uevent_value(blob, "PCI_ID"));
|
||||
assert_eq!(None, uevent_value(blob, "MISSING"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_sort_key_keeps_card_order_numeric() {
|
||||
let mut names = ["card10".to_owned(), "card2".to_owned(), "card0".to_owned()];
|
||||
names.sort_unstable_by_key(|name| card_sort_key(name));
|
||||
assert_eq!(["card0", "card2", "card10"], names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_sysfs_fixture_ports_legacy_gpu_contract() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let pci0 = root.join("devices/pci0000:00/0000:00:02.0");
|
||||
let pci1 = root.join("devices/pci0000:01/0000:01:00.0");
|
||||
create_dir_all(root.join("card0")).unwrap();
|
||||
create_dir_all(root.join("card1")).unwrap();
|
||||
create_dir_all(&pci0).unwrap();
|
||||
create_dir_all(&pci1).unwrap();
|
||||
symlink(
|
||||
"../devices/pci0000:00/0000:00:02.0",
|
||||
root.join("card0/device"),
|
||||
)
|
||||
.unwrap();
|
||||
symlink(
|
||||
"../devices/pci0000:01/0000:01:00.0",
|
||||
root.join("card1/device"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let card0 = root.join("card0/device");
|
||||
let card1 = root.join("card1/device");
|
||||
write(card0.join("vendor"), "0x8086\n").unwrap();
|
||||
write(card0.join("device"), "0x46a6\n").unwrap();
|
||||
write(card0.join("subsystem_vendor"), "0x1028\n").unwrap();
|
||||
write(card0.join("subsystem_device"), "0x0b19\n").unwrap();
|
||||
write(card0.join("uevent"), "DRIVER=i915\nPCI_ID=8086:46A6\n").unwrap();
|
||||
write(card0.join("mem_info_vram_total"), "268435456\n").unwrap();
|
||||
|
||||
write(card1.join("vendor"), "0x10de\n").unwrap();
|
||||
write(card1.join("device"), "0x1f99\n").unwrap();
|
||||
write(card1.join("uevent"), "PCI_ID=10DE:1F99\n").unwrap();
|
||||
symlink("/sys/bus/pci/drivers/nvidia", card1.join("driver")).unwrap();
|
||||
|
||||
let info = linux_gpu_info_from_drm_root(root);
|
||||
assert_eq!(SOURCE_LINUX_SYSFS, info.source);
|
||||
assert_eq!(2, info.devices.len());
|
||||
assert!(info.devices[0].active);
|
||||
assert_eq!(0x8086, info.devices[0].vendor_id);
|
||||
assert_eq!(Some("Intel".to_owned()), info.devices[0].vendor_name);
|
||||
assert_eq!(
|
||||
Some("Intel GPU (8086:46A6)".to_owned()),
|
||||
info.devices[0].device_string
|
||||
);
|
||||
assert_eq!(Some("i915".to_owned()), info.devices[0].driver_vendor);
|
||||
assert_eq!(Some("0000:00:02.0".to_owned()), info.devices[0].pci_path);
|
||||
assert_eq!(Some(268_435_456.0), info.devices[0].dedicated_video_memory);
|
||||
assert!(!info.devices[1].active);
|
||||
assert_eq!(Some("nvidia".to_owned()), info.devices[1].driver_vendor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
mod platform {
|
||||
use super::{PlatformGpuInfo, SOURCE_LINUX_SYSFS, empty_info};
|
||||
|
||||
pub(super) fn gpu_info() -> PlatformGpuInfo {
|
||||
empty_info(SOURCE_LINUX_SYSFS)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base_device_sets_required_contract_fields() {
|
||||
let device = base_device(true, 0x10de, 0x2684, SOURCE_DXGI);
|
||||
assert!(device.active);
|
||||
assert_eq!(0x10de, device.vendor_id);
|
||||
assert_eq!(0x2684, device.device_id);
|
||||
assert_eq!(Some("NVIDIA".to_owned()), device.vendor_name);
|
||||
assert_eq!(SOURCE_DXGI, device.source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_info_preserves_source_and_empty_devices() {
|
||||
let info = empty_info(SOURCE_LINUX_SYSFS);
|
||||
assert_eq!(SOURCE_LINUX_SYSFS, info.source);
|
||||
assert!(info.devices.is_empty());
|
||||
assert!(info.error.is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user