mod common;
use std::process::Command;
use boko::model::Format;
fn epubcheck_errors(bytes: &[u8], name: &str) -> Option<usize> {
let dir = std::env::temp_dir().join("boko-export-validity");
std::fs::create_dir_all(&dir).ok()?;
let path = dir.join(name);
std::fs::write(&path, bytes).ok()?;
let output = Command::new("epubcheck").arg(&path).output().ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let errors = text.lines().find_map(|line| {
let rest = line.strip_prefix("Messages: ")?;
let mut parts = rest.split(" / ");
let fatals: usize = parts.next()?.split_whitespace().next()?.parse().ok()?;
let errors: usize = parts.next()?.split_whitespace().next()?.parse().ok()?;
Some(fatals + errors)
});
std::fs::remove_file(&path).ok();
errors
}
#[test]
fn epub_to_epub_export_passes_epubcheck() {
let mut book = common::open_fixture("epictetus.epub");
let bytes = common::export_to_bytes(&mut book, Format::Epub);
match epubcheck_errors(&bytes, "from_epub.epub") {
Some(errors) => assert_eq!(errors, 0, "epubcheck reported {errors} errors"),
None => eprintln!("epubcheck not available; skipping validation"),
}
}
#[test]
fn kfx_to_epub_export_is_nearly_clean_under_epubcheck() {
let mut book = common::open_fixture("epictetus.kfx");
let bytes = common::export_to_bytes(&mut book, Format::Epub);
match epubcheck_errors(&bytes, "from_kfx.epub") {
Some(errors) => assert!(
errors <= 3,
"epubcheck reported {errors} errors (expected <= 3)"
),
None => eprintln!("epubcheck not available; skipping validation"),
}
}
#[test]
fn kfx_to_azw3_toc_targets_are_distinct() {
let mut book = common::open_fixture("epictetus.kfx");
let reimported = common::roundtrip(&mut book, Format::Azw3);
let toc = reimported.toc();
assert!(
toc.len() > 3,
"expected a real TOC, got {} entries",
toc.len()
);
let mut hrefs = std::collections::HashSet::new();
fn collect<'a>(
entries: &'a [boko::model::TocEntry],
out: &mut std::collections::HashSet<&'a str>,
) {
for e in entries {
out.insert(e.href.as_str());
collect(&e.children, out);
}
}
collect(toc, &mut hrefs);
assert!(
hrefs.len() > 3,
"TOC entries collapsed to too few targets: {hrefs:?}"
);
}
#[test]
fn rtl_page_progression_direction_survives_epub_roundtrip() {
use common::{Doc, EpubBuilder, Nav};
let epub = EpubBuilder::new("RTL Book")
.language("ar")
.direction("rtl")
.doc(Doc::new(
"text/ch1.xhtml",
"الفصل",
"<h1>عنوان</h1><p>نص عربي.</p>",
))
.nav(vec![Nav::new("عنوان", "text/ch1.xhtml")])
.build();
let mut book = boko::Book::from_bytes(&epub, Format::Epub).expect("import rtl epub");
assert_eq!(
book.metadata().page_progression_direction.as_deref(),
Some("rtl"),
"RTL direction must be read from the source spine"
);
let bytes = common::export_to_bytes(&mut book, Format::Epub);
let reimported = boko::Book::from_bytes(&bytes, Format::Epub).expect("reimport rtl epub");
assert_eq!(
reimported.metadata().page_progression_direction.as_deref(),
Some("rtl"),
"RTL direction must survive the EPUB round trip"
);
if let Some(errors) = epubcheck_errors(&bytes, "rtl.epub") {
assert_eq!(
errors, 0,
"epubcheck reported {errors} errors on the RTL export"
);
}
}