CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
Completes the English-only standardization for code, comments, scripts and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en, untouched). Covers the remaining crates/, apps/gui/ui/ and apps/gui/src-tauri/ comments, .gitea/ workflow and template comments, .githooks/ hooks, and the scripts/ source comments for the files already renamed in a prior commit (check.sh, generate-logo.py, install-hooks.sh). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
297 lines
10 KiB
Rust
297 lines
10 KiB
Rust
//! Disk health (SMART), notifications, user settings, and resumable recovery journal tests.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use guara_core::*;
|
|
use serde_json::json;
|
|
|
|
// ------------------------------------------------------------------ disk health (SMART)
|
|
|
|
#[test]
|
|
fn smart_reprovado_e_setores_ruins_viram_alerta_serio() {
|
|
let v = json!({
|
|
"model_name": "WDC WD5000", "serial_number": "ABC123",
|
|
"power_on_time": {"hours": 43000}, "temperature": {"current": 38},
|
|
"smart_status": {"passed": false},
|
|
"ata_smart_attributes": {"table": [
|
|
{"id": 5, "raw": {"value": 120}}, {"id": 197, "raw": {"value": 3}}, {"id": 199, "raw": {"value": 0}}
|
|
]}
|
|
});
|
|
let h = health::parse_smartctl(&v);
|
|
assert!(h.available);
|
|
assert_eq!(h.status, "failing");
|
|
assert!(h
|
|
.details
|
|
.iter()
|
|
.any(|(k, v)| k == "Setores realocados" && v == "120"));
|
|
assert!(h
|
|
.details
|
|
.iter()
|
|
.any(|(k, _)| k == "Setores pendentes de realocação"));
|
|
assert!(
|
|
h.advice.iter().any(|a| a.contains("imagem")),
|
|
"deve recomendar a imagem primeiro"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn smart_saudavel_nvme_desgastado_e_nvme_critico() {
|
|
let ok = json!({"smart_status": {"passed": true}, "ata_smart_attributes": {"table": [{"id": 5, "raw": {"value": 0}}, {"id": 197, "raw": {"value": 0}}]}});
|
|
let h = health::parse_smartctl(&ok);
|
|
assert_eq!(h.status, "ok");
|
|
assert!(h.advice.is_empty());
|
|
|
|
let worn = json!({"smart_status": {"passed": true}, "nvme_smart_health_information_log": {"critical_warning": 0, "percentage_used": 93, "media_errors": 0, "available_spare": 80, "available_spare_threshold": 10}});
|
|
assert_eq!(health::parse_smartctl(&worn).status, "warning");
|
|
|
|
let crit = json!({"smart_status": {"passed": true}, "nvme_smart_health_information_log": {"critical_warning": 4, "percentage_used": 10, "media_errors": 0}});
|
|
assert_eq!(health::parse_smartctl(&crit).status, "failing");
|
|
|
|
let media = json!({"nvme_smart_health_information_log": {"critical_warning": 0, "percentage_used": 10, "media_errors": 7}});
|
|
assert_eq!(health::parse_smartctl(&media).status, "failing");
|
|
}
|
|
|
|
#[test]
|
|
fn smart_ssd_sata_traz_ficha_metricas_e_tabela() {
|
|
let v = json!({
|
|
"device": {"protocol": "ATA"},
|
|
"model_name": "Samsung SSD 860 EVO 500GB", "firmware_version": "RVT04B6Q",
|
|
"serial_number": "S3Z1NB0K", "sata_version": {"string": "SATA 3.2"},
|
|
"interface_speed": {"max": {"string": "6.0 Gb/s"}, "current": {"string": "6.0 Gb/s"}},
|
|
"user_capacity": {"bytes": 500107862016u64}, "logical_block_size": 512, "rotation_rate": 0,
|
|
"smart_support": {"enabled": true}, "trim": {"supported": true},
|
|
"smart_status": {"passed": true}, "temperature": {"current": 31},
|
|
"power_on_time": {"hours": 12034}, "power_cycle_count": 811,
|
|
"ata_smart_attributes": {"table": [
|
|
{"id": 5, "name": "Reallocated_Sector_Ct", "value": 100, "worst": 100, "thresh": 10, "raw": {"value": 0, "string": "0"}},
|
|
{"id": 177, "name": "Wear_Leveling_Count", "value": 92, "worst": 92, "thresh": 0, "raw": {"value": 41, "string": "41"}},
|
|
{"id": 241, "name": "Total_LBAs_Written", "value": 99, "worst": 99, "thresh": 0, "raw": {"value": 2000000000u64, "string": "2000000000"}}
|
|
]}
|
|
});
|
|
let h = health::parse_smartctl(&v);
|
|
assert_eq!(h.status, "ok");
|
|
assert_eq!(h.health_pct, Some(92));
|
|
assert_eq!(h.temperature_c, Some(31));
|
|
assert_eq!(h.power_cycles, Some(811));
|
|
assert_eq!(h.host_writes, Some(2_000_000_000 * 512));
|
|
assert!(h
|
|
.info
|
|
.iter()
|
|
.any(|(k, v)| k == "Capacidade" && v == "500,1 GB"));
|
|
assert!(h
|
|
.info
|
|
.iter()
|
|
.any(|(k, v)| k == "Recursos" && v.contains("TRIM")));
|
|
assert_eq!(h.attributes.len(), 3);
|
|
assert_eq!(h.attributes[0].id, "05");
|
|
assert_eq!(h.attributes[0].name, "Reallocated Sector Ct");
|
|
assert_eq!(h.attributes[1].id, "B1");
|
|
}
|
|
|
|
#[test]
|
|
fn smart_atributo_abaixo_do_limite_reprova_e_nvme_vira_tabela() {
|
|
let v = json!({"smart_status": {"passed": true}, "ata_smart_attributes": {"table": [
|
|
{"id": 1, "name": "Raw_Read_Error_Rate", "value": 40, "worst": 40, "thresh": 51, "raw": {"value": 9}}
|
|
]}});
|
|
let h = health::parse_smartctl(&v);
|
|
assert_eq!(h.attributes[0].status, "failing");
|
|
assert_eq!(h.status, "failing");
|
|
|
|
let nv = json!({"smart_status": {"passed": true}, "nvme_smart_health_information_log": {
|
|
"critical_warning": 0, "temperature": 40, "available_spare": 100, "available_spare_threshold": 10,
|
|
"percentage_used": 3, "data_units_read": 1000, "data_units_written": 2000,
|
|
"power_cycles": 50, "power_on_hours": 900, "unsafe_shutdowns": 4, "media_errors": 0}});
|
|
let h = health::parse_smartctl(&nv);
|
|
assert_eq!(h.health_pct, Some(97));
|
|
assert_eq!(h.host_writes, Some(2000 * 512_000));
|
|
assert_eq!(h.power_cycles, Some(50));
|
|
assert!(h
|
|
.attributes
|
|
.iter()
|
|
.any(|a| a.id == "0D" && a.name == "Unsafe Shutdowns" && a.raw == "4"));
|
|
}
|
|
|
|
#[test]
|
|
fn imagem_de_disco_nao_tem_smart() {
|
|
let f = tempfile::NamedTempFile::new().unwrap();
|
|
let h = health::check(f.path().to_str().unwrap());
|
|
assert!(!h.available);
|
|
assert_eq!(h.status, "unknown");
|
|
}
|
|
|
|
// ------------------------------------------------------------------ settings
|
|
|
|
#[test]
|
|
fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
|
|
init();
|
|
// legacy v0.3 settings file containing only `theme`: newer fields use defaults
|
|
std::fs::create_dir_all(settings::config_dir()).unwrap();
|
|
std::fs::write(
|
|
settings::config_dir().join("settings.json"),
|
|
br#"{"theme":"dark"}"#,
|
|
)
|
|
.unwrap();
|
|
let s = settings::load();
|
|
assert_eq!(
|
|
(s.theme.as_str(), s.lang.as_str(), s.notify, s.maps),
|
|
("dark", "auto", true, false)
|
|
);
|
|
settings::save(&settings::Settings {
|
|
theme: "light".into(),
|
|
lang: "en".into(),
|
|
ui_lang: Some("en".into()),
|
|
notify: false,
|
|
maps: true,
|
|
updates: false,
|
|
})
|
|
.unwrap();
|
|
let s = settings::load();
|
|
assert_eq!(
|
|
(
|
|
s.theme.as_str(),
|
|
s.lang.as_str(),
|
|
s.notify,
|
|
s.maps,
|
|
s.updates
|
|
),
|
|
("light", "en", false, true, false)
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ notifications
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn notificacao_chama_o_notify_send() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let bin = tempfile::tempdir().unwrap();
|
|
let marker = bin.path().join("recebido");
|
|
let script = bin.path().join("notify-send");
|
|
std::fs::write(
|
|
&script,
|
|
format!("#!/bin/sh\nprintf '%s|' \"$@\" > '{}'\n", marker.display()),
|
|
)
|
|
.unwrap();
|
|
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
let old = std::env::var("PATH").unwrap_or_default();
|
|
std::env::set_var("PATH", format!("{}:{old}", bin.path().display()));
|
|
notify::send(notify::Notice::ScanDone {
|
|
count: 7,
|
|
target: "disco".into(),
|
|
});
|
|
for _ in 0..100 {
|
|
if marker.exists() && !std::fs::read(&marker).unwrap().is_empty() {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
}
|
|
std::env::set_var("PATH", old);
|
|
let got = String::from_utf8(std::fs::read(&marker).unwrap()).unwrap();
|
|
assert!(got.contains("Guará: "), "{got}");
|
|
assert!(
|
|
got.contains("7 arquivo(s) encontrados em disco.")
|
|
|| got.contains("7 file(s) found in disco."),
|
|
"{got}"
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ resumable recovery
|
|
|
|
#[test]
|
|
fn recuperacao_interrompida_retoma_pulando_o_que_ja_foi_conferido() {
|
|
let Some(dir) = fixtures() else { return };
|
|
let img = dir.join("fat32.img");
|
|
let res = scan_path(
|
|
&img,
|
|
&ScanOptions {
|
|
mode: ScanMode::Quick,
|
|
..Default::default()
|
|
},
|
|
);
|
|
assert_eq!(res.found.len(), 7);
|
|
let src = source::open_path(&img).unwrap();
|
|
let files: Vec<&Found> = res.found.iter().collect();
|
|
let out = tempfile::tempdir().unwrap();
|
|
let key = "disco-de-teste";
|
|
|
|
// 1st run: abort after writing 3 of the 7 files
|
|
let n = std::sync::atomic::AtomicUsize::new(0);
|
|
let cancel = || n.fetch_add(1, std::sync::atomic::Ordering::Relaxed) >= 3;
|
|
let r1 = recover::recover_resumable(
|
|
src.as_ref(),
|
|
&files,
|
|
out.path(),
|
|
recover::RecoverLayout::Folders,
|
|
key,
|
|
true,
|
|
&cancel,
|
|
&mut |_, _, _| {},
|
|
)
|
|
.unwrap();
|
|
assert!(r1.cancelled);
|
|
assert_eq!(r1.recovered.len(), 3);
|
|
let journal = std::fs::read_to_string(out.path().join(recover::JOURNAL)).unwrap();
|
|
assert_eq!(journal.lines().count(), 3);
|
|
|
|
// 2nd run: resume skips the 3 verified files and writes the remaining 4
|
|
let r2 = recover::recover_resumable(
|
|
src.as_ref(),
|
|
&files,
|
|
out.path(),
|
|
recover::RecoverLayout::Folders,
|
|
key,
|
|
true,
|
|
&|| false,
|
|
&mut |_, _, _| {},
|
|
)
|
|
.unwrap();
|
|
assert!(!r2.cancelled);
|
|
assert_eq!((r2.skipped, r2.recovered.len()), (3, 7));
|
|
let got = hashes_in(out.path());
|
|
for (h, p) in expected(&dir, "fat32") {
|
|
assert!(got.contains_key(&h), "faltou {p}");
|
|
}
|
|
assert_eq!(
|
|
std::fs::read_to_string(out.path().join(recover::JOURNAL))
|
|
.unwrap()
|
|
.lines()
|
|
.count(),
|
|
7
|
|
);
|
|
|
|
// corrupt one recovered file on disk: resume re-recovers that file and skips the other 6
|
|
let victim = out.path().join(&r2.recovered[0].path);
|
|
std::fs::write(&victim, b"lixo").unwrap();
|
|
let r3 = recover::recover_resumable(
|
|
src.as_ref(),
|
|
&files,
|
|
out.path(),
|
|
recover::RecoverLayout::Folders,
|
|
key,
|
|
true,
|
|
&|| false,
|
|
&mut |_, _, _| {},
|
|
)
|
|
.unwrap();
|
|
assert_eq!((r3.skipped, r3.recovered.len(), r3.failed.len()), (6, 7, 0));
|
|
assert!(
|
|
hashes_in(out.path()).contains_key(&r2.recovered[0].sha256),
|
|
"o arquivo danificado deveria ter sido refeito"
|
|
);
|
|
|
|
// a different `source_key` does not reuse the previous disk's recovery journal
|
|
let r4 = recover::recover_resumable(
|
|
src.as_ref(),
|
|
&files,
|
|
out.path(),
|
|
recover::RecoverLayout::Folders,
|
|
"outro-disco",
|
|
true,
|
|
&|| false,
|
|
&mut |_, _, _| {},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(r4.skipped, 0);
|
|
let _ = json!({});
|
|
}
|