#[cfg(feature = "vision")]
use captchaforge::vision;
use proptest::prelude::*;
#[cfg(feature = "vision")]
fn arbitrary_rgb_image() -> impl Strategy<Value = image::RgbImage> {
(1u32..800u32, 1u32..800u32).prop_flat_map(|(w, h)| {
proptest::collection::vec(any::<u8>(), (w * h * 3) as usize).prop_map(move |bytes| {
image::RgbImage::from_raw(w, h, bytes).unwrap_or_else(|| image::RgbImage::new(w, h))
})
})
}
proptest! {
#[test]
#[cfg(feature = "vision")]
fn yolo_detect_never_panics_on_random_images(img in arbitrary_rgb_image()) {
let rt = tokio::runtime::Runtime::new().unwrap();
let hub = vision::ModelHub::new();
let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
let mut detector = vision::YoloDetector::load(&path).unwrap();
let dyn_img = image::DynamicImage::ImageRgb8(img);
let _ = detector.detect(&dyn_img);
}
}
#[test]
#[cfg(feature = "vision")]
fn yolo_detect_empty_image_returns_no_detections() {
let rt = tokio::runtime::Runtime::new().unwrap();
let hub = vision::ModelHub::new();
let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
let mut detector = vision::YoloDetector::load(&path).unwrap();
let img = image::DynamicImage::new_rgb8(640, 480);
let dets = detector.detect(&img).unwrap();
assert!(dets.is_empty(), "solid-color image should yield no detections");
}
#[test]
#[cfg(feature = "vision")]
fn yolo_detect_small_image_still_runs() {
let rt = tokio::runtime::Runtime::new().unwrap();
let hub = vision::ModelHub::new();
let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
let mut detector = vision::YoloDetector::load(&path).unwrap();
let img = image::DynamicImage::new_rgb8(10, 10);
let _ = detector.detect(&img);
}
proptest! {
#[test]
#[cfg(feature = "vision")]
fn crnn_recognize_never_panics_on_random_images(img in arbitrary_rgb_image()) {
let rt = tokio::runtime::Runtime::new().unwrap();
let hub = vision::ModelHub::new();
let path = rt.block_on(async { hub.resolve(vision::ModelId::CrnnText).await });
prop_assume!(path.is_ok(), "CRNN model not available — skipping");
let path = path.unwrap();
let charset = vision::crnn::default_charset();
let mut recognizer = vision::crnn::CrnnRecognizer::load(&path, charset).unwrap();
let dyn_img = image::DynamicImage::ImageRgb8(img);
let _ = recognizer.recognize(&dyn_img);
}
}
#[test]
#[cfg(feature = "vision")]
fn task_mapping_is_exhaustive_for_common_captcha_tasks() {
use vision::yolo::task_to_coco_classes;
let known_tasks = [
("Select all images with traffic lights", true),
("Select all images with buses", true),
("Select all images with cars", true),
("Select all images with vehicles", true),
("Select all images with bicycles", true),
("Select all images with motorcycles", true),
("Select all images with trains", true),
("Select all images with boats", true),
("Select all images with fire hydrants", true),
("Select all images with cats", true),
("Select all images with dogs", true),
("Select all images with people", true),
("Select all images with crosswalks", false),
("Select all images with chimneys", false),
("Select all images with stairs", false),
("Select all images with palm trees", false),
("Select all images with bridges", false),
];
for (task, expected_known) in &known_tasks {
let result = task_to_coco_classes(task);
if *expected_known {
assert!(
result.is_some(),
"task '{}' should map to COCO classes",
task
);
} else {
assert!(
result.is_none(),
"task '{}' should NOT map to COCO classes (needs VLM fallback)",
task
);
}
}
}
#[test]
#[cfg(feature = "vision")]
fn model_hub_with_custom_root() {
let tmp = tempfile::tempdir().unwrap();
let hub = vision::ModelHub::with_root(tmp.path());
assert!(hub.local_path(vision::ModelId::YoloV8n).is_none());
}
#[test]
#[cfg(feature = "vision")]
fn model_hub_detects_cached_yolov8n() {
let hub = vision::ModelHub::new();
let path = hub.local_path(vision::ModelId::YoloV8n);
assert!(path.is_some(), "YOLOv8n should be cached after download");
}