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>
153 lines
5.0 KiB
Rust
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);
|
|
}
|
|
}
|