use smallvec::SmallVec;
use tempfile::TempDir;
use tephra::Position;
use tephra::event::{Event, EventRef, EventType, Tag, Tags};
use tephra::index::{ActiveTail, IndexSet, search};
use tephra::log::set::{SegmentConfig, SegmentSet};
use tephra::query::{Matches, Query, QueryItem};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0 >> 17
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
const TYPES: [&str; 3] = ["Registered", "Enrolled", "Renamed"];
const TAGS: [&str; 6] = [
"course:c1",
"course:c2",
"student:s1",
"student:s2",
"team:t1",
"team:t2",
];
fn event_type(s: &str) -> EventType {
EventType::new(s).unwrap()
}
fn pick_tags(start: usize, k: usize) -> Tags {
let picked: SmallVec<[Tag; 4]> = (0..k)
.map(|i| Tag::new(TAGS[(start + i) % TAGS.len()]).unwrap())
.collect();
Tags::new(picked).unwrap()
}
fn random_event(rng: &mut Rng) -> Event {
let ty = event_type(TYPES[rng.below(TYPES.len() as u64) as usize]);
let k = rng.below(4) as usize; let start = rng.below(TAGS.len() as u64) as usize;
Event::new(&ty, &pick_tags(start, k), b"payload").unwrap()
}
fn random_query(rng: &mut Rng) -> Query {
match rng.below(6) {
0 => Query::all(),
1 => Query::items(Vec::new()),
_ => {
let n_items = 1 + rng.below(3) as usize;
let items = (0..n_items)
.map(|_| {
let n_types = rng.below(3) as usize; let type_start = rng.below(TYPES.len() as u64) as usize;
let types = (0..n_types)
.map(|i| event_type(TYPES[(type_start + i) % TYPES.len()]))
.collect();
let n_tags = rng.below(4) as usize; let tag_start = rng.below(TAGS.len() as u64) as usize;
QueryItem::new(types, pick_tags(tag_start, n_tags))
})
.collect::<Vec<_>>();
Query::items(items)
}
}
}
fn scan_baseline(set: &SegmentSet, query: &Query, after: Position) -> Vec<Position> {
let mut out = Vec::new();
let mut scan = set.scan_after(after);
while let Some(item) = scan.next() {
let record = item.unwrap();
let event = EventRef::from_bytes(record.data).unwrap();
if query.matches(event) {
out.push(record.position);
}
}
out
}
#[test]
fn index_search_agrees_with_scan_over_random_workload() {
let dir = TempDir::new().unwrap();
let mut set = SegmentSet::open(dir.path(), SegmentConfig::new(1 << 20)).unwrap();
let index = ActiveTail::new(set.next_position());
let mut rng = Rng(0x1234_5678_9ABC_DEF0);
for _ in 0..400 {
let event = random_event(&mut rng);
let range = set.append_batch(&[event.as_bytes()]).unwrap();
index.push(range.first, event.as_ref()).unwrap();
let last = set.last_position().get();
for _ in 0..4 {
let query = random_query(&mut rng);
let after = Position::new(rng.below(last + 1));
let from_index: Vec<Position> = search(&index.view_full(), &query, after).collect();
let from_scan = scan_baseline(&set, &query, after);
assert_eq!(
from_index, from_scan,
"index disagreed with scan for query {query:?} after {after}"
);
}
}
}
#[test]
fn index_set_agrees_with_scan_across_many_sealed_segments() {
let dir = TempDir::new().unwrap();
let mut set = SegmentSet::open(dir.path(), SegmentConfig::new(1 << 9)).unwrap();
let mut rng = Rng(0x0BAD_C0DE_F00D_1337);
for _ in 0..400 {
let event = random_event(&mut rng);
set.append_batch(&[event.as_bytes()]).unwrap();
}
assert!(
set.sealed_len() >= 5,
"small segments should have sealed many: {}",
set.sealed_len()
);
let index = IndexSet::open(&set).unwrap();
let last = set.last_position().get();
let mut rng = Rng(0xFEED_FACE_CAFE_BEEF);
for _ in 0..2000 {
let query = random_query(&mut rng);
let after = Position::new(rng.below(last + 1));
let from_index: Vec<Position> = index.search_all(&query, after).unwrap().collect();
let from_scan = scan_baseline(&set, &query, after);
assert_eq!(
from_index, from_scan,
"IndexSet disagreed with scan for query {query:?} after {after}"
);
}
let reopened = IndexSet::open(&set).unwrap();
let q = Query::item(QueryItem::of_types(vec![
EventType::new("Enrolled").unwrap(),
]));
let from_index: Vec<Position> = reopened.search_all(&q, Position::ZERO).unwrap().collect();
let from_scan = scan_baseline(&set, &q, Position::ZERO);
assert_eq!(from_index, from_scan);
}