Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/package/typst-gather/tests/integration.rs
6460 views
1
//! Integration tests for typst-gather.
2
//!
3
//! These tests verify the full gathering workflow including:
4
//! - Local package copying
5
//! - Dependency scanning from .typ files
6
//! - Preview package caching (requires network)
7
8
use std::collections::HashSet;
9
use std::fs;
10
use std::path::Path;
11
12
use tempfile::TempDir;
13
use typst_gather::{gather_packages, find_imports, Config, PackageEntry};
14
15
/// Helper to create a minimal local package with typst.toml
16
fn create_local_package(dir: &Path, name: &str, version: &str, typ_content: Option<&str>) {
17
fs::create_dir_all(dir).unwrap();
18
19
let manifest = format!(
20
r#"[package]
21
name = "{name}"
22
version = "{version}"
23
entrypoint = "lib.typ"
24
"#
25
);
26
fs::write(dir.join("typst.toml"), manifest).unwrap();
27
28
let content = typ_content.unwrap_or("// Empty package\n");
29
fs::write(dir.join("lib.typ"), content).unwrap();
30
}
31
32
mod local_packages {
33
use super::*;
34
35
#[test]
36
fn cache_single_local_package() {
37
let src_dir = TempDir::new().unwrap();
38
let cache_dir = TempDir::new().unwrap();
39
40
create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);
41
42
let entries = vec![PackageEntry::Local {
43
name: "my-pkg".to_string(),
44
dir: src_dir.path().to_path_buf(),
45
}];
46
47
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
48
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
49
50
assert_eq!(result.stats.copied, 1);
51
assert_eq!(result.stats.failed, 0);
52
53
// Verify package was copied to correct location
54
let cached = cache_dir.path().join("local/my-pkg/1.0.0");
55
assert!(cached.exists());
56
assert!(cached.join("typst.toml").exists());
57
assert!(cached.join("lib.typ").exists());
58
}
59
60
#[test]
61
fn cache_local_package_overwrites_existing() {
62
let src_dir = TempDir::new().unwrap();
63
let cache_dir = TempDir::new().unwrap();
64
65
// Create initial version
66
create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some("// v1"));
67
68
let entries = vec![PackageEntry::Local {
69
name: "my-pkg".to_string(),
70
dir: src_dir.path().to_path_buf(),
71
}];
72
73
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
74
gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local);
75
76
// Update source
77
fs::write(src_dir.path().join("lib.typ"), "// v2").unwrap();
78
79
// Cache again
80
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
81
assert_eq!(result.stats.copied, 1);
82
83
// Verify new content
84
let cached_lib = cache_dir.path().join("local/my-pkg/1.0.0/lib.typ");
85
let content = fs::read_to_string(cached_lib).unwrap();
86
assert_eq!(content, "// v2");
87
}
88
89
#[test]
90
fn cache_multiple_local_packages() {
91
let src1 = TempDir::new().unwrap();
92
let src2 = TempDir::new().unwrap();
93
let cache_dir = TempDir::new().unwrap();
94
95
create_local_package(src1.path(), "pkg-one", "1.0.0", None);
96
create_local_package(src2.path(), "pkg-two", "2.0.0", None);
97
98
let entries = vec![
99
PackageEntry::Local {
100
name: "pkg-one".to_string(),
101
dir: src1.path().to_path_buf(),
102
},
103
PackageEntry::Local {
104
name: "pkg-two".to_string(),
105
dir: src2.path().to_path_buf(),
106
},
107
];
108
109
let configured_local: HashSet<String> = ["pkg-one".to_string(), "pkg-two".to_string()].into_iter().collect();
110
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
111
112
assert_eq!(result.stats.copied, 2);
113
assert!(cache_dir.path().join("local/pkg-one/1.0.0").exists());
114
assert!(cache_dir.path().join("local/pkg-two/2.0.0").exists());
115
}
116
117
#[test]
118
fn fail_on_name_mismatch() {
119
let src_dir = TempDir::new().unwrap();
120
let cache_dir = TempDir::new().unwrap();
121
122
// Create package with different name in manifest
123
create_local_package(src_dir.path(), "actual-name", "1.0.0", None);
124
125
let entries = vec![PackageEntry::Local {
126
name: "wrong-name".to_string(),
127
dir: src_dir.path().to_path_buf(),
128
}];
129
130
let configured_local: HashSet<String> = ["wrong-name".to_string()].into_iter().collect();
131
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
132
133
assert_eq!(result.stats.copied, 0);
134
assert_eq!(result.stats.failed, 1);
135
}
136
137
#[test]
138
fn fail_on_missing_manifest() {
139
let src_dir = TempDir::new().unwrap();
140
let cache_dir = TempDir::new().unwrap();
141
142
// Create directory without typst.toml
143
fs::create_dir_all(src_dir.path()).unwrap();
144
fs::write(src_dir.path().join("lib.typ"), "// no manifest").unwrap();
145
146
let entries = vec![PackageEntry::Local {
147
name: "my-pkg".to_string(),
148
dir: src_dir.path().to_path_buf(),
149
}];
150
151
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
152
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
153
154
assert_eq!(result.stats.copied, 0);
155
assert_eq!(result.stats.failed, 1);
156
}
157
158
#[test]
159
fn fail_on_nonexistent_directory() {
160
let cache_dir = TempDir::new().unwrap();
161
162
let entries = vec![PackageEntry::Local {
163
name: "my-pkg".to_string(),
164
dir: "/nonexistent/path/to/package".into(),
165
}];
166
167
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
168
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
169
170
assert_eq!(result.stats.copied, 0);
171
assert_eq!(result.stats.failed, 1);
172
}
173
174
#[test]
175
fn preserves_subdirectories() {
176
let src_dir = TempDir::new().unwrap();
177
let cache_dir = TempDir::new().unwrap();
178
179
create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);
180
181
// Add subdirectory with files
182
let sub = src_dir.path().join("src/utils");
183
fs::create_dir_all(&sub).unwrap();
184
fs::write(sub.join("helper.typ"), "// helper").unwrap();
185
186
let entries = vec![PackageEntry::Local {
187
name: "my-pkg".to_string(),
188
dir: src_dir.path().to_path_buf(),
189
}];
190
191
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
192
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
193
194
assert_eq!(result.stats.copied, 1);
195
196
let cached_helper = cache_dir
197
.path()
198
.join("local/my-pkg/1.0.0/src/utils/helper.typ");
199
assert!(cached_helper.exists());
200
}
201
}
202
203
mod dependency_scanning {
204
use super::*;
205
206
#[test]
207
fn find_imports_in_single_file() {
208
let dir = TempDir::new().unwrap();
209
210
let content = r#"
211
#import "@preview/cetz:0.4.1": canvas
212
#import "@preview/fletcher:0.5.3"
213
214
= Document
215
"#;
216
fs::write(dir.path().join("main.typ"), content).unwrap();
217
218
let imports = find_imports(dir.path());
219
220
assert_eq!(imports.len(), 2);
221
let names: Vec<_> = imports.iter().map(|s| s.name.as_str()).collect();
222
assert!(names.contains(&"cetz"));
223
assert!(names.contains(&"fletcher"));
224
}
225
226
#[test]
227
fn find_imports_in_nested_files() {
228
let dir = TempDir::new().unwrap();
229
230
fs::write(
231
dir.path().join("main.typ"),
232
r#"#import "@preview/cetz:0.4.1""#,
233
)
234
.unwrap();
235
236
let sub = dir.path().join("chapters");
237
fs::create_dir_all(&sub).unwrap();
238
fs::write(sub.join("intro.typ"), r#"#import "@preview/fletcher:0.5.3""#).unwrap();
239
240
let imports = find_imports(dir.path());
241
242
assert_eq!(imports.len(), 2);
243
}
244
245
#[test]
246
fn ignore_non_typ_files() {
247
let dir = TempDir::new().unwrap();
248
249
fs::write(
250
dir.path().join("main.typ"),
251
r#"#import "@preview/cetz:0.4.1""#,
252
)
253
.unwrap();
254
fs::write(
255
dir.path().join("notes.txt"),
256
r#"#import "@preview/ignored:1.0.0""#,
257
)
258
.unwrap();
259
260
let imports = find_imports(dir.path());
261
262
assert_eq!(imports.len(), 1);
263
assert_eq!(imports[0].name, "cetz");
264
}
265
266
#[test]
267
fn find_includes() {
268
let dir = TempDir::new().unwrap();
269
270
let content = r#"#include "@preview/template:1.0.0""#;
271
fs::write(dir.path().join("main.typ"), content).unwrap();
272
273
let imports = find_imports(dir.path());
274
275
assert_eq!(imports.len(), 1);
276
assert_eq!(imports[0].name, "template");
277
}
278
279
#[test]
280
fn ignore_relative_imports() {
281
let dir = TempDir::new().unwrap();
282
283
let content = r#"
284
#import "@preview/cetz:0.4.1"
285
#import "utils.typ"
286
#import "../shared/common.typ"
287
"#;
288
fs::write(dir.path().join("main.typ"), content).unwrap();
289
290
let imports = find_imports(dir.path());
291
292
assert_eq!(imports.len(), 1);
293
assert_eq!(imports[0].name, "cetz");
294
}
295
296
#[test]
297
fn empty_directory() {
298
let dir = TempDir::new().unwrap();
299
let imports = find_imports(dir.path());
300
assert!(imports.is_empty());
301
}
302
}
303
304
mod config_integration {
305
use super::*;
306
307
#[test]
308
fn parse_and_cache_local_from_toml() {
309
let src_dir = TempDir::new().unwrap();
310
let cache_dir = TempDir::new().unwrap();
311
312
create_local_package(src_dir.path(), "my-pkg", "1.0.0", None);
313
314
let toml = format!(
315
r#"
316
destination = "{}"
317
318
[local]
319
my-pkg = "{}"
320
"#,
321
cache_dir.path().display(),
322
src_dir.path().display()
323
);
324
325
let config = Config::parse(&toml).unwrap();
326
let dest = config.destination.clone().unwrap();
327
let configured_local: HashSet<String> = config.local.keys().cloned().collect();
328
let entries = config.into_entries();
329
let result = gather_packages(&dest, entries, &[], &configured_local);
330
331
assert_eq!(result.stats.copied, 1);
332
assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists());
333
}
334
335
#[test]
336
fn empty_config_does_nothing() {
337
let cache_dir = TempDir::new().unwrap();
338
339
let toml = format!(r#"destination = "{}""#, cache_dir.path().display());
340
let config = Config::parse(&toml).unwrap();
341
let dest = config.destination.clone().unwrap();
342
let configured_local: HashSet<String> = config.local.keys().cloned().collect();
343
let entries = config.into_entries();
344
let result = gather_packages(&dest, entries, &[], &configured_local);
345
346
assert_eq!(result.stats.downloaded, 0);
347
assert_eq!(result.stats.copied, 0);
348
assert_eq!(result.stats.skipped, 0);
349
assert_eq!(result.stats.failed, 0);
350
}
351
352
#[test]
353
fn missing_destination_returns_none() {
354
let config = Config::parse("").unwrap();
355
assert!(config.destination.is_none());
356
}
357
358
#[test]
359
fn parse_discover_field() {
360
let toml = r#"
361
destination = "/cache"
362
discover = "/path/to/templates"
363
"#;
364
let config = Config::parse(toml).unwrap();
365
assert_eq!(
366
config.discover,
367
vec![std::path::PathBuf::from("/path/to/templates")]
368
);
369
}
370
371
#[test]
372
fn parse_discover_array() {
373
let toml = r#"
374
destination = "/cache"
375
discover = ["template.typ", "typst-show.typ"]
376
"#;
377
let config = Config::parse(toml).unwrap();
378
assert_eq!(
379
config.discover,
380
vec![
381
std::path::PathBuf::from("template.typ"),
382
std::path::PathBuf::from("typst-show.typ"),
383
]
384
);
385
}
386
}
387
388
mod unconfigured_local {
389
use super::*;
390
391
#[test]
392
fn detects_unconfigured_local_imports() {
393
let cache_dir = TempDir::new().unwrap();
394
let discover_dir = TempDir::new().unwrap();
395
396
// Create a .typ file that imports @local/my-pkg
397
let content = r#"#import "@local/my-pkg:1.0.0""#;
398
fs::write(discover_dir.path().join("template.typ"), content).unwrap();
399
400
// Don't configure my-pkg in the local section
401
let configured_local: HashSet<String> = HashSet::new();
402
let discover = vec![discover_dir.path().to_path_buf()];
403
404
let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local);
405
406
// Should have one unconfigured local
407
assert_eq!(result.unconfigured_local.len(), 1);
408
assert_eq!(result.unconfigured_local[0].0, "my-pkg");
409
}
410
411
#[test]
412
fn configured_local_not_reported() {
413
let cache_dir = TempDir::new().unwrap();
414
let discover_dir = TempDir::new().unwrap();
415
416
// Create a .typ file that imports @local/my-pkg
417
let content = r#"#import "@local/my-pkg:1.0.0""#;
418
fs::write(discover_dir.path().join("template.typ"), content).unwrap();
419
420
// Configure my-pkg (even though we don't actually copy it)
421
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
422
let discover = vec![discover_dir.path().to_path_buf()];
423
424
let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local);
425
426
// Should have no unconfigured local
427
assert!(result.unconfigured_local.is_empty());
428
}
429
}
430
431
/// Tests that require network access.
432
/// Run with: cargo test -- --ignored
433
mod network {
434
use super::*;
435
436
#[test]
437
#[ignore = "requires network access"]
438
fn download_preview_package() {
439
let cache_dir = TempDir::new().unwrap();
440
441
let entries = vec![PackageEntry::Preview {
442
name: "example".to_string(),
443
version: "0.1.0".to_string(),
444
}];
445
446
let configured_local = HashSet::new();
447
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
448
449
assert_eq!(result.stats.downloaded, 1);
450
assert_eq!(result.stats.failed, 0);
451
452
let cached = cache_dir.path().join("preview/example/0.1.0");
453
assert!(cached.exists());
454
assert!(cached.join("typst.toml").exists());
455
}
456
457
#[test]
458
#[ignore = "requires network access"]
459
fn download_package_with_dependencies() {
460
let cache_dir = TempDir::new().unwrap();
461
462
// cetz has dependencies that should be auto-downloaded
463
let entries = vec![PackageEntry::Preview {
464
name: "cetz".to_string(),
465
version: "0.3.4".to_string(),
466
}];
467
468
let configured_local = HashSet::new();
469
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
470
471
// Should download cetz plus its dependencies
472
assert!(result.stats.downloaded >= 1);
473
assert_eq!(result.stats.failed, 0);
474
}
475
476
#[test]
477
#[ignore = "requires network access"]
478
fn skip_already_cached() {
479
let cache_dir = TempDir::new().unwrap();
480
481
let entries = vec![PackageEntry::Preview {
482
name: "example".to_string(),
483
version: "0.1.0".to_string(),
484
}];
485
486
let configured_local = HashSet::new();
487
488
// First download
489
let result1 = gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local);
490
assert_eq!(result1.stats.downloaded, 1);
491
492
// Second run should skip
493
let result2 = gather_packages(cache_dir.path(), entries, &[], &configured_local);
494
assert_eq!(result2.stats.downloaded, 0);
495
assert_eq!(result2.stats.skipped, 1);
496
}
497
498
#[test]
499
#[ignore = "requires network access"]
500
fn fail_on_nonexistent_package() {
501
let cache_dir = TempDir::new().unwrap();
502
503
let entries = vec![PackageEntry::Preview {
504
name: "this-package-does-not-exist-12345".to_string(),
505
version: "0.0.0".to_string(),
506
}];
507
508
let configured_local = HashSet::new();
509
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
510
511
assert_eq!(result.stats.downloaded, 0);
512
assert_eq!(result.stats.failed, 1);
513
}
514
515
#[test]
516
#[ignore = "requires network access"]
517
fn local_package_triggers_preview_deps() {
518
let src_dir = TempDir::new().unwrap();
519
let cache_dir = TempDir::new().unwrap();
520
521
// Create local package that imports a preview package
522
let content = r#"
523
#import "@preview/example:0.1.0"
524
525
#let my-func() = []
526
"#;
527
create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some(content));
528
529
let entries = vec![PackageEntry::Local {
530
name: "my-pkg".to_string(),
531
dir: src_dir.path().to_path_buf(),
532
}];
533
534
let configured_local: HashSet<String> = ["my-pkg".to_string()].into_iter().collect();
535
let result = gather_packages(cache_dir.path(), entries, &[], &configured_local);
536
537
assert_eq!(result.stats.copied, 1);
538
assert!(result.stats.downloaded >= 1); // Should have downloaded example
539
540
assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists());
541
assert!(cache_dir.path().join("preview/example/0.1.0").exists());
542
}
543
}
544
545