use std::path::Path;
use crate::server::database::PvDatabase;
use super::error::AutosaveResult;
use super::save_file::{self, MalformedLine, SaveEntry, read_partial_save_file};
#[derive(Debug, Clone)]
pub enum MatchResult {
Match,
Mismatch {
saved: String,
live: String,
},
PvNotFound,
ParseError,
DisconnectedAtSave,
}
#[derive(Debug, Clone)]
pub struct VerifyEntry {
pub pv_name: String,
pub saved_value: String,
pub live_value: Option<String>,
pub result: MatchResult,
}
#[derive(Debug, Clone, Default)]
pub struct VerifyReport {
pub entries: Vec<VerifyEntry>,
pub malformed: Vec<MalformedLine>,
pub truncated: bool,
}
pub async fn verify(db: &PvDatabase, save_file_path: &Path) -> AutosaveResult<VerifyReport> {
let read = read_partial_save_file(save_file_path).await?;
let entries = read
.contents
.entries
.iter()
.map(|entry| {
let (result, live_value) = classify(db, entry);
VerifyEntry {
pv_name: entry.pv_name.clone(),
saved_value: entry.value.clone(),
live_value,
result,
}
})
.collect();
Ok(VerifyReport {
entries,
malformed: read.contents.malformed,
truncated: !read.complete,
})
}
fn classify(db: &PvDatabase, entry: &SaveEntry) -> (MatchResult, Option<String>) {
if !entry.connected {
return (MatchResult::DisconnectedAtSave, None);
}
let Ok(live) = db.get_pv(&entry.pv_name) else {
return (MatchResult::PvNotFound, None);
};
let live_str = save_file::value_to_save_str(&live);
let Some(parsed) = save_file::parse_save_value(&entry.value, &live) else {
return (MatchResult::ParseError, Some(live_str));
};
if parsed == live {
(MatchResult::Match, Some(live_str))
} else {
(
MatchResult::Mismatch {
saved: entry.value.clone(),
live: live_str.clone(),
},
Some(live_str),
)
}
}
pub fn format_verify_report(report: &VerifyReport) -> String {
let mut out = String::new();
if report.truncated {
out.push_str("asVerify: Can't find <END> marker. File may be bad.\n");
}
let mut match_count = 0;
let mut mismatch_count = 0;
let mut not_found_count = 0;
let mut parse_error_count = 0;
let mut disconnected_count = 0;
for entry in &report.entries {
match &entry.result {
MatchResult::Match => {
match_count += 1;
}
MatchResult::Mismatch { saved, live } => {
mismatch_count += 1;
out.push_str(&format!(
"MISMATCH: {} saved={} live={}\n",
entry.pv_name, saved, live
));
}
MatchResult::PvNotFound => {
not_found_count += 1;
out.push_str(&format!("NOT_FOUND: {}\n", entry.pv_name));
}
MatchResult::ParseError => {
parse_error_count += 1;
out.push_str(&format!(
"PARSE_ERROR: {} saved={}\n",
entry.pv_name, entry.saved_value
));
}
MatchResult::DisconnectedAtSave => {
disconnected_count += 1;
out.push_str(&format!(
"NOT_CHECKED: {} (disconnected at save)\n",
entry.pv_name
));
}
}
}
for line in &report.malformed {
out.push_str(&format!(
"MALFORMED: line {}: {}\n",
line.line_no, line.text
));
}
out.push_str(&format!(
"\nSummary: {} match, {} mismatch, {} not found, {} parse errors, \
{} not checked (disconnected at save), {} malformed lines{}\n",
match_count,
mismatch_count,
not_found_count,
parse_error_count,
disconnected_count,
report.malformed.len(),
if report.truncated {
" -- INCOMPLETE FILE, no <END> marker"
} else {
""
}
));
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
use crate::server::records::ao::AoRecord;
#[epics_macros_rs::epics_test]
async fn verify_on_corrupt_save_file_reports_it_and_still_compares() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("corrupt.sav");
crate::runtime::fs::write(&path, "# autosave-rs V1.0\nPV1 1.0\nPV2 2.0\n")
.await
.unwrap();
let db = PvDatabase::new();
db.add_record("PV1", Box::new(AoRecord::new(1.0)))
.await
.unwrap();
let report = verify(&db, &path)
.await
.expect("a truncated file is verified, not refused");
assert!(report.truncated, "the missing marker must be reported");
assert_eq!(report.entries.len(), 2, "both lines must be compared");
assert!(matches!(report.entries[0].result, MatchResult::Match));
assert!(matches!(report.entries[1].result, MatchResult::PvNotFound));
let text = format_verify_report(&report);
assert!(
text.contains("Can't find <END> marker"),
"the report must open with the truncation; got: {text:?}"
);
assert!(
text.contains("INCOMPLETE FILE"),
"the summary line must carry it too; got: {text:?}"
);
}
#[epics_macros_rs::epics_test]
async fn verify_on_valid_save_file_succeeds() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ok.sav");
crate::runtime::fs::write(&path, "# autosave-rs V1.0\nPV1 1.0\n<END>\n")
.await
.unwrap();
let db = PvDatabase::new();
db.add_record("PV1", Box::new(AoRecord::new(1.0)))
.await
.unwrap();
let report = verify(&db, &path).await.expect("valid file must verify");
assert_eq!(report.entries.len(), 1);
assert!(matches!(report.entries[0].result, MatchResult::Match));
}
}