//! Provides a test context that builds on `re_viewer_context`. #![expect(clippy::unwrap_used)] // This is only a test use std::sync::Arc; use std::sync::atomic::AtomicBool; use ahash::HashMap; use egui::os::OperatingSystem; use parking_lot::{Mutex, RwLock}; use re_chunk::{Chunk, ChunkBuilder, TimelineName}; use re_chunk_store::LatestAtQuery; use re_entity_db::{EntityDb, InstancePath}; use re_log_types::external::re_tuid::Tuid; use re_log_types::{ ApplicationId, EntityPath, EntityPathPart, SetStoreInfo, StoreId, StoreInfo, StoreKind, }; use re_sdk_types::archetypes::RecordingInfo; use re_sdk_types::{Component as _, ComponentDescriptor}; use re_types_core::reflection::Reflection; use re_ui::Help; use re_viewer_context::{ AppContext, AppOptions, ApplicationSelectionState, BlueprintContext, CommandReceiver, CommandSender, ComponentUiRegistry, DataQueryResult, FallbackProviderRegistry, ItemCollection, NeedsRepaint, Route, StoreHub, StoreViewContext, SystemCommand, SystemCommandSender as _, TimeControl, TimeControlCommand, ViewClass, ViewClassRegistry, ViewId, ViewStates, ViewerContext, blueprint_timeline, command_channel, }; pub mod external { pub use egui_kittest; } /// Harness to execute code that rely on [`crate::ViewerContext`]. /// /// Example: /// ```rust /// use re_test_context::TestContext; /// use re_viewer_context::ViewerContext; /// /// let mut test_context = TestContext::new(); /// test_context.run_in_egui_central_panel(|ctx: &ViewerContext, _| { /// /* do something with ctx */ /// }); /// /// // To get proper UI:s, also run this: /// // test_context.component_ui_registry = re_component_ui::create_component_ui_registry(); /// // re_data_ui::register_component_uis(&mut test_context.component_ui_registry); /// ``` pub struct TestContext { pub app_options: AppOptions, /// The recording store id that this test context was created with. pub recording_store_id: StoreId, /// The application id that this test context was created with. pub application_id: ApplicationId, /// Store hub prepopulated with a single recording & a blueprint recording. pub store_hub: Mutex, pub view_class_registry: ViewClassRegistry, // Mutex is needed, so we can update these from the `run` method pub selection_state: Mutex, pub focused_item: Mutex>, // RwLock so we can have `handle_system_commands` take an immutable reference to self. pub time_ctrl: RwLock, pub view_states: Mutex, // Populating this in `run` would pull in too many dependencies into the test harness for now. pub query_results: HashMap, pub blueprint_query: LatestAtQuery, pub component_ui_registry: ComponentUiRegistry, pub component_fallback_registry: FallbackProviderRegistry, pub reflection: Reflection, pub connection_registry: re_redap_client::ConnectionRegistryHandle, command_sender: CommandSender, command_receiver: CommandReceiver, pub egui_render_state: Mutex>, called_setup_kittest_for_rendering: AtomicBool, } pub struct TestBlueprintCtx<'a> { command_sender: &'a CommandSender, current_blueprint: &'a EntityDb, default_blueprint: Option<&'a EntityDb>, blueprint_query: &'a re_chunk::LatestAtQuery, } impl BlueprintContext for TestBlueprintCtx<'_> { fn command_sender(&self) -> &CommandSender { self.command_sender } fn current_blueprint(&self) -> &EntityDb { self.current_blueprint } fn default_blueprint(&self) -> Option<&EntityDb> { self.default_blueprint } fn blueprint_query(&self) -> &re_chunk::LatestAtQuery { self.blueprint_query } } // TODO(grtlr): Could we consolidate this with `TestBlueprintCtx`? /// Extension trait for test-specific blueprint operations. /// /// This trait provides `save_visualizers` for any type that implements `BlueprintContext`, /// allowing tests to save visualizers with deterministic IDs via `ViewerContext`. pub trait VisualizerBlueprintContext: BlueprintContext { /// Saves visualizers to blueprint with deterministic IDs. /// /// This assigns deterministic visualizer IDs based on the entity path /// and index of the visualizer, making the output reproducible for tests. fn save_visualizers( &self, entity_path: &EntityPath, view_id: ViewId, visualizers: impl IntoIterator>, ) { use re_sdk_types::AsComponents; use re_sdk_types::blueprint::archetypes as bp_archetypes; use re_sdk_types::blueprint::components::VisualizerInstructionId; let base_override_path = bp_archetypes::ViewContents::blueprint_base_visualizer_path_for_entity( view_id.uuid(), entity_path, ); let mut ids = Vec::new(); for (i, visualizer) in visualizers.into_iter().enumerate() { let mut visualizer = visualizer.into(); // Generate a deterministic ID based on entity path hash and visualizer index visualizer.id = VisualizerInstructionId::new_deterministic(entity_path, i); ids.push(visualizer.id); let visualizer_path = base_override_path .clone() .join(&EntityPath::from_single_string(visualizer.id.to_string())); let mut instruction = bp_archetypes::VisualizerInstruction::new(visualizer.visualizer_type.clone()); if !visualizer.mappings.is_empty() { instruction = instruction.with_component_map(visualizer.mappings.clone()); } self.save_blueprint_archetypes( visualizer_path, std::iter::chain( std::iter::once(&instruction as &dyn AsComponents), visualizer .overrides .iter() .map(|batch| batch as &dyn AsComponents), ), ); } self.save_blueprint_archetype( base_override_path, &bp_archetypes::ActiveVisualizers::new(ids), ); } } impl VisualizerBlueprintContext for T {} impl Default for TestContext { fn default() -> Self { Self::new() } } impl TestContext { pub fn new() -> Self { Self::new_with_store_info(StoreInfo::testing()) } pub fn new_with_store_info(store_info: StoreInfo) -> Self { Self::new_with_store_info_and_config( store_info, re_chunk_store::ChunkStoreConfig::from_env().unwrap_or_default(), ) } pub fn new_with_store_info_and_config( store_info: StoreInfo, store_config: re_chunk_store::ChunkStoreConfig, ) -> Self { re_log::setup_logging(); let application_id = store_info.application_id().clone(); let recording_store_id = store_info.store_id.clone(); let mut recording_store = EntityDb::with_store_config(recording_store_id.clone(), true, store_config); recording_store.set_store_info(SetStoreInfo { row_id: Tuid::new(), info: store_info, }); { // Set RecordingInfo: recording_store .set_recording_property( EntityPath::properties(), RecordingInfo::descriptor_name(), &re_sdk_types::components::Name::from("Test recording"), ) .unwrap(); recording_store .set_recording_property( EntityPath::properties(), RecordingInfo::descriptor_start_time(), &re_sdk_types::components::Timestamp::from( "2025-06-28T19:26:42Z" .parse::() .unwrap() .as_nanosecond() as i64, ), ) .unwrap(); } { // Set some custom recording properties: recording_store .set_recording_property( EntityPath::properties() / EntityPathPart::from("episode"), ComponentDescriptor { archetype: None, component: "location".into(), component_type: Some(re_sdk_types::components::Text::name()), }, &re_sdk_types::components::Text::from("Swallow Falls"), ) .unwrap(); recording_store .set_recording_property( EntityPath::properties() / EntityPathPart::from("episode"), ComponentDescriptor { archetype: None, component: "weather".into(), component_type: Some(re_sdk_types::components::Text::name()), }, &re_sdk_types::components::Text::from("Cloudy with meatballs"), ) .unwrap(); } let blueprint_id = StoreId::random(StoreKind::Blueprint, application_id.clone()); let blueprint_store = EntityDb::new(blueprint_id.clone()); let mut store_hub = StoreHub::test_hub(); store_hub.insert_entity_db(recording_store); store_hub.insert_entity_db(blueprint_store); store_hub .set_cloned_blueprint_active_for_app(&blueprint_id) .expect("Failed to set blueprint as active"); let (command_sender, command_receiver) = command_channel(); let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); let time_ctrl = { let ctx = TestBlueprintCtx { command_sender: &command_sender, current_blueprint: store_hub .active_blueprint_for_app(&application_id) .expect("We should have an active blueprint now"), default_blueprint: store_hub.default_blueprint_for_app(&application_id), blueprint_query: &blueprint_query, }; TimeControl::from_blueprint(&ctx) }; let component_ui_registry = ComponentUiRegistry::new(); let component_fallback_registry = re_component_fallbacks::create_component_fallback_registry(); let reflection = re_sdk_types::reflection::generate_reflection().expect("Failed to generate reflection"); Self { app_options: AppOptions::test(), recording_store_id, application_id, view_class_registry: Default::default(), selection_state: Default::default(), focused_item: Default::default(), time_ctrl: RwLock::new(time_ctrl), view_states: Default::default(), blueprint_query, query_results: Default::default(), component_ui_registry, component_fallback_registry, reflection, connection_registry: re_redap_client::ConnectionRegistry::new_without_stored_credentials(), command_sender, command_receiver, // Created lazily since each egui_kittest harness needs a new one. egui_render_state: Mutex::new(None), called_setup_kittest_for_rendering: AtomicBool::new(false), store_hub: Mutex::new(store_hub), } } /// Create a new test context that knows about a specific view class. /// /// This is useful for tests that need test a single view class. /// /// Note that it's important to first register the view class before adding any entities, /// otherwise the `VisualizerEntitySubscriber` for our visualizers doesn't exist yet, /// and thus will not find anything applicable to the visualizer. pub fn new_with_view_class() -> Self { let mut test_context = Self::new(); test_context.register_view_class::(); test_context } } /// Create an `egui_wgpu::RenderState` for tests. 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(), // None of these matter for tests as we're not going to draw to a surfaces. surface: egui_wgpu::SurfaceConfig { present_mode: wgpu::PresentMode::Immediate, desired_maximum_frame_latency: None, }, on_surface_status: Arc::new(|_| { unreachable!("tests aren't expected to draw to surfaces"); }), }; let compatible_surface = None; let render_state = pollster::block_on(egui_wgpu::RenderState::create( &config, &shared_wgpu_setup.instance, compatible_surface, egui_wgpu::RendererOptions::PREDICTABLE, )) .expect("Failed to set up egui_wgpu::RenderState"); // Put re_renderer::RenderContext into the callback resources so that render callbacks can access it. 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, |_| re_renderer::RenderConfig::testing(), ) .expect("Failed to initialize re_renderer"), ); render_state } /// Instance & adapter struct SharedWgpuResources { instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, // Sharing the queue across parallel running tests should work fine in theory - it's obviously threadsafe. // Note though that this becomes an odd sync point that is shared with all tests that put in work here. queue: wgpu::Queue, } static SHARED_WGPU_RENDERER_SETUP: std::sync::LazyLock = std::sync::LazyLock::new(init_shared_renderer_setup); fn init_shared_renderer_setup() -> SharedWgpuResources { let instance = wgpu::Instance::new(re_renderer::device_caps::testing_instance_descriptor()); let adapter = pollster::block_on(re_renderer::device_caps::select_testing_adapter(&instance)); let is_ci = std::env::var("CI").is_ok(); if is_ci { assert!( adapter.get_info().device_type == wgpu::DeviceType::Cpu, "We require a software renderer for CI tests. GPU based ones have been unreliable in the past, see https://github.com/rerun-io/rerun/issues/11359." ); } let device_caps = re_renderer::device_caps::DeviceCaps::from_adapter(&adapter) .expect("Failed to determine device capabilities"); let (device, queue) = pollster::block_on(adapter.request_device(&device_caps.device_descriptor())) .expect("Failed to request device."); SharedWgpuResources { instance, adapter, device, queue, } } impl TestContext { /// Used to get a context with helper functions to write & read from blueprints. pub fn with_blueprint_ctx(&self, f: impl FnOnce(TestBlueprintCtx<'_>, &StoreHub) -> R) -> R { let store_hub = self .store_hub .try_lock() .expect("Failed to get lock for blueprint ctx"); f( TestBlueprintCtx { command_sender: &self.command_sender, current_blueprint: store_hub .active_blueprint_for_app(&self.application_id) .expect("The test context should always have an active blueprint"), default_blueprint: store_hub.default_blueprint_for_app(&self.application_id), blueprint_query: &self.blueprint_query, }, &store_hub, ) } /// Helper function to send a [`SystemCommand::TimeControlCommands`] command /// with the current store id. pub fn send_time_commands( &self, store_id: StoreId, commands: impl IntoIterator, ) { let commands: Vec<_> = commands.into_iter().collect(); if !commands.is_empty() { self.command_sender .send_system(SystemCommand::TimeControlCommands { store_id, time_commands: commands, }); } } pub fn set_active_timeline(&self, timeline_name: impl Into) { let store_id = self.active_store_id(); self.send_time_commands( store_id, [TimeControlCommand::SetActiveTimeline(timeline_name.into())], ); self.handle_system_commands(&egui::Context::default()); } /// Set up for rendering UI, with not 3D/2D in it. pub fn setup_kittest_for_rendering_ui( &self, size: impl Into, ) -> egui_kittest::HarnessBuilder<()> { self.setup_kittest_for_rendering(re_ui::testing::TestOptions::Gui, size.into()) } /// Set up for rendering 3D/2D and maybe UI. /// /// This has slightly higher error tolerances than [`Self::setup_kittest_for_rendering_ui`]. pub fn setup_kittest_for_rendering_3d( &self, size: impl Into, ) -> egui_kittest::HarnessBuilder<()> { self.setup_kittest_for_rendering(re_ui::testing::TestOptions::Rendering3D, size.into()) } fn setup_kittest_for_rendering( &self, option: re_ui::testing::TestOptions, size: egui::Vec2, ) -> egui_kittest::HarnessBuilder<()> { // Egui kittests insists on having a fresh render state for each test. let new_render_state = create_egui_renderstate(); let builder = re_ui::testing::new_harness(option, size).renderer( // Note that render state clone is mostly cloning of inner `Arc`. // This does _not_ duplicate re_renderer's context contained within. 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 } /// Timeline the recording config is using by default. pub fn active_timeline(&self) -> Option { self.time_ctrl.read().timeline().copied() } pub fn active_blueprint(&mut self) -> &mut EntityDb { let store_hub = self.store_hub.get_mut(); let blueprint_id = store_hub .active_blueprint_id_for_app(&self.application_id) .expect("expected an active blueprint") .clone(); store_hub.entity_db_mut(&blueprint_id).unwrap() } pub fn active_store_id(&self) -> StoreId { self.recording_store_id.clone() } pub fn edit_selection(&self, edit_fn: impl FnOnce(&mut ApplicationSelectionState)) { let mut selection_state = self.selection_state.lock(); edit_fn(&mut selection_state); // the selection state is double-buffered, so let's ensure it's updated selection_state.on_frame_start(|item| Some(item.clone()), None); } /// Log an entity to the recording store. /// /// The provided closure should add content using the [`ChunkBuilder`] passed as argument. pub fn log_entity( &mut self, entity_path: impl Into, build_chunk: impl FnOnce(ChunkBuilder) -> ChunkBuilder, ) { let builder = build_chunk(Chunk::builder(entity_path)); let chunk = Arc::new(builder.build().expect("chunk should be successfully built")); let store_hub = self.store_hub.get_mut(); store_hub .add_chunk_for_tests(&self.recording_store_id, &chunk) .expect("chunk should be successfully added"); } pub fn add_chunks(&mut self, chunks: impl Iterator) { let store_hub = self.store_hub.get_mut(); for chunk in chunks { store_hub .add_chunk_for_tests(&self.recording_store_id, &Arc::new(chunk)) .unwrap(); } } pub fn add_rrd_manifest(&mut self, rrd_manifest: Arc) { let store_hub = self.store_hub.get_mut(); let active_recording = store_hub.entity_db_mut(&self.recording_store_id).unwrap(); active_recording.add_rrd_manifest_message(rrd_manifest); active_recording.mark_rrd_manifest_complete(); // Pretend like we are connected to a real redap server: active_recording.data_source = Some(re_log_channel::LogSource::RedapGrpcStream { uri: "rerun+http://localhost:51234/dataset/187A3200CAE4DD795748a7ad187e21a3?segment_id=6977dcfd524a45b3b786c9a5a0bde4e1".parse().unwrap(), open_behavior: re_log_channel::RecordingOpenBehavior::OpenAndSelect, table_blueprint: None, }); } /// Register a view class. pub fn register_view_class(&mut self) { self.view_class_registry .add_class::( &self.reflection, &self.app_options, &mut self.component_fallback_registry, ) .expect("registering a class should succeed"); } /// Run the provided closure with a [`StoreViewContext`] produced by the [`Self`]. /// /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test /// relies on system commands. pub fn run_recording( &self, egui_ctx: &egui::Context, func: impl FnOnce(&StoreViewContext<'_>), ) { self.run(egui_ctx, |viewer_ctx| { func(&viewer_ctx.active_recording_store_view_context()); }); } /// Run the provided closure with a [`ViewerContext`] produced by the [`Self`]. /// /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test /// relies on system commands. pub fn run(&self, egui_ctx: &egui::Context, func: impl FnOnce(&ViewerContext<'_>)) { re_log::PanicOnWarnScope::new(); // TODO(andreas): There should be a way to opt-out of this. re_ui::apply_style_and_install_loaders(egui_ctx); let mut store_hub = self.store_hub.lock(); store_hub.begin_frame_caches(Some(&self.recording_store_id)); let db = store_hub.entity_db_mut(&self.recording_store_id).unwrap(); if db.can_fetch_chunks_from_redap() { let (rrd_manifest, storage_engine) = db.rrd_manifest_index_mut_and_storage_engine(); let options = re_entity_db::ChunkPrefetchOptions::default(); // Budget of 0 bytes so that we don't try to load anything. let mut budget = re_entity_db::RemainingByteBudget::new(0, options.max_bytes_on_wire_at_once); let _err = rrd_manifest.prefetch_chunks( storage_engine.store(), &options, self.time_ctrl.read().time_cursor(), &mut budget, &|_| panic!("We have 0 bytes allowed memory"), ); } // Ensure the per-store cache exists with up-to-date visualizer subscribers. store_hub.entity_db_and_cache(&self.recording_store_id, &self.view_class_registry); let route = Route::LocalRecording { recording_id: self.recording_store_id.clone(), }; let active_time_ctrl = self.time_ctrl.read(); let (storage_context, store_context) = store_hub.read_context(&route, &active_time_ctrl); let store_context = store_context .expect("a `Route::LocalRecording` must always have an active store context"); let visualizable_entities_per_visualizer = store_context .caches .visualizable_entities_for_visualizer_systems(); let indicated_entities_per_visualizer = store_context.caches.indicated_entities_per_visualizer(); let drag_and_drop_manager = re_viewer_context::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::() .expect("No re_renderer::RenderContext in egui_render_state"); render_ctx.begin_frame(); let mut selection_state = self.selection_state.lock(); let mut focused_item = self.focused_item.lock(); let ctx = ViewerContext { app_ctx: AppContext { is_test: true, app_options: &self.app_options, reflection: &self.reflection, egui_ctx, command_sender: &self.command_sender, render_ctx, connection_registry: &self.connection_registry, storage_context: &storage_context, active_store_context: Some(&store_context), component_ui_registry: &self.component_ui_registry, view_class_registry: &self.view_class_registry, component_fallback_registry: &self.component_fallback_registry, route: &Route::LocalRecording { recording_id: self.recording_store_id.clone(), }, selection_state: &selection_state, focused_item: &focused_item, drag_and_drop_manager: &drag_and_drop_manager, connected_receivers: &Default::default(), auth_context: None, login_enabled: false, login_signed_in_url: None, }, store_context: &store_context, visualizable_entities_per_visualizer: &visualizable_entities_per_visualizer, indicated_entities_per_visualizer: &indicated_entities_per_visualizer, query_results: &self.query_results, time_ctrl: &self.time_ctrl.read(), blueprint_time_ctrl: &Default::default(), blueprint_query: &self.blueprint_query, }; func(&ctx); // If re_renderer was used, `setup_kittest_for_rendering` should have been called. 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(); selection_state.on_frame_start(|item| Some(item.clone()), None); *focused_item = None; } /// Run the given function with a [`ViewerContext`] produced by the [`Self`], in the context of /// an [`egui::CentralPanel`]. /// /// Prefer not using this in conjunction with `egui_kittest`'s harness and use /// `egui_kittest::Harness::build_ui` instead, calling [`Self::run_ui`] inside the closure. /// /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test /// relies on system commands. /// /// Notes: /// - Uses [`egui::__run_test_ctx`]. /// - There is a possibility that the closure will be called more than once, see /// [`egui::Context::run_ui`]. Use [`Self::run_once_in_egui_central_panel`] if you want to ensure /// that the closure is called exactly once. //TODO(ab): should this be removed entirely in favor of `run_once_in_egui_central_panel`? pub fn run_in_egui_central_panel( &self, mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui), ) { egui::__run_test_ui(|ui| { egui::CentralPanel::default().show(ui, |ui| { let egui_ctx = ui.ctx().clone(); self.run(&egui_ctx, |ctx| { func(ctx, ui); }); }); }); } /// Run the provided closure with a [`ViewerContext`] produced by the [`Self`] inside an existing [`egui::Ui`]. /// /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test /// relies on system commands. pub fn run_ui(&self, ui: &mut egui::Ui, func: impl FnOnce(&ViewerContext<'_>, &mut egui::Ui)) { self.run(&ui.ctx().clone(), |ctx| { func(ctx, ui); }); } /// Run the given function once with a [`ViewerContext`] produced by the [`Self`], in the /// context of an [`egui::CentralPanel`]. /// /// IMPORTANT: call [`Self::handle_system_commands`] after calling this function if your test /// relies on system commands. /// /// Notes: /// - Uses [`egui::__run_test_ui`]. pub fn run_once_in_egui_central_panel( &self, func: impl FnOnce(&ViewerContext<'_>, &mut egui::Ui) -> R, ) -> R { let mut func = Some(func); let mut result = None; egui::__run_test_ui(|ui| { egui::CentralPanel::default().show(ui, |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") } /// Applies a fragment. /// /// Does *not* switch the active recording. fn go_to_dataset_data(&self, store_id: StoreId, fragment: re_uri::Fragment) { let time_commands = TimeControlCommand::from_url_fragment(&fragment); if let Some(selection) = fragment.selection { let re_log_types::DataPath { entity_path, instance, component, } = selection; let item = if let Some(component) = component { re_viewer_context::Item::from(re_log_types::ComponentPath::new( entity_path, component, )) } else if let Some(instance) = instance { re_viewer_context::Item::from(InstancePath::instance(entity_path, instance)) } else { re_viewer_context::Item::from(entity_path) }; self.command_sender .send_system(SystemCommand::set_selection(item)); } if !time_commands.is_empty() { self.command_sender .send_system(SystemCommand::TimeControlCommands { store_id, time_commands, }); } } /// Best-effort attempt to meaningfully handle some of the system commands. pub fn handle_system_commands(&self, egui_ctx: &egui::Context) { while let Some((_from_where, command)) = self.command_receiver.recv_system() { let mut handled = true; let command_name = format!("{command:?}"); match command { SystemCommand::SetUrlFragment { store_id, fragment } => { // This adds new system commands, which will be handled later in the loop. self.go_to_dataset_data(store_id, fragment); } SystemCommand::CopyViewerUrl(_) => { // Ignore this trying to copy to the clipboard. } SystemCommand::LoadDataSource(data_source) => { if let Some(re_uri::RedapUri::DatasetData(uri)) = data_source .as_uri() .and_then(|uri| uri.parse::().ok()) { self.go_to_dataset_data(uri.store_id(), uri.fragment); } else { handled = false; } } SystemCommand::AppendToStore(store_id, chunks) => { let mut store_hub = self .store_hub .try_lock() .expect("Failed to lock store hub mutex"); for chunk in chunks { store_hub .add_chunk_for_tests(&store_id, &Arc::new(chunk)) .expect("Updating the chunk store failed"); } } SystemCommand::DropEntity(store_id, entity_path) => { let mut store_hub = self .store_hub .try_lock() .expect("Failed to lock store hub mutex"); assert_eq!( Some(&store_id), store_hub.active_blueprint_id_for_app(&self.application_id) ); store_hub .entity_db_mut(&store_id) .unwrap() .drop_entity_path_recursive(&entity_path); } SystemCommand::SetSelection(set) => { self.selection_state.lock().set_selection(set); } SystemCommand::SetFocus(item) => { *self.focused_item.lock() = Some(item); } SystemCommand::TimeControlCommands { store_id, time_commands, } => { self.with_blueprint_ctx(|blueprint_ctx, hub| { let mut time_ctrl = self.time_ctrl.write(); let entity_db = hub .store_bundle() .get(&store_id) .expect("Invalid store id in `SystemCommand::TimeControlCommands`"); let blueprint_ctx = Some(&blueprint_ctx).filter(|_| store_id.is_recording()); // We can ignore the response in the test context. let res = time_ctrl.handle_time_commands( blueprint_ctx, entity_db, &time_commands, ); if res.needs_repaint == NeedsRepaint::Yes { egui_ctx.request_repaint(); } }); } // not implemented SystemCommand::ActivateApp(_) | SystemCommand::CloseApp(_) | SystemCommand::CloseRecordingOrTable(_) | SystemCommand::AddReceiver { .. } | SystemCommand::ResetViewer | SystemCommand::SetRoute(_) | SystemCommand::OpenSettings | SystemCommand::OpenChunkStoreBrowser { .. } | SystemCommand::ResetRoute | SystemCommand::ClearActiveBlueprint | SystemCommand::ClearActiveBlueprintAndEnableHeuristics | SystemCommand::AddRedapServer { .. } | SystemCommand::RefreshRedapServer(_) | SystemCommand::RefreshRedapEntry { .. } | SystemCommand::RemoveRedapServer(_) | SystemCommand::EditRedapServerModal { .. } | SystemCommand::RedapServer(_) | SystemCommand::Table(_) | SystemCommand::UndoBlueprint { .. } | SystemCommand::RedoBlueprint { .. } | SystemCommand::CloseAllEntries | SystemCommand::SetAuthCredentials { .. } | SystemCommand::OnAuthChanged(_) | SystemCommand::Logout | SystemCommand::SaveScreenshot { .. } | SystemCommand::ShowNotification { .. } | SystemCommand::ReadbackAndSaveTexture { .. } => 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:?}",); } } } pub fn test_help_view(help: impl Fn(OperatingSystem) -> Help) { use egui::os::OperatingSystem; let mut snapshot_results = egui_kittest::SnapshotResults::new(); for os in [OperatingSystem::Mac, OperatingSystem::Windows] { let mut harness = egui_kittest::Harness::builder().build_ui(|ui| { ui.set_os(os); re_ui::apply_style_and_install_loaders(ui.ctx()); help(os).ui(ui); }); let help_view = help(os); let name = format!( "help_view_{}_{os:?}", help_view .title() .expect("View help texts should have titles") ) .replace(' ', "_") .to_lowercase(); harness.fit_contents(); harness.snapshot(&name); snapshot_results.extend_harness(&mut harness); } } /// Helper function to save the active recording to file for troubleshooting. /// /// Note: Right now it _only_ saves the recording and blueprints are ignored. pub fn save_recording_to_file(&self, path: impl AsRef) -> anyhow::Result<()> { let mut file = std::fs::File::create(path)?; let store_hub = self.store_hub.lock(); let Some(recording_entity_db) = store_hub.entity_db(&self.recording_store_id) else { anyhow::bail!("no active recording"); }; let messages = recording_entity_db.to_messages(None); let encoding_options = re_log_encoding::rrd::EncodingOptions::PROTOBUF_COMPRESSED; re_log_encoding::Encoder::encode_into( re_build_info::CrateVersion::LOCAL, encoding_options, messages, &mut file, )?; Ok(()) } /// Helper function to save the active blueprint to file for troubleshooting. pub fn save_blueprint_to_file(&self, path: impl AsRef) -> anyhow::Result<()> { let mut file = std::fs::File::create(path)?; let store_hub = self.store_hub.lock(); let Some(blueprint_entity_db) = store_hub.active_blueprint_for_app(&self.application_id) else { anyhow::bail!("no active blueprint"); }; let messages = blueprint_entity_db.to_messages(None); let encoding_options = re_log_encoding::rrd::EncodingOptions::PROTOBUF_COMPRESSED; re_log_encoding::Encoder::encode_into( re_build_info::CrateVersion::LOCAL, encoding_options, messages, &mut file, )?; Ok(()) } } #[cfg(test)] mod test { use re_entity_db::InstancePath; use re_viewer_context::Item; use super::*; /// Test that `TestContext:edit_selection` works as expected, aka. its side effects are visible /// from `TestContext::run`. #[test] fn test_edit_selection() { let test_context = TestContext::new(); let item = Item::InstancePath(InstancePath::entity_all("/entity/path")); 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) ); }); } }