Further improve IDE tests (#5602)

This commit is contained in:
Laurenz 2024-12-18 20:26:39 +01:00 committed by GitHub
parent 45c866fbb9
commit db06dbf976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 207 additions and 148 deletions

View File

@ -1506,14 +1506,13 @@ impl BracketMode {
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use std::collections::BTreeSet;
use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source, VirtualPath};
use typst::World;
use super::{autocomplete, Completion};
use crate::tests::{SourceExt, TestWorld};
use crate::tests::{FilePos, TestWorld, WorldLike};
/// Quote a string.
macro_rules! q {
@ -1585,60 +1584,50 @@ mod tests {
}
#[track_caller]
fn test(text: &str, cursor: isize) -> Response {
let world = TestWorld::new(text);
test_with_world(&world, cursor)
fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
let world = world.acquire();
let world = world.borrow();
let doc = typst::compile(world).output.ok();
test_with_doc(world, pos, doc.as_ref())
}
#[track_caller]
fn test_with_world(world: &TestWorld, cursor: isize) -> Response {
let doc = typst::compile(&world).output.ok();
test_full(world, &world.main, doc.as_ref(), cursor)
}
#[track_caller]
fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response {
let doc = typst::compile(&world).output.ok();
let id = FileId::new(None, VirtualPath::new(path));
let source = world.source(id).unwrap();
test_full(world, &source, doc.as_ref(), cursor)
}
#[track_caller]
fn test_full(
world: &TestWorld,
source: &Source,
fn test_with_doc(
world: impl WorldLike,
pos: impl FilePos,
doc: Option<&PagedDocument>,
cursor: isize,
) -> Response {
autocomplete(world, doc, source, source.cursor(cursor), true)
let world = world.acquire();
let world = world.borrow();
let (source, cursor) = pos.resolve(world);
autocomplete(world, doc, &source, cursor, true)
}
#[test]
fn test_autocomplete_hash_expr() {
test("#i", 2).must_include(["int", "if conditional"]);
test("#i", -1).must_include(["int", "if conditional"]);
}
#[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("#().", -1).must_include(["insert", "remove", "len", "all"]);
test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]);
}
/// Test that extra space before '.' is handled correctly.
#[test]
fn test_autocomplete_whitespace() {
test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]);
test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .a}", 7).must_include(["at", "any", "all"]);
test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]);
test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .a}", -2).must_include(["at", "any", "all"]);
}
/// Test that the `before_window` doesn't slice into invalid byte
/// boundaries.
#[test]
fn test_autocomplete_before_window_char_boundary() {
test("😀😀 #text(font: \"\")", -2);
test("😀😀 #text(font: \"\")", -3);
}
/// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels,
@ -1655,7 +1644,7 @@ mod tests {
let end = world.main.len_bytes();
world.main.edit(end..end, " #cite()");
test_full(&world, &world.main, doc.as_ref(), -1)
test_with_doc(&world, -2, doc.as_ref())
.must_include(["netwok", "glacier-melt", "supplement"])
.must_exclude(["bib"]);
}
@ -1679,13 +1668,13 @@ mod tests {
#[test]
fn test_autocomplete_positional_param() {
// No string given yet.
test("#numbering()", -1).must_include(["string", "integer"]);
test("#numbering()", -2).must_include(["string", "integer"]);
// String is already given.
test("#numbering(\"foo\", )", -1)
test("#numbering(\"foo\", )", -2)
.must_include(["integer"])
.must_exclude(["string"]);
// Integer is already given, but numbering is variadic.
test("#numbering(\"foo\", 1, )", -1)
test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"])
.must_exclude(["string"]);
}
@ -1700,14 +1689,14 @@ mod tests {
"#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)",
);
test_with_world(&world, -1)
test(&world, -2)
.must_include(["clrs", "aqua"])
.must_exclude(["nums", "a", "b"]);
}
#[test]
fn test_autocomplete_packages() {
test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]);
test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]);
}
#[test]
@ -1721,29 +1710,28 @@ mod tests {
.with_asset_at("assets/rhino.png", "rhino.png")
.with_asset_at("data/example.csv", "example.csv");
test_with_path(&world, "main.typ", -1)
test(&world, -2)
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
.must_exclude([q!("assets/tiger.jpg")]);
test_with_path(&world, "content/c.typ", -1)
test(&world, ("content/c.typ", -2))
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
.must_exclude([q!("c.typ")]);
test_with_path(&world, "content/a.typ", -1)
test(&world, ("content/a.typ", -2))
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
.must_exclude([q!("../data/example.csv"), q!("b.typ")]);
test_with_path(&world, "content/b.typ", -2)
.must_include([q!("../data/example.csv")]);
test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
}
#[test]
fn test_autocomplete_figure_snippets() {
test("#figure()", -1)
test("#figure()", -2)
.must_apply("image", "image(\"${}\"),")
.must_apply("table", "table(\n ${}\n),");
test("#figure(cap)", -1).must_apply("caption", "caption: [${}]");
test("#figure(cap)", -2).must_apply("caption", "caption: [${}]");
}
#[test]
@ -1752,10 +1740,10 @@ mod tests {
.with_source("second.typ", "#import \"other.typ\": th")
.with_source("other.typ", "#let this = 1; #let that = 2");
test_with_path(&world, "main.typ", 21)
test(&world, ("main.typ", 21))
.must_include(["*", "this", "that"])
.must_exclude(["figure"]);
test_with_path(&world, "second.typ", 23)
test(&world, ("second.typ", 23))
.must_include(["this", "that"])
.must_exclude(["*", "figure"]);
}

