Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tests/unit/project/file-information-cache.test.ts
12925 views
1
/*
2
* file-information-cache.test.ts
3
*
4
* Tests for fileInformationCache path normalization
5
* Related to issue #13955
6
*
7
* Copyright (C) 2026 Posit Software, PBC
8
*/
9
10
import { unitTest } from "../../test.ts";
11
import { assert } from "testing/asserts";
12
import { asMappedString } from "../../../src/core/lib/mapped-text.ts";
13
import { existsSync } from "../../../src/deno_ral/fs.ts";
14
import { join, relative } from "../../../src/deno_ral/path.ts";
15
import {
16
ensureFileInformationCache,
17
FileInformationCacheMap,
18
} from "../../../src/project/project-shared.ts";
19
import { createMockProjectContext } from "./utils.ts";
20
21
// deno-lint-ignore require-await
22
unitTest(
23
"fileInformationCache - same path returns same entry",
24
async () => {
25
const project = createMockProjectContext();
26
27
// Use cross-platform absolute path (backslashes on Windows, forward on Linux)
28
const path1 = join(project.dir, "doc.qmd");
29
const path2 = join(project.dir, "doc.qmd");
30
31
const entry1 = ensureFileInformationCache(project, path1);
32
const entry2 = ensureFileInformationCache(project, path2);
33
34
assert(
35
entry1 === entry2,
36
"Same path should return same cache entry",
37
);
38
assert(
39
project.fileInformationCache.size === 1,
40
"Should have exactly one cache entry",
41
);
42
},
43
);
44
45
// deno-lint-ignore require-await
46
unitTest(
47
"fileInformationCache - different paths create different entries",
48
async () => {
49
const project = createMockProjectContext();
50
51
const path1 = join(project.dir, "doc1.qmd");
52
const path2 = join(project.dir, "doc2.qmd");
53
54
const entry1 = ensureFileInformationCache(project, path1);
55
const entry2 = ensureFileInformationCache(project, path2);
56
57
assert(
58
entry1 !== entry2,
59
"Different paths should return different cache entries",
60
);
61
assert(
62
project.fileInformationCache.size === 2,
63
"Should have two cache entries for different paths",
64
);
65
},
66
);
67
68
// deno-lint-ignore require-await
69
unitTest(
70
"fileInformationCache - cache entry persists across calls",
71
async () => {
72
const project = createMockProjectContext();
73
74
const path = join(project.dir, "doc.qmd");
75
76
// First call creates entry
77
const entry1 = ensureFileInformationCache(project, path);
78
// Modify the entry
79
entry1.metadata = { title: "Test" };
80
81
// Second call should return same entry with our modification
82
const entry2 = ensureFileInformationCache(project, path);
83
84
assert(
85
entry2.metadata?.title === "Test",
86
"Cache entry should persist modifications",
87
);
88
assert(
89
entry1 === entry2,
90
"Should return same cache entry object",
91
);
92
},
93
);
94
95
// deno-lint-ignore require-await
96
unitTest(
97
"ensureFileInformationCache - creates FileInformationCacheMap when cache is missing",
98
async () => {
99
const project = createMockProjectContext();
100
// Simulate minimal ProjectContext without cache (as in command-utils.ts)
101
// deno-lint-ignore no-explicit-any
102
(project as any).fileInformationCache = undefined;
103
104
ensureFileInformationCache(project, join(project.dir, "doc.qmd"));
105
106
assert(
107
project.fileInformationCache instanceof FileInformationCacheMap,
108
"Should create FileInformationCacheMap, not plain Map",
109
);
110
},
111
);
112
113
// deno-lint-ignore require-await
114
unitTest(
115
"fileInformationCache - relative and absolute paths share same entry",
116
async () => {
117
const project = createMockProjectContext();
118
119
const absolutePath = join(project.dir, "subdir", "page.qmd");
120
const relativePath = relative(Deno.cwd(), absolutePath);
121
122
const entry1 = ensureFileInformationCache(project, relativePath);
123
const entry2 = ensureFileInformationCache(project, absolutePath);
124
125
assert(
126
entry1 === entry2,
127
"Relative and absolute paths to same file should share a cache entry",
128
);
129
assert(
130
project.fileInformationCache.size === 1,
131
"Should have exactly one cache entry",
132
);
133
},
134
);
135
136
// deno-lint-ignore require-await
137
unitTest(
138
"fileInformationCache - invalidateForFile deletes transient notebook file",
139
async () => {
140
const project = createMockProjectContext();
141
const sourcePath = join(project.dir, "doc.qmd");
142
143
// Create a real temp file simulating a transient .quarto_ipynb
144
const notebookPath = join(project.dir, "doc.quarto_ipynb");
145
Deno.writeTextFileSync(notebookPath, '{"cells": []}');
146
assert(existsSync(notebookPath), "Temp notebook file should exist");
147
148
// Populate cache entry with a transient target pointing to the file
149
const entry = ensureFileInformationCache(project, sourcePath);
150
entry.target = {
151
source: sourcePath,
152
input: notebookPath,
153
markdown: asMappedString(""),
154
metadata: {},
155
data: { transient: true, kernelspec: {} },
156
};
157
158
// Invalidate the cache entry for this file
159
project.fileInformationCache.invalidateForFile(sourcePath);
160
161
// The transient file should be deleted from disk
162
assert(
163
!existsSync(notebookPath),
164
"Transient notebook file should be deleted on invalidation",
165
);
166
// The cache entry should be removed
167
assert(
168
!project.fileInformationCache.has(sourcePath),
169
"Cache entry should be removed after invalidation",
170
);
171
},
172
);
173
174
// deno-lint-ignore require-await
175
unitTest(
176
"fileInformationCache - invalidateForFile preserves non-transient files",
177
async () => {
178
const project = createMockProjectContext();
179
const sourcePath = join(project.dir, "notebook.ipynb");
180
181
// Create a real file simulating a user's .ipynb (non-transient)
182
const notebookPath = join(project.dir, "notebook.ipynb");
183
Deno.writeTextFileSync(notebookPath, '{"cells": []}');
184
185
// Populate cache entry with a non-transient target
186
const entry = ensureFileInformationCache(project, sourcePath);
187
entry.target = {
188
source: sourcePath,
189
input: notebookPath,
190
markdown: asMappedString(""),
191
metadata: {},
192
data: { transient: false, kernelspec: {} },
193
};
194
195
// Invalidate the cache entry
196
project.fileInformationCache.invalidateForFile(sourcePath);
197
198
// The non-transient file should NOT be deleted
199
assert(
200
existsSync(notebookPath),
201
"Non-transient file should be preserved on invalidation",
202
);
203
// But the cache entry should still be removed
204
assert(
205
!project.fileInformationCache.has(sourcePath),
206
"Cache entry should be removed after invalidation",
207
);
208
},
209
);
210
211
// deno-lint-ignore require-await
212
unitTest(
213
"fileInformationCache - invalidateForFile handles entry with no target",
214
async () => {
215
const project = createMockProjectContext();
216
const sourcePath = join(project.dir, "doc.qmd");
217
218
// Populate cache entry with metadata only (no target)
219
const entry = ensureFileInformationCache(project, sourcePath);
220
entry.metadata = { title: "Test" };
221
222
// Should not throw
223
project.fileInformationCache.invalidateForFile(sourcePath);
224
225
assert(
226
!project.fileInformationCache.has(sourcePath),
227
"Cache entry should be removed even without a target",
228
);
229
},
230
);
231
232
// deno-lint-ignore require-await
233
unitTest(
234
"fileInformationCache - invalidateForFile is a no-op for missing keys",
235
async () => {
236
const project = createMockProjectContext();
237
238
// Should not throw on a key that doesn't exist
239
project.fileInformationCache.invalidateForFile(
240
join(project.dir, "nonexistent.qmd"),
241
);
242
243
assert(
244
project.fileInformationCache.size === 0,
245
"Cache should remain empty",
246
);
247
},
248
);
249
250