//! Shared test fixtures for the RETRIEVE executor unit tests. //! //! `tests.rs` and `tests_part2.rs` were split solely to keep each file under the //! ≤ 600-line cap (`CODING_GUIDELINES` §9), but both need the SAME scaffolding: //! a minimal schema, the built-in profile registry, an item-insertion helper, //! and an executor constructor. Rather than copy-paste these verbatim across the //! two modules (a DRY hazard — a fix to one fixture silently skips the other), //! they live here once and are imported via `use super::test_support::*`. #![allow(clippy::unwrap_used)] use std::{sync::RwLock, time::Duration}; use roaring::RoaringBitmap; use super::RetrieveExecutor; use crate::{ ranking::{builtins::register_builtins, registry::ProfileRegistry}, schema::{DecaySpec, EntityKind, SchemaBuilder, Timestamp, Window}, signals::SignalLedger, storage::indexes::{bitmap::BitmapIndex, range::RangeIndex}, }; /// Minimal item schema: the generic signal vocabulary the built-in profiles read. pub(super) fn test_schema() -> crate::schema::Schema { let mut builder = SchemaBuilder::new(); for sig in &["view", "like", "share", "skip", "completion"] { let _ = builder .signal( sig, EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::OneHour, Window::TwentyFourHours, Window::SevenDays]) .velocity(true) .add(); } builder.build().unwrap() } /// Profile registry pre-loaded with the built-in profiles. pub(super) fn setup_registry() -> ProfileRegistry { let mut reg = ProfileRegistry::new(); register_builtins(&mut reg).unwrap(); reg } /// Add an item to the in-memory indexes and universe bitmap. #[allow(clippy::too_many_arguments)] pub(super) fn add_item( category_idx: &BitmapIndex, format_idx: &BitmapIndex, creator_idx: &BitmapIndex, duration_idx: &RangeIndex, created_at_idx: &RangeIndex, universe: &mut RoaringBitmap, id: u64, category: &str, format: &str, creator: u64, ) { let id_u32 = id as u32; category_idx.insert(id_u32, category); format_idx.insert(id_u32, format); creator_idx.insert(id_u32, creator.to_string()); duration_idx.insert(id_u32, 0u32); created_at_idx.insert(id_u32, Timestamp::now().as_nanos()); universe.insert(id_u32); } /// Add an item whose `created_at` is `age_hours` before [`FIXED_NOW_NS`], so a /// test can make entity-ID order and creation order disagree. #[allow(clippy::too_many_arguments)] pub(super) fn add_item_aged( category_idx: &BitmapIndex, format_idx: &BitmapIndex, creator_idx: &BitmapIndex, duration_idx: &RangeIndex, created_at_idx: &RangeIndex, universe: &mut RoaringBitmap, id: u64, age_hours: u64, ) { let id_u32 = id as u32; category_idx.insert(id_u32, "jazz"); format_idx.insert(id_u32, "video"); creator_idx.insert(id_u32, "1"); duration_idx.insert(id_u32, 0u32); created_at_idx.insert(id_u32, FIXED_NOW_NS - age_hours * HOUR_NS); universe.insert(id_u32); } /// Fixed query clock for age-sensitive fixtures. pub(super) const FIXED_NOW_NS: u64 = 1_708_000_000_000_000_000; /// One hour in nanoseconds. pub(super) const HOUR_NS: u64 = 3_600_000_000_000; /// Items-only length-prefixed metadata encoding that `db::deserialize_metadata` /// reads back, mirroring `db::metadata::serialize_metadata` (not visible here). pub(super) fn encode_meta(pairs: &[(&str, &str)]) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(&(pairs.len() as u32).to_le_bytes()); for (k, v) in pairs { buf.extend_from_slice(&(k.len() as u32).to_le_bytes()); buf.extend_from_slice(k.as_bytes()); buf.extend_from_slice(&(v.len() as u32).to_le_bytes()); buf.extend_from_slice(v.as_bytes()); } buf } /// Storage holding a `created_at` metadata row per `(id, age_hours)` pair, which /// is what the Stage-3 point-read loads for an age-derived sort. pub(super) fn storage_with_ages(ages: &[(u64, u64)]) -> crate::storage::InMemoryBackend { use crate::storage::{StorageEngine, Tag, encode_key}; let storage = crate::storage::InMemoryBackend::new(); for &(id, age_h) in ages { let key = encode_key(crate::schema::EntityId::new(id), Tag::Meta, b""); storage .put( &key, &encode_meta(&[("created_at", &(FIXED_NOW_NS - age_h * HOUR_NS).to_string())]), ) .unwrap(); } storage } /// Build an executor from test indexes. #[allow(clippy::too_many_arguments)] pub(super) fn make_executor<'a>( ledger: &'a SignalLedger, profile_reg: &'a ProfileRegistry, cat: &'a BitmapIndex, fmt: &'a BitmapIndex, creator: &'a BitmapIndex, tag: &'a BitmapIndex, dur: &'a RangeIndex, ts: &'a RangeIndex, universe: &'a RwLock, ) -> RetrieveExecutor<'a> { RetrieveExecutor::new( ledger, profile_reg, Some(cat), Some(fmt), Some(creator), Some(tag), Some(dur), Some(ts), Some(universe), ) }