Skip to content

Commit 2c40a60

Browse files
committed
Add tests for the dependency parser
1 parent 034d05c commit 2c40a60

File tree

2 files changed

+363
-0
lines changed

2 files changed

+363
-0
lines changed

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"test:codeactions": "wireit",
4141
"test:copy": "wireit",
4242
"test:delete": "wireit",
43+
"test:dependency-parser": "wireit",
4344
"test:diagnostic": "wireit",
4445
"test:errors-analysis": "wireit",
4546
"test:errors-usage": "wireit",
@@ -93,6 +94,7 @@
9394
"test:codeactions",
9495
"test:copy",
9596
"test:delete",
97+
"test:dependency-parser",
9698
"test:diagnostic",
9799
"test:errors-analysis",
98100
"test:errors-usage",
@@ -223,6 +225,17 @@
223225
"files": [],
224226
"output": []
225227
},
228+
"test:dependency-parser": {
229+
"command": "uvu lib/test \"^dependency-parser\\.test\\.js$\"",
230+
"env": {
231+
"NODE_OPTIONS": "--enable-source-maps"
232+
},
233+
"dependencies": [
234+
"build"
235+
],
236+
"files": [],
237+
"output": []
238+
},
226239
"test:diagnostic": {
227240
"command": "uvu lib/test \"^diagnostic\\.test\\.js$\"",
228241
"env": {

src/test/dependency-parser.test.ts

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {suite} from 'uvu';
8+
import * as assert from 'uvu/assert';
9+
import {
10+
parseDependency,
11+
type ParsedPackage,
12+
type ParsedScript,
13+
} from '../analysis/dependency-parser.js';
14+
import type {DiagnosticWithoutFile} from '../error.js';
15+
16+
const test = suite<object>();
17+
18+
const cases: Array<
19+
[
20+
/**
21+
* Dependency specifier, e.g. "./pkg#scr".
22+
*/
23+
string,
24+
25+
/**
26+
* A string the same length as the dependency specifier, where each
27+
* character represents how we should interpret the corresponding character
28+
* from the dependency, with P = package, S = script, E = error, and
29+
* anything else is ignored (e.g. "PPPPP_SSS"). This way we can clearly
30+
* annotate the expected character ranges for each parsed component.
31+
*/
32+
string,
33+
(
34+
| {
35+
package: ParsedPackage;
36+
script: ParsedScript;
37+
inverted?: true;
38+
}
39+
| Omit<DiagnosticWithoutFile, 'location'>
40+
),
41+
]
42+
> = [
43+
[
44+
'',
45+
'',
46+
{
47+
severity: 'error',
48+
message: 'Dependency cannot be empty',
49+
},
50+
],
51+
[
52+
'./pkg#scr',
53+
'PPPPP_SSS',
54+
{
55+
package: {kind: 'path', path: './pkg'},
56+
script: {kind: 'name', name: 'scr'},
57+
},
58+
],
59+
[
60+
'scr',
61+
'SSS',
62+
{
63+
package: {kind: 'this'},
64+
script: {kind: 'name', name: 'scr'},
65+
},
66+
],
67+
[
68+
'#scr',
69+
'_SSS',
70+
{
71+
severity: 'error',
72+
message: 'Package specifier cannot be empty',
73+
},
74+
],
75+
[
76+
'./pkg#scr#',
77+
'PPPPP_SSSE',
78+
{
79+
severity: 'error',
80+
message: 'Unexpected additional delimiter "#"',
81+
},
82+
],
83+
[
84+
'./pkg##',
85+
'PPPPP_E',
86+
{
87+
severity: 'error',
88+
message: 'Unexpected additional delimiter "#"',
89+
},
90+
],
91+
[
92+
'./pkg',
93+
'EEEEE',
94+
{
95+
severity: 'error',
96+
message:
97+
`Cross-package dependency must use syntax ` +
98+
`"<relative-path>#<script-name>", but there's no ` +
99+
`"#" character in "./pkg".`,
100+
},
101+
],
102+
[
103+
String.raw`./\#pkg\##scr`,
104+
String.raw`PPPPPPPPP_SSS`,
105+
{
106+
package: {kind: 'path', path: './#pkg#'},
107+
script: {kind: 'name', name: 'scr'},
108+
},
109+
],
110+
[
111+
String.raw`./pkg#\#scr\#`,
112+
String.raw`PPPPP_SSSSSSS`,
113+
{
114+
package: {kind: 'path', path: './pkg'},
115+
script: {kind: 'name', name: '#scr#'},
116+
},
117+
],
118+
[
119+
String.raw`\./scr`,
120+
String.raw`_SSSSS`,
121+
{
122+
package: {kind: 'this'},
123+
script: {kind: 'name', name: './scr'},
124+
},
125+
],
126+
[
127+
'!./pkg#scr',
128+
'_PPPPP_SSS',
129+
{
130+
inverted: true,
131+
package: {kind: 'path', path: './pkg'},
132+
script: {kind: 'name', name: 'scr'},
133+
},
134+
],
135+
[
136+
'!scr',
137+
'_SSS',
138+
{
139+
inverted: true,
140+
package: {kind: 'this'},
141+
script: {kind: 'name', name: 'scr'},
142+
},
143+
],
144+
[
145+
'!!scr',
146+
'_SSSS',
147+
{
148+
inverted: true,
149+
package: {kind: 'this'},
150+
script: {kind: 'name', name: '!scr'},
151+
},
152+
],
153+
[
154+
String.raw`\!scr`,
155+
String.raw`_SSSS`,
156+
{
157+
package: {kind: 'this'},
158+
script: {kind: 'name', name: '!scr'},
159+
},
160+
],
161+
[
162+
'./pkg:scr',
163+
'PPPPP_SSS',
164+
{
165+
package: {kind: 'path', path: './pkg'},
166+
script: {kind: 'name', name: 'scr'},
167+
},
168+
],
169+
[
170+
'./pkg:foo#scr',
171+
'PPPPPPPPP_SSS',
172+
{
173+
package: {
174+
kind: 'path',
175+
path: './pkg:foo',
176+
},
177+
script: {kind: 'name', name: 'scr'},
178+
},
179+
],
180+
[
181+
String.raw`./pkg:foo\#scr`,
182+
String.raw`PPPPP_SSSSSSSS`,
183+
{
184+
package: {kind: 'path', path: './pkg'},
185+
script: {kind: 'name', name: 'foo#scr'},
186+
},
187+
],
188+
[
189+
'#foo',
190+
'_SSS',
191+
{
192+
severity: 'error',
193+
message: 'Package specifier cannot be empty',
194+
},
195+
],
196+
[
197+
'./pkg:<workspaces>',
198+
'PPPPP_EEEEEEEEEEEE',
199+
{
200+
severity: 'error',
201+
message: 'Unknown special script "workspaces"',
202+
},
203+
],
204+
[
205+
'./pkg#<>',
206+
'PPPPP_EE',
207+
{
208+
severity: 'error',
209+
message: 'Unexpected ">". Escape as "\\>" if you meant it literally.',
210+
},
211+
],
212+
[
213+
'<>',
214+
'EE',
215+
{
216+
severity: 'error',
217+
message: 'Unexpected ">". Escape as "\\>" if you meant it literally.',
218+
},
219+
],
220+
[
221+
'<workspaces>#scr',
222+
'PPPPPPPPPPPP_SSS',
223+
{
224+
package: {kind: 'workspaces'},
225+
script: {kind: 'name', name: 'scr'},
226+
},
227+
],
228+
[
229+
'<dependencies>#scr',
230+
'PPPPPPPPPPPPPP_SSS',
231+
{
232+
package: {kind: 'dependencies'},
233+
script: {kind: 'name', name: 'scr'},
234+
},
235+
],
236+
[
237+
'<dependencies>#<this>',
238+
'PPPPPPPPPPPPPP_SSSSSS',
239+
{
240+
package: {kind: 'dependencies'},
241+
script: {kind: 'this'},
242+
},
243+
],
244+
[
245+
String.raw`./pkg\*#scr\*`,
246+
String.raw`PPPPPPP_SSSSS`,
247+
{
248+
package: {kind: 'path', path: './pkg*'},
249+
script: {kind: 'name', name: 'scr*'},
250+
},
251+
],
252+
[
253+
String.raw`./pkg\{foo,bar}#scr\{foo,bar}`,
254+
String.raw`PPPPPPPPPPPPPPP_SSSSSSSSSSSSS`,
255+
{
256+
package: {
257+
kind: 'path',
258+
path: './pkg{foo,bar}',
259+
},
260+
script: {
261+
kind: 'name',
262+
name: 'scr{foo,bar}',
263+
},
264+
},
265+
],
266+
[
267+
'npm-package#scr',
268+
'PPPPPPPPPPP_SSS',
269+
{
270+
package: {
271+
kind: 'npm',
272+
package: 'npm-package',
273+
},
274+
script: {kind: 'name', name: 'scr'},
275+
},
276+
],
277+
[
278+
'package#scr:dee:doo',
279+
'PPPPPPP_SSSSSSSSSSS',
280+
{
281+
package: {
282+
kind: 'npm',
283+
package: 'package',
284+
},
285+
script: {
286+
kind: 'name',
287+
name: 'scr:dee:doo',
288+
},
289+
},
290+
],
291+
] as const;
292+
293+
for (const [dependency, rangeString, valueOrError] of cases) {
294+
test(dependency, () => {
295+
const actual = parseDependency(dependency);
296+
const ranges = extractRanges(rangeString);
297+
let expected: ReturnType<typeof parseDependency>;
298+
if ('severity' in valueOrError) {
299+
expected = {
300+
ok: false,
301+
error: {
302+
...valueOrError,
303+
location: {range: ranges.E},
304+
},
305+
};
306+
} else {
307+
expected = {
308+
ok: true,
309+
value: {
310+
...valueOrError,
311+
inverted: valueOrError.inverted ?? false,
312+
package: {...valueOrError.package, range: ranges.P},
313+
script: {...valueOrError.script, range: ranges.S},
314+
},
315+
};
316+
}
317+
assert.equal(actual, expected);
318+
});
319+
}
320+
321+
function extractRanges(str: string) {
322+
const result = {
323+
S: {offset: 0, length: 0},
324+
P: {offset: 0, length: 0},
325+
E: {offset: 0, length: 0},
326+
};
327+
for (const match of str.matchAll(/([SPE])\1*/g)) {
328+
result[match[1] as 'S' | 'P' | 'E'] = {
329+
offset: match.index,
330+
length: match[0].length,
331+
};
332+
}
333+
return result;
334+
}
335+
336+
test('extractRanges', () => {
337+
assert.equal(extractRanges('PPP_SS_E'), {
338+
P: {offset: 0, length: 3},
339+
S: {offset: 4, length: 2},
340+
E: {offset: 7, length: 1},
341+
});
342+
343+
assert.equal(extractRanges(''), {
344+
P: {offset: 0, length: 0},
345+
S: {offset: 0, length: 0},
346+
E: {offset: 0, length: 0},
347+
});
348+
});
349+
350+
test.run();

0 commit comments

Comments
 (0)