use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ahash::HashMap;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use re_chunk::{Chunk, ChunkBuilder};
use re_chunk_store::LatestAtQuery;
use re_entity_db::EntityDb;
use re_log_types::{EntityPath, StoreId, StoreKind, Timeline};
use re_types_core::reflection::Reflection;
use crate::{
blueprint_timeline, command_channel, ApplicationSelectionState, CommandReceiver, CommandSender,
ComponentUiRegistry, DataQueryResult, ItemCollection, RecordingConfig, StoreContext,
SystemCommand, ViewClass, ViewClassRegistry, ViewId, ViewStates, ViewerContext,
};
pub trait HarnessExt {
fn snapshot_with_broken_pixels_threshold(
&mut self,
name: &str,
num_pixels: u64,
broken_percent_threshold: f64,
);
}
impl HarnessExt for egui_kittest::Harness<'_> {
fn snapshot_with_broken_pixels_threshold(
&mut self,
name: &str,
num_pixels: u64,
broken_percent_threshold: f64,
) {
match self.try_snapshot(name) {
Ok(_) => {}
Err(err) => match err {
egui_kittest::SnapshotError::Diff {
name,
diff: num_broken_pixels,
diff_path,
} => {
let broken_percent = num_broken_pixels as f64 / num_pixels as f64;
re_log::debug!(num_pixels, num_broken_pixels, broken_percent);
assert!(
broken_percent <= broken_percent_threshold,
"{name} failed because {broken_percent} > {broken_percent_threshold}\n{diff_path:?}"
);
}
_ => panic!("{name} failed: {err}"),
},
}
}
}
pub struct TestContext {
pub recording_store: EntityDb,
pub blueprint_store: EntityDb,
pub view_class_registry: ViewClassRegistry,
pub selection_state: ApplicationSelectionState,
pub recording_config: RecordingConfig,
pub view_states: Mutex<ViewStates>,
pub query_results: HashMap<ViewId, DataQueryResult>,
pub blueprint_query: LatestAtQuery,
pub component_ui_registry: ComponentUiRegistry,
pub reflection: Reflection,
command_sender: CommandSender,
command_receiver: CommandReceiver,
egui_render_state: Mutex<Option<egui_wgpu::RenderState>>,
called_setup_kittest_for_rendering: AtomicBool,
}
impl Default for TestContext {
fn default() -> Self {
re_log::setup_logging();
let recording_store = EntityDb::new(StoreId::random(StoreKind::Recording));
let blueprint_store = EntityDb::new(StoreId::random(StoreKind::Blueprint));
let (command_sender, command_receiver) = command_channel();
let recording_config = RecordingConfig::default();
let blueprint_query = LatestAtQuery::latest(blueprint_timeline());
let component_ui_registry = ComponentUiRegistry::new(Box::new(
|_ctx, _ui, _ui_layout, _query, _db, _entity_path, _row_id, _component| {},
));
let reflection =
re_types::reflection::generate_reflection().expect("Failed to generate reflection");
Self {
recording_store,
blueprint_store,
view_class_registry: Default::default(),
selection_state: Default::default(),
recording_config,
view_states: Default::default(),
blueprint_query,
query_results: Default::default(),
component_ui_registry,
reflection,
command_sender,
command_receiver,
egui_render_state: Mutex::new(None),
called_setup_kittest_for_rendering: AtomicBool::new(false),
}
}
}
fn create_egui_renderstate() -> egui_wgpu::RenderState {
re_tracing::profile_function!();
let shared_wgpu_setup = &*SHARED_WGPU_RENDERER_SETUP;
let config = egui_wgpu::WgpuConfiguration {
wgpu_setup: egui_wgpu::WgpuSetupExisting {
instance: shared_wgpu_setup.instance.clone(),
adapter: shared_wgpu_setup.adapter.clone(),
device: shared_wgpu_setup.device.clone(),
queue: shared_wgpu_setup.queue.clone(),
}
.into(),
present_mode: wgpu::PresentMode::Immediate,
desired_maximum_frame_latency: None,
on_surface_error: Arc::new(|_| {
unreachable!("tests aren't expected to draw to surfaces");
}),
};
let compatible_surface = None;
let msaa_samples = 1;
let depth_format = None;
let dithering = false;
let render_state = pollster::block_on(egui_wgpu::RenderState::create(
&config,
&shared_wgpu_setup.instance,
compatible_surface,
depth_format,
msaa_samples,
dithering,
))
.expect("Failed to set up egui_wgpu::RenderState");
render_state.renderer.write().callback_resources.insert(
re_renderer::RenderContext::new(
&shared_wgpu_setup.adapter,
shared_wgpu_setup.device.clone(),
shared_wgpu_setup.queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
)
.expect("Failed to initialize re_renderer"),
);
render_state
}
struct SharedWgpuResources {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
static SHARED_WGPU_RENDERER_SETUP: Lazy<SharedWgpuResources> =
Lazy::new(init_shared_renderer_setup);
fn init_shared_renderer_setup() -> SharedWgpuResources {
let instance = wgpu::Instance::new(&re_renderer::config::testing_instance_descriptor());
let adapter = re_renderer::config::select_testing_adapter(&instance);
let device_caps = re_renderer::config::DeviceCaps::from_adapter(&adapter)
.expect("Failed to determine device capabilities");
let (device, queue) =
pollster::block_on(adapter.request_device(&device_caps.device_descriptor(), None))
.expect("Failed to request device.");
SharedWgpuResources {
instance,
adapter,
device,
queue,
}
}
impl TestContext {
pub fn setup_kittest_for_rendering(&self) -> egui_kittest::HarnessBuilder<()> {
let new_render_state = create_egui_renderstate();
let builder = egui_kittest::Harness::builder().renderer(
egui_kittest::wgpu::WgpuTestRenderer::from_render_state(new_render_state.clone()),
);
self.egui_render_state.lock().replace(new_render_state);
self.called_setup_kittest_for_rendering
.store(true, std::sync::atomic::Ordering::Relaxed);
builder
}
pub fn active_timeline(&self) -> re_chunk::Timeline {
*self.recording_config.time_ctrl.read().timeline()
}
pub fn set_active_timeline(&self, timeline: Timeline) {
self.recording_config
.time_ctrl
.write()
.set_timeline(timeline);
}
pub fn edit_selection(&mut self, edit_fn: impl FnOnce(&mut ApplicationSelectionState)) {
edit_fn(&mut self.selection_state);
self.selection_state.on_frame_start(|_| true, None);
}
pub fn log_entity(
&mut self,
entity_path: EntityPath,
build_chunk: impl FnOnce(ChunkBuilder) -> ChunkBuilder,
) {
let builder = build_chunk(Chunk::builder(entity_path));
self.recording_store
.add_chunk(&Arc::new(
builder.build().expect("chunk should be successfully built"),
))
.expect("chunk should be successfully added");
}
pub fn register_view_class<T: ViewClass + Default + 'static>(&mut self) {
self.view_class_registry
.add_class::<T>()
.expect("registering a class should succeed");
}
pub fn run(&self, egui_ctx: &egui::Context, func: impl FnOnce(&ViewerContext<'_>)) {
re_log::PanicOnWarnScope::new(); re_ui::apply_style_and_install_loaders(egui_ctx);
let store_context = StoreContext {
app_id: "rerun_test".into(),
blueprint: &self.blueprint_store,
default_blueprint: None,
recording: &self.recording_store,
bundle: &Default::default(),
caches: &Default::default(),
hub: &Default::default(),
should_enable_heuristics: false,
};
let indicated_entities_per_visualizer = self
.view_class_registry
.indicated_entities_per_visualizer(&store_context.recording.store_id());
let drag_and_drop_manager = crate::DragAndDropManager::new(ItemCollection::default());
let mut context_render_state = self.egui_render_state.lock();
let render_state = context_render_state.get_or_insert_with(create_egui_renderstate);
let mut egui_renderer = render_state.renderer.write();
let render_ctx = egui_renderer
.callback_resources
.get_mut::<re_renderer::RenderContext>()
.expect("No re_renderer::RenderContext in egui_render_state");
render_ctx.begin_frame();
let ctx = ViewerContext {
app_options: &Default::default(),
cache: &Default::default(),
reflection: &self.reflection,
component_ui_registry: &self.component_ui_registry,
view_class_registry: &self.view_class_registry,
store_context: &store_context,
maybe_visualizable_entities_per_visualizer: &Default::default(),
indicated_entities_per_visualizer: &indicated_entities_per_visualizer,
query_results: &self.query_results,
rec_cfg: &self.recording_config,
blueprint_cfg: &Default::default(),
selection_state: &self.selection_state,
blueprint_query: &self.blueprint_query,
egui_ctx,
render_ctx,
command_sender: &self.command_sender,
focused_item: &None,
drag_and_drop_manager: &drag_and_drop_manager,
};
func(&ctx);
let num_view_builders_created = render_ctx.active_frame.num_view_builders_created();
let called_setup_kittest_for_rendering = self
.called_setup_kittest_for_rendering
.load(std::sync::atomic::Ordering::Relaxed);
assert!(num_view_builders_created == 0 || called_setup_kittest_for_rendering,
"Rendering with `re_renderer` requires setting up kittest with `TestContext::setup_kittest_for_rendering`
to ensure that kittest & re_renderer use the same graphics device.");
render_ctx.before_submit();
}
pub fn run_in_egui_central_panel(
&self,
mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui),
) {
egui::__run_test_ctx(|ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let egui_ctx = ui.ctx().clone();
self.run(&egui_ctx, |ctx| {
func(ctx, ui);
});
});
});
}
pub fn run_once_in_egui_central_panel<R>(
&self,
func: impl FnOnce(&ViewerContext<'_>, &mut egui::Ui) -> R,
) -> R {
let mut func = Some(func);
let mut result = None;
egui::__run_test_ctx(|ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let egui_ctx = ui.ctx().clone();
self.run(&egui_ctx, |ctx| {
if let Some(func) = func.take() {
result = Some(func(ctx, ui));
}
});
});
});
result.expect("Function should have been called at least once")
}
pub fn handle_system_commands(&mut self) {
while let Some(command) = self.command_receiver.recv_system() {
let mut handled = true;
let command_name = format!("{command:?}");
match command {
SystemCommand::UpdateBlueprint(store_id, chunks) => {
assert_eq!(store_id, self.blueprint_store.store_id());
for chunk in chunks {
self.blueprint_store
.add_chunk(&Arc::new(chunk))
.expect("Updating the blueprint chunk store failed");
}
}
SystemCommand::DropEntity(store_id, entity_path) => {
assert_eq!(store_id, self.blueprint_store.store_id());
self.blueprint_store
.drop_entity_path_recursive(&entity_path);
}
SystemCommand::SetSelection(item) => {
self.selection_state.set_selection(item);
}
SystemCommand::SetActiveTimeline { rec_id, timeline } => {
assert_eq!(rec_id, self.recording_store.store_id());
self.recording_config
.time_ctrl
.write()
.set_timeline(timeline);
}
SystemCommand::SetFocus(_)
| SystemCommand::ActivateApp(_)
| SystemCommand::CloseApp(_)
| SystemCommand::LoadDataSource(_)
| SystemCommand::ClearSourceAndItsStores(_)
| SystemCommand::AddReceiver(_)
| SystemCommand::ResetViewer
| SystemCommand::ClearActiveBlueprint
| SystemCommand::ClearActiveBlueprintAndEnableHeuristics
| SystemCommand::ActivateRecording(_)
| SystemCommand::CloseStore(_)
| SystemCommand::UndoBlueprint { .. }
| SystemCommand::RedoBlueprint { .. }
| SystemCommand::CloseAllRecordings => handled = false,
#[cfg(debug_assertions)]
SystemCommand::EnableInspectBlueprintTimeline(_) => handled = false,
#[cfg(not(target_arch = "wasm32"))]
SystemCommand::FileSaver(_) => handled = false,
}
if !handled {
eprintln!("Ignored system command: {command_name:?}",);
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Item;
use re_entity_db::InstancePath;
#[test]
fn test_edit_selection() {
let mut test_context = TestContext::default();
let item = Item::InstancePath(InstancePath::entity_all("/entity/path".into()));
test_context.edit_selection(|selection_state| {
selection_state.set_selection(item.clone());
});
test_context.run_in_egui_central_panel(|ctx, _| {
assert_eq!(
ctx.selection_state.selected_items().single_item(),
Some(&item)
);
});
}
}