5e8020aa38
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/ video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits, block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
125 lines
4.8 KiB
Rust
125 lines
4.8 KiB
Rust
//! Public-surface tests of the graph view: headless frames with a warm-up, the
|
|
//! filter counts, the local-mode depth, the hostile-input cap and a frame-time
|
|
//! measurement on a 5 000-node graph (run with `--release --ignored`).
|
|
|
|
use std::time::Instant;
|
|
|
|
use egui::{Rect, Vec2};
|
|
use notescraft_themes::builtin;
|
|
use notescraft_ui_egui::graph_view::{GraphData, GraphView, MAX_VIEW_EDGES, MAX_VIEW_NODES, show_graph};
|
|
use notescraft_ui_egui::theme::Tokens;
|
|
|
|
/// Frames drawn before measuring; the first frame only installs the fonts.
|
|
const WARM_UP: usize = 2;
|
|
|
|
fn tokens() -> Option<Tokens> {
|
|
builtin("catppuccin-mocha").map(Tokens::new)
|
|
}
|
|
|
|
fn data(paths: usize, edges: &[(usize, usize)]) -> GraphData {
|
|
GraphData { paths: (0..paths).map(|node| format!("note-{node}")).collect(), edges: edges.to_vec(), unresolved: Vec::new(), tags: vec![Vec::new(); paths] }
|
|
}
|
|
|
|
fn run(view: &mut GraphView, graph: &GraphData) -> usize {
|
|
let Some(tokens) = tokens() else { return 0 };
|
|
let ctx = egui::Context::default();
|
|
let mut shapes = 0;
|
|
for _ in 0..WARM_UP {
|
|
let mut out = ctx.run_ui(frame_input(Vec::new()), |ui| {
|
|
show_graph(ui, &tokens, view, graph);
|
|
});
|
|
shapes = out.shapes.len();
|
|
out.textures_delta.clear();
|
|
}
|
|
shapes
|
|
}
|
|
|
|
fn frame_input(events: Vec<egui::Event>) -> egui::RawInput {
|
|
egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::pos2(0.0, 0.0), Vec2::new(1024.0, 768.0))), events, ..Default::default() }
|
|
}
|
|
|
|
#[test]
|
|
fn a_graph_draws_after_a_warm_up_frame() {
|
|
let mut view = GraphView::new();
|
|
let graph = data(4, &[(0, 1), (1, 2), (2, 3)]);
|
|
assert!(run(&mut view, &graph) > 0, "the canvas, the nodes and the filter card are painted");
|
|
assert_eq!(view.node_count(), 4);
|
|
assert_eq!(view.visible_count(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn filters_change_the_visible_node_count() {
|
|
let mut view = GraphView::new();
|
|
let graph = data(4, &[(0, 1), (1, 2), (2, 3)]);
|
|
let _ = run(&mut view, &graph);
|
|
assert!(run(&mut view, &graph) > 0, "the canvas and the filter card are painted");
|
|
assert_eq!(view.visible_count(), 4);
|
|
view.set_orphans_only(true);
|
|
assert_eq!(view.visible_count(), 0, "no note is an orphan");
|
|
view.set_show_tags(true);
|
|
assert_eq!(view.visible_count(), 0, "no note carries a tag");
|
|
view.set_orphans_only(false);
|
|
let mut tagged = graph.clone();
|
|
tagged.tags[3] = vec!["rust".to_string()];
|
|
let _ = run(&mut view, &tagged);
|
|
assert_eq!(view.visible_count(), 1, "only the tagged note");
|
|
view.set_show_tags(false);
|
|
assert_eq!(view.visible_count(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn local_mode_narrows_the_graph_to_the_active_note() {
|
|
let mut view = GraphView::new();
|
|
let graph = data(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
|
|
view.set_active(Some("note-0".to_string()));
|
|
view.set_local(true);
|
|
view.set_depth(1);
|
|
let _ = run(&mut view, &graph);
|
|
assert_eq!(view.node_count(), 2, "the note and its neighbour");
|
|
view.set_depth(3);
|
|
let _ = run(&mut view, &graph);
|
|
assert_eq!(view.node_count(), 4, "three hops along the chain");
|
|
view.set_depth(9);
|
|
assert_eq!(view.depth(), 3, "the stepper clamps");
|
|
}
|
|
|
|
#[test]
|
|
fn a_flood_of_edges_is_capped_and_never_drawn() {
|
|
let mut view = GraphView::new();
|
|
let edges: Vec<(usize, usize)> = (0..300_000).map(|edge| (edge % 900, (edge * 7 + 3) % 900)).collect();
|
|
let graph = data(900, &edges);
|
|
assert_eq!(edges.len(), 300_000, "hostile input");
|
|
assert!(edges.len() > MAX_VIEW_EDGES, "over the documented cap");
|
|
assert!(!view.set_data(&graph), "refused, the previous graph is kept");
|
|
assert_eq!(view.node_count(), 0, "nothing is drawn");
|
|
let _ = run(&mut view, &graph);
|
|
assert_eq!(view.node_count(), 0);
|
|
const { assert!(MAX_VIEW_NODES >= 5_000, "the documented cap leaves room for a real vault") };
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "release-only frame-time measurement; run with --release --ignored"]
|
|
fn five_thousand_nodes_and_fifteen_thousand_edges_draw_per_frame() {
|
|
let Some(tokens) = tokens() else { return };
|
|
let nodes = 5_000;
|
|
let edges: Vec<(usize, usize)> = (0..15_000).map(|edge| (edge % nodes, (edge * 7 + 11) % nodes)).collect();
|
|
let graph = data(nodes, &edges);
|
|
let mut view = GraphView::new();
|
|
let ctx = egui::Context::default();
|
|
let mut worst = 0.0_f64;
|
|
for frame in 0..30 {
|
|
let started = Instant::now();
|
|
let mut out = ctx.run_ui(frame_input(Vec::new()), |ui| {
|
|
show_graph(ui, &tokens, &mut view, &graph);
|
|
});
|
|
out.textures_delta.clear();
|
|
let millis = started.elapsed().as_secs_f64() * 1000.0;
|
|
if frame >= WARM_UP {
|
|
worst = worst.max(millis);
|
|
}
|
|
}
|
|
eprintln!("graph view: {nodes} nodes, {} edges, worst frame {worst:.1} ms", graph.edges.len());
|
|
assert_eq!(view.node_count(), nodes);
|
|
assert!(worst < 1_000.0, "a frame must stay under a second even unoptimised: {worst:.1} ms");
|
|
}
|