forgewright 0.2.0

Standalone UI automation — CDP for browsers, UIA for Windows desktop apps
Documentation
//! Deferred Poller — tick-driven visual verification without timers.
//!
//! After the InputSpooler drains a macro and produces an ActuationReport,
//! the DeferredPoller queues the verification. It does NOT use arbitrary
//! sleep/timer delays (which cause race conditions). Instead, it waits
//! for the GPU to signal that the frame containing the final actuation
//! tick has been presented.
//!
//! Flow:
//!   1. Spooler drain completes → ActuationReport with `completed_on_tick`
//!   2. DeferredPoller::enqueue(report)
//!   3. Game loop: after frame.present(), call poller.on_frame_rendered(current_tick)
//!   4. Any pending report whose completed_on_tick <= rendered_tick fires visual-gate
//!
//! Zero-alloc hot path: the pending array is pre-allocated with fixed capacity.
//! The on_frame_rendered check is O(n) where n is pending count (typically 0-2).

use serde::Serialize;

// ─── Constants ───────────────────────────────────────────────────────────────

/// Maximum concurrent pending verifications. Fixed capacity, no realloc.
pub const MAX_PENDING: usize = 16;

// ─── PendingVerification ─────────────────────────────────────────────────────

/// A queued verification waiting for the GPU to catch up to a specific tick.
#[derive(Debug, Clone, Copy)]
pub struct PendingVerification {
    /// The macro/task that completed.
    pub task_id: u32,
    /// The tick on which the final input was applied.
    pub completed_on_tick: u64,
    /// Whether this slot is occupied.
    active: bool,
}

impl Default for PendingVerification {
    fn default() -> Self {
        Self {
            task_id: 0,
            completed_on_tick: 0,
            active: false,
        }
    }
}

// ─── VerificationResult ──────────────────────────────────────────────────────

/// Emitted when a pending verification fires (GPU caught up to the tick).
#[derive(Debug, Clone, Copy, Serialize)]
pub struct VerificationResult {
    pub task_id: u32,
    pub completed_on_tick: u64,
    /// The GPU tick that triggered the verification (>= completed_on_tick).
    pub rendered_tick: u64,
    /// Latency in ticks between actuation and visual confirmation.
    pub latency_ticks: u64,
}


// ─── DeferredPoller ──────────────────────────────────────────────────────────

/// Tick-driven deferred visual verification poller.
///
/// Pre-allocated fixed-size array. No heap allocation on the hot path.
/// Call `enqueue()` when a macro finishes, `on_frame_rendered()` after
/// every `frame.present()`.
pub struct DeferredPoller {
    /// Fixed-capacity pending verification slots.
    pending: [PendingVerification; MAX_PENDING],
    /// Number of active entries (for fast empty check).
    count: usize,
}

impl DeferredPoller {
    /// Create a new poller with all slots empty.
    pub fn new() -> Self {
        Self {
            pending: [PendingVerification::default(); MAX_PENDING],
            count: 0,
        }
    }

    /// Queue a verification for after the GPU renders the given tick.
    ///
    /// Returns `false` if the queue is full (all MAX_PENDING slots occupied).
    pub fn enqueue(&mut self, task_id: u32, completed_on_tick: u64) -> bool {
        if self.count >= MAX_PENDING {
            return false;
        }
        for slot in &mut self.pending {
            if !slot.active {
                slot.task_id = task_id;
                slot.completed_on_tick = completed_on_tick;
                slot.active = true;
                self.count += 1;
                return true;
            }
        }
        false
    }

    /// Called after `frame.present()` with the tick ID that was just rendered.
    ///
    /// Any pending verification whose `completed_on_tick <= rendered_tick`
    /// is ready — the GPU has caught up. Returns a fixed-size array of
    /// results (up to MAX_PENDING) and the count of fired verifications.
    ///
    /// This is the hot path — no allocation, no branching beyond the scan.
    pub fn on_frame_rendered(
        &mut self,
        rendered_tick: u64,
    ) -> ([VerificationResult; MAX_PENDING], usize) {
        let mut results = [VerificationResult {
            task_id: 0,
            completed_on_tick: 0,
            rendered_tick: 0,
            latency_ticks: 0,
        }; MAX_PENDING];
        let mut fired = 0usize;

        for slot in &mut self.pending {
            if slot.active && rendered_tick >= slot.completed_on_tick {
                results[fired] = VerificationResult {
                    task_id: slot.task_id,
                    completed_on_tick: slot.completed_on_tick,
                    rendered_tick,
                    latency_ticks: rendered_tick - slot.completed_on_tick,
                };
                fired += 1;
                slot.active = false;
                self.count -= 1;
            }
        }

        (results, fired)
    }

