use crate::compat::format;
use crate::compat::String;
pub const DIAGNOSTIC_BIT_COUNT: usize = 12;
pub const DIAGNOSTIC_NAMES: [&str; DIAGNOSTIC_BIT_COUNT] = [
"clipboard_mime",
"displays",
"display_of_window",
"display_generation",
"dpi",
"system_theme",
"text_scale",
"safe_area",
"ime",
"accessibility",
"file_dialog",
"open_external",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticStatus {
Pass,
Skip,
Fail,
}
impl DiagnosticStatus {
pub const fn as_str(self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Skip => "skip",
Self::Fail => "fail",
}
}
pub const fn code(self) -> u8 {
match self {
Self::Pass => 0,
Self::Skip => 1,
Self::Fail => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiagnosticEntry {
pub bit: u8,
pub name: &'static str,
pub status: DiagnosticStatus,
pub detail: &'static str,
}
impl DiagnosticEntry {
pub const fn mask(self) -> u32 {
if matches!(self.status, DiagnosticStatus::Pass) {
1 << self.bit
} else {
0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlatformDiagnostics {
pub backend: &'static str,
pub entries: [DiagnosticEntry; DIAGNOSTIC_BIT_COUNT],
}
impl PlatformDiagnostics {
pub fn mask(&self) -> u32 {
self.entries.iter().fold(0, |mask, entry| mask | entry.mask())
}
pub fn passed(&self) -> usize {
self.entries.iter().filter(|e| e.status == DiagnosticStatus::Pass).count()
}
pub fn skipped(&self) -> usize {
self.entries.iter().filter(|e| e.status == DiagnosticStatus::Skip).count()
}
pub fn failed(&self) -> usize {
self.entries.iter().filter(|e| e.status == DiagnosticStatus::Fail).count()
}
pub fn is_pass(&self) -> bool {
self.failed() == 0
}
pub fn to_json(&self) -> String {
let mut out = format!(
"{{\"backend\":\"{}\",\"mask\":{},\"passed\":{},\"skipped\":{},\"failed\":{},\"entries\":[",
escape_json(self.backend),
self.mask(),
self.passed(),
self.skipped(),
self.failed()
);
for (index, entry) in self.entries.iter().enumerate() {
if index > 0 {
out.push(',');
}
out.push_str(&format!(
"{{\"bit\":{},\"name\":\"{}\",\"status\":\"{}\",\"detail\":\"{}\"}}",
entry.bit,
escape_json(entry.name),
entry.status.as_str(),
escape_json(entry.detail)
));
}
out.push_str("]}");
out
}
pub fn summary(&self) -> String {
format!(
"{}: passed {}/{} skipped {} failed {} (mask 0x{:03x})",
self.backend,
self.passed(),
DIAGNOSTIC_BIT_COUNT,
self.skipped(),
self.failed(),
self.mask()
)
}
}
fn escape_json(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.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 entry(bit: usize, status: DiagnosticStatus, detail: &'static str) -> DiagnosticEntry {
DiagnosticEntry { bit: bit as u8, name: DIAGNOSTIC_NAMES[bit], status, detail }
}
pub fn run() -> PlatformDiagnostics {
let platform = crate::platform::get_platform();
let capabilities = platform.capabilities();
let mut entries = [entry(0, DiagnosticStatus::Skip, ""); DIAGNOSTIC_BIT_COUNT];
entries[0] = match platform.clipboard_backend() {
None if capabilities.clipboard => entry(
0,
DiagnosticStatus::Fail,
"capabilities().clipboard is true but clipboard_backend() returned None",
),
None => entry(0, DiagnosticStatus::Skip, "no rich clipboard backend"),
Some(clipboard) => {
const PROBE_TYPE: &str = "application/vnd.rust-widgets.probe+json";
const PROBE_BYTES: &[u8] = b"{\"probe\":true,\"bytes\":\"\x00\xff\x80\"}";
let previous = clipboard.get_contents();
let written = clipboard.set_contents_with_type(PROBE_TYPE, PROBE_BYTES);
let readable = written
&& clipboard.has_format(PROBE_TYPE)
&& matches!(clipboard.get_contents_for_type(PROBE_TYPE), Some(bytes) if bytes == PROBE_BYTES);
if let Some(previous) = previous {
if !clipboard.set_contents(previous) {
log::warn!("[diagnostics] could not restore the previous clipboard contents");
}
}
if readable {
entry(0, DiagnosticStatus::Pass, "")
} else if written {
entry(0, DiagnosticStatus::Fail, "custom MIME write succeeded but read did not")
} else {
entry(0, DiagnosticStatus::Fail, "custom MIME write was refused")
}
}
};
let displays = platform.displays();
entries[1] = if displays.is_empty() {
entry(1, DiagnosticStatus::Skip, "backend reports no displays")
} else if displays.iter().all(|d| d.scale > 0.0 && d.bounds.width > 0 && d.bounds.height > 0) {
entry(1, DiagnosticStatus::Pass, "")
} else {
entry(1, DiagnosticStatus::Fail, "a display has a non-positive scale or bounds")
};
let probe_window = crate::create_window("Platform Diagnostics", 0, 0, 64, 64);
entries[2] = if displays.is_empty() {
entry(2, DiagnosticStatus::Skip, "no displays to place a window on")
} else if probe_window == 0 {
entry(2, DiagnosticStatus::Skip, "create_window returned 0")
} else {
match platform.display_of_window(probe_window) {
Some(id) if displays.iter().any(|d| d.id == id) => entry(2, DiagnosticStatus::Pass, ""),
Some(_) => entry(2, DiagnosticStatus::Fail, "answered a display id not in displays()"),
None => entry(2, DiagnosticStatus::Fail, "display_of_window returned None"),
}
};
entries[3] = if displays.is_empty() {
entry(3, DiagnosticStatus::Skip, "backend reports no displays")
} else {
let first = platform.display_generation();
let second = platform.display_generation();
if first != 0 && first == second {
entry(3, DiagnosticStatus::Pass, "")
} else if first == 0 {
entry(3, DiagnosticStatus::Fail, "display_generation returned 0")
} else {
entry(
3,
DiagnosticStatus::Fail,
"display_generation changed between two immediate reads",
)
}
};
let dpi = platform.dpi_scale_factor();
entries[4] = if !capabilities.dpi_scaling {
entry(4, DiagnosticStatus::Skip, "backend does not declare dpi_scaling")
} else if dpi.is_finite() && dpi > 0.0 {
entry(4, DiagnosticStatus::Pass, "")
} else {
entry(4, DiagnosticStatus::Fail, "dpi_scale_factor is not a positive finite number")
};
entries[5] = match platform.system_theme() {
Some(_) => entry(5, DiagnosticStatus::Pass, ""),
None => entry(5, DiagnosticStatus::Skip, "no system appearance source on this backend"),
};
let text_scale = platform.text_scale();
entries[6] = if text_scale.is_finite() && text_scale > 0.0 {
entry(6, DiagnosticStatus::Pass, "")
} else {
entry(6, DiagnosticStatus::Fail, "text_scale is not a positive finite number")
};
entries[7] = if probe_window == 0 {
entry(7, DiagnosticStatus::Skip, "no probe window to read insets from")
} else {
match platform.safe_area_insets(probe_window) {
Some(_) => entry(7, DiagnosticStatus::Pass, ""),
None => entry(7, DiagnosticStatus::Skip, "backend cannot read safe-area insets"),
}
};
if probe_window != 0 {
let _ = crate::control_backend::get_control_backend().destroy_widget(probe_window);
}
entries[8] = match platform.ime_bridge() {
Some(_) => entry(8, DiagnosticStatus::Pass, ""),
None => entry(8, DiagnosticStatus::Skip, "no IME bridge"),
};
entries[9] = match platform.accessibility_bridge() {
Some(_) => entry(9, DiagnosticStatus::Pass, ""),
None => entry(9, DiagnosticStatus::Skip, "no accessibility bridge"),
};
entries[10] = if capabilities.file_dialog {
entry(10, DiagnosticStatus::Pass, "")
} else {
entry(10, DiagnosticStatus::Skip, "no host path picker")
};
entries[11] = if capabilities.open_external {
entry(11, DiagnosticStatus::Pass, "")
} else {
entry(11, DiagnosticStatus::Skip, "no external handler")
};
PlatformDiagnostics { backend: platform.backend_name(), entries }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bit_names_are_stable_and_unique() {
assert_eq!(DIAGNOSTIC_NAMES.len(), DIAGNOSTIC_BIT_COUNT);
let mut seen = [false; DIAGNOSTIC_BIT_COUNT];
for name in DIAGNOSTIC_NAMES {
assert!(!name.is_empty());
let index = DIAGNOSTIC_NAMES.iter().position(|n| *n == name).unwrap();
assert!(!seen[index], "duplicate diagnostic name: {name}");
seen[index] = true;
}
}
#[test]
fn mask_counts_only_passes() {
let entries = core::array::from_fn(|bit| entry(bit, DiagnosticStatus::Pass, ""));
let result = PlatformDiagnostics { backend: "test", entries };
assert_eq!(result.mask(), (1 << DIAGNOSTIC_BIT_COUNT) - 1);
assert_eq!(result.passed(), DIAGNOSTIC_BIT_COUNT);
assert!(result.is_pass());
}
#[test]
fn skipped_facts_do_not_fail_a_run() {
let mut entries = core::array::from_fn(|bit| entry(bit, DiagnosticStatus::Pass, ""));
entries[3] = entry(3, DiagnosticStatus::Skip, "not applicable");
let result = PlatformDiagnostics { backend: "test", entries };
assert!(result.is_pass(), "a named skip must not count as failure");
assert_eq!(result.skipped(), 1);
assert_eq!(result.mask() & (1 << 3), 0);
}
#[test]
fn failed_facts_fail_a_run() {
let mut entries = core::array::from_fn(|bit| entry(bit, DiagnosticStatus::Pass, ""));
entries[7] = entry(7, DiagnosticStatus::Fail, "returned a bad value");
let result = PlatformDiagnostics { backend: "test", entries };
assert!(!result.is_pass());
assert_eq!(result.failed(), 1);
}
#[test]
fn json_escapes_quotes_and_reports_every_entry() {
let entries = core::array::from_fn(|bit| {
entry(bit, DiagnosticStatus::Skip, "a \"quoted\" \\ reason")
});
let result = PlatformDiagnostics { backend: "backend \"x\"", entries };
let json = result.to_json();
assert!(json.starts_with("{\"backend\":\"backend \\\"x\\\"\""));
assert!(json.contains("\"mask\":0"));
assert_eq!(json.matches("\"name\":").count(), DIAGNOSTIC_BIT_COUNT);
assert!(json.contains("a \\\"quoted\\\" \\\\ reason"));
assert!(json.ends_with("]}"));
}
#[test]
fn every_entry_reports_its_own_bit_and_name() {
let entries = core::array::from_fn(|bit| entry(bit, DiagnosticStatus::Pass, ""));
let result = PlatformDiagnostics { backend: "test", entries };
for (index, item) in result.entries.iter().enumerate() {
assert_eq!(item.bit as usize, index);
assert_eq!(item.name, DIAGNOSTIC_NAMES[index]);
}
}
}