captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Property-based tests for the vision backend.
//!
//! Verifies that inference is robust against edge-case inputs:
//! - Extreme image sizes
//! - Random noise images
//! - Empty / tiny images
//! - Invalid model paths
//!
//! Run with: cargo test --features vision --test vision_property

#[cfg(feature = "vision")]
use captchaforge::vision;
use proptest::prelude::*;

// ---------------------------------------------------------------------------
// YOLO Detector, robustness against arbitrary images
// ---------------------------------------------------------------------------

#[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);
        // Must not panic (returns Ok or Err, never unwinds).
        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);
    // Should not panic (letterboxing handles arbitrary sizes).
    let _ = detector.detect(&img);
}

// ---------------------------------------------------------------------------
// CRNN Recognizer, robustness
// ---------------------------------------------------------------------------

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);
    }
}

// ---------------------------------------------------------------------------
// Task-to-COCO mapping, coverage
// ---------------------------------------------------------------------------

#[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
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Model Hub, path handling
// ---------------------------------------------------------------------------

#[test]
#[cfg(feature = "vision")]
fn model_hub_with_custom_root() {
    let tmp = tempfile::tempdir().unwrap();
    let hub = vision::ModelHub::with_root(tmp.path());
    // No models cached yet.
    assert!(hub.local_path(vision::ModelId::YoloV8n).is_none());
}

#[test]
#[cfg(feature = "vision")]
fn model_hub_detects_cached_yolov8n() {
    let hub = vision::ModelHub::new();
    // After previous tests, YOLOv8n should be cached.
    let path = hub.local_path(vision::ModelId::YoloV8n);
    assert!(path.is_some(), "YOLOv8n should be cached after download");
}