Files
Marcos Paulo 5e8020aa38
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Obsidian basics, editor previews, graph rework, query blocks, stacked tabs, pop-out windows
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>
2026-10-19 11:21:19 -03:00

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");
}