111 lines
3.9 KiB
Rust
111 lines
3.9 KiB
Rust
//! Integration tests for the OCR analysis backend
|
|
//! (`analysis::ocr::OcrAnalyzeDispatcher`). A stub OCR engine stands in for
|
|
//! `ocrs` (whose `.rten` models aren't shipped to CI), so these pin the
|
|
//! storage→OCR→persist wiring: the dispatcher reads the page image, runs the
|
|
//! engine, and persists the lines via the shared `persist_analysis` path —
|
|
//! landing `page_ocr_text` rows and a populated `search_doc` exactly like the
|
|
//! vision backend. Each `#[sqlx::test]` gets a fresh migrated DB.
|
|
|
|
mod common;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use mangalord::analysis::daemon::AnalyzeDispatcher;
|
|
use mangalord::analysis::ocr::test_support::StubOcrEngine;
|
|
use mangalord::analysis::ocr::OcrAnalyzeDispatcher;
|
|
use mangalord::domain::page_analysis::AnalysisStatus;
|
|
use mangalord::repo;
|
|
use mangalord::storage::{LocalStorage, Storage};
|
|
use sqlx::PgPool;
|
|
use tempfile::TempDir;
|
|
use uuid::Uuid;
|
|
|
|
/// Seed a manga → chapter → page chain whose page points at `storage_key`,
|
|
/// and return the page id.
|
|
async fn seed_page(pool: &PgPool, storage_key: &str) -> Uuid {
|
|
let manga_id: Uuid =
|
|
sqlx::query_scalar("INSERT INTO mangas (title) VALUES ('M') RETURNING id")
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap();
|
|
let chapter_id: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO chapters (manga_id, number) VALUES ($1, 1) RETURNING id",
|
|
)
|
|
.bind(manga_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query_scalar(
|
|
"INSERT INTO pages (chapter_id, page_number, storage_key, content_type) \
|
|
VALUES ($1, 1, $2, 'image/png') RETURNING id",
|
|
)
|
|
.bind(chapter_id)
|
|
.bind(storage_key)
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
fn ocr_dispatcher(
|
|
pool: &PgPool,
|
|
storage: Arc<dyn Storage>,
|
|
lines: &[&str],
|
|
) -> OcrAnalyzeDispatcher {
|
|
OcrAnalyzeDispatcher {
|
|
db: pool.clone(),
|
|
storage,
|
|
engine: StubOcrEngine::new(lines),
|
|
max_image_bytes: 8 * 1024 * 1024,
|
|
ocr_permits: Arc::new(tokio::sync::Semaphore::new(1)),
|
|
}
|
|
}
|
|
|
|
#[sqlx::test(migrations = "./migrations")]
|
|
async fn dispatch_persists_ocr_lines_and_search_doc(pool: PgPool) {
|
|
let dir = TempDir::new().unwrap();
|
|
let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(dir.path()));
|
|
let key = "mangas/x/p1.png";
|
|
storage.put(key, &common::fake_png_bytes()).await.unwrap();
|
|
let page_id = seed_page(&pool, key).await;
|
|
|
|
let dispatcher = ocr_dispatcher(&pool, Arc::clone(&storage), &["Hello there", "general"]);
|
|
dispatcher.dispatch(page_id).await.unwrap();
|
|
|
|
// Two OCR rows, in order, with the recognized text.
|
|
let rows: Vec<(String, i32)> = sqlx::query_as(
|
|
"SELECT text, ord FROM page_ocr_text WHERE page_id = $1 ORDER BY ord",
|
|
)
|
|
.bind(page_id)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(rows.len(), 2);
|
|
assert_eq!(rows[0].0, "Hello there");
|
|
assert_eq!(rows[1].0, "general");
|
|
|
|
// The analysis row is `done`, stamped with the ocrs model label, and has a
|
|
// non-empty tsvector so text search works.
|
|
let row = repo::page_analysis::load(&pool, page_id).await.unwrap().unwrap();
|
|
assert_eq!(row.status, AnalysisStatus::Done);
|
|
assert_eq!(row.model.as_deref(), Some("ocrs"));
|
|
let has_doc: bool = sqlx::query_scalar(
|
|
"SELECT search_doc IS NOT NULL AND search_doc != ''::tsvector \
|
|
FROM page_analysis WHERE page_id = $1",
|
|
)
|
|
.bind(page_id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(has_doc, "search_doc must be populated from OCR text");
|
|
}
|
|
|
|
#[sqlx::test(migrations = "./migrations")]
|
|
async fn dispatch_missing_page_is_noop(pool: PgPool) {
|
|
let dir = TempDir::new().unwrap();
|
|
let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(dir.path()));
|
|
// A page id that was never inserted — the dispatcher must treat it as a
|
|
// deleted page and succeed without writing anything.
|
|
let dispatcher = ocr_dispatcher(&pool, storage, &["whatever"]);
|
|
dispatcher.dispatch(Uuid::new_v4()).await.unwrap();
|
|
}
|