use std::io::Cursor;
use pdfluent::prelude::*;
const FIXTURE_PATH: &str = "tests/fixtures/sample.pdf";
#[test]
fn open_from_path_parses_fixture() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open path");
assert_eq!(doc.page_count(), 1, "fixture has 1 page");
}
#[test]
fn from_bytes_parses_same_content_as_open() {
let bytes = std::fs::read(FIXTURE_PATH).expect("read fixture");
let doc = PdfDocument::from_bytes(&bytes).expect("from_bytes");
assert_eq!(doc.page_count(), 1);
}
#[test]
fn from_reader_reads_stream() {
let bytes = std::fs::read(FIXTURE_PATH).expect("read fixture");
let cursor = Cursor::new(bytes);
let doc = PdfDocument::from_reader(cursor).expect("from_reader");
assert_eq!(doc.page_count(), 1);
}
#[test]
fn create_produces_minimal_parseable_doc() {
let doc = PdfDocument::create();
assert_eq!(doc.page_count(), 1, "create() seeds a single blank page");
}
#[test]
fn open_missing_file_returns_file_not_found() {
let err = PdfDocument::open("/nonexistent/path/to/file.pdf").unwrap_err();
assert!(
matches!(err, pdfluent::Error::FileNotFound { .. }),
"expected FileNotFound, got {err:?}",
);
}
#[test]
fn from_bytes_with_memory_limit_rejects_large_input() {
let bytes = std::fs::read(FIXTURE_PATH).expect("read fixture");
let err =
PdfDocument::from_bytes_with(&bytes, pdfluent::OpenOptions::new().strict_memory_limit(16))
.unwrap_err();
assert!(
matches!(err, pdfluent::Error::MemoryBudgetExceeded { .. }),
"expected MemoryBudgetExceeded, got {err:?}",
);
}
#[test]
fn version_reads_header() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let v = doc.version();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 7);
assert_eq!(format!("{v}"), "1.7");
}
#[test]
fn text_extracts_fixture_content() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let text = doc.text().expect("text");
assert!(
text.contains("PDFluent test fixture"),
"expected fixture text; got {text:?}",
);
}
#[test]
fn text_with_layout_produces_blocks() {
let doc = PdfDocument::open_with(
FIXTURE_PATH,
pdfluent::OpenOptions::new().with_license_key("tier:developer"),
)
.expect("open");
let blocks = doc.text_with_layout().expect("text_with_layout");
assert!(
!blocks.is_empty(),
"expected at least one text block for fixture with content",
);
for block in &blocks {
assert_eq!(block.page, 1);
assert_eq!(block.bbox.len(), 4);
}
}
#[test]
fn page_by_index_returns_borrowed_handle() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let page = doc.page(1).expect("page 1");
assert_eq!(page.number(), 1);
}
#[test]
fn page_out_of_range_errors() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
assert!(doc.page(0).is_err(), "page 0 is invalid (1-based)");
assert!(doc.page(999).is_err(), "page 999 is beyond doc length");
}
#[test]
fn pages_iterator_yields_exactly_page_count() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let count: usize = doc.pages().count();
assert_eq!(count, doc.page_count());
}
#[test]
fn save_roundtrip_via_path() {
let tmp = std::env::temp_dir().join("pdfluent-test-save-roundtrip.pdf");
let _ = std::fs::remove_file(&tmp);
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save(&tmp).expect("first save to new path");
let reloaded = PdfDocument::open(&tmp).expect("re-open saved");
assert_eq!(reloaded.page_count(), doc.page_count());
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn save_with_default_refuses_existing_file_without_writing() {
let tmp = std::env::temp_dir().join("pdfluent-test-save-default-refuses-existing.pdf");
let original = b"do-not-overwrite".to_vec();
let _ = std::fs::remove_file(&tmp);
std::fs::write(&tmp, &original).expect("seed existing file");
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let err = doc
.save_with(&tmp, pdfluent::SaveOptions::default())
.unwrap_err();
match err {
pdfluent::Error::Io { source, path } => {
assert_eq!(source.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(path.as_deref(), Some(tmp.as_path()));
}
other => panic!("expected Error::Io(AlreadyExists), got {other:?}"),
}
assert_eq!(
std::fs::read(&tmp).expect("re-read existing file"),
original,
"save_with(default) must not write any bytes when the target exists",
);
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn save_with_overwrite_true_replaces_existing_file() {
let tmp = std::env::temp_dir().join("pdfluent-test-save-overwrite-replaces-existing.pdf");
let original = b"old-bytes".to_vec();
let _ = std::fs::remove_file(&tmp);
std::fs::write(&tmp, &original).expect("seed existing file");
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save_with(&tmp, pdfluent::SaveOptions::new().with_overwrite(true))
.expect("with_overwrite(true) must replace existing bytes");
let overwritten = std::fs::read(&tmp).expect("read overwritten file");
assert_ne!(overwritten, original);
assert!(
overwritten.starts_with(b"%PDF-"),
"overwrite=true should replace the file with a PDF payload",
);
let _ = std::fs::remove_file(&tmp);
}
#[cfg(unix)]
#[test]
fn open_with_strict_memory_limit_rejects_oversized_unreadable_file_before_read() {
use std::os::unix::fs::PermissionsExt;
let tmp = std::env::temp_dir().join("pdfluent-test-open-memory-limit-before-read.pdf");
let _ = std::fs::remove_file(&tmp);
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&tmp)
.expect("create temp file");
file.set_len(8 * 1024 * 1024)
.expect("create sparse oversized file");
drop(file);
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o000))
.expect("remove read permissions");
let err = PdfDocument::open_with(&tmp, pdfluent::OpenOptions::new().strict_memory_limit(1024))
.unwrap_err();
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))
.expect("restore permissions for cleanup");
let _ = std::fs::remove_file(&tmp);
match err {
pdfluent::Error::MemoryBudgetExceeded { requested, limit } => {
assert_eq!(requested, 8 * 1024 * 1024);
assert_eq!(limit, 1024);
}
other => panic!("expected MemoryBudgetExceeded before any fs::read, got {other:?}"),
}
}
#[test]
fn save_refuses_to_clobber_existing_file_by_default() {
let tmp = std::env::temp_dir().join("pdfluent-test-save-refuse-clobber.pdf");
let _ = std::fs::remove_file(&tmp);
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save(&tmp).expect("first save creates new file");
let err = doc.save(&tmp).unwrap_err();
match err {
pdfluent::Error::Io { source, .. } => {
assert_eq!(source.kind(), std::io::ErrorKind::AlreadyExists);
}
other => panic!("expected Error::Io(AlreadyExists), got {other:?}"),
}
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn save_with_overwrite_true_clobbers() {
let tmp = std::env::temp_dir().join("pdfluent-test-overwrite-true.pdf");
let _ = std::fs::remove_file(&tmp);
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save(&tmp).expect("first save");
doc.save_with(&tmp, pdfluent::SaveOptions::new().with_overwrite(true))
.expect("with_overwrite(true) must clobber");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn to_bytes_returns_parseable_output() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let bytes = doc.to_bytes().expect("to_bytes");
assert!(
bytes.starts_with(b"%PDF-"),
"serialised output must begin with %PDF- header",
);
let reloaded = PdfDocument::from_bytes(&bytes).expect("re-parse serialised bytes");
assert_eq!(reloaded.page_count(), doc.page_count());
}
#[test]
fn write_to_forwards_full_bytes() {
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
let mut buf: Vec<u8> = Vec::new();
doc.write_to(&mut buf).expect("write_to");
assert!(buf.starts_with(b"%PDF-"));
let reloaded = PdfDocument::from_bytes(&buf).expect("re-parse write_to output");
assert_eq!(reloaded.page_count(), doc.page_count());
}
#[test]
fn save_with_overwrite_false_refuses_existing_file() {
let tmp = std::env::temp_dir().join("pdfluent-test-overwrite-false.pdf");
let _ = std::fs::remove_file(&tmp);
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save(&tmp).expect("first save");
let err = doc
.save_with(&tmp, pdfluent::SaveOptions::new().with_overwrite(false))
.unwrap_err();
match err {
pdfluent::Error::Io { source, .. } => {
assert_eq!(source.kind(), std::io::ErrorKind::AlreadyExists);
}
other => panic!("expected Error::Io(AlreadyExists), got {other:?}"),
}
doc.save_with(&tmp, pdfluent::SaveOptions::new().with_overwrite(true))
.expect("overwrite=true must succeed");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn open_with_memory_limit_rejects_before_reading_large_file() {
let fixture_size = std::fs::metadata(FIXTURE_PATH).unwrap().len() as usize;
let tiny_limit = fixture_size / 2;
let err = PdfDocument::open_with(
FIXTURE_PATH,
pdfluent::OpenOptions::new().strict_memory_limit(tiny_limit),
)
.unwrap_err();
match err {
pdfluent::Error::MemoryBudgetExceeded { requested, limit } => {
assert_eq!(
requested, fixture_size,
"requested should be the file size derived from metadata",
);
assert_eq!(limit, tiny_limit);
}
other => panic!("expected MemoryBudgetExceeded, got {other:?}"),
}
}
#[test]
fn save_with_linearize_is_noop_in_1_0() {
let tmp = std::env::temp_dir().join("pdfluent-test-linearize-noop.pdf");
let _ = std::fs::remove_file(&tmp);
let doc = PdfDocument::open(FIXTURE_PATH).expect("open");
doc.save_with(&tmp, pdfluent::SaveOptions::new().with_linearize(true))
.expect("save_with linearize=true must not error in 1.0");
let _ = std::fs::remove_file(&tmp);
}