disc: add progress_snapshot_from_mapfile one-shot + locate_ranges test

The library now offers a one-shot PassProgress snapshot built from a mapfile on
disk + title, so a client gets the fully-rendered drilldown without parsing the
mapfile itself — used for autorip's pass-boundary and mux-entry paints (no live
callback yet at those points). Adds a locate_ranges test covering in-feature vs
out-of-feature at-risk time (ported from autorip's removed from_map tests).
This commit is contained in:
Matthew Jackson
2026-06-30 16:09:28 -07:00
parent 0f0c496a4d
commit 65c6835363
+72
View File
@@ -685,6 +685,44 @@ pub fn locate_ranges(raw: &[(u64, u64)], title: &DiscTitle) -> crate::progress::
}
}
/// One-shot progress snapshot built from a mapfile on disk plus the title. The
/// library reads + parses the mapfile HERE so a client (autorip) gets a
/// fully-rendered [`crate::progress::PassProgress`] without ever touching
/// mapfile internals — used for the pass-boundary paint (before the live
/// callback stream begins) and the terminal done-card verdict. Returns `None`
/// if the mapfile can't be read. `work_done`/`work_total` are `0`: this is a
/// point-in-time snapshot, not a per-pass progress tick.
pub fn progress_snapshot_from_mapfile(
mapfile_path: &std::path::Path,
title: Option<&DiscTitle>,
kind: crate::progress::PassKind,
bytes_total_disc: u64,
) -> Option<crate::progress::PassProgress> {
use mapfile::SectorStatus::{NonScraped, NonTrimmed, Unreadable};
let map = mapfile::Mapfile::load(mapfile_path).ok()?;
let stats = map.stats();
// MAYBE set = not-yet-good (NonTrimmed/NonScraped/Unreadable), excluding
// NonTried (the unread remainder) — same set the live patch emitter uses.
let maybe = map.ranges_with(&[NonTrimmed, NonScraped, Unreadable]);
let located = title.map(|t| locate_ranges(&maybe, t)).unwrap_or_default();
let main_bad = title.map(|t| bytes_bad_in_title(t, &maybe)).unwrap_or(0);
Some(crate::progress::PassProgress {
kind,
work_done: 0,
work_total: 0,
bytes_good_total: stats.bytes_good,
bytes_unreadable_total: stats.bytes_unreadable,
bytes_pending_total: stats.bytes_pending,
bytes_retryable_total: stats.bytes_retryable,
bytes_total_disc,
disc_duration_secs: title.map(|t| t.duration_secs),
bytes_bad_in_main_title: main_bad,
main_title_duration_secs: title.map(|t| t.duration_secs),
main_title_size_bytes: title.map(|t| t.size_bytes),
located,
})
}
// ─── Display helpers ────────────────────────────────────────────────────────
impl Codec {
@@ -4181,6 +4219,40 @@ mod tests {
}
}
#[test]
fn locate_ranges_at_risk_in_vs_out_of_feature() {
// The honest-"Maybe" behaviour the rendered drilldown must preserve:
// in-feature damage counts as movie time at risk; out-of-feature damage
// is still located but reads 0:00. (Ported from autorip's old
// RipProgress::from_map tests when that logic moved into the library.)
// bps = size_bytes / duration_secs = 4096 B/s → 4096 B == 1000 ms.
let mut title = title_with_video(Codec::Hevc, Resolution::R2160p);
title.duration_secs = 100.0;
title.size_bytes = 409_600;
// Feature extent = sectors [10,110) → bytes [20480, 225280).
title.extents = vec![Extent {
start_lba: 10,
sector_count: 100,
}];
// In-feature pending range: 4096 B == 1000 ms of movie at risk.
let in_feat = locate_ranges(&[(40_960, 4096)], &title);
assert_eq!(in_feat.num_ranges, 1);
assert!(
(in_feat.main_at_risk_ms - 1000.0).abs() < 1.0,
"in-feature damage must count as at-risk movie time, got {}",
in_feat.main_at_risk_ms
);
// Out-of-feature range: located, but zero movie time at risk.
let out_feat = locate_ranges(&[(2_000_000, 100_000)], &title);
assert_eq!(out_feat.num_ranges, 1, "still a located range");
assert_eq!(
out_feat.main_at_risk_ms, 0.0,
"out-of-feature damage must not read as movie loss"
);
}
/// Build a DiscTitle with full control over the fields the title
/// sorter cares about. Used by the canonical-title-order tests.
fn title_with(