use std::collections::HashSet;
use std::fmt::{self, Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
use typst::foundations::Bytes;
#[test]
fn test_help() {
let output = exec().arg("--help").must_succeed();
output
.stdout
.must_contain("Compiles an input file")
.must_contain("https://typst.app/docs/tutorial/");
}
#[test]
fn test_compile_pdf() {
let project = tempfs();
let title = "Hello from CLI";
let hello = project.write("hello.typ", format!("#set document(title: \"{title}\")"));
exec().arg("compile").arg(&hello).must_succeed();
project.read("hello.pdf").must_start_with("%PDF").must_contain(title);
}
#[test]
fn test_compile_pdf_version() {
let project = tempfs();
let output = exec().arg("--version").must_succeed();
let version = output
.stdout
.lines()
.flat_map(|line| line.split_whitespace())
.nth(1)
.unwrap();
let hello = project.write("hello.typ", "Hi");
exec().arg("compile").arg(&hello).must_succeed();
project
.read("hello.pdf")
.must_contain(format!("/Creator(Typst {version})").as_bytes());
}
#[test]
fn test_eval() {
let output = exec().arg("eval").arg("1+2").must_succeed();
output.stdout.must_match_lines(["3"]);
}
#[test]
fn test_fonts_embedded() {
let output = exec().arg("fonts").arg("--ignore-system-fonts").must_succeed();
output.stdout.must_match_lines([
"DejaVu Sans Mono",
"Libertinus Serif",
"New Computer Modern",
"New Computer Modern Math",
]);
}
#[test]
fn test_fonts_path() {
let fonts = tempfs();
let mut expected = HashSet::new();
for (i, data) in typst_dev_assets::fonts().enumerate() {
let font = typst::text::Font::new(Bytes::new(data), 0).unwrap();
fonts.write(format!("{i}.ttf"), data);
expected.insert(font.info().family.clone());
}
let output = exec()
.arg("fonts")
.arg("--ignore-embedded-fonts")
.arg("--ignore-system-fonts")
.arg("--font-path")
.arg(fonts.path())
.must_succeed();
let found = output
.stdout
.lines()
.map(|line| line.to_string())
.collect::<HashSet<_>>();
assert_eq!(found, expected);
}
#[test]
fn test_info() {
let output = exec().arg("info").must_succeed();
output.stderr.must_start_with("Version");
}
#[test]
fn test_deps() {
let project = tempfs();
let main = project.write("main.typ", "#image(\"tiger.jpg\")");
project.write("tiger.jpg", typst_dev_assets::get_by_name("tiger.jpg").unwrap());
let output = exec().arg("compile").arg(main).arg("--deps").arg("-").must_succeed();
output.stdout.must_contain("tiger.jpg").must_contain("main.typ");
}
#[test]
fn test_path_resolved() {
let project = tempfs();
let main = project.write("main.typ", "#include \"dir/a.typ\"");
project.write("dir/a.typ", "#include \"/dir/b.typ\"");
project.write("dir/b.typ", "#import \"../utils.typ\": f; #f()!");
project.write("utils.typ", "#let f() = panic(42)");
let output = exec().arg("compile").arg(&main).must_fail();
output.stderr.must_contain("error: panicked with: 42");
}
#[test]
fn test_path_unresolved() {
let project = tempfs();
let main = project.write("main.typ", "#include \"other.typ\"");
let output = exec().arg("compile").arg(&main).must_fail();
output
.stderr
.must_contain("error: file not found")
.must_contain("#include \"other.typ\"");
}
#[test]
fn test_path_project_root() {
let project = tempfs();
let main = project.write("src/main.typ", "#include \"/a.typ\"");
project.write("a.typ", "#panic(42)");
let output = exec()
.arg("compile")
.arg(&main)
.arg("--root")
.arg(project.path())
.must_fail();
output.stderr.must_contain("error: panicked with: 42");
}
#[test]
fn test_package_resolved() {
let project = tempfs();
let package = tempfs();
let main = project.write("main.typ", "#import \"@local/demo:0.1.0\": f; #f()");
package.write(
"local/demo/0.1.0/typst.toml",
r#"[package]
name = "demo"
version = "0.1.0"
entrypoint = "lib.typ""#,
);
package.write("local/demo/0.1.0/lib.typ", "#import \"utils.typ\": f");
package.write("local/demo/0.1.0/utils.typ", "#let f() = panic(42)");
let output = exec()
.arg("compile")
.arg(&main)
.arg("--package-path")
.arg(package.path())
.must_fail();
output.stderr.must_contain("error: panicked with: 42");
}
#[test]
fn test_package_unresolved() {
let project = tempfs();
let package = tempfs();
let main = project.write("main.typ", "#import \"@local/demo:0.1.0\": f; #f()");
let output = exec()
.arg("compile")
.arg(&main)
.arg("--package-path")
.arg(package.path())
.must_fail();
output
.stderr
.must_contain("error: package not found (searched for @local/demo:0.1.0)");
}
#[test]
fn test_path_to_package() {
let project = tempfs();
let package = tempfs();
let main = project.write(
"main.typ",
"#import \"@local/demo:0.1.0\": g
#let x = g(path(\"a.typ\")) // from project
#let y = g(\"a.typ\") // from package
#panic((x, y))",
);
project.write("a.typ", "#let f() = 7");
package.write(
"local/demo/0.1.0/typst.toml",
r#"[package]
name = "demo"
version = "0.1.0"
entrypoint = "lib.typ""#,
);
package.write("local/demo/0.1.0/lib.typ", "#let g(p) = { import p: f; f() }");
package.write("local/demo/0.1.0/a.typ", "#let f() = 42");
let output = exec()
.arg("compile")
.arg(&main)
.arg("--package-path")
.arg(package.path())
.must_fail();
output.stderr.must_contain("error: panicked with: (7, 42)");
}
#[test]
fn test_network_access_hint() {
let project = tempfs();
let main = project.write("main.typ", "#image(\"https://example.org/image.png\")");
let output = exec().arg("compile").arg(main).must_fail();
output.stderr.must_contain("hint: network access is not supported");
}
#[test]
fn test_tracepoints() {
let project = tempfs();
let main = project.write(
"main.typ",
r#"#show strong: _ => include "chap" + "ter1.typ"
*Slightly unusual
strong text*"#,
);
project.write(
"chapter1.typ",
r#"#import "system.typ": my-figure
#my-figure(
"tigers.jpg"
)"#,
);
project.write("system.typ", "#let my-figure(p) = image(p)");
let output = exec().arg("compile").arg(&main).must_fail();
output
.stderr
.must_contain("while calling `my-figure` at")
.must_contain("chapter1.typ:2:12")
.must_contain("my-figure(…)");
output
.stderr
.must_contain("while including `chapter1.typ` at")
.must_contain("main.typ:1:19")
.must_contain(r#"include "chap" + "ter1.typ""#);
output
.stderr
.must_contain("while showing strong element at")
.must_contain("main.typ:2:11")
.must_contain("*Slightly unusual…*");
}
#[test]
fn test_target_available() {
let project = tempfs();
let main = project.write("main.typ", "#context target()");
exec().arg("compile").arg(&main).must_succeed();
}
fn exec() -> Command {
Command::new(env!("CARGO_BIN_EXE_typst"))
}
trait CommandExt {
fn must_succeed(&mut self) -> TestOutput;
fn must_fail(&mut self) -> TestOutput;
}
impl CommandExt for Command {
#[track_caller]
fn must_succeed(&mut self) -> TestOutput {
let output = self.output().unwrap();
assert!(
output.status.success(),
"process failed ({}):\n{}",
output.status,
Stream(output.stderr),
);
output.into()
}
#[track_caller]
fn must_fail(&mut self) -> TestOutput {
let output = self.output().unwrap();
assert!(!output.status.success(), "process succeeded ({})", output.status);
output.into()
}
}
struct TestOutput {
stdout: Stream,
stderr: Stream,
}
impl From<Output> for TestOutput {
fn from(value: Output) -> Self {
Self {
stdout: Stream(value.stdout),
stderr: Stream(value.stderr),
}
}
}
#[track_caller]
fn tempfs() -> TempFs {
TempFs(tempfile::tempdir().unwrap())
}
struct TempFs(TempDir);
impl TempFs {
fn path(&self) -> &Path {
self.0.path()
}
fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
self.path().join(path)
}
#[track_caller]
fn write(&self, path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> PathBuf {
let full = self.resolve(path);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full, data).unwrap();
full
}
#[track_caller]
fn read(&self, path: impl AsRef<Path>) -> Stream<Vec<u8>> {
Stream(std::fs::read(self.resolve(path)).unwrap())
}
}
struct Stream<T = Vec<u8>>(T);
impl<T: AsRef<[u8]>> Stream<T> {
#[track_caller]
fn must_contain(&self, data: impl Debug + AsRef<[u8]>) -> &Self {
assert!(self.contains(data.as_ref()), "{self:?} did not contain {data:?}",);
self
}
#[track_caller]
fn must_start_with(&self, data: impl Debug + AsRef<[u8]>) -> &Self {
assert!(
self.0.as_ref().starts_with(data.as_ref()),
"{self:?} did not start with {data:?}",
);
self
}
#[track_caller]
fn must_match_lines<'s>(&self, lines: impl IntoIterator<Item = &'s str>) -> &Self {
assert_eq!(
self.lines().collect::<Vec<_>>(),
lines.into_iter().collect::<Vec<_>>(),
);
self
}
fn contains(&self, data: impl AsRef<[u8]>) -> bool {
memchr::memmem::find(self.0.as_ref(), data.as_ref()).is_some()
}
fn lines(&self) -> impl Iterator<Item = &str> {
std::str::from_utf8(self.0.as_ref())
.unwrap_or_else(|_| panic!("{self} is not valid utf-8"))
.lines()
}
}
impl<T: AsRef<[u8]>> Debug for Stream<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&String::from_utf8_lossy(self.0.as_ref()), f)
}
}
impl<T: AsRef<[u8]>> Display for Stream<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&String::from_utf8_lossy(self.0.as_ref()), f)
}
}