use std::fmt::Write as _;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::{Context, SubscriberExt};
use ciborium::Value as CborValue;
use vantage_diorama::{Lens, LoadState};
use vantage_types::Record;
use vantage_vista::{Column, Vista, VistaMetadata, mocks::MockShell};
mod support;
use support::chunk::{Backend, master as chunk_master};
struct CaptureLayer(Arc<Mutex<Vec<String>>>);
struct FlatVisitor {
message: String,
fields: String,
}
impl Visit for FlatVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let _ = write!(self.message, "{value:?}");
} else {
let _ = write!(self.fields, " {}={value:?}", field.name());
}
}
}
impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
if event.metadata().target() != "vantage_diorama::debug" {
return;
}
let mut v = FlatVisitor {
message: String::new(),
fields: String::new(),
};
event.record(&mut v);
self.0
.lock()
.unwrap()
.push(format!("{}{}", v.message, v.fields));
}
}
#[allow(dead_code)]
pub fn capture() -> (tracing::subscriber::DefaultGuard, Arc<Mutex<Vec<String>>>) {
let log = Arc::new(Mutex::new(Vec::new()));
let subscriber = tracing_subscriber::registry().with(CaptureLayer(log.clone()));
(tracing::subscriber::set_default(subscriber), log)
}
#[allow(dead_code)]
pub fn lines_containing(log: &Arc<Mutex<Vec<String>>>, needle: &str) -> Vec<String> {
log.lock()
.unwrap()
.iter()
.filter(|l| l.contains(needle))
.cloned()
.collect()
}
fn master() -> Vista {
let metadata = VistaMetadata::new()
.with_column(Column::new("id", "String").with_flag("id"))
.with_id_column("id");
Vista::new("books", Box::new(MockShell::new().with_metadata(metadata)))
}
#[tokio::test]
async fn builder_flag_reaches_the_dio_and_off_means_off() {
let lens = Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.runtime(tokio::runtime::Handle::current())
.build()
.unwrap(),
);
let dio = lens.make_dio(master()).await.unwrap();
assert!(dio.debug_tap().enabled());
assert_eq!(dio.debug_tap().ds(), "faker-ds");
let quiet = Arc::new(
Lens::new()
.cache_in_memory()
.runtime(tokio::runtime::Handle::current())
.build()
.unwrap(),
);
let dio = quiet.make_dio(master()).await.unwrap();
assert!(!dio.debug_tap().enabled());
}
#[tokio::test]
async fn census_lines_fire_on_scenery_open_and_drop() {
let (_guard, log) = capture();
let lens = Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.runtime(tokio::runtime::Handle::current())
.build()
.unwrap(),
);
let dio = lens.make_dio(master()).await.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
let opens = lines_containing(&log, "+1 table scenery");
assert_eq!(opens.len(), 1);
assert!(opens[0].contains("census"), "line: {}", opens[0]);
assert!(opens[0].contains("now 1 table"), "line: {}", opens[0]);
assert!(opens[0].contains("rss"), "census carries process stats");
drop(scenery);
let closes = lines_containing(&log, "-1 table scenery");
assert_eq!(closes.len(), 1);
}
async fn wait_until(label: &str, mut pred: impl FnMut() -> bool) {
tokio::time::timeout(std::time::Duration::from_secs(2), async {
while !pred() {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for: {label}"));
}
#[tokio::test]
async fn load_lifecycle_is_correlated_and_cache_hits_are_logged() {
let (_guard, log) = capture();
let backend: Backend = Arc::new(Mutex::new(
(0..100)
.map(|i| {
let mut r = Record::new();
r.insert("v".to_string(), CborValue::Text(format!("row{i}")));
(format!("id{i}"), r)
})
.collect(),
));
let lens = {
let backend = backend.clone();
Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_load_chunk(move |_dio, range, _query, sink| {
let backend = backend.clone();
async move {
let rows = backend.lock().unwrap().clone();
sink.set_total(rows.len());
for idx in range {
if let Some((id, r)) = rows.get(idx) {
sink.push(idx, id.clone(), r.clone()).await?;
}
}
Ok(())
}
})
.build()
.unwrap(),
)
};
let dio = lens
.make_dio(chunk_master(&[("v", "String")]))
.await
.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
scenery.set_viewport(0..100);
wait_until("first load return", || {
lines_containing(&log, "got").len() == 1
})
.await;
let dispatch = lines_containing(&log, "asks for");
let ret = lines_containing(&log, "got");
assert_eq!(dispatch.len(), 1);
assert_eq!(ret.len(), 1);
let req = dispatch[0]
.split("fetch #")
.nth(1)
.expect("dispatch names its request")
.split_whitespace()
.next()
.expect("request id")
.to_string();
assert!(
ret[0].contains(&format!("fetch #{req} got")),
"the return must carry the same request id: {}",
ret[0]
);
assert!(ret[0].contains("rows in"));
let states = lines_containing(&log, "→ complete");
assert!(
states.iter().any(|l| l.contains("→ complete")),
"{states:?}"
);
let totals = lines_containing(&log, "total");
assert!(
totals
.iter()
.any(|l| l.contains("100 rows") && l.contains("stated by the source")),
"{totals:?}"
);
scenery.set_viewport(0..30);
wait_until("cache hit on second pass", || {
lines_containing(&log, "served locally").len() == 1
})
.await;
assert_eq!(lines_containing(&log, "asks for").len(), 1, "no re-fetch");
}
#[tokio::test]
async fn column_line_exposes_undemanded_wide_fields() {
let (_guard, log) = capture();
let lens = Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_load_chunk(move |_dio, range, _query, sink| async move {
for idx in range {
if idx != 0 {
continue;
}
let mut r = Record::new();
r.insert("id".to_string(), CborValue::Text("row0".to_string()));
r.insert("name".to_string(), CborValue::Text("Row Zero".to_string()));
for n in 1..=50 {
r.insert(format!("extra_{n:04}"), CborValue::Text("x".repeat(1024)));
}
sink.set_total(1);
sink.push(idx, "row0".to_string(), r).await?;
}
Ok(())
})
.build()
.unwrap(),
);
let dio = lens
.make_dio(chunk_master(&[("name", "String")]))
.await
.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
scenery.set_viewport(0..1);
wait_until("first load return", || {
lines_containing(&log, "got").len() == 1
})
.await;
let cols = lines_containing(&log, "payload");
assert_eq!(cols.len(), 1, "{cols:?}");
assert!(cols[0].contains("columns"), "{}", cols[0]);
assert!(cols[0].contains("52 columns received"), "{}", cols[0]);
assert!(
cols[0].contains("KB") || cols[0].contains("B"),
"{}",
cols[0]
);
assert!(
cols[0].contains("KB") || cols[0].contains("MB"),
"a wide payload must report in KB or MB, not bytes: {}",
cols[0]
);
}
#[tokio::test]
async fn column_line_dedups_across_multiple_wide_rows() {
let (_guard, log) = capture();
let lens = Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_load_chunk(move |_dio, range, _query, sink| async move {
sink.set_total(10);
for idx in range {
if idx >= 10 {
continue;
}
let mut r = Record::new();
r.insert("id".to_string(), CborValue::Text(format!("row{idx}")));
r.insert("name".to_string(), CborValue::Text(format!("Row {idx}")));
for n in 1..=50 {
r.insert(format!("extra_{n:04}"), CborValue::Text("x".repeat(1024)));
}
sink.push(idx, format!("row{idx}"), r).await?;
}
Ok(())
})
.build()
.unwrap(),
);
let dio = lens
.make_dio(chunk_master(&[("name", "String")]))
.await
.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
scenery.set_viewport(0..10);
wait_until("first load return", || {
lines_containing(&log, "got").len() == 1
})
.await;
let cols = lines_containing(&log, "payload");
assert_eq!(cols.len(), 1, "{cols:?}");
assert!(cols[0].contains("columns"), "{}", cols[0]);
assert!(cols[0].contains("52 columns received"), "{}", cols[0]);
assert!(cols[0].contains("columns"), "{}", cols[0]);
assert!(
cols[0].contains("KB") || cols[0].contains("MB"),
"a wide payload must report in KB or MB, not bytes: {}",
cols[0]
);
}
#[tokio::test]
async fn two_pass_list_and_detail_lifecycle_is_logged() {
let (_guard, log) = capture();
let lens = Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_list_page(move |_dio, q| async move {
let start = q.offset.min(40);
let end = (q.offset + q.limit).min(40);
let mut out = Vec::new();
for i in start..end {
let mut r = Record::new();
r.insert("name".to_string(), CborValue::Text(format!("Row {i}")));
out.push((format!("id{i}"), r));
}
Ok(out)
})
.on_load_detail(move |_dio, id| async move {
let mut r = Record::new();
r.insert(
"detail".to_string(),
CborValue::Text(format!("Detail for {id}")),
);
Ok(r)
})
.build()
.unwrap(),
);
let dio = lens.make_dio(master()).await.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
wait_until("list page return", || {
lines_containing(&log, "list #1 got").len() == 1
})
.await;
let dispatch = lines_containing(&log, "list #1 asks");
let ret = lines_containing(&log, "list #1 got");
assert_eq!(dispatch.len(), 1, "{dispatch:?}");
assert_eq!(ret.len(), 1, "{ret:?}");
let req = dispatch[0]
.split("list #")
.nth(1)
.expect("list dispatch names its request")
.split_whitespace()
.next()
.expect("request id")
.to_string();
assert!(
ret[0].contains(&format!("list #{req} got")),
"the return must carry the same request id: {}",
ret[0]
);
assert!(dispatch[0].contains("offset 0"), "{}", dispatch[0]);
assert!(dispatch[0].contains("ids from"), "{}", dispatch[0]);
assert!(ret[0].contains("40 ids"), "{}", ret[0]);
assert!(ret[0].contains("40 known so far"), "{}", ret[0]);
assert!(ret[0].contains("all of them"), "{}", ret[0]);
scenery.set_viewport(0..10);
wait_until("detail pass requested=10", || {
lines_containing(&log, "hydrate")
.iter()
.any(|l| l.contains("10 rows in view"))
})
.await;
let detail_lines = lines_containing(&log, "hydrate");
assert!(
detail_lines.iter().any(|l| l.contains("10 rows in view")),
"{detail_lines:?}"
);
assert!(
detail_lines[0].contains("need detail"),
"{}",
detail_lines[0]
);
}
#[tokio::test]
async fn cache_writes_report_new_updated_and_percentage() {
let (_guard, log) = capture();
let backend: Backend = Arc::new(Mutex::new(
(0..100)
.map(|i| {
let mut r = Record::new();
r.insert("v".to_string(), CborValue::Text(format!("row{i}")));
(format!("id{i}"), r)
})
.collect(),
));
let lens = {
let backend = backend.clone();
Arc::new(
Lens::new()
.cache_in_memory()
.debug_datasource("faker-ds")
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_load_chunk(move |_dio, range, _query, sink| {
let backend = backend.clone();
async move {
let rows = backend.lock().unwrap().clone();
sink.set_total(rows.len());
for idx in range {
if let Some((id, r)) = rows.get(idx) {
sink.push(idx, id.clone(), r.clone()).await?;
}
}
Ok(())
}
})
.build()
.unwrap(),
)
};
let dio = lens
.make_dio(chunk_master(&[("v", "String")]))
.await
.unwrap();
let scenery = dio.table_scenery().open().await.unwrap();
scenery.set_viewport(0..30);
wait_until("first load return", || {
lines_containing(&log, "got").len() == 1
})
.await;
let writes = lines_containing(&log, "new,");
assert_eq!(writes.len(), 1, "{writes:?}");
assert!(writes[0].contains("+30 new"), "{}", writes[0]);
assert!(writes[0].contains("0 updated"), "{}", writes[0]);
assert!(writes[0].contains("of 100"), "{}", writes[0]);
assert!(writes[0].contains("(30%)"), "{}", writes[0]);
}
#[tokio::test]
async fn a_non_debug_lens_emits_nothing_on_the_load_path() {
let (_guard, log) = capture();
let backend: Backend = Arc::new(Mutex::new(
(0..100)
.map(|i| {
let mut r = Record::new();
r.insert("v".to_string(), CborValue::Text(format!("row{i}")));
(format!("id{i}"), r)
})
.collect(),
));
let lens = {
let backend = backend.clone();
Arc::new(
Lens::new()
.cache_in_memory()
.viewport_debounce(std::time::Duration::from_millis(1))
.runtime(tokio::runtime::Handle::current())
.on_load_chunk(move |_dio, range, _query, sink| {
let backend = backend.clone();
async move {
let rows = backend.lock().unwrap().clone();
sink.set_total(rows.len());
for idx in range {
if let Some((id, r)) = rows.get(idx) {
sink.push(idx, id.clone(), r.clone()).await?;
}
}
Ok(())
}
})
.build()
.unwrap(),
)
};
let dio = lens
.make_dio(chunk_master(&[("v", "String")]))
.await
.unwrap();
assert!(!dio.debug_tap().enabled());
let scenery = dio.table_scenery().open().await.unwrap();
scenery.set_viewport(0..100);
wait_until("first load reaches Complete", || {
scenery.load_state() == LoadState::Complete
})
.await;
scenery.set_viewport(0..30);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert_eq!(scenery.load_state(), LoadState::Complete);
assert!(
log.lock().unwrap().is_empty(),
"debug-off lens must emit nothing: {:?}",
log.lock().unwrap()
);
}