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

153 lines
5.0 KiB
Rust

//! Hostile fence inputs for `layout_job`: random fence soup, thousands of fences, huge bodies and
//! labels, unclosed fences and mismatched fence lengths never panic, never lose a byte and stay fast.
use std::time::{Duration, Instant};
use notescraft_editor::colors::EditorColors;
use notescraft_editor::highlight::layout_job;
/// Pieces a fence-heavy note is built from; CRLF, wide chars and bare ticks included.
const PIECES: [&str; 13] = ["```", "~~~", "rust", "json", "\n", "`", "é", "{", "}", "\"", " ", "\r\n", " "];
/// How many random documents the sweep builds.
const CASES: u32 = 3000;
/// No call may take longer than this.
const BUDGET: Duration = Duration::from_secs(3);
fn resolver(_target: &str) -> bool {
true
}
/// Tiny deterministic xorshift generator (no rand dependency).
struct Rng(u32);
impl Rng {
fn next(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
}
/// Layout one document and assert the invariants: same text, contiguous sections, full coverage,
/// no section past the end and no empty-range section between two others.
fn check(name: &str, text: &str, started: Instant) {
let colors = EditorColors::fallback_dark();
let job = layout_job(text, &colors, 14.0, 400.0, &resolver);
assert_eq!(job.text, text, "{name}: the job must carry the source");
let mut next = 0usize;
for section in &job.sections {
let (start, end) = (section.byte_range.start.0, section.byte_range.end.0);
assert!(start <= end, "{name}: section range {start}..{end} is reversed");
assert!(end <= text.len(), "{name}: section ends at {end}, past the text");
assert_eq!(start, next, "{name}: gap or overlap at {next}");
assert!(text.is_char_boundary(start) && text.is_char_boundary(end), "{name}: range {start}..{end} splits a char");
next = end;
}
assert_eq!(next, text.len(), "{name}: coverage incomplete");
let elapsed = started.elapsed();
assert!(elapsed < BUDGET, "{name}: layout took {elapsed:?}, over the {BUDGET:?} budget");
}
#[test]
fn random_fence_soup_keeps_the_text_and_the_time() {
let started = Instant::now();
let mut rng = Rng(0xF3C7_5EED);
for case in 0..CASES {
let pieces = rng.next() as usize % 40 + 1;
let mut text = String::new();
for _ in 0..pieces {
let pick = (rng.next() % u32::try_from(PIECES.len()).unwrap_or(1)) as usize;
if let Some(piece) = PIECES.get(pick) {
text.push_str(piece);
}
}
check(&format!("case {case}"), &text, started);
}
}
#[test]
fn five_thousand_fences_in_a_row() {
let started = Instant::now();
let mut text = String::new();
for n in 0..5000 {
text.push_str("```rust\nfn f");
text.push_str(&n.to_string());
text.push_str("() {}\n```\n");
}
check("5000 fences", &text, started);
}
#[test]
fn a_three_hundred_kilobyte_fence_body() {
let started = Instant::now();
let mut text = String::from("```json\n{\"a\":[");
text.push_str(&"1,".repeat(160_000));
text.push_str("1]}\n```\n");
assert!(text.len() > 300 * 1024, "the fixture must be over 300 KB");
check("300 KB body", &text, started);
}
#[test]
fn a_ten_kilobyte_fence_label() {
let started = Instant::now();
let text = format!("```{}\nbody\n```\n", "r".repeat(10 * 1024));
check("10 KB label", &text, started);
}
#[test]
fn an_unclosed_fence_at_eof() {
let started = Instant::now();
for text in ["```rust\nfn f() {}", "```", "````rust\n```\n````", "~~~json\n{", "\n```\n", "```\r\n"] {
check("unclosed", text, started);
}
}
#[test]
fn mismatched_and_nested_fence_lengths() {
let started = Instant::now();
for text in [
"````rust\n```\ninner\n```\n````\n",
"```rust\n````\n```\n",
"`````\n`````\n",
"``````json\n```\n``````\n",
"~~~rust\n```json\n{x}\n```\n~~~\n",
"``` \n```\n",
"```json\r\n{\"a\":1}\r\n```\r\n",
] {
check("mismatched", text, started);
}
}
#[test]
fn crlf_documents_with_mixed_fences() {
let started = Instant::now();
let mut rng = Rng(0x0BAD_C0DE);
for case in 0..200 {
let pieces = rng.next() as usize % 30 + 1;
let mut text = String::new();
for _ in 0..pieces {
let pick = (rng.next() % u32::try_from(PIECES.len()).unwrap_or(1)) as usize;
if let Some(piece) = PIECES.get(pick) {
text.push_str(piece);
}
}
check(&format!("crlf case {case}"), &text, started);
}
check("lone cr", "# T\r```rust\rfn f() {}\r```\r", started);
}
#[test]
fn documents_of_every_size_around_the_fence_cap() {
let started = Instant::now();
for len in [0_usize, 1, 2, 3, 7, 15, 16, 17, 63, 64, 65, 1023, 1024, 4095] {
let mut text = "```rust\n".repeat(len);
text.push('x');
check(&format!("{len} fences"), &text, started);
}
}