View File

@ -86,6 +86,7 @@ pub fn definition(
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use std::ops::Range;
use typst::foundations::{IntoValue, NativeElement};
@ -93,7 +94,7 @@ mod tests {
use typst::WorldExt;
use super::{definition, Definition};
use crate::tests::{SourceExt, TestWorld};
use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = (TestWorld, Option<Definition>);
@ -132,23 +133,19 @@ mod tests {
}
#[track_caller]
fn test(text: &str, cursor: isize, side: Side) -> Response {
let world = TestWorld::new(text);
test_with_world(world, cursor, side)
}
#[track_caller]
fn test_with_world(world: TestWorld, cursor: isize, side: Side) -> Response {
let doc = typst::compile(&world).output.ok();
let source = &world.main;
let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side);
(world, def)
fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
let world = world.acquire();
let world = world.borrow();
let doc = typst::compile(world).output.ok();
let (source, cursor) = pos.resolve(world);
let def = definition(world, doc.as_ref(), &source, cursor, side);
(world.clone(), def)
}
#[test]
fn test_definition_let() {
test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6);
test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6);
test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6);
test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6);
}
#[test]
@ -158,33 +155,33 @@ mod tests {
// The span is at the args here because that's what the function value's
// span is. Not ideal, but also not too big of a big deal.
test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11);
test(&world, -2, Side::Before).must_be_at("other.typ", 8..11);
}
#[test]
fn test_definition_cross_file() {
let world = TestWorld::new("#import \"other.typ\": x; #x")
.with_source("other.typ", "#let x = 1");
test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6);
test(&world, -2, Side::After).must_be_at("other.typ", 5..6);
}
#[test]
fn test_definition_import() {
let world = TestWorld::new("#import \"other.typ\" as o: x")
.with_source("other.typ", "#let x = 1");
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
}
#[test]
fn test_definition_include() {
let world = TestWorld::new("#include \"other.typ\"")
.with_source("other.typ", "Hello there");
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
}
#[test]
fn test_definition_ref() {
test("#figure[] <hi> See @hi", 21, Side::After).must_be_at("main.typ", 1..9);
test("#figure[] <hi> See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
}
#[test]

View File

@ -182,12 +182,13 @@ mod tests {
//! ))
//! ```
use std::borrow::Borrow;
use std::num::NonZeroUsize;
use typst::layout::{Abs, Point, Position};
use super::{jump_from_click, jump_from_cursor, Jump};
use crate::tests::TestWorld;
use crate::tests::{FilePos, TestWorld, WorldLike};
fn point(x: f64, y: f64) -> Point {
Point::new(Abs::pt(x), Abs::pt(y))
@ -211,10 +212,11 @@ mod tests {
}
#[track_caller]
fn test_click(text: &str, click: Point, expected: Option<Jump>) {
let world = TestWorld::new(text);
let doc = typst::compile(&world).output.unwrap();
let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click);
fn test_click(world: impl WorldLike, click: Point, expected: Option<Jump>) {
let world = world.acquire();
let world = world.borrow();
let doc = typst::compile(world).output.unwrap();
let jump = jump_from_click(world, &doc, &doc.pages[0].frame, click);
if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) =
(&jump, &expected)
{
@ -227,10 +229,12 @@ mod tests {
}
#[track_caller]
fn test_cursor(text: &str, cursor: usize, expected: Option<Position>) {
let world = TestWorld::new(text);
let doc = typst::compile(&world).output.unwrap();
let pos = jump_from_cursor(&doc, &world.main, cursor);
fn test_cursor(world: impl WorldLike, pos: impl FilePos, expected: Option<Position>) {
let world = world.acquire();
let world = world.borrow();
let doc = typst::compile(world).output.unwrap();
let (source, cursor) = pos.resolve(world);
let pos = jump_from_cursor(&doc, &source, cursor);
assert_eq!(!pos.is_empty(), expected.is_some());
if let (Some(pos), Some(expected)) = (pos.first(), expected) {
assert_eq!(pos.page, expected.page);

View File

@ -266,53 +266,79 @@ pub enum DerefTarget<'a> {
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use ecow::EcoString;
use typst::syntax::{LinkedNode, Side};
use crate::{named_items, tests::TestWorld};
use super::named_items;
use crate::tests::{FilePos, WorldLike};
type Response = Vec<EcoString>;
trait ResponseExt {
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 {
#[track_caller]
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in includes {
assert!(
self.iter().any(|v| v == item),
"{item:?} was not contained in {self:?}",
);
}
self
}
#[track_caller]
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in excludes {
assert!(
!self.iter().any(|v| v == item),
"{item:?} was wrongly contained in {self:?}",
);
}
self
}
}
#[track_caller]
fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool {
let world = TestWorld::new(text);
let src = world.main.clone();
let node = LinkedNode::new(src.root());
fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
let world = world.acquire();
let world = world.borrow();
let (source, cursor) = pos.resolve(world);
let node = LinkedNode::new(source.root());
let leaf = node.leaf_at(cursor, Side::After).unwrap();
let res = named_items(&world, leaf, |s| {
if containing == s.name() {
return Some(true);
}
None
let mut items = vec![];
named_items(world, leaf, |s| {
items.push(s.name().clone());
None::<()>
});
res.unwrap_or_default()
items
}
#[test]
fn test_simple_named_items() {
// Has named items
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a"));
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a"));
// Doesn't have named items
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
fn test_named_items_simple() {
let s = "#let a = 1;#let b = 2;";
test(s, 8).must_include(["a"]).must_exclude(["b"]);
test(s, 15).must_include(["b"]);
}
#[test]
fn test_param_named_items() {
// Has named items
assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a"));
assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a"));
fn test_named_items_param() {
let pos = "#let f(a) = 1;#let b = 2;";
test(pos, 12).must_include(["a"]);
test(pos, 19).must_include(["b", "f"]).must_exclude(["a"]);
// Doesn't have named items
assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a"));
assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b"));
let named = "#let f(a: b) = 1;#let b = 2;";
test(named, 15).must_include(["a", "f"]).must_exclude(["b"]);
}
#[test]
fn test_import_named_items() {
// Cannot test much.
assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a"));
fn test_named_items_import() {
test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]);
}
}

View File

@ -1,4 +1,6 @@
use std::borrow::Borrow;
use std::collections::HashMap;
use std::sync::Arc;
use ecow::EcoString;
use typst::diag::{FileError, FileResult};
@ -13,10 +15,10 @@ use typst::{Library, World};
use crate::IdeWorld;
/// A world for IDE testing.
#[derive(Clone)]
pub struct TestWorld {
pub main: Source,
assets: HashMap<FileId, Bytes>,
sources: HashMap<FileId, Source>,
files: Arc<TestFiles>,
base: &'static TestBase,
}
@ -29,8 +31,7 @@ impl TestWorld {
let main = Source::new(Self::main_id(), text.into());
Self {
main,
assets: HashMap::new(),
sources: HashMap::new(),
files: Arc::new(TestFiles::default()),
base: singleton!(TestBase, TestBase::default()),
}
}
@ -39,7 +40,7 @@ impl TestWorld {
pub fn with_source(mut self, path: &str, text: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path));
let source = Source::new(id, text.into());
self.sources.insert(id, source);
Arc::make_mut(&mut self.files).sources.insert(id, source);
self
}
@ -55,7 +56,7 @@ impl TestWorld {
let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap();
let bytes = Bytes::from_static(data);
self.assets.insert(id, bytes);
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self
}
@ -81,7 +82,7 @@ impl World for TestWorld {
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main.id() {
Ok(self.main.clone())
} else if let Some(source) = self.sources.get(&id) {
} else if let Some(source) = self.files.sources.get(&id) {
Ok(source.clone())
} else {
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
@ -89,7 +90,7 @@ impl World for TestWorld {
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
match self.assets.get(&id) {
match self.files.assets.get(&id) {
Some(bytes) => Ok(bytes.clone()),
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
}
@ -111,8 +112,8 @@ impl IdeWorld for TestWorld {
fn files(&self) -> Vec<FileId> {
std::iter::once(self.main.id())
.chain(self.sources.keys().copied())
.chain(self.assets.keys().copied())
.chain(self.files.sources.keys().copied())
.chain(self.files.assets.keys().copied())
.collect()
}
@ -133,20 +134,11 @@ impl IdeWorld for TestWorld {
}
}
/// Extra methods for [`Source`].
pub trait SourceExt {
/// Negative cursors index from the back.
fn cursor(&self, cursor: isize) -> usize;
}
impl SourceExt for Source {
fn cursor(&self, cursor: isize) -> usize {
if cursor < 0 {
self.len_bytes().checked_add_signed(cursor).unwrap()
} else {
cursor as usize
}
}
/// Test-specific files.
#[derive(Default, Clone)]
struct TestFiles {
assets: HashMap<FileId, Bytes>,
sources: HashMap<FileId, Source>,
}
/// Shared foundation of all test worlds.
@ -186,3 +178,58 @@ fn library() -> Library {
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
lib
}
/// The input to a test: Either just a string or a full `TestWorld`.
pub trait WorldLike {
type World: Borrow<TestWorld>;
fn acquire(self) -> Self::World;
}
impl<'a> WorldLike for &'a TestWorld {
type World = &'a TestWorld;
fn acquire(self) -> Self::World {
self
}
}
impl WorldLike for &str {
type World = TestWorld;
fn acquire(self) -> Self::World {
TestWorld::new(self)
}
}
/// Specifies a position in a file for a test.
pub trait FilePos {
fn resolve(self, world: &TestWorld) -> (Source, usize);
}
impl FilePos for isize {
#[track_caller]
fn resolve(self, world: &TestWorld) -> (Source, usize) {
(world.main.clone(), cursor(&world.main, self))
}
}
impl FilePos for (&str, isize) {
#[track_caller]
fn resolve(self, world: &TestWorld) -> (Source, usize) {
let id = FileId::new(None, VirtualPath::new(self.0));
let source = world.source(id).unwrap();
let cursor = cursor(&source, self.1);
(source, cursor)
}
}
/// Resolve a signed index (negative from the back) to a unsigned index.
#[track_caller]
fn cursor(source: &Source, cursor: isize) -> usize {
if cursor < 0 {
source.len_bytes().checked_add_signed(cursor + 1).unwrap()
} else {
cursor as usize
}
}

View File

@ -274,10 +274,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use typst::syntax::Side;
use super::{tooltip, Tooltip};
use crate::tests::{SourceExt, TestWorld};
use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Option<Tooltip>;
@ -308,21 +310,17 @@ mod tests {
}
#[track_caller]
fn test(text: &str, cursor: isize, side: Side) -> Response {
let world = TestWorld::new(text);
test_with_world(&world, cursor, side)
}
#[track_caller]
fn test_with_world(world: &TestWorld, cursor: isize, side: Side) -> Response {
let source = &world.main;
let doc = typst::compile(&world).output.ok();
tooltip(world, doc.as_ref(), source, source.cursor(cursor), side)
fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
let world = world.acquire();
let world = world.borrow();
let (source, cursor) = pos.resolve(world);
let doc = typst::compile(world).output.ok();
tooltip(world, doc.as_ref(), &source, cursor, side)
}
#[test]
fn test_tooltip() {
test("#let x = 1 + 2", 14, Side::After).must_be_none();
test("#let x = 1 + 2", -1, Side::After).must_be_none();
test("#let x = 1 + 2", 5, Side::After).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
@ -330,7 +328,7 @@ mod tests {
#[test]
fn test_tooltip_empty_contextual() {
test("#{context}", 10, Side::Before).must_be_code("context()");
test("#{context}", -1, Side::Before).must_be_code("context()");
}
#[test]
@ -358,8 +356,7 @@ mod tests {
fn test_tooltip_star_import() {
let world = TestWorld::new("#import \"other.typ\": *")
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
test_with_world(&world, 21, Side::Before).must_be_none();
test_with_world(&world, 21, Side::After)
.must_be_text("This star imports `a`, `b`, and `c`");
test(&world, -2, Side::Before).must_be_none();
test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`");
}
}