Path: blob/main/package/typst-gather/tests/integration.rs
6460 views
//! Integration tests for typst-gather.1//!2//! These tests verify the full gathering workflow including:3//! - Local package copying4//! - Dependency scanning from .typ files5//! - Preview package caching (requires network)67use std::collections::HashSet;8use std::fs;9use std::path::Path;1011use tempfile::TempDir;12use typst_gather::{gather_packages, find_imports, Config, PackageEntry};1314/// Helper to create a minimal local package with typst.toml15fn create_local_package(dir: &Path, name: &str, version: &str, typ_content: Option<&str>) {16fs::create_dir_all(dir).unwrap();1718let manifest = format!(19r#"[package]20name = "{name}"21version = "{version}"22entrypoint = "lib.typ"23"#24);25fs::write(dir.join("typst.toml"), manifest).unwrap();2627let content = typ_content.unwrap_or("// Empty package\n");28fs::write(dir.join("lib.typ"), content).unwrap();29}3031mod local_packages {32use super::*;3334#[test]35fn cache_single_local_package() {36let src_dir = TempDir::new().unwrap();37let cache_dir = TempDir::new().unwrap();3839create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);4041let entries = vec![PackageEntry::Local {42name: "my-pkg".to_string(),43dir: src_dir.path().to_path_buf(),44}];4546let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();47let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);4849assert_eq!(result.stats.copied, 1);50assert_eq!(result.stats.failed, 0);5152// Verify package was copied to correct location53let cached = cache_dir.path().join("local/my-pkg/1.0.0");54assert!(cached.exists());55assert!(cached.join("typst.toml").exists());56assert!(cached.join("lib.typ").exists());57}5859#[test]60fn cache_local_package_overwrites_existing() {61let src_dir = TempDir::new().unwrap();62let cache_dir = TempDir::new().unwrap();6364// Create initial version65create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some("// v1"));6667let entries = vec![PackageEntry::Local {68name: "my-pkg".to_string(),69dir: src_dir.path().to_path_buf(),70}];7172let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();73gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local);7475// Update source76fs::write(src_dir.path().join("lib.typ"), "// v2").unwrap();7778// Cache again79let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);80assert_eq!(result.stats.copied, 1);8182// Verify new content83let cached_lib = cache_dir.path().join("local/my-pkg/1.0.0/lib.typ");84let content = fs::read_to_string(cached_lib).unwrap();85assert_eq!(content, "// v2");86}8788#[test]89fn cache_multiple_local_packages() {90let src1 = TempDir::new().unwrap();91let src2 = TempDir::new().unwrap();92let cache_dir = TempDir::new().unwrap();9394create_local_package(src1.path(), "pkg-one", "1.0.0", None);95create_local_package(src2.path(), "pkg-two", "2.0.0", None);9697let entries = vec![98PackageEntry::Local {99name: "pkg-one".to_string(),100dir: src1.path().to_path_buf(),101},102PackageEntry::Local {103name: "pkg-two".to_string(),104dir: src2.path().to_path_buf(),105},106];107108let configured_local: HashSet<String> = ["pkg-one".to_string(), "pkg-two".to_string()].into_iter().collect();109let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);110111assert_eq!(result.stats.copied, 2);112assert!(cache_dir.path().join("local/pkg-one/1.0.0").exists());113assert!(cache_dir.path().join("local/pkg-two/2.0.0").exists());114}115116#[test]117fn fail_on_name_mismatch() {118let src_dir = TempDir::new().unwrap();119let cache_dir = TempDir::new().unwrap();120121// Create package with different name in manifest122create_local_package(src_dir.path(), "actual-name", "1.0.0", None);123124let entries = vec![PackageEntry::Local {125name: "wrong-name".to_string(),126dir: src_dir.path().to_path_buf(),127}];128129let configured_local: HashSet<String> = ["wrong-name".to_string()].into_iter().collect();130let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);131132assert_eq!(result.stats.copied, 0);133assert_eq!(result.stats.failed, 1);134}135136#[test]137fn fail_on_missing_manifest() {138let src_dir = TempDir::new().unwrap();139let cache_dir = TempDir::new().unwrap();140141// Create directory without typst.toml142fs::create_dir_all(src_dir.path()).unwrap();143fs::write(src_dir.path().join("lib.typ"), "// no manifest").unwrap();144145let entries = vec![PackageEntry::Local {146name: "my-pkg".to_string(),147dir: src_dir.path().to_path_buf(),148}];149150let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();151let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);152153assert_eq!(result.stats.copied, 0);154assert_eq!(result.stats.failed, 1);155}156157#[test]158fn fail_on_nonexistent_directory() {159let cache_dir = TempDir::new().unwrap();160161let entries = vec![PackageEntry::Local {162name: "my-pkg".to_string(),163dir: "/nonexistent/path/to/package".into(),164}];165166let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();167let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);168169assert_eq!(result.stats.copied, 0);170assert_eq!(result.stats.failed, 1);171}172173#[test]174fn preserves_subdirectories() {175let src_dir = TempDir::new().unwrap();176let cache_dir = TempDir::new().unwrap();177178create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);179180// Add subdirectory with files181let sub = src_dir.path().join("src/utils");182fs::create_dir_all(&sub).unwrap();183fs::write(sub.join("helper.typ"), "// helper").unwrap();184185let entries = vec![PackageEntry::Local {186name: "my-pkg".to_string(),187dir: src_dir.path().to_path_buf(),188}];189190let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();191let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);192193assert_eq!(result.stats.copied, 1);194195let cached_helper = cache_dir196.path()197.join("local/my-pkg/1.0.0/src/utils/helper.typ");198assert!(cached_helper.exists());199}200}201202mod dependency_scanning {203use super::*;204205#[test]206fn find_imports_in_single_file() {207let dir = TempDir::new().unwrap();208209let content = r#"210#import "@preview/cetz:0.4.1": canvas211#import "@preview/fletcher:0.5.3"212213= Document214"#;215fs::write(dir.path().join("main.typ"), content).unwrap();216217let imports = find_imports(dir.path());218219assert_eq!(imports.len(), 2);220let names: Vec<_> = imports.iter().map(|s| s.name.as_str()).collect();221assert!(names.contains(&"cetz"));222assert!(names.contains(&"fletcher"));223}224225#[test]226fn find_imports_in_nested_files() {227let dir = TempDir::new().unwrap();228229fs::write(230dir.path().join("main.typ"),231r#"#import "@preview/cetz:0.4.1""#,232)233.unwrap();234235let sub = dir.path().join("chapters");236fs::create_dir_all(&sub).unwrap();237fs::write(sub.join("intro.typ"), r#"#import "@preview/fletcher:0.5.3""#).unwrap();238239let imports = find_imports(dir.path());240241assert_eq!(imports.len(), 2);242}243244#[test]245fn ignore_non_typ_files() {246let dir = TempDir::new().unwrap();247248fs::write(249dir.path().join("main.typ"),250r#"#import "@preview/cetz:0.4.1""#,251)252.unwrap();253fs::write(254dir.path().join("notes.txt"),255r#"#import "@preview/ignored:1.0.0""#,256)257.unwrap();258259let imports = find_imports(dir.path());260261assert_eq!(imports.len(), 1);262assert_eq!(imports[0].name, "cetz");263}264265#[test]266fn find_includes() {267let dir = TempDir::new().unwrap();268269let content = r#"#include "@preview/template:1.0.0""#;270fs::write(dir.path().join("main.typ"), content).unwrap();271272let imports = find_imports(dir.path());273274assert_eq!(imports.len(), 1);275assert_eq!(imports[0].name, "template");276}277278#[test]279fn ignore_relative_imports() {280let dir = TempDir::new().unwrap();281282let content = r#"283#import "@preview/cetz:0.4.1"284#import "utils.typ"285#import "../shared/common.typ"286"#;287fs::write(dir.path().join("main.typ"), content).unwrap();288289let imports = find_imports(dir.path());290291assert_eq!(imports.len(), 1);292assert_eq!(imports[0].name, "cetz");293}294295#[test]296fn empty_directory() {297let dir = TempDir::new().unwrap();298let imports = find_imports(dir.path());299assert!(imports.is_empty());300}301}302303mod config_integration {304use super::*;305306#[test]307fn parse_and_cache_local_from_toml() {308let src_dir = TempDir::new().unwrap();309let cache_dir = TempDir::new().unwrap();310311create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);312313let toml = format!(314r#"315destination = "{}"316317[local]318my-pkg = "{}"319"#,320cache_dir.path().display(),321src_dir.path().display()322);323324let config = Config::parse(&toml).unwrap();325let dest = config.destination.clone().unwrap();326let configured_local: HashSet<String> = config.local.keys().cloned().collect();327let entries = config.into_entries();328let result = gather_packages(&dest, entries, &[], &configured_local);329330assert_eq!(result.stats.copied, 1);331assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists());332}333334#[test]335fn empty_config_does_nothing() {336let cache_dir = TempDir::new().unwrap();337338let toml = format!(r#"destination = "{}""#, cache_dir.path().display());339let config = Config::parse(&toml).unwrap();340let dest = config.destination.clone().unwrap();341let configured_local: HashSet<String> = config.local.keys().cloned().collect();342let entries = config.into_entries();343let result = gather_packages(&dest, entries, &[], &configured_local);344345assert_eq!(result.stats.downloaded, 0);346assert_eq!(result.stats.copied, 0);347assert_eq!(result.stats.skipped, 0);348assert_eq!(result.stats.failed, 0);349}350351#[test]352fn missing_destination_returns_none() {353let config = Config::parse("").unwrap();354assert!(config.destination.is_none());355}356357#[test]358fn parse_discover_field() {359let toml = r#"360destination = "/cache"361discover = "/path/to/templates"362"#;363let config = Config::parse(toml).unwrap();364assert_eq!(365config.discover,366vec![std::path::PathBuf::from("/path/to/templates")]367);368}369370#[test]371fn parse_discover_array() {372let toml = r#"373destination = "/cache"374discover = ["template.typ", "typst-show.typ"]375"#;376let config = Config::parse(toml).unwrap();377assert_eq!(378config.discover,379vec![380std::path::PathBuf::from("template.typ"),381std::path::PathBuf::from("typst-show.typ"),382]383);384}385}386387mod unconfigured_local {388use super::*;389390#[test]391fn detects_unconfigured_local_imports() {392let cache_dir = TempDir::new().unwrap();393let discover_dir = TempDir::new().unwrap();394395// Create a .typ file that imports @local/my-pkg396let content = r#"#import "@local/my-pkg:1.0.0""#;397fs::write(discover_dir.path().join("template.typ"), content).unwrap();398399// Don't configure my-pkg in the local section400let configured_local: HashSet<String> = HashSet::new();401let discover = vec![discover_dir.path().to_path_buf()];402403let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local);404405// Should have one unconfigured local406assert_eq!(result.unconfigured_local.len(), 1);407assert_eq!(result.unconfigured_local[0].0, "my-pkg");408}409410#[test]411fn configured_local_not_reported() {412let cache_dir = TempDir::new().unwrap();413let discover_dir = TempDir::new().unwrap();414415// Create a .typ file that imports @local/my-pkg416let content = r#"#import "@local/my-pkg:1.0.0""#;417fs::write(discover_dir.path().join("template.typ"), content).unwrap();418419// Configure my-pkg (even though we don't actually copy it)420let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();421let discover = vec![discover_dir.path().to_path_buf()];422423let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local);424425// Should have no unconfigured local426assert!(result.unconfigured_local.is_empty());427}428}429430/// Tests that require network access.431/// Run with: cargo test -- --ignored432mod network {433use super::*;434435#[test]436#[ignore = "requires network access"]437fn download_preview_package() {438let cache_dir = TempDir::new().unwrap();439440let entries = vec![PackageEntry::Preview {441name: "example".to_string(),442version: "0.1.0".to_string(),443}];444445let configured_local = HashSet::new();446let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);447448assert_eq!(result.stats.downloaded, 1);449assert_eq!(result.stats.failed, 0);450451let cached = cache_dir.path().join("preview/example/0.1.0");452assert!(cached.exists());453assert!(cached.join("typst.toml").exists());454}455456#[test]457#[ignore = "requires network access"]458fn download_package_with_dependencies() {459let cache_dir = TempDir::new().unwrap();460461// cetz has dependencies that should be auto-downloaded462let entries = vec![PackageEntry::Preview {463name: "cetz".to_string(),464version: "0.3.4".to_string(),465}];466467let configured_local = HashSet::new();468let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);469470// Should download cetz plus its dependencies471assert!(result.stats.downloaded >= 1);472assert_eq!(result.stats.failed, 0);473}474475#[test]476#[ignore = "requires network access"]477fn skip_already_cached() {478let cache_dir = TempDir::new().unwrap();479480let entries = vec![PackageEntry::Preview {481name: "example".to_string(),482version: "0.1.0".to_string(),483}];484485let configured_local = HashSet::new();486487// First download488let result1 = gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local);489assert_eq!(result1.stats.downloaded, 1);490491// Second run should skip492let result2 = gather_packages(cache_dir.path(), entries, &[], &configured_local);493assert_eq!(result2.stats.downloaded, 0);494assert_eq!(result2.stats.skipped, 1);495}496497#[test]498#[ignore = "requires network access"]499fn fail_on_nonexistent_package() {500let cache_dir = TempDir::new().unwrap();501502let entries = vec![PackageEntry::Preview {503name: "this-package-does-not-exist-12345".to_string(),504version: "0.0.0".to_string(),505}];506507let configured_local = HashSet::new();508let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);509510assert_eq!(result.stats.downloaded, 0);511assert_eq!(result.stats.failed, 1);512}513514#[test]515#[ignore = "requires network access"]516fn local_package_triggers_preview_deps() {517let src_dir = TempDir::new().unwrap();518let cache_dir = TempDir::new().unwrap();519520// Create local package that imports a preview package521let content = r#"522#import "@preview/example:0.1.0"523524#let my-func() = []525"#;526create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some(content));527528let entries = vec![PackageEntry::Local {529name: "my-pkg".to_string(),530dir: src_dir.path().to_path_buf(),531}];532533let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();534let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);535536assert_eq!(result.stats.copied, 1);537assert!(result.stats.downloaded >= 1); // Should have downloaded example538539assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists());540assert!(cache_dir.path().join("preview/example/0.1.0").exists());541}542}543544545