use std::path::Path;
use liteparse::config::OutputFormat;
use liteparse::conversion::convert_data_to_pdf;
use liteparse::ocr_merge::ComplexityReason;
use liteparse::types::PdfInput;
use liteparse::{LiteParse, LiteParseConfig};
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_screenshot_image_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let lit = LiteParse::new(LiteParseConfig::default());
let results = lit
.screenshot("../../integration_tests_data/receipt.png", None)
.await
.expect("Should be able to screenshot converted image");
assert_eq!(results.len(), 1);
assert!(results[0].width > 0);
assert!(results[0].height > 0);
assert!(!results[0].image_bytes.is_empty());
}
#[tokio::test]
#[serial]
async fn test_screenshot_pdf_integration() {
let lit = LiteParse::new(LiteParseConfig::default());
let results = lit
.screenshot("../../integration_tests_data/sample.pdf", None)
.await
.expect("Should be able to screenshot PDF");
assert_eq!(results.len(), 1);
assert!(!results[0].image_bytes.is_empty());
}
#[tokio::test]
async fn test_screenshot_rejects_text_file() {
let dir = tempfile::tempdir().unwrap();
let txt_path = dir.path().join("notes.txt");
std::fs::write(&txt_path, "hello").unwrap();
let lit = LiteParse::new(LiteParseConfig::default());
let err = lit
.screenshot(txt_path.to_str().unwrap(), None)
.await
.unwrap_err()
.to_string();
assert!(err.contains("Cannot screenshot text-based format"));
}
#[tokio::test]
#[serial]
async fn test_convert_data_to_pdf_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let fixture_path = "../../integration_tests_data/receipt.png";
let data = tokio::fs::read(fixture_path)
.await
.expect("Should be able to read file");
let (converted, _temps) = convert_data_to_pdf(data, None)
.await
.expect("Should be able to convert data to PDF");
assert!(Path::new(&converted.pdf_path).exists());
}
#[tokio::test]
#[serial]
async fn test_parse_bytes_image_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let fixture_path = "../../integration_tests_data/receipt.png";
let lit = LiteParse::new(LiteParseConfig::default());
let data = tokio::fs::read(fixture_path)
.await
.expect("Should be able to read file");
let input = PdfInput::Bytes(data);
let parsed = lit
.parse_input(input)
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 1);
}
#[tokio::test]
#[serial]
async fn test_parse_bytes_office_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let fixture_path = "../../integration_tests_data/sample3.doc";
let lit = LiteParse::new(LiteParseConfig::default());
let data = tokio::fs::read(fixture_path)
.await
.expect("Should be able to read file");
let input = PdfInput::Bytes(data);
let parsed = lit
.parse_input(input)
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 2);
}
#[tokio::test]
#[serial]
async fn test_parse_image_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let lit = LiteParse::new(LiteParseConfig::default());
let parsed = lit
.parse("../../integration_tests_data/receipt.png")
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 1);
}
#[tokio::test]
#[serial]
async fn test_parse_office_doc_integration() {
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let lit = LiteParse::new(LiteParseConfig::default());
let parsed = lit
.parse("../../integration_tests_data/sample3.doc")
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 2);
}
#[tokio::test]
#[serial]
async fn test_parse_pdf_integration() {
let lit = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
extract_document_metadata: true,
..LiteParseConfig::default()
});
let parsed = lit
.parse("../../integration_tests_data/sample.pdf")
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 1);
let doc_meta = parsed.doc_meta.expect("doc_meta requested");
assert!(doc_meta.file_version.is_some());
assert_eq!(doc_meta.is_encrypted, Some(false));
assert!(doc_meta.raw_file_size.is_some_and(|size| size > 0));
assert!(doc_meta.eof_section_count.is_some_and(|count| count > 0));
assert_eq!(doc_meta.signature_count, Some(0));
}
#[tokio::test]
#[serial]
async fn test_doc_meta_absent_unless_requested() {
let lit = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
..LiteParseConfig::default()
});
let parsed = lit
.parse("../../integration_tests_data/sample.pdf")
.await
.expect("Should be able to parse");
assert!(parsed.doc_meta.is_none());
}
#[tokio::test]
#[serial]
async fn test_parse_bytes_pdf_integration() {
let fixture_path = "../../integration_tests_data/sample.pdf";
let lit = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
extract_document_metadata: true,
..LiteParseConfig::default()
});
let data = tokio::fs::read(fixture_path)
.await
.expect("Should be able to read file");
let expected_size = data.len() as u64;
let input = PdfInput::Bytes(data);
let parsed = lit
.parse_input(input)
.await
.expect("Should be able to parse");
assert_eq!(parsed.pages.len(), 1);
assert_eq!(
parsed.doc_meta.and_then(|meta| meta.raw_file_size),
Some(expected_size)
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_parse_does_not_crash() {
use std::sync::Arc;
use tokio::task::JoinSet;
let env_var = std::env::var("SKIP_INTEGRATION_TESTS");
if let Ok(v) = env_var
&& v == "yes"
{
return;
}
let lit = Arc::new(LiteParse::new(LiteParseConfig {
ocr_enabled: false,
quiet: true,
..LiteParseConfig::default()
}));
let bytes = tokio::fs::read("../../integration_tests_data/sample.pdf")
.await
.expect("fixture exists");
let mut set: JoinSet<usize> = JoinSet::new();
for _ in 0..16 {
let lit = lit.clone();
let bytes = bytes.clone();
set.spawn(async move {
let parsed = lit
.parse_input(PdfInput::Bytes(bytes))
.await
.expect("parse should succeed");
parsed.pages.len()
});
}
let mut total = 0;
while let Some(joined) = set.join_next().await {
total += joined.expect("task panicked");
}
assert_eq!(total, 16);
}
#[tokio::test]
#[serial]
async fn test_annotation_text_complexity_reason() {
let lit = LiteParse::new(LiteParseConfig::default());
let stats = lit
.is_complex(PdfInput::Path(
"../../integration_tests_data/annotation_text.pdf".into(),
))
.await
.expect("is_complex should succeed");
assert_eq!(stats.len(), 1);
let page = &stats[0];
assert_eq!(page.text_length, 0, "annotation text is not extractable");
assert!(page.needs_ocr);
assert!(page.reasons.contains(&ComplexityReason::NoText));
assert!(
page.reasons.contains(&ComplexityReason::AnnotationText),
"expected annotation-text, got {:?}",
page.reasons
);
}
#[tokio::test]
#[serial]
async fn test_filled_acroform_values_are_extracted_as_text() {
let lit = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
output_format: OutputFormat::Markdown,
..Default::default()
});
let parsed = lit
.parse("../../integration_tests_data/filled_acroform.pdf")
.await
.expect("filled form should parse");
for (expected, case) in [
(
"ACROFORM-CUSTOMER-7319",
"painted directly by the appearance",
),
("2026-07-28", "painted through a nested form XObject"),
("50.00", "painted by the appearance and the content stream"),
] {
assert_eq!(
parsed.text.matches(expected).count(),
1,
"visible form value should appear exactly once ({case}): {expected}"
);
assert!(
parsed.pages[0]
.text_items
.iter()
.any(|item| item.text.contains(expected)),
"form value should be a positioned text item ({case}): {expected}"
);
}
assert!(
!parsed.text.contains("DEFAULT-ONLY-SHOULD-NOT-APPEAR"),
"an unpainted default choice must not be treated as a filled value"
);
assert!(
!parsed.text.contains("ANNOTATION-ONLY-SHOULD-NOT-APPEAR"),
"non-widget annotation appearances must not become page text"
);
assert!(
!parsed.text.contains("HIDDEN-SHOULD-NOT-APPEAR"),
"a hidden widget is never rendered, so its value is not visible text"
);
assert_eq!(
parsed.text.matches("PREPRINTED-LABEL").count(),
1,
"page text under a widget rect must survive flattening exactly once"
);
assert!(
parsed.pages[0].form_fields.is_none(),
"default text extraction must not enable structured form metadata"
);
assert!(
parsed.pages[2]
.text_items
.iter()
.any(|item| item.text.contains("NESTED-ONLY-VALUE")),
"a widget whose value is painted only through a nested form XObject \
must still be detected and flattened"
);
let with_metadata = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
output_format: OutputFormat::Markdown,
extract_annotations: true,
extract_form_fields: true,
..Default::default()
})
.parse("../../integration_tests_data/filled_acroform.pdf")
.await
.expect("filled form should parse with structured metadata");
assert_eq!(
with_metadata.pages[0].annotations.as_ref().unwrap().len(),
6
);
assert!(
with_metadata.pages[0]
.annotations
.as_ref()
.unwrap()
.iter()
.any(|annotation| {
annotation.subtype == "freetext"
&& annotation.contents.as_deref() == Some("ANNOTATION-ONLY-SHOULD-NOT-APPEAR")
}),
"non-widget annotation metadata should remain available when requested"
);
let fields = with_metadata.pages[0].form_fields.as_ref().unwrap();
assert_eq!(fields.len(), 5);
assert!(fields.iter().any(|field| {
field.name.as_deref() == Some("customer_name")
&& field.value.as_deref() == Some("ACROFORM-CUSTOMER-7319")
}));
assert!(fields.iter().any(|field| {
field.name.as_deref() == Some("default_only_choice")
&& field.value.as_deref() == Some("DEFAULT-ONLY-SHOULD-NOT-APPEAR")
}));
assert_eq!(
with_metadata.pages[1].annotations.as_ref().unwrap().len(),
1
);
let second_page_fields = with_metadata.pages[1].form_fields.as_ref().unwrap();
assert_eq!(second_page_fields.len(), 1);
assert_eq!(
second_page_fields[0].name.as_deref(),
Some("complexity_sentinel")
);
assert_eq!(second_page_fields[0].value.as_deref(), Some("OK"));
let third_page_fields = with_metadata.pages[2].form_fields.as_ref().unwrap();
assert_eq!(third_page_fields.len(), 1);
assert_eq!(third_page_fields[0].name.as_deref(), Some("nested_only"));
let complexity = LiteParse::new(LiteParseConfig {
ocr_enabled: false,
..Default::default()
})
.is_complex(PdfInput::Path(
"../../integration_tests_data/filled_acroform.pdf".into(),
))
.await
.expect("complexity analysis should run on the flattened document");
assert_eq!(complexity.len(), 3);
assert!(
!complexity[1]
.reasons
.contains(&ComplexityReason::AnnotationText),
"widget text is extractable after flattening, so it is not annotation-only text"
);
}