59d5182799
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
178 lines
7.5 KiB
Rust
178 lines
7.5 KiB
Rust
//! Integration tests over a hand-built fixture vault (SPEC 6 item 4, 12).
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use notescraft_backlinks::{ContentSource, MAX_MENTIONS, MAX_NOTE_BYTES, UnlinkedMention, group_by_note, link_mention, linked_mentions, unlinked_mentions};
|
|
use notescraft_index::Index;
|
|
|
|
/// Paths and bodies of the fixture vault, target note first.
|
|
fn fixture() -> Vec<(&'static str, &'static str)> {
|
|
vec![
|
|
("Plan.md", "---\naliases: [Roadmap]\n---\n# Q1\nbody of the plan\n"),
|
|
("todo.md", "todo [[Plan]] now\n"),
|
|
("alias.md", "see [[Roadmap]] for it\n"),
|
|
("heading.md", "[[Plan#Q1]] check\n"),
|
|
("embed.md", "![[Plan]]!\n"),
|
|
("marklink.md", "[good Plan label](plan.md)\n"),
|
|
("plain.md", "read the Plan today\n"),
|
|
("case.md", "the PLAN is here\n"),
|
|
("plural.md", "many Plans today\n"),
|
|
("code.md", "```\nPlan inside\n```\nspan `Plan` end\n"),
|
|
("fm.md", "---\nrelated: Plan\n---\nnothing here\n"),
|
|
("Café.md", "unicode title note\n"),
|
|
("uni.md", "café and cafés\n"),
|
|
("empty.md", "no mentions at all\n"),
|
|
]
|
|
}
|
|
|
|
/// The fixture as an index and a `BTreeMap` content source.
|
|
fn vault() -> (Index, BTreeMap<String, String>) {
|
|
let mut index = Index::new();
|
|
let mut map = BTreeMap::new();
|
|
for (path, body) in fixture() {
|
|
index.update_note(path, body).unwrap();
|
|
map.insert(String::from(path), String::from(body));
|
|
}
|
|
(index, map)
|
|
}
|
|
|
|
#[test]
|
|
fn linked_mentions_cover_wikilink_alias_heading_embed_and_markdown_link() {
|
|
let (index, map) = vault();
|
|
let mentions = linked_mentions(&index, &map, "Plan.md");
|
|
assert_eq!(mentions.iter().map(|m| m.from.as_str()).collect::<Vec<_>>(), vec!["alias.md", "embed.md", "heading.md", "marklink.md", "todo.md"]);
|
|
assert_eq!(
|
|
mentions.iter().map(|m| m.matched.as_str()).collect::<Vec<_>>(),
|
|
vec!["[[Roadmap]]", "![[Plan]]", "[[Plan#Q1]]", "[good Plan label](plan.md)", "[[Plan]]"]
|
|
);
|
|
for mention in &mentions {
|
|
assert_eq!(mention.line, 1, "every fixture link sits on line 1");
|
|
assert!(mention.snippet.chars().count() <= 200);
|
|
let body = map.content(&mention.from).unwrap_or_default();
|
|
assert_eq!(body.get(mention.range.clone()), Some(mention.matched.as_str()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn linked_mentions_are_grouped_by_note() {
|
|
let (index, map) = vault();
|
|
let grouped = group_by_note(linked_mentions(&index, &map, "Plan.md"));
|
|
assert_eq!(grouped.len(), 5);
|
|
assert_eq!(grouped["todo.md"][0].matched, "[[Plan]]");
|
|
assert_eq!(grouped.keys().next(), Some(&String::from("alias.md")));
|
|
}
|
|
|
|
#[test]
|
|
fn unlinked_mentions_cover_plain_case_and_skip_plural_code_and_frontmatter() {
|
|
let (index, map) = vault();
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
assert_eq!(report.skipped_large, 0);
|
|
assert!(!report.truncated);
|
|
assert_eq!(report.mentions.iter().map(|m| m.from.as_str()).collect::<Vec<_>>(), vec!["case.md", "plain.md"]);
|
|
assert_eq!(report.mentions[0].matched, "PLAN");
|
|
assert_eq!(report.mentions[0].name, "Plan");
|
|
assert_eq!(report.mentions[1].matched, "Plan");
|
|
for mention in &report.mentions {
|
|
let body = map.content(&mention.from).unwrap_or_default();
|
|
assert_eq!(body.get(mention.range.clone()), Some(mention.matched.as_str()));
|
|
let at = body.find(&mention.matched).unwrap_or(0);
|
|
assert_eq!(mention.line, 1);
|
|
assert_eq!(mention.range.start, at);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_words_match_case_insensitively_and_reject_grammar() {
|
|
let (index, map) = vault();
|
|
let report = unlinked_mentions(&index, &map, "Café.md");
|
|
assert_eq!(report.mentions.len(), 1, "`cafés` is a different word");
|
|
assert_eq!(report.mentions[0].from, "uni.md");
|
|
assert_eq!(report.mentions[0].matched, "café");
|
|
assert_eq!(report.mentions[0].name, "Café");
|
|
}
|
|
|
|
#[test]
|
|
fn link_mention_plain_form_bare_wikilink() {
|
|
let (index, map) = vault_with(&[("plain2.md", "read the Plan today\n")]);
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
let Some(mention) = report.mentions.iter().find(|m| m.from == "plain2.md") else { panic!("no mention in plain2.md") };
|
|
let updated = link_mention(&map.content("plain2.md").unwrap_or_default(), mention, "Plan").unwrap_or_default();
|
|
assert_eq!(updated, "read the [[Plan]] today\n");
|
|
}
|
|
|
|
#[test]
|
|
fn link_mention_keepcase_form_is_aliased_wikilink() {
|
|
let (index, map) = vault_with(&[("case2.md", "the PLAN is here\n")]);
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
let Some(mention) = report.mentions.iter().find(|m| m.from == "case2.md") else { panic!("no mention in case2.md") };
|
|
let updated = link_mention(&map.content("case2.md").unwrap_or_default(), mention, "Plan").unwrap_or_default();
|
|
assert_eq!(updated, "the [[Plan|PLAN]] is here\n");
|
|
}
|
|
|
|
#[test]
|
|
fn link_mention_rejects_stale_out_of_bounds_and_broken_chars() {
|
|
let mention =
|
|
UnlinkedMention { from: String::from("a.md"), range: 4..8, line: 1, snippet: String::new(), matched: String::from("Plan"), name: String::from("Plan") };
|
|
// the text at the range changed
|
|
assert_eq!(
|
|
link_mention("read the Roadmap now", &mention, "Plan").err().map(|e| e.to_string()),
|
|
Some(String::from("the note changed since the mention was found"))
|
|
);
|
|
// the range is outside the content
|
|
assert!(link_mention("short", &mention, "Plan").is_err(), "range 4..8 does not fit `short`");
|
|
// the range splits a char: `é` is two bytes
|
|
let unicode = UnlinkedMention { range: 3..4, matched: String::from("é"), ..mention.clone() };
|
|
assert!(link_mention("café", &unicode, "Plan").is_err(), "3..4 splits `é`");
|
|
// empty title
|
|
assert!(link_mention("read the Plan", &mention, "").is_err(), "no title, nothing to link");
|
|
}
|
|
|
|
#[test]
|
|
fn large_notes_are_skipped_and_counted() {
|
|
let (mut index, mut map) = vault();
|
|
let mut body = String::from("Plan mention\n");
|
|
while body.len() <= MAX_NOTE_BYTES {
|
|
body.push_str("filler filler filler filler\n");
|
|
}
|
|
body.push_str("Plan again\n");
|
|
index.update_note("big.md", &body).unwrap();
|
|
map.insert(String::from("big.md"), body);
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
assert_eq!(report.skipped_large, 1);
|
|
assert!(!report.mentions.iter().any(|m| m.from == "big.md"), "the large note is not scanned");
|
|
}
|
|
|
|
#[test]
|
|
fn the_scan_stops_at_ten_thousand_mentions() {
|
|
let mut index = Index::new();
|
|
let mut body = String::new();
|
|
for _ in 0..12_000 {
|
|
body.push_str("Plan ");
|
|
}
|
|
index.update_note("many.md", &body).unwrap();
|
|
index.update_note("Plan.md", "target\n").unwrap();
|
|
let mut map = BTreeMap::new();
|
|
map.insert(String::from("many.md"), body);
|
|
map.insert(String::from("Plan.md"), String::from("target\n"));
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
assert!(report.truncated);
|
|
assert_eq!(report.mentions.len(), MAX_MENTIONS);
|
|
}
|
|
|
|
/// Build the fixture with extra notes (indexed and stored) on top.
|
|
fn vault_with(extra: &[(&str, &str)]) -> (Index, BTreeMap<String, String>) {
|
|
let (mut index, mut map) = vault();
|
|
for (path, body) in extra {
|
|
index.update_note(path, body).unwrap();
|
|
map.insert(String::from(*path), String::from(*body));
|
|
}
|
|
(index, map)
|
|
}
|
|
|
|
#[test]
|
|
fn alias_occurrences_are_found_too() {
|
|
let (index, map) = vault_with(&[("roadmap-note.md", "the Roadmap says so\n")]);
|
|
let report = unlinked_mentions(&index, &map, "Plan.md");
|
|
assert!(report.mentions.iter().any(|m| m.from == "roadmap-note.md" && m.name == "Roadmap"));
|
|
}
|