#![cfg_attr(target_arch = "aarch64", allow(stable_features))]
#![cfg_attr(
target_arch = "aarch64",
feature(stdarch_neon_dotprod, stdarch_neon_i8mm)
)]
#![deny(unsafe_code)]
pub mod adaptive;
#[cfg(feature = "native")]
pub mod cli;
#[cfg(feature = "native")]
pub mod conformance;
#[cfg(feature = "native")]
pub mod dist;
#[cfg(feature = "native")]
pub mod doctor;
pub mod error;
pub mod native_engine;
#[cfg(feature = "pdf")]
pub mod pdf;
pub mod preprocess;
pub mod progress;
pub mod quant;
#[cfg(feature = "native")]
pub mod resident;
#[cfg(feature = "native")]
pub mod robot;
pub mod simd;
#[cfg(feature = "native")]
pub mod storage;
pub mod tokenizer;
#[cfg(feature = "native")]
pub use cli::cli_main;
pub use error::{FocrError, FocrResult};
pub use native_engine::model_arch;
pub use native_engine::{ExtractedFigure, LayoutSpan, RecognizedDocument};
#[cfg(feature = "native")]
use std::path::Path;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "native")]
use std::sync::{Arc, Mutex, MutexGuard};
#[cfg(feature = "native")]
use std::time::Duration;
#[cfg(feature = "native")]
use asupersync::runtime::{Runtime, RuntimeBuilder};
#[cfg(feature = "native")]
use native_engine::OcrModel;
pub(crate) const UNLIMITED_OCR_ARTIFACT_VERSION: &str = "0.7.0";
pub const MODEL_PATH_ENV: &str = "FOCR_MODEL_PATH";
pub const FOCR_PROJECT_LICENSE_NOTICE: &str =
"franken_ocr - Copyright (c) 2026 Jeffrey Emanuel, MIT License (with OpenAI/Anthropic Rider)";
pub const FOCR_MODEL_LICENSE_NOTICE: &str =
"Baidu Unlimited-OCR - Copyright (c) 2026 Baidu, MIT License";
pub const DEFAULT_MODEL_PATH: &str = "models/unlimited-ocr.focrq";
#[cfg(feature = "native")]
const DEFAULT_FORWARD_STAGE_BUDGET_MS: u64 = 10 * 60 * 1000;
static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
pub fn request_shutdown() {
SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
}
#[must_use]
pub fn shutdown_requested() -> bool {
SHUTDOWN_REQUESTED.load(Ordering::Relaxed)
}
pub fn reset_shutdown() {
SHUTDOWN_REQUESTED.store(false, Ordering::SeqCst);
}
pub fn cancel_checkpoint() -> FocrResult<()> {
if shutdown_requested() {
return Err(FocrError::Cancelled);
}
Ok(())
}
pub fn thread_budget() -> usize {
static BUDGET: OnceLock<usize> = OnceLock::new();
*BUDGET.get_or_init(|| {
std::env::var("FOCR_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(default_thread_budget)
})
}
fn default_thread_budget() -> usize {
let physical = num_cpus::get_physical();
if cfg!(target_os = "ios") {
physical.saturating_sub(1).max(1)
} else {
physical
}
}
pub fn init_kernel_pool() -> usize {
static INIT: OnceLock<usize> = OnceLock::new();
*INIT.get_or_init(|| {
let width = thread_budget();
let _ = rayon::ThreadPoolBuilder::new()
.num_threads(width)
.thread_name(|i| format!("focr-kernel-{i}"))
.start_handler(|_| apple_qos::pin_worker_to_user_initiated())
.build_global();
rayon::current_num_threads()
})
}
mod apple_qos {
#[cfg(target_vendor = "apple")]
#[allow(unsafe_code)]
pub(super) fn pin_worker_to_user_initiated() {
unsafe {
libc::pthread_set_qos_class_self_np(libc::qos_class_t::QOS_CLASS_USER_INITIATED, 0);
}
}
#[cfg(not(target_vendor = "apple"))]
pub(super) fn pin_worker_to_user_initiated() {}
}
pub fn kernel_pool_width() -> usize {
init_kernel_pool()
}
#[cfg(feature = "native")]
pub fn stream_pages<T, P, C>(capacity: usize, mut produce: P, mut consume: C) -> FocrResult<usize>
where
T: Send + 'static,
P: FnMut() -> FocrResult<Option<T>> + Send + 'static,
C: FnMut(T),
{
let (tx, rx) = std::sync::mpsc::sync_channel::<T>(capacity.max(1));
let worker = std::thread::Builder::new()
.name("focr-page-stream".into())
.spawn(move || -> FocrResult<()> {
loop {
match produce()? {
Some(item) => {
if tx.send(item).is_err() {
return Err(FocrError::Cancelled);
}
}
None => return Ok(()),
}
}
})
.map_err(|e| FocrError::Other(anyhow::anyhow!("page-stream worker spawn: {e}")))?;
let mut n = 0usize;
loop {
match rx.recv_timeout(Duration::from_millis(40)) {
Ok(item) => {
consume(item);
n += 1;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
if worker.is_finished() {
while let Ok(item) = rx.try_recv() {
consume(item);
n += 1;
}
break;
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
worker
.join()
.map_err(|_| FocrError::Other(anyhow::anyhow!("page-stream worker panicked")))??;
Ok(n)
}
pub type PageSink = Box<dyn FnMut(usize, &str) + Send>;
#[cfg(feature = "native")]
pub struct OcrEngine {
runtime: Runtime,
model: Mutex<Option<Arc<OcrModel>>>,
}
#[cfg(feature = "native")]
impl OcrEngine {
#[must_use]
pub fn take_music_page_meta(&self) -> Option<native_engine::MusicPageMeta> {
self.model
.lock()
.ok()
.and_then(|slot| slot.as_ref().map(std::sync::Arc::clone))
.and_then(|model| model.take_music_meta())
}
pub fn new() -> FocrResult<Self> {
let _ = init_kernel_pool();
let runtime = RuntimeBuilder::new()
.worker_threads(2)
.blocking_threads(1, 4)
.thread_name_prefix("focr")
.build()
.map_err(|e| FocrError::Other(anyhow::anyhow!("asupersync runtime build: {e}")))?;
Ok(Self {
runtime,
model: Mutex::new(None),
})
}
#[must_use]
pub fn model_path() -> std::path::PathBuf {
std::env::var_os(MODEL_PATH_ENV)
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::PathBuf::from(DEFAULT_MODEL_PATH))
}
fn model_at(&self, path: &Path) -> FocrResult<Arc<OcrModel>> {
{
let guard = self.model_guard()?;
if let Some(m) = guard.as_ref()
&& m.path() == path
{
return Ok(Arc::clone(m));
}
}
let loaded = OcrModel::load(path)?;
let loaded_path = loaded.path().to_path_buf();
let mut guard = self.model_guard()?;
if let Some(m) = guard.as_ref()
&& m.path() == loaded_path
{
return Ok(Arc::clone(m));
}
*guard = Some(Arc::clone(&loaded));
Ok(loaded)
}
fn model_guard(&self) -> FocrResult<MutexGuard<'_, Option<Arc<OcrModel>>>> {
self.model
.lock()
.map_err(|_| FocrError::Other(anyhow::anyhow!("OcrEngine model mutex poisoned")))
}
pub fn recognize(&self, image_path: &Path) -> FocrResult<String> {
self.recognize_with_model(&Self::model_path(), image_path)
}
pub fn recognize_with_model(&self, model_path: &Path, image_path: &Path) -> FocrResult<String> {
let model = self.model_at(model_path)?;
let image_path = image_path.to_path_buf();
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize(&image_path),
)
}
pub fn recognize_dynamic(&self, image: image::DynamicImage) -> FocrResult<String> {
self.recognize_dynamic_with_model(&Self::model_path(), image)
}
pub fn recognize_dynamic_with_model(
&self,
model_path: &Path,
image: image::DynamicImage,
) -> FocrResult<String> {
let model = self.model_at(model_path)?;
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize_dynamic(image),
)
}
pub fn recognize_with_layout(&self, image_path: &Path) -> FocrResult<RecognizedDocument> {
self.recognize_with_layout_model(&Self::model_path(), image_path)
}
pub fn recognize_with_layout_model(
&self,
model_path: &Path,
image_path: &Path,
) -> FocrResult<RecognizedDocument> {
let model = self.model_at(model_path)?;
let image_path = image_path.to_path_buf();
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize_with_layout(&image_path),
)
}
pub fn recognize_dynamic_with_layout(
&self,
image: image::DynamicImage,
) -> FocrResult<RecognizedDocument> {
self.recognize_dynamic_with_layout_model(&Self::model_path(), image)
}
pub fn recognize_dynamic_with_layout_model(
&self,
model_path: &Path,
image: image::DynamicImage,
) -> FocrResult<RecognizedDocument> {
let model = self.model_at(model_path)?;
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize_dynamic_with_layout(image),
)
}
pub fn recognize_with_figures(
&self,
image_path: &Path,
) -> FocrResult<(RecognizedDocument, Vec<ExtractedFigure>)> {
self.recognize_with_figures_model(&Self::model_path(), image_path)
}
pub fn recognize_with_figures_model(
&self,
model_path: &Path,
image_path: &Path,
) -> FocrResult<(RecognizedDocument, Vec<ExtractedFigure>)> {
let model = self.model_at(model_path)?;
let image_path = image_path.to_path_buf();
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize_with_figures(&image_path),
)
}
pub fn recognize_dynamic_with_figures(
&self,
image: image::DynamicImage,
) -> FocrResult<(RecognizedDocument, Vec<ExtractedFigure>)> {
self.recognize_dynamic_with_figures_model(&Self::model_path(), image)
}
pub fn recognize_dynamic_with_figures_model(
&self,
model_path: &Path,
image: image::DynamicImage,
) -> FocrResult<(RecognizedDocument, Vec<ExtractedFigure>)> {
let model = self.model_at(model_path)?;
self.run_blocking_stage_with_budget(
"forward",
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS),
move || model.recognize_dynamic_with_figures(image),
)
}
pub fn recognize_batch(&self, images: &[&Path]) -> FocrResult<Vec<FocrResult<String>>> {
self.recognize_batch_with_model(&Self::model_path(), images)
}
pub fn recognize_batch_with_model(
&self,
model_path: &Path,
images: &[&Path],
) -> FocrResult<Vec<FocrResult<String>>> {
let model = self.model_at(model_path)?;
let owned: Vec<std::path::PathBuf> = images.iter().map(|p| p.to_path_buf()).collect();
let count = u32::try_from(owned.len().max(1)).unwrap_or(u32::MAX);
let budget =
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS).map(|per_image| {
per_image
.checked_mul(count)
.unwrap_or_else(|| Duration::from_secs(u64::MAX / 2))
});
self.run_blocking_stage_with_budget("forward-batch", budget, move || {
let refs: Vec<&Path> = owned.iter().map(std::path::PathBuf::as_path).collect();
Ok(model.recognize_batch(&refs))
})
}
pub fn recognize_multi_page(&self, images: &[&Path]) -> FocrResult<String> {
self.recognize_multi_page_with_model(&Self::model_path(), images)
}
pub fn recognize_multi_page_with_model(
&self,
model_path: &Path,
images: &[&Path],
) -> FocrResult<String> {
let model = self.model_at(model_path)?;
let owned: Vec<std::path::PathBuf> = images.iter().map(|p| p.to_path_buf()).collect();
let count = u32::try_from(owned.len().max(1)).unwrap_or(u32::MAX);
let budget =
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS).map(|per_image| {
per_image
.checked_mul(count)
.unwrap_or_else(|| Duration::from_secs(u64::MAX / 2))
});
self.run_blocking_stage_with_budget("forward-multi-page", budget, move || {
let refs: Vec<&Path> = owned.iter().map(std::path::PathBuf::as_path).collect();
model.recognize_multi_page(&refs)
})
}
pub fn recognize_multi_page_dynamic(
&self,
images: Vec<image::DynamicImage>,
) -> FocrResult<String> {
self.recognize_multi_page_dynamic_with_model(&Self::model_path(), images)
}
pub fn recognize_multi_page_dynamic_with_model(
&self,
model_path: &Path,
images: Vec<image::DynamicImage>,
) -> FocrResult<String> {
let model = self.model_at(model_path)?;
let count = u32::try_from(images.len().max(1)).unwrap_or(u32::MAX);
let budget =
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS).map(|per_image| {
per_image
.checked_mul(count)
.unwrap_or_else(|| Duration::from_secs(u64::MAX / 2))
});
self.run_blocking_stage_with_budget("forward-multi-page", budget, move || {
model.recognize_multi_page_dynamic(images)
})
}
pub fn recognize_multi_page_dynamic_streaming_with_model(
&self,
model_path: &Path,
images: Vec<image::DynamicImage>,
mut on_page: PageSink,
) -> FocrResult<String> {
let model = self.model_at(model_path)?;
let count = u32::try_from(images.len().max(1)).unwrap_or(u32::MAX);
let budget =
Self::stage_budget("FORWARD", DEFAULT_FORWARD_STAGE_BUDGET_MS).map(|per_image| {
per_image
.checked_mul(count)
.unwrap_or_else(|| Duration::from_secs(u64::MAX / 2))
});
self.run_blocking_stage_with_budget("forward-multi-page", budget, move || {
model.recognize_multi_page_dynamic_streaming(images, &mut *on_page)
})
}
fn stage_budget(stage: &str, default_ms: u64) -> Option<Duration> {
let key = format!("FOCR_STAGE_BUDGET_{stage}_MS");
match std::env::var(&key) {
Ok(raw) => {
let trimmed = raw.trim();
if trimmed == "0" || trimmed.eq_ignore_ascii_case("unlimited") {
return None;
}
let millis = trimmed
.parse::<u64>()
.ok()
.filter(|&ms| ms > 0)
.unwrap_or(default_ms);
Some(Duration::from_millis(millis))
}
Err(_) => Some(Duration::from_millis(default_ms)),
}
}
fn run_blocking_stage_with_budget<T, F>(
&self,
stage: &'static str,
budget: Option<Duration>,
op: F,
) -> FocrResult<T>
where
T: Send + 'static,
F: FnOnce() -> FocrResult<T> + Send + 'static,
{
self.runtime.block_on(async move {
let Some(budget) = budget else {
return asupersync::runtime::spawn_blocking(op).await;
};
match asupersync::time::timeout(
asupersync::time::wall_now(),
budget,
asupersync::runtime::spawn_blocking(op),
)
.await
{
Ok(result) => result,
Err(_) => Err(FocrError::Timeout(format!(
"{stage} stage exceeded {}ms budget",
budget.as_millis()
))),
}
})
}
}
#[cfg(all(test, feature = "native"))]
mod tests {
use super::*;
fn log_line(test: &str, phase: &str, outcome: &str, extra: &str) {
eprintln!(
"{{\"test\":\"{test}\",\"phase\":\"{phase}\",\"outcome\":\"{outcome}\"{}{extra}}}",
if extra.is_empty() { "" } else { "," }
);
}
#[test]
fn engine_owns_single_runtime_and_drops_clean() {
let engine = OcrEngine::new().expect("engine builds");
let runtime_witness = engine
.runtime
.block_on(engine.runtime.handle().spawn(async {
Runtime::current_handle().expect("spawned task has a runtime handle")
}));
let out = engine
.run_blocking_stage_with_budget("drop-probe", Some(Duration::from_secs(5)), || Ok(42u8))
.expect("stage runs");
assert_eq!(out, 42);
log_line(
"engine_owns_single_runtime_and_drops_clean",
"live",
"pass",
"",
);
drop(engine);
assert!(
matches!(
runtime_witness.try_spawn(async { 42u8 }),
Err(asupersync::runtime::state::SpawnError::RuntimeUnavailable)
),
"dropped engine runtime should reject new tasks"
);
assert!(
runtime_witness.spawn_blocking(|| {}).is_none(),
"dropped engine runtime should not accept blocking tasks"
);
assert!(
runtime_witness.blocking_handle().is_none(),
"dropped engine runtime should not expose its blocking pool"
);
log_line(
"engine_owns_single_runtime_and_drops_clean",
"dropped",
"pass",
"",
);
}
#[test]
fn cancellation_token_into_closure_aborts() {
reset_shutdown();
let engine = OcrEngine::new().expect("engine builds");
let out = engine.run_blocking_stage_with_budget(
"cancel-probe",
Some(Duration::from_secs(10)),
|| {
for step in 0..1_000_000u64 {
cancel_checkpoint()?;
if step == 3 {
request_shutdown();
}
}
Ok(0u64)
},
);
reset_shutdown();
assert!(
matches!(out, Err(FocrError::Cancelled)),
"expected Cancelled, got {out:?}"
);
assert_eq!(FocrError::Cancelled.exit_code(), 6, "exit code contract");
log_line(
"cancellation_token_into_closure_aborts",
"aborted",
"pass",
"",
);
}
#[test]
fn bounded_stream_backpressure() {
use std::sync::atomic::{AtomicI64, Ordering};
static IN_FLIGHT: AtomicI64 = AtomicI64::new(0);
static MAX_SEEN: AtomicI64 = AtomicI64::new(0);
IN_FLIGHT.store(0, Ordering::SeqCst);
MAX_SEEN.store(0, Ordering::SeqCst);
let total = 24u32;
let mut produced = 0u32;
let n = stream_pages(
2,
move || {
if produced == total {
return Ok(None);
}
produced += 1;
let now = IN_FLIGHT.fetch_add(1, Ordering::SeqCst) + 1;
MAX_SEEN.fetch_max(now, Ordering::SeqCst);
Ok(Some(produced))
},
|item: u32| {
std::thread::sleep(Duration::from_millis(5));
IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
let _ = item;
},
)
.expect("stream completes");
assert_eq!(n, total as usize, "every page delivered in order");
let max = MAX_SEEN.load(Ordering::SeqCst);
assert!(
max <= 4,
"in-flight items {max} exceeded capacity(2)+channel slack — backpressure broken"
);
log_line(
"bounded_stream_backpressure",
"drained",
"pass",
&format!("\"max_in_flight\":{max},\"n\":{n}"),
);
}
#[test]
fn thread_budget_reads_env_then_physical() {
let budget = thread_budget();
match std::env::var("FOCR_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
{
Some(env) if env > 0 => assert_eq!(budget, env, "env wins"),
_ => assert_eq!(budget, num_cpus::get_physical(), "physical cores"),
}
assert!(budget > 0);
assert!(
budget
<= std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(usize::MAX),
"physical budget cannot exceed logical width"
);
log_line(
"thread_budget_reads_env_then_physical",
"resolved",
"pass",
&format!("\"threads\":{budget}"),
);
}
struct TempModel(std::path::PathBuf);
impl TempModel {
fn write_focrq(bytes: &[u8]) -> std::io::Result<Self> {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
let path = std::env::temp_dir().join(format!(
"franken_ocr_engine_format_mismatch_{}_{}.focrq",
std::process::id(),
nanos
));
std::fs::write(&path, bytes)?;
Ok(Self(path))
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempModel {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn future_focrq_preamble() -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(native_engine::weights::FOCRQ_MAGIC);
blob.extend_from_slice(&(native_engine::weights::FOCRQ_FORMAT_VERSION + 1).to_le_bytes());
blob.push(0);
blob.extend_from_slice(&[0u8; 32]);
blob.extend_from_slice(&0u64.to_le_bytes());
blob
}
#[test]
fn engine_constructs_without_model() {
let engine = OcrEngine::new().expect("runtime builds");
assert!(
engine.model_guard().expect("mutex").is_none(),
"model must be loaded lazily, not at construction"
);
}
#[test]
fn recognize_missing_model_is_clean_model_not_found() {
let engine = OcrEngine::new().expect("runtime builds");
let err = engine
.recognize_with_model(
Path::new("/nonexistent/franken_ocr/model.focrq"),
Path::new("/some/document.png"),
)
.expect_err("absent model must error");
assert!(
matches!(err, FocrError::ModelNotFound(_)),
"expected ModelNotFound, got {err:?}"
);
assert_eq!(err.exit_code(), 3, "ModelNotFound must map to exit code 3");
}
#[test]
fn public_recognize_without_weights_is_model_not_found() {
if std::env::var_os(MODEL_PATH_ENV).is_none()
&& !std::path::Path::new(DEFAULT_MODEL_PATH).exists()
&& native_engine::OcrModel::resolve_model(Path::new(DEFAULT_MODEL_PATH)).is_err()
{
let engine = OcrEngine::new().expect("runtime builds");
let err = engine
.recognize(Path::new("/some/document.png"))
.expect_err("absent default model must error");
assert!(matches!(err, FocrError::ModelNotFound(_)));
}
}
#[test]
fn model_path_falls_back_to_default_when_env_unset() {
if std::env::var_os(MODEL_PATH_ENV).is_none() {
assert_eq!(
OcrEngine::model_path(),
std::path::PathBuf::from(DEFAULT_MODEL_PATH)
);
}
}
#[test]
fn repeated_missing_model_stays_model_not_found() {
let engine = OcrEngine::new().expect("runtime builds");
let p = Path::new("/nonexistent/franken_ocr/model.focrq");
let img = Path::new("/some/document.png");
for _ in 0..3 {
let err = engine.recognize_with_model(p, img).expect_err("absent");
assert!(matches!(err, FocrError::ModelNotFound(_)));
}
}
#[test]
fn blocking_stage_runs_on_runtime_blocking_pool() {
let engine = OcrEngine::new().expect("runtime builds");
let thread_name = engine
.run_blocking_stage_with_budget("test", Some(Duration::from_secs(1)), || {
let thread = std::thread::current();
Ok(thread.name().unwrap_or("<unnamed>").to_string())
})
.expect("stage should complete");
assert!(
thread_name.contains("-blocking-"),
"stage ran on {thread_name:?}, not the runtime blocking pool"
);
}
#[test]
fn blocking_stage_timeout_maps_to_stable_error() {
let engine = OcrEngine::new().expect("runtime builds");
let started = std::time::Instant::now();
let err = engine
.run_blocking_stage_with_budget("test-timeout", Some(Duration::from_millis(10)), || {
std::thread::sleep(Duration::from_millis(100));
Ok(())
})
.expect_err("slow blocking stage must time out");
assert!(
matches!(err, FocrError::Timeout(_)),
"expected Timeout, got {err:?}"
);
assert_eq!(err.exit_code(), error::EXIT_TIMEOUT);
assert!(
started.elapsed() < Duration::from_millis(500),
"timeout wrapper waited for the whole blocking closure"
);
}
#[test]
fn public_engine_preserves_focrq_format_mismatch_robot_code()
-> Result<(), Box<dyn std::error::Error>> {
let model = TempModel::write_focrq(&future_focrq_preamble())?;
let engine = OcrEngine::new()?;
let result = engine.recognize_with_model(model.path(), Path::new("/some/document.png"));
let Err(err) = result else {
return Err(std::io::Error::other(
"future .focrq version unexpectedly succeeded before forward",
)
.into());
};
assert!(
matches!(err, FocrError::FormatMismatch(_)),
"expected FormatMismatch, got {err:?}"
);
assert_eq!(err.exit_code(), error::EXIT_FORMAT_MISMATCH);
let event = robot::run_error_event(&err);
assert_eq!(event["event"], "run_error");
assert_eq!(event["error_kind"], "format_mismatch");
assert_eq!(event["code"], error::EXIT_FORMAT_MISMATCH);
Ok(())
}
}