Simplify autocompletion tests

A

A
This commit is contained in:
Laurenz 2024-11-09 11:37:01 +01:00
parent 8d4f01d284
commit 6f294eac56
2 changed files with 88 additions and 35 deletions

View File

@ -1353,53 +1353,106 @@ impl<'a> CompletionContext<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::autocomplete; use std::collections::BTreeSet;
use typst::model::Document;
use super::{autocomplete, Completion};
use crate::tests::TestWorld; use crate::tests::TestWorld;
#[track_caller] type Response = Option<(usize, Vec<Completion>)>;
fn test(text: &str, cursor: usize, contains: &[&str], excludes: &[&str]) {
let world = TestWorld::new(text);
let doc = typst::compile(&world).output.ok();
let (_, completions) =
autocomplete(&world, doc.as_ref(), &world.main, cursor, true)
.unwrap_or_default();
let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect(); trait ResponseExt {
for item in contains { fn labels(&self) -> BTreeSet<&str>;
assert!(labels.contains(item), "{item:?} was not contained in {labels:?}"); fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
} }
impl ResponseExt for Response {
fn labels(&self) -> BTreeSet<&str> {
match self {
None => BTreeSet::new(),
Some((_, completions)) => {
completions.iter().map(|c| c.label.as_str()).collect()
}
}
}
#[track_caller]
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
let labels = self.labels();
for item in includes {
assert!(
labels.contains(item),
"{item:?} was not contained in {labels:?}",
);
}
self
}
#[track_caller]
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
let labels = self.labels();
for item in excludes { for item in excludes {
assert!(!labels.contains(item), "{item:?} was not excluded in {labels:?}"); assert!(
!labels.contains(item),
"{item:?} was wrongly contained in {labels:?}",
);
} }
self
}
}
#[track_caller]
fn test(text: &str, cursor: isize) -> Response {
let world = TestWorld::new(text);
test_with_world(&world, cursor)
}
#[track_caller]
fn test_with_world(world: &TestWorld, cursor: isize) -> Response {
let doc = typst::compile(&world).output.ok();
test_with_world_and_doc(world, doc.as_ref(), cursor)
}
#[track_caller]
fn test_with_world_and_doc(
world: &TestWorld,
doc: Option<&Document>,
cursor: isize,
) -> Response {
let cursor = if cursor < 0 {
world.main.len_bytes().checked_add_signed(cursor).unwrap()
} else {
cursor as usize
};
autocomplete(&world, doc, &world.main, cursor, true)
} }
#[test] #[test]
fn test_autocomplete() { fn test_autocomplete_hash_expr() {
test("#i", 2, &["int", "if conditional"], &["foo"]); test("#i", 2).must_include(["int", "if conditional"]);
test("#().", 4, &["insert", "remove", "len", "all"], &["foo"]);
} }
#[test]
fn test_autocomplete_array_method() {
test("#().", 4).must_include(["insert", "remove", "len", "all"]);
test("#{ let x = (1, 2, 3); x. }", -2).must_include(["at", "push", "pop"]);
}
/// Test that extra space before '.' is handled correctly.
#[test] #[test]
fn test_autocomplete_whitespace() { fn test_autocomplete_whitespace() {
//Check that extra space before '.' is handled correctly. test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]);
test("#() .", 5, &[], &["insert", "remove", "len", "all"]); test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]);
test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]); test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .a}", 7).must_include(["at", "any", "all"]);
test("#() .a", 6, &[], &["insert", "remove", "len", "all"]);
test("#{() .a}", 7, &["at", "any", "all"], &["foo"]);
} }
/// Test that the `before_window` doesn't slice into invalid byte
/// boundaries.
#[test] #[test]
fn test_autocomplete_before_window_char_boundary() { fn test_autocomplete_before_window_char_boundary() {
// Check that the `before_window` doesn't slice into invalid byte test("😀😀 #text(font: \"\")", -2);
// boundaries.
let s = "😀😀 #text(font: \"\")";
test(s, s.len() - 2, &[], &[]);
}
#[test]
fn test_autocomplete_mutable_method() {
let s = "#{ let x = (1, 2, 3); x. }";
test(s, s.len() - 2, &["at", "push", "pop"], &[]);
} }
} }

View File

@ -100,7 +100,7 @@ mod tests {
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Smart}; use typst::foundations::{Bytes, Datetime, Smart};
use typst::layout::{Abs, Margin, PageElem}; use typst::layout::{Abs, Margin, PageElem};
use typst::syntax::{FileId, Source}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{singleton, LazyHash};
use typst::{Library, World}; use typst::{Library, World};
@ -117,7 +117,7 @@ mod tests {
/// This is cheap because the shared base for all test runs is lazily /// This is cheap because the shared base for all test runs is lazily
/// initialized just once. /// initialized just once.
pub fn new(text: &str) -> Self { pub fn new(text: &str) -> Self {
let main = Source::detached(text); let main = Source::new(Self::main_id(), text.into());
Self { Self {
main, main,
base: singleton!(TestBase, TestBase::default()), base: singleton!(TestBase, TestBase::default()),
@ -126,7 +126,7 @@ mod tests {
/// The ID of the main file in a `TestWorld`. /// The ID of the main file in a `TestWorld`.
pub fn main_id() -> FileId { pub fn main_id() -> FileId {
*singleton!(FileId, Source::detached("").id()) *singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
} }
} }