-
Notifications
You must be signed in to change notification settings - Fork 538
Kittest utility toolbelt #11435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Kittest utility toolbelt #11435
Changes from all commits
6079a95
46ee2a3
999dbf7
3e7ee32
ea06a3b
94914fc
ca71681
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
use re_viewer_context::StoreHub; | ||
|
||
use crate::App; | ||
|
||
#[cfg(feature = "testing")] | ||
pub trait AppTestingExt { | ||
fn testonly_get_store_hub(&mut self) -> &mut StoreHub; | ||
fn testonly_set_test_hook(&mut self, func: crate::app_state::TestHookFn); | ||
} | ||
|
||
#[cfg(feature = "testing")] | ||
impl AppTestingExt for App { | ||
fn testonly_get_store_hub(&mut self) -> &mut StoreHub { | ||
self.store_hub | ||
.as_mut() | ||
.expect("store_hub should be initialized") | ||
} | ||
|
||
fn testonly_set_test_hook(&mut self, func: crate::app_state::TestHookFn) { | ||
self.state.test_hook = Some(func); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
use std::sync::Arc; | ||
|
||
use egui_kittest::{SnapshotOptions, kittest::Queryable as _}; | ||
use re_sdk::{ | ||
Component as _, ComponentDescriptor, EntityPath, EntityPathPart, RecordingInfo, StoreId, | ||
StoreKind, | ||
external::{ | ||
re_log_types::{SetStoreInfo, StoreInfo}, | ||
re_tuid::Tuid, | ||
}, | ||
log::Chunk, | ||
}; | ||
use re_viewer::{ | ||
SystemCommand, SystemCommandSender as _, | ||
external::{ | ||
re_chunk::{ChunkBuilder, LatestAtQuery}, | ||
re_entity_db::EntityDb, | ||
re_types, | ||
re_viewer_context::{self, ViewerContext, blueprint_timeline}, | ||
}, | ||
viewer_test_utils::AppTestingExt as _, | ||
}; | ||
use re_viewport_blueprint::ViewportBlueprint; | ||
|
||
// Kittest harness utilities specific to the Rerun app. | ||
pub trait HarnessExt { | ||
// Initializes the chuck store with a new, empty recording and blueprint. | ||
fn init_recording(&mut self); | ||
|
||
// Runs a function with the `ViewerContext` generated by the actual Rerun application. | ||
fn run_with_viewer_context(&mut self, func: impl FnOnce(&ViewerContext<'_>) + 'static); | ||
|
||
// Removes all views and containers from the current blueprint. | ||
fn clear_current_blueprint(&mut self); | ||
|
||
// Sets up a new viewport blueprint and saves the new one in the chunk store. | ||
fn setup_viewport_blueprint( | ||
&mut self, | ||
setup_blueprint: impl FnOnce(&ViewerContext<'_>, &mut ViewportBlueprint) + 'static, | ||
); | ||
|
||
// Logs an entity to the active recording. | ||
fn log_entity( | ||
&mut self, | ||
entity_path: impl Into<EntityPath>, | ||
build_chunk: impl FnOnce(ChunkBuilder) -> ChunkBuilder, | ||
); | ||
|
||
// Clicks a node in the UI by its label. | ||
fn click_label(&mut self, label: &str); | ||
fn right_click_label(&mut self, label: &str); | ||
|
||
// Takes a snapshot of the current app state with good-enough snapshot options. | ||
fn snapshot_app(&mut self, snapshot_name: &str); | ||
|
||
// Prints the current viewer state. Don't merge code that calls this. | ||
#[allow(unused)] | ||
fn debug_viewer_state(&mut self); | ||
Comment on lines
+56
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not merge code that calls it? Is it very slow? If it's called from a test, stdout/stderr only reaches the terminal output if the tests fails. |
||
|
||
fn toggle_blueprint_panel(&mut self) { | ||
self.click_label("Blueprint panel toggle"); | ||
} | ||
|
||
fn toggle_time_panel(&mut self) { | ||
self.click_label("Time panel toggle"); | ||
} | ||
|
||
fn toggle_selection_panel(&mut self) { | ||
self.click_label("Selection panel toggle"); | ||
} | ||
Comment on lines
+60
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be a lot nicer to have idempotent commands here, i.e. |
||
} | ||
|
||
impl HarnessExt for egui_kittest::Harness<'_, re_viewer::App> { | ||
fn clear_current_blueprint(&mut self) { | ||
self.setup_viewport_blueprint(|_viewer_context, blueprint| { | ||
for item in blueprint.contents_iter() { | ||
blueprint.remove_contents(item); | ||
} | ||
}); | ||
self.run_ok(); | ||
} | ||
|
||
fn setup_viewport_blueprint( | ||
&mut self, | ||
setup_blueprint: impl FnOnce(&ViewerContext<'_>, &mut ViewportBlueprint) + 'static, | ||
) { | ||
self.run_with_viewer_context(|viewer_context| { | ||
let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); | ||
let mut viewport_blueprint = | ||
ViewportBlueprint::from_db(viewer_context.blueprint_db(), &blueprint_query); | ||
setup_blueprint(viewer_context, &mut viewport_blueprint); | ||
viewport_blueprint.save_to_blueprint_store(viewer_context); | ||
}); | ||
self.run_ok(); | ||
} | ||
|
||
fn run_with_viewer_context(&mut self, func: impl FnOnce(&ViewerContext<'_>) + 'static) { | ||
self.state_mut().testonly_set_test_hook(Box::new(func)); | ||
self.run_ok(); | ||
} | ||
|
||
fn log_entity( | ||
&mut self, | ||
entity_path: impl Into<EntityPath>, | ||
build_chunk: impl FnOnce(ChunkBuilder) -> ChunkBuilder, | ||
) { | ||
let app = self.state_mut(); | ||
let builder = build_chunk(Chunk::builder(entity_path)); | ||
let store_hub = app.testonly_get_store_hub(); | ||
let active_recording = store_hub | ||
.active_recording_mut() | ||
.expect("active_recording should be initialized"); | ||
active_recording | ||
.add_chunk(&Arc::new( | ||
builder.build().expect("chunk should be successfully built"), | ||
)) | ||
.expect("chunk should be successfully added"); | ||
self.run_ok(); | ||
} | ||
|
||
fn init_recording(&mut self) { | ||
let app = self.state_mut(); | ||
let store_hub = app.testonly_get_store_hub(); | ||
|
||
let store_info = StoreInfo::testing(); | ||
let application_id = store_info.application_id().clone(); | ||
let recording_store_id = store_info.store_id.clone(); | ||
let mut recording_store = EntityDb::new(recording_store_id.clone()); | ||
|
||
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_types::components::Name::from("Test recording"), | ||
) | ||
.expect("Failed to set recording name"); | ||
recording_store | ||
.set_recording_property( | ||
EntityPath::properties(), | ||
RecordingInfo::descriptor_start_time(), | ||
&re_types::components::Timestamp::now(), | ||
) | ||
.expect("Failed to set recording start time"); | ||
} | ||
{ | ||
// 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_types::components::Text::name()), | ||
}, | ||
&re_types::components::Text::from("Swallow Falls"), | ||
) | ||
.expect("Failed to set recording property"); | ||
recording_store | ||
.set_recording_property( | ||
EntityPath::properties() / EntityPathPart::from("episode"), | ||
ComponentDescriptor { | ||
archetype: None, | ||
component: "weather".into(), | ||
component_type: Some(re_types::components::Text::name()), | ||
}, | ||
&re_types::components::Text::from("Cloudy with meatballs"), | ||
) | ||
.expect("Failed to set recording property"); | ||
} | ||
|
||
let blueprint_id = StoreId::random(StoreKind::Blueprint, application_id); | ||
let blueprint_store = EntityDb::new(blueprint_id.clone()); | ||
|
||
store_hub.insert_entity_db(recording_store); | ||
store_hub.insert_entity_db(blueprint_store); | ||
store_hub.set_active_recording_id(recording_store_id.clone()); | ||
store_hub | ||
.set_cloned_blueprint_active_for_app(&blueprint_id) | ||
.expect("Failed to set blueprint as active"); | ||
|
||
app.command_sender.send_system(SystemCommand::SetSelection( | ||
re_viewer_context::Item::StoreId(recording_store_id.clone()).into(), | ||
)); | ||
self.run_ok(); | ||
} | ||
|
||
fn click_label(&mut self, label: &str) { | ||
self.get_by_label(label).click(); | ||
self.run_ok(); | ||
} | ||
|
||
fn right_click_label(&mut self, label: &str) { | ||
self.get_by_label(label).click_secondary(); | ||
self.run_ok(); | ||
} | ||
Comment on lines
+193
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate having these helpers to not having to call In other words: I believe kittest's API is a bit too low-level for most tests. I don't think there is anything actionable here, except food for thought, and ping @lucasmerlin |
||
|
||
fn debug_viewer_state(&mut self) { | ||
println!( | ||
"Active recording: {:#?}", | ||
self.state_mut().testonly_get_store_hub().active_recording() | ||
); | ||
println!( | ||
"Active blueprint: {:#?}", | ||
self.state_mut().testonly_get_store_hub().active_blueprint() | ||
); | ||
self.setup_viewport_blueprint(|_viewer_context, blueprint| { | ||
println!("Blueprint view count: {}", blueprint.views.len()); | ||
for id in blueprint.view_ids() { | ||
println!("View id: {id}"); | ||
} | ||
println!( | ||
"Display mode: {:?}", | ||
_viewer_context.global_context.display_mode | ||
); | ||
}); | ||
} | ||
|
||
fn snapshot_app(&mut self, snapshot_name: &str) { | ||
self.run_ok(); | ||
// TODO(aedm): there is a nondeterministic font rendering issue. | ||
self.snapshot_options( | ||
snapshot_name, | ||
&SnapshotOptions::new().failed_pixel_count_threshold(0), | ||
); | ||
Comment on lines
+226
to
+230
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an open issue about this we can link to? Did you investigate doing the manual texture filtering in egui-wgpu? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
use re_integration_test::HarnessExt as _; | ||
use re_sdk::TimePoint; | ||
use re_sdk::log::RowId; | ||
use re_view_text_document::TextDocumentView; | ||
use re_viewer::external::re_types; | ||
use re_viewer::external::re_viewer_context::ViewClass as _; | ||
use re_viewer::viewer_test_utils; | ||
use re_viewport_blueprint::ViewBlueprint; | ||
|
||
#[tokio::test(flavor = "multi_thread")] | ||
pub async fn test_single_text_document() { | ||
Comment on lines
+10
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need tokio here at all? |
||
let mut harness = viewer_test_utils::viewer_harness(); | ||
harness.init_recording(); | ||
harness.toggle_selection_panel(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See this is the problem with non-idempotency. Is this opening or closing the selection panel? 🤷 |
||
harness.snapshot("single_text_document_1"); | ||
|
||
// Log some data | ||
harness.log_entity("txt/hello", |builder| { | ||
builder.with_archetype( | ||
RowId::new(), | ||
TimePoint::STATIC, | ||
&re_types::archetypes::TextDocument::new("Hello World!"), | ||
) | ||
}); | ||
|
||
// Set up the viewport blueprint | ||
harness.clear_current_blueprint(); | ||
harness.setup_viewport_blueprint(|_viewer_context, blueprint| { | ||
blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( | ||
TextDocumentView::identifier(), | ||
)); | ||
}); | ||
|
||
harness.snapshot("single_text_document_2"); | ||
Comment on lines
+10
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is so cool |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, put
#![cfg(feature = "testing")]
at the top of the file!