From 63d213b5d923c568e6eda5627d81f3903e899a10 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 25 May 2024 17:19:25 +0200 Subject: [PATCH] Test harness and "demo test" for the selection panel --- crates/re_selection_panel/src/lib.rs | 30 +++++ crates/re_viewer/src/app_state.rs | 4 +- crates/re_viewer_context/src/lib.rs | 1 + .../re_viewer_context/src/selection_state.rs | 6 +- crates/re_viewer_context/src/test_context.rs | 115 ++++++++++++++++++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 crates/re_viewer_context/src/test_context.rs diff --git a/crates/re_selection_panel/src/lib.rs b/crates/re_selection_panel/src/lib.rs index 9b5ff318510ae..ac1f307b4a429 100644 --- a/crates/re_selection_panel/src/lib.rs +++ b/crates/re_selection_panel/src/lib.rs @@ -8,3 +8,33 @@ mod space_view_entity_picker; mod space_view_space_origin_ui; pub use selection_panel::SelectionPanel; + +#[cfg(test)] +mod test { + use super::*; + use re_data_store::LatestAtQuery; + use re_viewer_context::{blueprint_timeline, Item, SpaceViewId}; + use re_viewport_blueprint::ViewportBlueprint; + + #[test] + fn test_selection_panel() { + re_log::setup_logging(); + + let mut test_ctx = re_viewer_context::test_context::TestContext::default(); + test_ctx.edit_selection(|selection_state| { + selection_state.set_selection(Item::SpaceView(SpaceViewId::random())); + }); + + test_ctx.run(|ctx, ui| { + let (sender, _) = std::sync::mpsc::channel(); + let blueprint = ViewportBlueprint::try_from_db( + ctx.store_context.blueprint, + &LatestAtQuery::latest(blueprint_timeline()), + sender, + ); + + let mut selection_panel = SelectionPanel::default(); + selection_panel.show_panel(ctx, &blueprint, &mut Default::default(), ui, true); + }); + } +} diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 765e1d0423420..efcb8f7bb0104 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -191,7 +191,9 @@ impl AppState { viewport.is_item_valid(store_context, item) }, - re_viewer_context::Item::StoreId(store_context.recording.store_id().clone()), + Some(re_viewer_context::Item::StoreId( + store_context.recording.store_id().clone(), + )), ); if ui.input(|i| i.key_pressed(egui::Key::Escape)) { diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 4dad50df7018c..353201524b87a 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -20,6 +20,7 @@ mod space_view; mod store_context; pub mod store_hub; mod tensor; +pub mod test_context; mod time_control; mod typed_entity_collections; mod utils; diff --git a/crates/re_viewer_context/src/selection_state.rs b/crates/re_viewer_context/src/selection_state.rs index 9c289345fabd5..902d8a189d2c7 100644 --- a/crates/re_viewer_context/src/selection_state.rs +++ b/crates/re_viewer_context/src/selection_state.rs @@ -230,7 +230,7 @@ impl ApplicationSelectionState { pub fn on_frame_start( &mut self, item_retain_condition: impl Fn(&Item) -> bool, - fallback_selection: Item, + fallback_selection: Option, ) { // Use a different name so we don't get a collision in puffin. re_tracing::profile_scope!("SelectionState::on_frame_start"); @@ -243,7 +243,9 @@ impl ApplicationSelectionState { let selection_this_frame = self.selection_this_frame.get_mut(); selection_this_frame.retain(|item, _| item_retain_condition(item)); if selection_this_frame.is_empty() { - *selection_this_frame = ItemCollection::from(fallback_selection); + if let Some(fallback_selection) = fallback_selection { + *selection_this_frame = ItemCollection::from(fallback_selection); + } } // Hovering needs to be refreshed every frame: If it wasn't hovered last frame, it's no longer hovered! diff --git a/crates/re_viewer_context/src/test_context.rs b/crates/re_viewer_context/src/test_context.rs new file mode 100644 index 0000000000000..0b99c436d6ac7 --- /dev/null +++ b/crates/re_viewer_context/src/test_context.rs @@ -0,0 +1,115 @@ +use crate::{ + command_channel, ApplicationSelectionState, ComponentUiRegistry, StoreContext, ViewerContext, +}; + +use re_data_store::LatestAtQuery; +use re_entity_db::EntityDb; +use re_log_types::{StoreId, StoreKind}; + +/// Harness to execute code that rely on [`crate::ViewerContext`]. +/// +/// Example: +/// ```rust +/// use re_viewer_context::test_context::TestContext; +/// use re_viewer_context::ViewerContext; +/// +/// let mut test_context = TestContext::default(); +/// test_context.run(|ctx: &ViewerContext, _| { +/// /* do something with ctx */ +/// }); +/// ``` +pub struct TestContext { + recording_store: EntityDb, + blueprint_store: EntityDb, + selection_state: ApplicationSelectionState, +} + +impl Default for TestContext { + fn default() -> Self { + let recording_store = EntityDb::new(StoreId::random(StoreKind::Recording)); + let blueprint_store = EntityDb::new(StoreId::random(StoreKind::Blueprint)); + Self { + recording_store, + blueprint_store, + selection_state: Default::default(), + } + } +} + +impl TestContext { + pub fn edit_selection(&mut self, edit_fn: impl FnOnce(&mut ApplicationSelectionState)) { + edit_fn(&mut self.selection_state); + + // the selection state is double-buffered, so let's ensure it's updated + self.selection_state.on_frame_start(|_| true, None); + } + + pub fn run(&self, mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui)) { + egui::__run_test_ui(|ui| { + let re_ui = re_ui::ReUi::load_and_apply(ui.ctx()); + let blueprint_query = LatestAtQuery::latest(re_log_types::Timeline::new( + "timeline", + re_log_types::TimeType::Time, + )); + let (command_sender, _) = command_channel(); + let component_ui_registry = ComponentUiRegistry::new(Box::new( + |_ctx, _ui, _ui_layout, _query, _db, _entity_path, _component, _instance| {}, + )); + + let store_context = StoreContext { + app_id: "rerun_test".into(), + blueprint: &self.blueprint_store, + default_blueprint: None, + recording: &self.recording_store, + bundle: &Default::default(), + hub: &Default::default(), + }; + + let ctx = ViewerContext { + app_options: &Default::default(), + cache: &Default::default(), + component_ui_registry: &component_ui_registry, + space_view_class_registry: &Default::default(), + store_context: &store_context, + applicable_entities_per_visualizer: &Default::default(), + indicated_entities_per_visualizer: &Default::default(), + query_results: &Default::default(), + rec_cfg: &Default::default(), + blueprint_cfg: &Default::default(), + selection_state: &self.selection_state, + blueprint_query: &blueprint_query, + re_ui: &re_ui, + render_ctx: None, + command_sender: &command_sender, + focused_item: &None, + }; + + func(&ctx, ui); + }); + } +} + +#[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(|ctx, _| { + assert_eq!( + ctx.selection_state.selected_items().single_item(), + Some(&item) + ); + }); + } +}