Skip to content

Commit d88658b

Browse files
authored
Blueprint save/load test (#11395)
### Related * Part of #8948 ### What Adds save/load/snapshot tests for blueprints. Note: the `check_rbl_import.py` can't yet be removed as it's testing blueprint imports with a different app id, this test doesn't cover it yet, this is where `App` and `TestContext` differ somewhat.
1 parent 83086c6 commit d88658b

File tree

7 files changed

+192
-28
lines changed

7 files changed

+192
-28
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10095,6 +10095,7 @@ dependencies = [
1009510095
"re_selection_panel",
1009610096
"re_smart_channel",
1009710097
"re_test_context",
10098+
"re_test_viewport",
1009810099
"re_time_panel",
1009910100
"re_tracing",
1010010101
"re_types",
@@ -10122,6 +10123,7 @@ dependencies = [
1012210123
"strum",
1012310124
"strum_macros",
1012410125
"tap",
10126+
"tempfile",
1012510127
"thiserror 1.0.69",
1012610128
"tokio",
1012710129
"url",

crates/viewer/re_viewer/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,9 @@ web-sys = { workspace = true, features = [
176176
[dev-dependencies]
177177
egui_kittest.workspace = true
178178
re_test_context.workspace = true
179+
re_test_viewport.workspace = true
180+
tempfile.workspace = true
179181
tokio.workspace = true
180182

181-
182183
[build-dependencies]
183184
re_build_tools.workspace = true
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Tests for saving/loading blueprints to/from a file.
2+
use std::path::Path;
3+
4+
use re_chunk::{RowId, TimePoint};
5+
use re_test_context::TestContext;
6+
use re_test_viewport::TestContextExt as _;
7+
use re_viewer_context::ViewClass as _;
8+
use re_viewport::ViewportUi;
9+
use re_viewport_blueprint::ViewBlueprint;
10+
11+
fn log_test_data_and_register_views(test_context: &mut TestContext, scalars_count: usize) {
12+
test_context.register_view_class::<re_view_dataframe::DataframeView>();
13+
test_context.register_view_class::<re_view_bar_chart::BarChartView>();
14+
15+
let timeline_a = re_chunk::Timeline::new_sequence("timeline_a");
16+
test_context.log_entity("scalar", |builder| {
17+
builder.with_archetype(
18+
RowId::new(),
19+
[(timeline_a, 0)],
20+
&re_types::archetypes::Scalars::single(scalars_count as f32),
21+
)
22+
});
23+
24+
let vector = (0..scalars_count).map(|i| i as f32).collect::<Vec<_>>();
25+
26+
test_context.log_entity("vector", |builder| {
27+
builder.with_archetype(
28+
RowId::new(),
29+
TimePoint::STATIC,
30+
&re_types::archetypes::BarChart::new(vector),
31+
)
32+
});
33+
}
34+
35+
fn setup_viewport(test_context: &mut TestContext) {
36+
let view_1 =
37+
ViewBlueprint::new_with_root_wildcard(re_view_bar_chart::BarChartView::identifier());
38+
let view_2 =
39+
ViewBlueprint::new_with_root_wildcard(re_view_dataframe::DataframeView::identifier());
40+
41+
test_context.setup_viewport_blueprint(|ctx, blueprint| {
42+
// Set the color override for the bar chart view.
43+
let color_override = re_types::archetypes::BarChart::default().with_color([255, 144, 1]); // #FF9001
44+
let override_path = re_viewport_blueprint::ViewContents::override_path_for_entity(
45+
view_1.id,
46+
&re_chunk::EntityPath::from("vector"),
47+
);
48+
ctx.save_blueprint_archetype(override_path.clone(), &color_override);
49+
50+
// Set the timeline for the dataframe view.
51+
let query = re_view_dataframe::Query::from_blueprint(ctx, view_2.id);
52+
query.save_timeline_name(ctx, &re_chunk::TimelineName::from("timeline_a"));
53+
54+
blueprint.add_views([view_1, view_2].into_iter(), None, None);
55+
});
56+
}
57+
58+
fn save_blueprint_to_file(test_context: &TestContext, path: &Path) {
59+
test_context
60+
.save_blueprint_to_file(path)
61+
.expect("Failed to save blueprint to file.");
62+
}
63+
64+
fn load_blueprint_from_file(test_context: &mut TestContext, path: &Path) {
65+
let file = std::fs::File::open(path).expect("Failed to open blueprint file.");
66+
let rbl_store =
67+
re_entity_db::StoreBundle::from_rrd(file).expect("Failed to load blueprint store");
68+
{
69+
let mut lock = test_context.store_hub.lock();
70+
let app_id = lock.active_app().expect("Missing active app").clone();
71+
lock.load_blueprint_store(rbl_store, &app_id)
72+
.expect("Failed to load blueprint store");
73+
}
74+
75+
// Trigger recalculation of visualizable entities and blueprint overrides.
76+
test_context.setup_viewport_blueprint(|_ctx, _blueprint| {});
77+
}
78+
79+
fn take_snapshot(test_context: &mut TestContext, snapshot_name: &str) {
80+
let mut harness = test_context
81+
.setup_kittest_for_rendering()
82+
.with_size(egui::vec2(600.0, 400.0))
83+
.build_ui(|ui| {
84+
test_context.run_ui(ui, |ctx, ui| {
85+
let viewport_blueprint = re_viewport_blueprint::ViewportBlueprint::from_db(
86+
ctx.blueprint_db(),
87+
&test_context.blueprint_query,
88+
);
89+
let viewport_ui = ViewportUi::new(viewport_blueprint);
90+
viewport_ui.viewport_ui(ui, ctx, &mut test_context.view_states.lock());
91+
});
92+
93+
test_context.handle_system_commands();
94+
});
95+
harness.run();
96+
harness.snapshot(snapshot_name);
97+
}
98+
99+
#[test]
100+
fn test_blueprint_change_and_restore() {
101+
let mut test_context = TestContext::new();
102+
log_test_data_and_register_views(&mut test_context, 16);
103+
let rbl_file = tempfile::NamedTempFile::new().unwrap();
104+
let rbl_path = rbl_file.path();
105+
106+
setup_viewport(&mut test_context);
107+
save_blueprint_to_file(&test_context, rbl_path);
108+
109+
// Remove the first view and add 3 new ones.
110+
test_context.setup_viewport_blueprint(|_ctx, blueprint| {
111+
let first_view_id = *blueprint.view_ids().next().unwrap();
112+
blueprint.remove_contents(re_viewer_context::Contents::View(first_view_id));
113+
blueprint.add_views([
114+
ViewBlueprint::new_with_root_wildcard(re_view_bar_chart::BarChartView::identifier()),
115+
ViewBlueprint::new_with_root_wildcard(re_view_bar_chart::BarChartView::identifier()),
116+
ViewBlueprint::new_with_root_wildcard(re_view_bar_chart::BarChartView::identifier()),
117+
].into_iter(), None, None);
118+
});
119+
120+
load_blueprint_from_file(&mut test_context, rbl_path);
121+
take_snapshot(&mut test_context, "blueprint_change_and_restore");
122+
}
123+
124+
#[test]
125+
fn test_blueprint_load_into_new_context() {
126+
let mut test_context = TestContext::new();
127+
log_test_data_and_register_views(&mut test_context, 10);
128+
129+
let rbl_file = tempfile::NamedTempFile::new().unwrap();
130+
let rbl_path = rbl_file.path();
131+
132+
setup_viewport(&mut test_context);
133+
save_blueprint_to_file(&test_context, rbl_path);
134+
take_snapshot(&mut test_context, "blueprint_load_into_new_context_1");
135+
136+
let mut test_context_2 = TestContext::new();
137+
log_test_data_and_register_views(&mut test_context_2, 20);
138+
139+
load_blueprint_from_file(&mut test_context_2, rbl_path);
140+
take_snapshot(&mut test_context_2, "blueprint_load_into_new_context_2");
141+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

crates/viewer/re_viewer_context/src/store_hub.rs

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -909,38 +909,49 @@ impl StoreHub {
909909
fn try_to_load_persisted_blueprint(&mut self, app_id: &ApplicationId) -> anyhow::Result<()> {
910910
re_tracing::profile_function!();
911911

912-
let Some(loader) = &self.persistence.loader else {
913-
return Ok(());
914-
};
912+
if let Some(loader) = &self.persistence.loader
913+
&& let Some(bundle) = (loader)(app_id)?
914+
{
915+
self.load_blueprint_store(bundle, app_id)?;
916+
}
915917

916-
if let Some(mut bundle) = (loader)(app_id)? {
917-
for store in bundle.drain_entity_dbs() {
918-
match store.store_kind() {
919-
StoreKind::Recording => {
920-
anyhow::bail!(
921-
"Found a recording in a blueprint file: {:?}",
922-
store.store_id()
923-
);
924-
}
925-
StoreKind::Blueprint => {}
926-
}
918+
Ok(())
919+
}
920+
921+
/// Load a blueprint and make it active for the given `ApplicationId`.
922+
pub fn load_blueprint_store(
923+
&mut self,
924+
mut blueprint_bundle: StoreBundle,
925+
app_id: &ApplicationId,
926+
) -> anyhow::Result<()> {
927+
re_tracing::profile_function!();
927928

928-
if store.application_id() != app_id {
929-
anyhow::bail!("Found app_id {}; expected {app_id}", store.application_id());
929+
for store in blueprint_bundle.drain_entity_dbs() {
930+
match store.store_kind() {
931+
StoreKind::Recording => {
932+
anyhow::bail!(
933+
"Found a recording in a blueprint file: {:?}",
934+
store.store_id()
935+
);
930936
}
937+
StoreKind::Blueprint => {}
938+
}
931939

932-
// We found the blueprint we were looking for; make it active.
933-
// borrow-checker won't let us just call `self.set_blueprint_for_app_id`
934-
re_log::debug!(
935-
"Activating new blueprint {:?} for {app_id}; loaded from disk",
936-
store.store_id(),
937-
);
938-
self.active_blueprint_by_app_id
939-
.insert(app_id.clone(), store.store_id().clone());
940-
self.blueprint_last_save
941-
.insert(store.store_id().clone(), store.generation());
942-
self.store_bundle.insert(store);
940+
if store.application_id() != app_id {
941+
anyhow::bail!("Found app_id {}; expected {app_id}", store.application_id());
943942
}
943+
944+
// We found the blueprint we were looking for; make it active.
945+
// borrow-checker won't let us just call `self.set_blueprint_for_app_id`
946+
re_log::debug!(
947+
"Activating new blueprint {:?} for {app_id}.",
948+
store.store_id(),
949+
);
950+
self.active_blueprint_by_app_id
951+
.insert(app_id.clone(), store.store_id().clone());
952+
self.blueprint_last_save
953+
.insert(store.store_id().clone(), store.generation());
954+
self.store_bundle.insert(store);
944955
}
945956

946957
Ok(())

0 commit comments

Comments
 (0)