    /// Number of pending verifications.
    pub fn pending_count(&self) -> usize {
        self.count
    }

    /// Whether the poller has any pending verifications.
    pub fn is_empty(&self) -> bool {
        self.count == 0
    }

    /// Clear all pending verifications.
    pub fn clear(&mut self) {
        for slot in &mut self.pending {
            slot.active = false;
        }
        self.count = 0;
    }
}

// ─── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    #[test]
    fn enqueue_and_fire_basic() {
        let mut poller = DeferredPoller::new();
        assert!(poller.is_empty());

        assert!(poller.enqueue(42, 100));
        assert_eq!(poller.pending_count(), 1);

        // GPU at tick 99 — not ready
        let (_, fired) = poller.on_frame_rendered(99);
        assert_eq!(fired, 0);
        assert_eq!(poller.pending_count(), 1);

        // GPU at tick 100 — fires
        let (results, fired) = poller.on_frame_rendered(100);
        assert_eq!(fired, 1);
        assert_eq!(results[0].task_id, 42);
        assert_eq!(results[0].completed_on_tick, 100);
        assert_eq!(results[0].latency_ticks, 0);
        assert!(poller.is_empty());
    }

    #[test]
    fn fires_when_gpu_ahead() {
        let mut poller = DeferredPoller::new();
        poller.enqueue(1, 50);
        let (results, fired) = poller.on_frame_rendered(200);
        assert_eq!(fired, 1);
        assert_eq!(results[0].latency_ticks, 150);
    }

    #[test]
    fn multiple_pending_different_ticks() {
        let mut poller = DeferredPoller::new();
        poller.enqueue(1, 100);
        poller.enqueue(2, 150);
        poller.enqueue(3, 200);

        let (_, fired) = poller.on_frame_rendered(150);
        assert_eq!(fired, 2);
        assert_eq!(poller.pending_count(), 1);

        let (results, fired) = poller.on_frame_rendered(200);
        assert_eq!(fired, 1);
        assert_eq!(results[0].task_id, 3);
        assert!(poller.is_empty());
    }

    #[test]
    fn queue_full_returns_false() {
        let mut poller = DeferredPoller::new();
        for i in 0..MAX_PENDING {
            assert!(poller.enqueue(i as u32, i as u64 * 10));
        }
        assert!(!poller.enqueue(999, 999));
    }

    #[test]
    fn clear_resets_all() {
        let mut poller = DeferredPoller::new();
        poller.enqueue(1, 100);
        poller.enqueue(2, 200);
        poller.clear();
        assert!(poller.is_empty());
        let (_, fired) = poller.on_frame_rendered(1000);
        assert_eq!(fired, 0);
    }

    #[test]
    fn slot_reuse_after_fire() {
        let mut poller = DeferredPoller::new();
        for i in 0..MAX_PENDING {
            poller.enqueue(i as u32, 10);
        }
        let (_, fired) = poller.on_frame_rendered(10);
        assert_eq!(fired, MAX_PENDING);
        assert!(poller.enqueue(99, 20));
        assert_eq!(poller.pending_count(), 1);
    }

    // ── Property tests ──────────────────────────────────────────────────

    proptest! {
        #[test]
        fn prop_all_enqueued_eventually_fire(
            tasks in proptest::collection::vec((0u32..1000, 0u64..10000), 1..=MAX_PENDING)
        ) {
            let mut poller = DeferredPoller::new();
            let mut max_tick = 0u64;
            for &(task_id, tick) in &tasks {
                poller.enqueue(task_id, tick);
                max_tick = max_tick.max(tick);
            }
            let (_, fired) = poller.on_frame_rendered(max_tick);
            prop_assert_eq!(fired, tasks.len());
            prop_assert!(poller.is_empty());
        }

        #[test]
        fn prop_latency_is_correct(
            completed_tick in 0u64..10000,
            rendered_tick in 0u64..20000,
        ) {
            if rendered_tick < completed_tick { return Ok(()); }
            let mut poller = DeferredPoller::new();
            poller.enqueue(1, completed_tick);
            let (results, fired) = poller.on_frame_rendered(rendered_tick);
            prop_assert_eq!(fired, 1);
            prop_assert_eq!(results[0].latency_ticks, rendered_tick - completed_tick);
        }

        #[test]
        fn prop_no_fire_before_tick(completed_tick in 1u64..10000) {
            let mut poller = DeferredPoller::new();
            poller.enqueue(1, completed_tick);
            let (_, fired) = poller.on_frame_rendered(completed_tick - 1);
            prop_assert_eq!(fired, 0);
            prop_assert_eq!(poller.pending_count(), 1);
        }
    }
}