Refresh redbear-info system tool
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
redbear-info
|
|
||||||
@@ -6,3 +6,6 @@ edition = "2024"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "redbear-info"
|
name = "redbear-info"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
toml = "0.8"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use std::path::PathBuf;
|
|||||||
use std::process;
|
use std::process;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ enum OutputMode {
|
|||||||
Table,
|
Table,
|
||||||
Json,
|
Json,
|
||||||
Test,
|
Test,
|
||||||
|
Quirks,
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +98,32 @@ struct HardwareReport {
|
|||||||
virtio_net_present: bool,
|
virtio_net_present: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct QuirkFile {
|
||||||
|
name: String,
|
||||||
|
pci_quirks: Vec<QuirkEntry>,
|
||||||
|
usb_quirks: Vec<UsbQuirkEntry>,
|
||||||
|
dmi_quirk_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QuirkEntry {
|
||||||
|
vendor: String,
|
||||||
|
device: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
flags: Vec<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UsbQuirkEntry {
|
||||||
|
vendor: String,
|
||||||
|
product: Option<String>,
|
||||||
|
flags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QuirksReport {
|
||||||
|
files_loaded: Vec<QuirkFile>,
|
||||||
|
load_errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
struct IntegrationCheck {
|
struct IntegrationCheck {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
category: &'static str,
|
category: &'static str,
|
||||||
@@ -391,12 +420,20 @@ fn run() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let runtime = Runtime::from_env();
|
let runtime = Runtime::from_env();
|
||||||
|
|
||||||
|
if options.mode == OutputMode::Quirks {
|
||||||
|
let quirks = collect_quirks(&runtime);
|
||||||
|
print_quirks(&quirks, options.verbose);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let report = collect_report(&runtime);
|
let report = collect_report(&runtime);
|
||||||
|
|
||||||
match options.mode {
|
match options.mode {
|
||||||
OutputMode::Table => print_table(&report, options.verbose),
|
OutputMode::Table => print_table(&report, options.verbose),
|
||||||
OutputMode::Json => print_json(&report),
|
OutputMode::Json => print_json(&report),
|
||||||
OutputMode::Test => print_tests(&report, options.verbose),
|
OutputMode::Test => print_tests(&report, options.verbose),
|
||||||
|
OutputMode::Quirks => {}
|
||||||
OutputMode::Help => {}
|
OutputMode::Help => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,6 +896,125 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_quirks(runtime: &Runtime) -> QuirksReport {
|
||||||
|
let mut files_loaded = Vec::new();
|
||||||
|
let mut load_errors = Vec::new();
|
||||||
|
|
||||||
|
let entries = match runtime.read_dir_names("/etc/quirks.d") {
|
||||||
|
Some(entries) => entries,
|
||||||
|
None => {
|
||||||
|
return QuirksReport {
|
||||||
|
files_loaded,
|
||||||
|
load_errors: vec!["quirks directory not found".to_string()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for name in entries.into_iter().filter(|name| name.ends_with(".toml")) {
|
||||||
|
let path = format!("/etc/quirks.d/{name}");
|
||||||
|
match runtime.read_to_string(&path) {
|
||||||
|
Some(content) => match parse_quirk_toml(&name, &content) {
|
||||||
|
Ok(file_quirks) => files_loaded.push(file_quirks),
|
||||||
|
Err(err) => load_errors.push(format!("{name}: {err}")),
|
||||||
|
},
|
||||||
|
None => load_errors.push(format!("{name}: read error")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QuirksReport {
|
||||||
|
files_loaded,
|
||||||
|
load_errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_quirk_toml(name: &str, content: &str) -> Result<QuirkFile, String> {
|
||||||
|
let document: Value = content
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format!("parse error: {err}"))?;
|
||||||
|
let table = document
|
||||||
|
.as_table()
|
||||||
|
.ok_or_else(|| "top-level document is not a table".to_string())?;
|
||||||
|
|
||||||
|
let mut file_quirks = QuirkFile {
|
||||||
|
name: name.to_string(),
|
||||||
|
pci_quirks: Vec::new(),
|
||||||
|
usb_quirks: Vec::new(),
|
||||||
|
dmi_quirk_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(entries) = table.get("pci_quirk").and_then(Value::as_array) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Some(quirk) = parse_pci_quirk(entry) {
|
||||||
|
file_quirks.pci_quirks.push(quirk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entries) = table.get("usb_quirk").and_then(Value::as_array) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Some(quirk) = parse_usb_quirk(entry) {
|
||||||
|
file_quirks.usb_quirks.push(quirk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entries) = table.get("dmi_system_quirk").and_then(Value::as_array) {
|
||||||
|
file_quirks.dmi_quirk_count = entries.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file_quirks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pci_quirk(entry: &Value) -> Option<QuirkEntry> {
|
||||||
|
let table = entry.as_table()?;
|
||||||
|
let vendor = table.get("vendor").and_then(|value| format_hex(value, 4))?;
|
||||||
|
let device = table.get("device").and_then(|value| format_hex(value, 4));
|
||||||
|
let class = table.get("class").and_then(|value| format_hex(value, 6));
|
||||||
|
let flags = table.get("flags").and_then(parse_string_array)?;
|
||||||
|
let description = table
|
||||||
|
.get("description")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::to_string);
|
||||||
|
|
||||||
|
Some(QuirkEntry {
|
||||||
|
vendor,
|
||||||
|
device,
|
||||||
|
class,
|
||||||
|
flags,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_usb_quirk(entry: &Value) -> Option<UsbQuirkEntry> {
|
||||||
|
let table = entry.as_table()?;
|
||||||
|
let vendor = table.get("vendor").and_then(|value| format_hex(value, 4))?;
|
||||||
|
let product = table
|
||||||
|
.get("product")
|
||||||
|
.and_then(|value| format_hex(value, 4));
|
||||||
|
let flags = table.get("flags").and_then(parse_string_array)?;
|
||||||
|
|
||||||
|
Some(UsbQuirkEntry {
|
||||||
|
vendor,
|
||||||
|
product,
|
||||||
|
flags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_hex(value: &Value, width: usize) -> Option<String> {
|
||||||
|
let raw = u64::try_from(value.as_integer()?).ok()?;
|
||||||
|
Some(format!("0x{raw:0width$X}", width = width))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_string_array(value: &Value) -> Option<Vec<String>> {
|
||||||
|
value.as_array().map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn inspect_integration<'a>(
|
fn inspect_integration<'a>(
|
||||||
runtime: &Runtime,
|
runtime: &Runtime,
|
||||||
network: &NetworkReport,
|
network: &NetworkReport,
|
||||||
@@ -961,14 +1117,29 @@ where
|
|||||||
if mode == OutputMode::Test {
|
if mode == OutputMode::Test {
|
||||||
return Err("cannot combine --json with --test".to_string());
|
return Err("cannot combine --json with --test".to_string());
|
||||||
}
|
}
|
||||||
|
if mode == OutputMode::Quirks {
|
||||||
|
return Err("cannot combine --json with --quirks".to_string());
|
||||||
|
}
|
||||||
mode = OutputMode::Json;
|
mode = OutputMode::Json;
|
||||||
}
|
}
|
||||||
"--test" => {
|
"--test" => {
|
||||||
if mode == OutputMode::Json {
|
if mode == OutputMode::Json {
|
||||||
return Err("cannot combine --test with --json".to_string());
|
return Err("cannot combine --test with --json".to_string());
|
||||||
}
|
}
|
||||||
|
if mode == OutputMode::Quirks {
|
||||||
|
return Err("cannot combine --test with --quirks".to_string());
|
||||||
|
}
|
||||||
mode = OutputMode::Test;
|
mode = OutputMode::Test;
|
||||||
}
|
}
|
||||||
|
"--quirks" => {
|
||||||
|
if mode == OutputMode::Json {
|
||||||
|
return Err("cannot combine --quirks with --json".to_string());
|
||||||
|
}
|
||||||
|
if mode == OutputMode::Test {
|
||||||
|
return Err("cannot combine --quirks with --test".to_string());
|
||||||
|
}
|
||||||
|
mode = OutputMode::Quirks;
|
||||||
|
}
|
||||||
"-h" | "--help" => mode = OutputMode::Help,
|
"-h" | "--help" => mode = OutputMode::Help,
|
||||||
_ => return Err(format!("unknown argument: {arg}")),
|
_ => return Err(format!("unknown argument: {arg}")),
|
||||||
}
|
}
|
||||||
@@ -1222,6 +1393,83 @@ fn print_table(report: &Report<'_>, verbose: bool) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_quirks(report: &QuirksReport, verbose: bool) {
|
||||||
|
println!("Red Bear OS Hardware Quirks Configuration");
|
||||||
|
println!("{DIVIDER}");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if report.files_loaded.is_empty()
|
||||||
|
&& report
|
||||||
|
.load_errors
|
||||||
|
.iter()
|
||||||
|
.any(|error| error.contains("quirks directory not found"))
|
||||||
|
{
|
||||||
|
println!(" Quirks directory: {}not found{}", YELLOW, RESET);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" Files loaded: {}", report.files_loaded.len());
|
||||||
|
|
||||||
|
for file in &report.files_loaded {
|
||||||
|
println!();
|
||||||
|
print_section_header(&format!("Quirks: {}", file.name));
|
||||||
|
|
||||||
|
if file.pci_quirks.is_empty() && file.usb_quirks.is_empty() && file.dmi_quirk_count == 0 {
|
||||||
|
println!(" (no entries)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" {:<4} {:<8} {:<18} {}",
|
||||||
|
"Type", "Vendor", "Match", "Flags"
|
||||||
|
);
|
||||||
|
for quirk in &file.pci_quirks {
|
||||||
|
let selector = quirk
|
||||||
|
.device
|
||||||
|
.as_deref()
|
||||||
|
.map(|device| format!("device={device}"))
|
||||||
|
.or_else(|| quirk.class.as_deref().map(|class| format!("class={class}")))
|
||||||
|
.unwrap_or_else(|| "match=unknown".to_string());
|
||||||
|
println!(
|
||||||
|
" {:<4} {:<8} {:<18} {}",
|
||||||
|
"PCI",
|
||||||
|
quirk.vendor,
|
||||||
|
selector,
|
||||||
|
quirk.flags.join(", ")
|
||||||
|
);
|
||||||
|
if verbose && let Some(description) = &quirk.description {
|
||||||
|
println!(" Description: {description}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for quirk in &file.usb_quirks {
|
||||||
|
let selector = quirk
|
||||||
|
.product
|
||||||
|
.as_deref()
|
||||||
|
.map(|p| format!("product={p}"))
|
||||||
|
.unwrap_or_else(|| "match=any".to_string());
|
||||||
|
println!(
|
||||||
|
" {:<4} {:<8} {:<18} {}",
|
||||||
|
"USB",
|
||||||
|
quirk.vendor,
|
||||||
|
selector,
|
||||||
|
quirk.flags.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.dmi_quirk_count > 0 {
|
||||||
|
println!(
|
||||||
|
" DMI: {} system rule(s) configured (runtime application uses acpid /scheme/acpi/dmi)",
|
||||||
|
file.dmi_quirk_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for error in &report.load_errors {
|
||||||
|
println!(" {}Error: {}{}", RED, error, RESET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_tests(report: &Report<'_>, verbose: bool) {
|
fn print_tests(report: &Report<'_>, verbose: bool) {
|
||||||
println!("Red Bear OS Runtime Test Hints");
|
println!("Red Bear OS Runtime Test Hints");
|
||||||
println!("{DIVIDER}");
|
println!("{DIVIDER}");
|
||||||
@@ -1556,7 +1804,7 @@ fn print_json(report: &Report<'_>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!("Usage: redbear-info [--verbose|-v] [--json|--test]");
|
println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks]");
|
||||||
println!();
|
println!();
|
||||||
println!("Passive runtime integration report for Red Bear OS.");
|
println!("Passive runtime integration report for Red Bear OS.");
|
||||||
println!();
|
println!();
|
||||||
@@ -1572,6 +1820,7 @@ fn print_help() {
|
|||||||
println!(" -v, --verbose Show evidence and claim limits");
|
println!(" -v, --verbose Show evidence and claim limits");
|
||||||
println!(" --json Print structured JSON");
|
println!(" --json Print structured JSON");
|
||||||
println!(" --test Print suggested diagnostic commands");
|
println!(" --test Print suggested diagnostic commands");
|
||||||
|
println!(" --quirks Print configured hardware quirk data");
|
||||||
println!(" -h, --help Show this help message");
|
println!(" -h, --help Show this help message");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2262,25 +2511,31 @@ mod tests {
|
|||||||
report.network.active_profile.as_deref(),
|
report.network.active_profile.as_deref(),
|
||||||
Some("wired-static")
|
Some("wired-static")
|
||||||
);
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.network_schemes
|
.network
|
||||||
.iter()
|
.network_schemes
|
||||||
.any(|name| name.contains("rtl8125")));
|
.iter()
|
||||||
|
.any(|name| name.contains("rtl8125"))
|
||||||
|
);
|
||||||
assert_eq!(report.network.wifi_control_state, ProbeState::Functional);
|
assert_eq!(report.network.wifi_control_state, ProbeState::Functional);
|
||||||
assert_eq!(report.network.wifi_interfaces, vec!["wlan0".to_string()]);
|
assert_eq!(report.network.wifi_interfaces, vec!["wlan0".to_string()]);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.wifi_firmware_status
|
.network
|
||||||
.as_deref()
|
.wifi_firmware_status
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("intel-bz-arrow-lake"));
|
.unwrap()
|
||||||
assert!(report
|
.contains("intel-bz-arrow-lake")
|
||||||
.network
|
);
|
||||||
.wifi_transport_status
|
assert!(
|
||||||
.as_deref()
|
report
|
||||||
.unwrap()
|
.network
|
||||||
.contains("memory_enabled=yes"));
|
.wifi_transport_status
|
||||||
|
.as_deref()
|
||||||
|
.unwrap()
|
||||||
|
.contains("memory_enabled=yes")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.network.wifi_transport_init_status.as_deref(),
|
report.network.wifi_transport_init_status.as_deref(),
|
||||||
Some("transport_init=stub")
|
Some("transport_init=stub")
|
||||||
@@ -2299,28 +2554,34 @@ mod tests {
|
|||||||
ProbeState::Functional
|
ProbeState::Functional
|
||||||
);
|
);
|
||||||
assert_eq!(report.network.bluetooth_adapters, vec!["hci0".to_string()]);
|
assert_eq!(report.network.bluetooth_adapters, vec!["hci0".to_string()]);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_transport_status
|
.network
|
||||||
.as_deref()
|
.bluetooth_transport_status
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("runtime_visibility=runtime-visible"));
|
.unwrap()
|
||||||
assert!(report
|
.contains("runtime_visibility=runtime-visible")
|
||||||
.network
|
);
|
||||||
.bluetooth_adapter_status
|
assert!(
|
||||||
.as_deref()
|
report
|
||||||
.unwrap()
|
.network
|
||||||
.contains("status=adapter-visible"));
|
.bluetooth_adapter_status
|
||||||
|
.as_deref()
|
||||||
|
.unwrap()
|
||||||
|
.contains("status=adapter-visible")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.network.bluetooth_scan_results,
|
report.network.bluetooth_scan_results,
|
||||||
vec!["demo-beacon".to_string(), "demo-sensor".to_string()]
|
vec!["demo-beacon".to_string(), "demo-sensor".to_string()]
|
||||||
);
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_connection_state
|
.network
|
||||||
.as_deref()
|
.bluetooth_connection_state
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF"));
|
.unwrap()
|
||||||
|
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.network.bluetooth_connect_result.as_deref(),
|
report.network.bluetooth_connect_result.as_deref(),
|
||||||
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
||||||
@@ -2331,39 +2592,51 @@ mod tests {
|
|||||||
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_read_char_result
|
.network
|
||||||
.as_deref()
|
.bluetooth_read_char_result
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("read_char_result=stub-value"));
|
.unwrap()
|
||||||
assert!(report
|
.contains("read_char_result=stub-value")
|
||||||
.network
|
);
|
||||||
.bluetooth_read_char_result
|
assert!(
|
||||||
.as_deref()
|
report
|
||||||
.unwrap()
|
.network
|
||||||
.contains("workload=battery-sensor-battery-level-read"));
|
.bluetooth_read_char_result
|
||||||
|
.as_deref()
|
||||||
|
.unwrap()
|
||||||
|
.contains("workload=battery-sensor-battery-level-read")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.network.bluetooth_bond_store_path.as_deref(),
|
report.network.bluetooth_bond_store_path.as_deref(),
|
||||||
Some("/var/lib/bluetooth/hci0/bonds")
|
Some("/var/lib/bluetooth/hci0/bonds")
|
||||||
);
|
);
|
||||||
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_claim_limit
|
.network
|
||||||
.contains("real pairing"));
|
.bluetooth_claim_limit
|
||||||
assert!(report
|
.contains("real pairing")
|
||||||
.network
|
);
|
||||||
.bluetooth_claim_limit
|
assert!(
|
||||||
.contains("general device traffic"));
|
report
|
||||||
assert!(report
|
.network
|
||||||
.network
|
.bluetooth_claim_limit
|
||||||
.bluetooth_claim_limit
|
.contains("general device traffic")
|
||||||
.contains("generic GATT"));
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_claim_limit
|
.network
|
||||||
.contains("write support"));
|
.bluetooth_claim_limit
|
||||||
|
.contains("generic GATT")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.network
|
||||||
|
.bluetooth_claim_limit
|
||||||
|
.contains("write support")
|
||||||
|
);
|
||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
@@ -2487,10 +2760,12 @@ mod tests {
|
|||||||
Some("/var/lib/bluetooth/hci0/bonds")
|
Some("/var/lib/bluetooth/hci0/bonds")
|
||||||
);
|
);
|
||||||
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_claim_limit
|
.network
|
||||||
.contains("stub bond files"));
|
.bluetooth_claim_limit
|
||||||
|
.contains("stub bond files")
|
||||||
|
);
|
||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
@@ -2522,12 +2797,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let report = collect_report(&Runtime::from_root(root.clone()));
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_connection_state
|
.network
|
||||||
.as_deref()
|
.bluetooth_connection_state
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF"));
|
.unwrap()
|
||||||
|
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.network.bluetooth_connect_result.as_deref(),
|
report.network.bluetooth_connect_result.as_deref(),
|
||||||
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
||||||
@@ -2538,18 +2815,22 @@ mod tests {
|
|||||||
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.network
|
report
|
||||||
.bluetooth_read_char_result
|
.network
|
||||||
.as_deref()
|
.bluetooth_read_char_result
|
||||||
.unwrap()
|
.as_deref()
|
||||||
.contains("read_char_result=stub-value"));
|
.unwrap()
|
||||||
assert!(report
|
.contains("read_char_result=stub-value")
|
||||||
.network
|
);
|
||||||
.bluetooth_read_char_result
|
assert!(
|
||||||
.as_deref()
|
report
|
||||||
.unwrap()
|
.network
|
||||||
.contains("peripheral_class=ble-battery-sensor"));
|
.bluetooth_read_char_result
|
||||||
|
.as_deref()
|
||||||
|
.unwrap()
|
||||||
|
.contains("peripheral_class=ble-battery-sensor")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
integration_state(&report, "redbear-btctl"),
|
integration_state(&report, "redbear-btctl"),
|
||||||
ProbeState::Functional
|
ProbeState::Functional
|
||||||
@@ -2581,7 +2862,11 @@ mod tests {
|
|||||||
fn stale_btusb_status_does_not_report_active_in_integrations() {
|
fn stale_btusb_status_does_not_report_active_in_integrations() {
|
||||||
let root = temp_root();
|
let root = temp_root();
|
||||||
write_file(&root, "/usr/bin/redbear-btusb", "");
|
write_file(&root, "/usr/bin/redbear-btusb", "");
|
||||||
write_file(&root, "/var/run/redbear-btusb/status", "transport=usb\nstartup=explicit\nupdated_at_epoch=1\nruntime_visibility=runtime-visible\n");
|
write_file(
|
||||||
|
&root,
|
||||||
|
"/var/run/redbear-btusb/status",
|
||||||
|
"transport=usb\nstartup=explicit\nupdated_at_epoch=1\nruntime_visibility=runtime-visible\n",
|
||||||
|
);
|
||||||
|
|
||||||
let report = collect_report(&Runtime::from_root(root.clone()));
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2680,26 +2965,112 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
assert!(output.contains("state"));
|
assert!(output.contains("state"));
|
||||||
assert!(report
|
assert!(
|
||||||
.integrations
|
report
|
||||||
.iter()
|
.integrations
|
||||||
.any(|item| item.check.name == "redbear-info"));
|
.iter()
|
||||||
assert!(report
|
.any(|item| item.check.name == "redbear-info")
|
||||||
.integrations
|
);
|
||||||
.iter()
|
assert!(
|
||||||
.any(|item| item.check.name == "redbear-netstat"));
|
report
|
||||||
assert!(report
|
.integrations
|
||||||
.integrations
|
.iter()
|
||||||
.iter()
|
.any(|item| item.check.name == "redbear-netstat")
|
||||||
.any(|item| item.check.name == "redbear-btusb"));
|
);
|
||||||
assert!(report
|
assert!(
|
||||||
.integrations
|
report
|
||||||
.iter()
|
.integrations
|
||||||
.any(|item| item.check.name == "redbear-btctl"));
|
.iter()
|
||||||
assert!(report
|
.any(|item| item.check.name == "redbear-btusb")
|
||||||
.integrations
|
);
|
||||||
.iter()
|
assert!(
|
||||||
.any(|item| item.check.name == "redbear-nmap"));
|
report
|
||||||
|
.integrations
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.check.name == "redbear-btctl")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.integrations
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.check.name == "redbear-nmap")
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_accepts_quirks_mode() {
|
||||||
|
let options = parse_args([
|
||||||
|
"redbear-info".to_string(),
|
||||||
|
"--quirks".to_string(),
|
||||||
|
"--verbose".to_string(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(options.mode, OutputMode::Quirks));
|
||||||
|
assert!(options.verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_args_rejects_quirks_with_other_output_modes() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_args([
|
||||||
|
"redbear-info".to_string(),
|
||||||
|
"--quirks".to_string(),
|
||||||
|
"--json".to_string(),
|
||||||
|
])
|
||||||
|
.err(),
|
||||||
|
Some("cannot combine --json with --quirks".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args([
|
||||||
|
"redbear-info".to_string(),
|
||||||
|
"--test".to_string(),
|
||||||
|
"--quirks".to_string(),
|
||||||
|
])
|
||||||
|
.err(),
|
||||||
|
Some("cannot combine --quirks with --test".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_quirks_reads_pci_class_and_usb_entries() {
|
||||||
|
let root = temp_root();
|
||||||
|
write_file(
|
||||||
|
&root,
|
||||||
|
"/etc/quirks.d/10-gpu.toml",
|
||||||
|
"[[pci_quirk]]\nvendor = 0x1002\nclass = 0x030000\nflags = [\"no_d3cold\", \"need_firmware\"]\ndescription = \"GPU class quirk\"\n\n[[pci_quirk]]\nvendor = 0x1002\ndevice = 0x744c\nflags = [\"need_iommu\"]\n\n[[usb_quirk]]\nvendor = 0x0bda\nproduct = 0x8153\nflags = [\"no_string_fetch\"]\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let report = collect_quirks(&Runtime::from_root(root.clone()));
|
||||||
|
assert!(report.load_errors.is_empty());
|
||||||
|
assert_eq!(report.files_loaded.len(), 1);
|
||||||
|
|
||||||
|
let file = &report.files_loaded[0];
|
||||||
|
assert_eq!(file.name, "10-gpu.toml");
|
||||||
|
assert_eq!(file.pci_quirks.len(), 2);
|
||||||
|
assert_eq!(file.usb_quirks.len(), 1);
|
||||||
|
assert_eq!(file.pci_quirks[0].vendor, "0x1002");
|
||||||
|
assert_eq!(file.pci_quirks[0].class.as_deref(), Some("0x030000"));
|
||||||
|
assert_eq!(
|
||||||
|
file.pci_quirks[0].description.as_deref(),
|
||||||
|
Some("GPU class quirk")
|
||||||
|
);
|
||||||
|
assert_eq!(file.pci_quirks[1].device.as_deref(), Some("0x744C"));
|
||||||
|
assert_eq!(file.usb_quirks[0].vendor, "0x0BDA");
|
||||||
|
assert_eq!(file.usb_quirks[0].product.as_deref(), Some("0x8153"));
|
||||||
|
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_quirks_reports_missing_directory() {
|
||||||
|
let root = temp_root();
|
||||||
|
|
||||||
|
let report = collect_quirks(&Runtime::from_root(root.clone()));
|
||||||
|
assert!(report.files_loaded.is_empty());
|
||||||
|
assert_eq!(report.load_errors, vec!["quirks directory not found"]);
|
||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user