tstyche 2.0.0-rc.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/tstyche.d.ts +112 -107
- package/build/tstyche.js +1669 -1673
- package/package.json +1 -1
package/build/tstyche.js
CHANGED
|
@@ -111,806 +111,119 @@ class ExitCodeHandler {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
class
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
static #timeout = Environment.#resolveTimeout();
|
|
120
|
-
static #typescriptPath = Environment.#resolveTypeScriptPath();
|
|
121
|
-
static get isCi() {
|
|
122
|
-
return Environment.#isCi;
|
|
123
|
-
}
|
|
124
|
-
static get noColor() {
|
|
125
|
-
return Environment.#noColor;
|
|
126
|
-
}
|
|
127
|
-
static get noInteractive() {
|
|
128
|
-
return Environment.#noInteractive;
|
|
129
|
-
}
|
|
130
|
-
static get storePath() {
|
|
131
|
-
return Environment.#storePath;
|
|
132
|
-
}
|
|
133
|
-
static get timeout() {
|
|
134
|
-
return Environment.#timeout;
|
|
135
|
-
}
|
|
136
|
-
static get typescriptPath() {
|
|
137
|
-
return Environment.#typescriptPath;
|
|
138
|
-
}
|
|
139
|
-
static #resolveIsCi() {
|
|
140
|
-
if (process.env["CI"] != null) {
|
|
141
|
-
return process.env["CI"] !== "";
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
static #resolveNoColor() {
|
|
146
|
-
if (process.env["TSTYCHE_NO_COLOR"] != null) {
|
|
147
|
-
return process.env["TSTYCHE_NO_COLOR"] !== "";
|
|
148
|
-
}
|
|
149
|
-
if (process.env["NO_COLOR"] != null) {
|
|
150
|
-
return process.env["NO_COLOR"] !== "";
|
|
151
|
-
}
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
static #resolveNoInteractive() {
|
|
155
|
-
if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
|
|
156
|
-
return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
|
|
157
|
-
}
|
|
158
|
-
return !process.stdout.isTTY;
|
|
159
|
-
}
|
|
160
|
-
static #resolveStorePath() {
|
|
161
|
-
if (process.env["TSTYCHE_STORE_PATH"] != null) {
|
|
162
|
-
return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
|
|
163
|
-
}
|
|
164
|
-
if (process.platform === "darwin") {
|
|
165
|
-
return Path.resolve(os.homedir(), "Library", "TSTyche");
|
|
166
|
-
}
|
|
167
|
-
if (process.env["LocalAppData"] != null) {
|
|
168
|
-
return Path.resolve(process.env["LocalAppData"], "TSTyche");
|
|
169
|
-
}
|
|
170
|
-
if (process.env["XDG_DATA_HOME"] != null) {
|
|
171
|
-
return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
|
|
172
|
-
}
|
|
173
|
-
return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
|
|
174
|
-
}
|
|
175
|
-
static #resolveTimeout() {
|
|
176
|
-
if (process.env["TSTYCHE_TIMEOUT"] != null) {
|
|
177
|
-
return Number(process.env["TSTYCHE_TIMEOUT"]);
|
|
178
|
-
}
|
|
179
|
-
return 30;
|
|
180
|
-
}
|
|
181
|
-
static #resolveTypeScriptPath() {
|
|
182
|
-
let moduleId = "typescript";
|
|
183
|
-
if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
|
|
184
|
-
moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
|
|
185
|
-
}
|
|
186
|
-
let resolvedPath;
|
|
187
|
-
try {
|
|
188
|
-
resolvedPath = Path.normalizeSlashes(createRequire(import.meta.url).resolve(moduleId));
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
}
|
|
192
|
-
return resolvedPath;
|
|
114
|
+
class ResultTiming {
|
|
115
|
+
end = Number.NaN;
|
|
116
|
+
start = Number.NaN;
|
|
117
|
+
get duration() {
|
|
118
|
+
return this.end - this.start;
|
|
193
119
|
}
|
|
194
120
|
}
|
|
195
121
|
|
|
196
|
-
|
|
197
|
-
|
|
122
|
+
class DescribeResult {
|
|
123
|
+
describe;
|
|
124
|
+
parent;
|
|
125
|
+
results = [];
|
|
126
|
+
timing = new ResultTiming();
|
|
127
|
+
constructor(describe, parent) {
|
|
128
|
+
this.describe = describe;
|
|
129
|
+
this.parent = parent;
|
|
130
|
+
}
|
|
198
131
|
}
|
|
199
132
|
|
|
200
|
-
var
|
|
201
|
-
(function (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Color["Cyan"] = "36";
|
|
209
|
-
Color["Gray"] = "90";
|
|
210
|
-
})(Color || (Color = {}));
|
|
133
|
+
var ResultStatus;
|
|
134
|
+
(function (ResultStatus) {
|
|
135
|
+
ResultStatus["Runs"] = "runs";
|
|
136
|
+
ResultStatus["Passed"] = "passed";
|
|
137
|
+
ResultStatus["Failed"] = "failed";
|
|
138
|
+
ResultStatus["Skipped"] = "skipped";
|
|
139
|
+
ResultStatus["Todo"] = "todo";
|
|
140
|
+
})(ResultStatus || (ResultStatus = {}));
|
|
211
141
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
142
|
+
class ExpectResult {
|
|
143
|
+
assertion;
|
|
144
|
+
diagnostics = [];
|
|
145
|
+
parent;
|
|
146
|
+
status = "runs";
|
|
147
|
+
timing = new ResultTiming();
|
|
148
|
+
constructor(assertion, parent) {
|
|
149
|
+
this.assertion = assertion;
|
|
150
|
+
this.parent = parent;
|
|
216
151
|
}
|
|
217
|
-
return (jsx("text", { indent: indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: "0" }) : undefined] }));
|
|
218
152
|
}
|
|
219
153
|
|
|
220
|
-
|
|
221
|
-
|
|
154
|
+
class ResultCount {
|
|
155
|
+
failed = 0;
|
|
156
|
+
passed = 0;
|
|
157
|
+
skipped = 0;
|
|
158
|
+
todo = 0;
|
|
159
|
+
get total() {
|
|
160
|
+
return this.failed + this.passed + this.skipped + this.todo;
|
|
161
|
+
}
|
|
222
162
|
}
|
|
223
163
|
|
|
224
|
-
class
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
|
|
235
|
-
}
|
|
236
|
-
#indentEachLine(lines, level) {
|
|
237
|
-
if (level === 0) {
|
|
238
|
-
return lines;
|
|
239
|
-
}
|
|
240
|
-
return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
|
|
164
|
+
class FileResult {
|
|
165
|
+
diagnostics = [];
|
|
166
|
+
expectCount = new ResultCount();
|
|
167
|
+
results = [];
|
|
168
|
+
status = "runs";
|
|
169
|
+
testCount = new ResultCount();
|
|
170
|
+
testFile;
|
|
171
|
+
timing = new ResultTiming();
|
|
172
|
+
constructor(testFile) {
|
|
173
|
+
this.testFile = testFile;
|
|
241
174
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (element.type === "text") {
|
|
253
|
-
const text = this.#visitChildren(element.props.children);
|
|
254
|
-
return this.#indentEachLine(text, element.props.indent);
|
|
255
|
-
}
|
|
256
|
-
return "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class ProjectResult {
|
|
178
|
+
compilerVersion;
|
|
179
|
+
diagnostics = [];
|
|
180
|
+
projectConfigFilePath;
|
|
181
|
+
results = [];
|
|
182
|
+
constructor(compilerVersion, projectConfigFilePath) {
|
|
183
|
+
this.compilerVersion = compilerVersion;
|
|
184
|
+
this.projectConfigFilePath = projectConfigFilePath;
|
|
257
185
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
return text.join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class Result {
|
|
189
|
+
expectCount = new ResultCount();
|
|
190
|
+
fileCount = new ResultCount();
|
|
191
|
+
resolvedConfig;
|
|
192
|
+
results = [];
|
|
193
|
+
targetCount = new ResultCount();
|
|
194
|
+
testCount = new ResultCount();
|
|
195
|
+
testFiles;
|
|
196
|
+
timing = new ResultTiming();
|
|
197
|
+
constructor(resolvedConfig, testFiles) {
|
|
198
|
+
this.resolvedConfig = resolvedConfig;
|
|
199
|
+
this.testFiles = testFiles;
|
|
274
200
|
}
|
|
275
201
|
}
|
|
276
202
|
|
|
277
|
-
|
|
278
|
-
|
|
203
|
+
class TargetResult {
|
|
204
|
+
results = new Map();
|
|
205
|
+
status = "runs";
|
|
206
|
+
testFiles;
|
|
207
|
+
timing = new ResultTiming();
|
|
208
|
+
versionTag;
|
|
209
|
+
constructor(versionTag, testFiles) {
|
|
210
|
+
this.versionTag = versionTag;
|
|
211
|
+
this.testFiles = testFiles;
|
|
212
|
+
}
|
|
279
213
|
}
|
|
280
214
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const lineStart = diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index, 0);
|
|
294
|
-
const lineEnd = index === lastLineInFile
|
|
295
|
-
? diagnosticOrigin.sourceFile.text.length
|
|
296
|
-
: diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index + 1, 0);
|
|
297
|
-
const lineNumberText = String(index + 1);
|
|
298
|
-
const lineText = diagnosticOrigin.sourceFile.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
|
|
299
|
-
if (index === markedLine) {
|
|
300
|
-
codeSpan.push(jsx(Line, { children: [jsx(Text, { color: "31", children: ">" }), jsx(Text, { children: " " }), lineNumberText.padStart(lineNumberMaxWidth), jsx(Text, { children: " " }), jsx(Text, { color: "90", children: "|" }), " ", lineText] }), jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 3), jsx(Text, { color: "90", children: "|" }), " ".repeat(markedCharacter + 1), jsx(Text, { color: "31", children: "^" })] }));
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
codeSpan.push(jsx(Line, { children: [" ".repeat(2), jsx(Text, { color: "90", children: [lineNumberText.padStart(lineNumberMaxWidth), " | ", lineText || ""] })] }));
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const breadcrumbs = diagnosticOrigin.breadcrumbs?.flatMap((ancestor) => [
|
|
307
|
-
jsx(Text, { color: "90", children: " ❭ " }),
|
|
308
|
-
jsx(Text, { children: ancestor }),
|
|
309
|
-
]);
|
|
310
|
-
const location = (jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 5), jsx(Text, { color: "90", children: "at" }), jsx(Text, { children: " " }), jsx(Text, { color: "36", children: Path.relative("", diagnosticOrigin.sourceFile.fileName) }), jsx(Text, { color: "90", children: [":", String(markedLine + 1), ":", String(markedCharacter + 1)] }), breadcrumbs] }));
|
|
311
|
-
return (jsx(Text, { children: [codeSpan, jsx(Line, {}), location] }));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function DiagnosticText({ diagnostic }) {
|
|
315
|
-
const code = typeof diagnostic.code === "string" ? jsx(Text, { color: "90", children: [" ", diagnostic.code] }) : undefined;
|
|
316
|
-
const text = Array.isArray(diagnostic.text) ? diagnostic.text : [diagnostic.text];
|
|
317
|
-
const message = text.map((text, index) => (jsx(Text, { children: [index === 1 ? jsx(Line, {}) : undefined, jsx(Line, { children: [text, code] })] })));
|
|
318
|
-
const related = diagnostic.related?.map((relatedDiagnostic) => jsx(DiagnosticText, { diagnostic: relatedDiagnostic }));
|
|
319
|
-
const codeSpan = diagnostic.origin ? (jsx(Text, { children: [jsx(Line, {}), jsx(CodeSpanText, { ...diagnostic.origin })] })) : undefined;
|
|
320
|
-
return (jsx(Text, { children: [message, codeSpan, jsx(Line, {}), jsx(Text, { indent: 2, children: related })] }));
|
|
321
|
-
}
|
|
322
|
-
function diagnosticText(diagnostic) {
|
|
323
|
-
let prefix;
|
|
324
|
-
switch (diagnostic.category) {
|
|
325
|
-
case "error": {
|
|
326
|
-
prefix = jsx(Text, { color: "31", children: "Error: " });
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
case "warning": {
|
|
330
|
-
prefix = jsx(Text, { color: "33", children: "Warning: " });
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return (jsx(Text, { children: [prefix, jsx(DiagnosticText, { diagnostic: diagnostic })] }));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function FileNameText({ filePath }) {
|
|
338
|
-
const relativePath = Path.relative("", filePath);
|
|
339
|
-
const lastPathSeparator = relativePath.lastIndexOf("/");
|
|
340
|
-
const directoryNameText = relativePath.slice(0, lastPathSeparator + 1);
|
|
341
|
-
const fileNameText = relativePath.slice(lastPathSeparator + 1);
|
|
342
|
-
return (jsx(Text, { children: [jsx(Text, { color: "90", children: directoryNameText }), fileNameText] }));
|
|
343
|
-
}
|
|
344
|
-
function fileStatusText(status, testFile) {
|
|
345
|
-
let statusColor;
|
|
346
|
-
let statusText;
|
|
347
|
-
switch (status) {
|
|
348
|
-
case "runs": {
|
|
349
|
-
statusColor = "33";
|
|
350
|
-
statusText = "runs";
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
case "passed": {
|
|
354
|
-
statusColor = "32";
|
|
355
|
-
statusText = "pass";
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
case "failed": {
|
|
359
|
-
statusColor = "31";
|
|
360
|
-
statusText = "fail";
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return (jsx(Line, { children: [jsx(Text, { color: statusColor, children: statusText }), " ", jsx(FileNameText, { filePath: testFile.path })] }));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function fileViewText(lines, addEmptyFinalLine) {
|
|
368
|
-
return (jsx(Text, { children: [[...lines], addEmptyFinalLine ? jsx(Line, {}) : undefined] }));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function formattedText(input) {
|
|
372
|
-
if (typeof input === "string") {
|
|
373
|
-
return jsx(Line, { children: input });
|
|
374
|
-
}
|
|
375
|
-
if (Array.isArray(input)) {
|
|
376
|
-
return jsx(Line, { children: JSON.stringify(input, null, 2) });
|
|
377
|
-
}
|
|
378
|
-
function sortObject(target) {
|
|
379
|
-
return Object.keys(target)
|
|
380
|
-
.sort()
|
|
381
|
-
.reduce((result, key) => {
|
|
382
|
-
result[key] = target[key];
|
|
383
|
-
return result;
|
|
384
|
-
}, {});
|
|
385
|
-
}
|
|
386
|
-
return jsx(Line, { children: JSON.stringify(sortObject(input), null, 2) });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const usageExamples = [
|
|
390
|
-
["tstyche", "Run all tests."],
|
|
391
|
-
["tstyche path/to/first.test.ts", "Only run the test files with matching path."],
|
|
392
|
-
["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."],
|
|
393
|
-
];
|
|
394
|
-
function HintText({ children }) {
|
|
395
|
-
return (jsx(Text, { indent: 1, color: "90", children: children }));
|
|
396
|
-
}
|
|
397
|
-
function HelpHeaderText({ tstycheVersion }) {
|
|
398
|
-
return (jsx(Line, { children: ["The TSTyche Type Test Runner", jsx(HintText, { children: tstycheVersion })] }));
|
|
399
|
-
}
|
|
400
|
-
function CommandText({ hint, text }) {
|
|
401
|
-
let hintText;
|
|
402
|
-
if (hint != null) {
|
|
403
|
-
hintText = jsx(HintText, { children: hint });
|
|
404
|
-
}
|
|
405
|
-
return (jsx(Line, { indent: 1, children: [jsx(Text, { color: "34", children: text }), hintText] }));
|
|
406
|
-
}
|
|
407
|
-
function OptionDescriptionText({ text }) {
|
|
408
|
-
return jsx(Line, { indent: 1, children: text });
|
|
409
|
-
}
|
|
410
|
-
function CommandLineUsageText() {
|
|
411
|
-
const usageText = usageExamples.map(([commandText, descriptionText]) => (jsx(Text, { children: [jsx(CommandText, { text: commandText }), jsx(OptionDescriptionText, { text: descriptionText }), jsx(Line, {})] })));
|
|
412
|
-
return jsx(Text, { children: usageText });
|
|
413
|
-
}
|
|
414
|
-
function CommandLineOptionNameText({ text }) {
|
|
415
|
-
return jsx(Text, { children: ["--", text] });
|
|
416
|
-
}
|
|
417
|
-
function CommandLineOptionHintText({ definition }) {
|
|
418
|
-
if (definition.brand === "list") {
|
|
419
|
-
return (jsx(Text, { children: [definition.brand, " of ", definition.items.brand, "s"] }));
|
|
420
|
-
}
|
|
421
|
-
return jsx(Text, { children: definition.brand });
|
|
422
|
-
}
|
|
423
|
-
function CommandLineOptionsText({ optionDefinitions }) {
|
|
424
|
-
const definitions = [...optionDefinitions.values()];
|
|
425
|
-
const optionsText = definitions.map((definition) => {
|
|
426
|
-
let hint;
|
|
427
|
-
if (definition.brand !== "bareTrue") {
|
|
428
|
-
hint = jsx(CommandLineOptionHintText, { definition: definition });
|
|
429
|
-
}
|
|
430
|
-
return (jsx(Text, { children: [jsx(CommandText, { text: jsx(CommandLineOptionNameText, { text: definition.name }), hint: hint }), jsx(OptionDescriptionText, { text: definition.description }), jsx(Line, {})] }));
|
|
431
|
-
});
|
|
432
|
-
return (jsx(Text, { children: [jsx(Line, { children: "Command Line Options" }), jsx(Line, {}), optionsText] }));
|
|
433
|
-
}
|
|
434
|
-
function HelpFooterText() {
|
|
435
|
-
return jsx(Line, { children: "To learn more, visit https://tstyche.org" });
|
|
436
|
-
}
|
|
437
|
-
function helpText(optionDefinitions, tstycheVersion) {
|
|
438
|
-
return (jsx(Text, { children: [jsx(HelpHeaderText, { tstycheVersion: tstycheVersion }), jsx(Line, {}), jsx(CommandLineUsageText, {}), jsx(Line, {}), jsx(CommandLineOptionsText, { optionDefinitions: optionDefinitions }), jsx(Line, {}), jsx(HelpFooterText, {}), jsx(Line, {})] }));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
class OutputService {
|
|
442
|
-
#isClear = false;
|
|
443
|
-
#noColor;
|
|
444
|
-
#scribbler;
|
|
445
|
-
#stderr;
|
|
446
|
-
#stdout;
|
|
447
|
-
constructor(options) {
|
|
448
|
-
this.#noColor = options?.noColor ?? Environment.noColor;
|
|
449
|
-
this.#stderr = options?.stderr ?? process.stderr;
|
|
450
|
-
this.#stdout = options?.stdout ?? process.stdout;
|
|
451
|
-
this.#scribbler = new Scribbler({ noColor: this.#noColor });
|
|
452
|
-
}
|
|
453
|
-
clearTerminal() {
|
|
454
|
-
if (!this.#isClear) {
|
|
455
|
-
this.#stdout.write("\u001B[2J\u001B[3J\u001B[H");
|
|
456
|
-
this.#isClear = true;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
eraseLastLine() {
|
|
460
|
-
this.#stdout.write("\u001B[1A\u001B[0K");
|
|
461
|
-
}
|
|
462
|
-
#write(stream, body) {
|
|
463
|
-
const elements = Array.isArray(body) ? body : [body];
|
|
464
|
-
for (const element of elements) {
|
|
465
|
-
stream.write(this.#scribbler.render(element));
|
|
466
|
-
}
|
|
467
|
-
this.#isClear = false;
|
|
468
|
-
}
|
|
469
|
-
writeError(body) {
|
|
470
|
-
this.#write(this.#stderr, body);
|
|
471
|
-
}
|
|
472
|
-
writeMessage(body) {
|
|
473
|
-
this.#write(this.#stdout, body);
|
|
474
|
-
}
|
|
475
|
-
writeWarning(body) {
|
|
476
|
-
this.#write(this.#stderr, body);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function RowText({ label, text }) {
|
|
481
|
-
return (jsx(Line, { children: [`${label}:`.padEnd(12), text] }));
|
|
482
|
-
}
|
|
483
|
-
function CountText({ failed, passed, skipped, todo, total }) {
|
|
484
|
-
return (jsx(Text, { children: [failed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "31", children: [String(failed), " failed"] }), jsx(Text, { children: ", " })] })) : undefined, skipped > 0 ? (jsx(Text, { children: [jsx(Text, { color: "33", children: [String(skipped), " skipped"] }), jsx(Text, { children: ", " })] })) : undefined, todo > 0 ? (jsx(Text, { children: [jsx(Text, { color: "35", children: [String(todo), " todo"] }), jsx(Text, { children: ", " })] })) : undefined, passed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "32", children: [String(passed), " passed"] }), jsx(Text, { children: ", " })] })) : undefined, jsx(Text, { children: [String(total), jsx(Text, { children: " total" })] })] }));
|
|
485
|
-
}
|
|
486
|
-
function DurationText({ duration }) {
|
|
487
|
-
const minutes = Math.floor(duration / 60);
|
|
488
|
-
const seconds = duration % 60;
|
|
489
|
-
return (jsx(Text, { children: [minutes > 0 ? `${String(minutes)}m ` : undefined, `${String(Math.round(seconds * 10) / 10)}s`] }));
|
|
490
|
-
}
|
|
491
|
-
function MatchText({ text }) {
|
|
492
|
-
if (typeof text === "string") {
|
|
493
|
-
return jsx(Text, { children: ["'", text, "'"] });
|
|
494
|
-
}
|
|
495
|
-
if (text.length <= 1) {
|
|
496
|
-
return jsx(Text, { children: ["'", ...text, "'"] });
|
|
497
|
-
}
|
|
498
|
-
const lastItem = text.pop();
|
|
499
|
-
return (jsx(Text, { children: [text.map((match, index, list) => (jsx(Text, { children: ["'", match, "'", index === list.length - 1 ? jsx(Text, { children: " " }) : jsx(Text, { color: "90", children: ", " })] }))), jsx(Text, { color: "90", children: "or" }), " '", lastItem, "'"] }));
|
|
500
|
-
}
|
|
501
|
-
function RanFilesText({ onlyMatch, pathMatch, skipMatch }) {
|
|
502
|
-
const testNameMatchText = [];
|
|
503
|
-
if (onlyMatch != null) {
|
|
504
|
-
testNameMatchText.push(jsx(Text, { children: [jsx(Text, { color: "90", children: "matching " }), jsx(MatchText, { text: onlyMatch })] }));
|
|
505
|
-
}
|
|
506
|
-
if (skipMatch != null) {
|
|
507
|
-
testNameMatchText.push(jsx(Text, { children: [onlyMatch == null ? undefined : jsx(Text, { color: "90", children: " and " }), jsx(Text, { color: "90", children: "not matching " }), jsx(MatchText, { text: skipMatch })] }));
|
|
508
|
-
}
|
|
509
|
-
let pathMatchText;
|
|
510
|
-
if (pathMatch.length > 0) {
|
|
511
|
-
pathMatchText = (jsx(Text, { children: [jsx(Text, { color: "90", children: "test files matching " }), jsx(MatchText, { text: pathMatch }), jsx(Text, { color: "90", children: "." })] }));
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
pathMatchText = jsx(Text, { color: "90", children: "all test files." });
|
|
515
|
-
}
|
|
516
|
-
return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Ran " }), testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: "tests " }) : undefined, testNameMatchText, testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: " in " }) : undefined, pathMatchText] }));
|
|
517
|
-
}
|
|
518
|
-
function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
|
|
519
|
-
const targetCountText = (jsx(RowText, { label: "Targets", text: jsx(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
|
|
520
|
-
const fileCountText = (jsx(RowText, { label: "Test files", text: jsx(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
|
|
521
|
-
const testCountText = (jsx(RowText, { label: "Tests", text: jsx(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
|
|
522
|
-
const assertionCountText = (jsx(RowText, { label: "Assertions", text: jsx(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
|
|
523
|
-
return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { duration: duration / 1000 }) }), jsx(Line, {}), jsx(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })] }));
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function StatusText({ status }) {
|
|
527
|
-
switch (status) {
|
|
528
|
-
case "fail":
|
|
529
|
-
return jsx(Text, { color: "31", children: "\u00D7" });
|
|
530
|
-
case "pass":
|
|
531
|
-
return jsx(Text, { color: "32", children: "+" });
|
|
532
|
-
case "skip":
|
|
533
|
-
return jsx(Text, { color: "33", children: "- skip" });
|
|
534
|
-
case "todo":
|
|
535
|
-
return jsx(Text, { color: "35", children: "- todo" });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
function testNameText(status, name, indent = 0) {
|
|
539
|
-
return (jsx(Line, { indent: indent + 1, children: [jsx(StatusText, { status: status }), " ", jsx(Text, { color: "90", children: name })] }));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
|
|
543
|
-
let projectPathText;
|
|
544
|
-
if (tsconfigFilePath != null) {
|
|
545
|
-
projectPathText = (jsx(Text, { color: "90", children: [" with ", Path.relative("", tsconfigFilePath)] }));
|
|
546
|
-
}
|
|
547
|
-
return (jsx(Text, { children: [options?.prependEmptyLine === true ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectPathText] }), jsx(Line, {})] }));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function waitingForFileChangesText() {
|
|
551
|
-
return jsx(Line, { children: "Waiting for file changes." });
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function watchUsageText() {
|
|
555
|
-
const usageText = Object.entries({ a: "run all tests", x: "exit" }).map(([key, action]) => {
|
|
556
|
-
return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Press" }), jsx(Text, { children: ` ${key} ` }), jsx(Text, { color: "90", children: `to ${action}.` })] }));
|
|
557
|
-
});
|
|
558
|
-
return jsx(Text, { children: usageText });
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
class FileViewService {
|
|
562
|
-
#indent = 0;
|
|
563
|
-
#lines = [];
|
|
564
|
-
#messages = [];
|
|
565
|
-
get hasErrors() {
|
|
566
|
-
return this.#messages.length > 0;
|
|
567
|
-
}
|
|
568
|
-
addMessage(message) {
|
|
569
|
-
this.#messages.push(message);
|
|
570
|
-
}
|
|
571
|
-
addTest(status, name) {
|
|
572
|
-
this.#lines.push(testNameText(status, name, this.#indent));
|
|
573
|
-
}
|
|
574
|
-
beginDescribe(name) {
|
|
575
|
-
this.#lines.push(describeNameText(name, this.#indent));
|
|
576
|
-
this.#indent++;
|
|
577
|
-
}
|
|
578
|
-
clear() {
|
|
579
|
-
this.#indent = 0;
|
|
580
|
-
this.#lines = [];
|
|
581
|
-
this.#messages = [];
|
|
582
|
-
}
|
|
583
|
-
endDescribe() {
|
|
584
|
-
this.#indent--;
|
|
585
|
-
}
|
|
586
|
-
getMessages() {
|
|
587
|
-
return this.#messages;
|
|
588
|
-
}
|
|
589
|
-
getViewText(options) {
|
|
590
|
-
return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
class RuntimeReporter {
|
|
595
|
-
#currentCompilerVersion;
|
|
596
|
-
#currentProjectConfigFilePath;
|
|
597
|
-
#fileCount = 0;
|
|
598
|
-
#fileView = new FileViewService();
|
|
599
|
-
#hasReportedAdds = false;
|
|
600
|
-
#hasReportedError = false;
|
|
601
|
-
#isFileViewExpanded = false;
|
|
602
|
-
#resolvedConfig;
|
|
603
|
-
#outputService;
|
|
604
|
-
#seenDeprecations = new Set();
|
|
605
|
-
constructor(resolvedConfig, outputService) {
|
|
606
|
-
this.#resolvedConfig = resolvedConfig;
|
|
607
|
-
this.#outputService = outputService;
|
|
608
|
-
}
|
|
609
|
-
get #isLastFile() {
|
|
610
|
-
return this.#fileCount === 0;
|
|
611
|
-
}
|
|
612
|
-
handleEvent([eventName, payload]) {
|
|
613
|
-
switch (eventName) {
|
|
614
|
-
case "deprecation:info": {
|
|
615
|
-
for (const diagnostic of payload.diagnostics) {
|
|
616
|
-
if (!this.#seenDeprecations.has(diagnostic.text.toString())) {
|
|
617
|
-
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
618
|
-
this.#seenDeprecations.add(diagnostic.text.toString());
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
case "run:start": {
|
|
624
|
-
this.#isFileViewExpanded = payload.result.testFiles.length === 1 && this.#resolvedConfig.watch !== true;
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
case "store:info": {
|
|
628
|
-
this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
629
|
-
this.#hasReportedAdds = true;
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
case "store:error": {
|
|
633
|
-
for (const diagnostic of payload.diagnostics) {
|
|
634
|
-
this.#outputService.writeError(diagnosticText(diagnostic));
|
|
635
|
-
}
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
|
-
case "target:start": {
|
|
639
|
-
this.#fileCount = payload.result.testFiles.length;
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
case "target:end": {
|
|
643
|
-
this.#currentCompilerVersion = undefined;
|
|
644
|
-
this.#currentProjectConfigFilePath = undefined;
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
case "project:info": {
|
|
648
|
-
if (this.#currentCompilerVersion !== payload.compilerVersion ||
|
|
649
|
-
this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
|
|
650
|
-
this.#outputService.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
|
|
651
|
-
prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError,
|
|
652
|
-
}));
|
|
653
|
-
this.#hasReportedAdds = false;
|
|
654
|
-
this.#currentCompilerVersion = payload.compilerVersion;
|
|
655
|
-
this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
|
|
656
|
-
}
|
|
657
|
-
break;
|
|
658
|
-
}
|
|
659
|
-
case "project:error": {
|
|
660
|
-
for (const diagnostic of payload.diagnostics) {
|
|
661
|
-
this.#outputService.writeError(diagnosticText(diagnostic));
|
|
662
|
-
}
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
case "file:start": {
|
|
666
|
-
if (!Environment.noInteractive) {
|
|
667
|
-
this.#outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
668
|
-
}
|
|
669
|
-
this.#fileCount--;
|
|
670
|
-
this.#hasReportedError = false;
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
case "file:error": {
|
|
674
|
-
for (const diagnostic of payload.diagnostics) {
|
|
675
|
-
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
676
|
-
}
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
case "file:end": {
|
|
680
|
-
if (!Environment.noInteractive) {
|
|
681
|
-
this.#outputService.eraseLastLine();
|
|
682
|
-
}
|
|
683
|
-
this.#outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
684
|
-
this.#outputService.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
|
|
685
|
-
if (this.#fileView.hasErrors) {
|
|
686
|
-
this.#outputService.writeError(this.#fileView.getMessages());
|
|
687
|
-
this.#hasReportedError = true;
|
|
688
|
-
}
|
|
689
|
-
this.#fileView.clear();
|
|
690
|
-
this.#seenDeprecations.clear();
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
693
|
-
case "describe:start": {
|
|
694
|
-
if (this.#isFileViewExpanded) {
|
|
695
|
-
this.#fileView.beginDescribe(payload.result.describe.name);
|
|
696
|
-
}
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
case "describe:end": {
|
|
700
|
-
if (this.#isFileViewExpanded) {
|
|
701
|
-
this.#fileView.endDescribe();
|
|
702
|
-
}
|
|
703
|
-
break;
|
|
704
|
-
}
|
|
705
|
-
case "test:skip": {
|
|
706
|
-
if (this.#isFileViewExpanded) {
|
|
707
|
-
this.#fileView.addTest("skip", payload.result.test.name);
|
|
708
|
-
}
|
|
709
|
-
break;
|
|
710
|
-
}
|
|
711
|
-
case "test:todo": {
|
|
712
|
-
if (this.#isFileViewExpanded) {
|
|
713
|
-
this.#fileView.addTest("todo", payload.result.test.name);
|
|
714
|
-
}
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
case "test:error": {
|
|
718
|
-
if (this.#isFileViewExpanded) {
|
|
719
|
-
this.#fileView.addTest("fail", payload.result.test.name);
|
|
720
|
-
}
|
|
721
|
-
for (const diagnostic of payload.diagnostics) {
|
|
722
|
-
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
723
|
-
}
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
case "test:fail": {
|
|
727
|
-
if (this.#isFileViewExpanded) {
|
|
728
|
-
this.#fileView.addTest("fail", payload.result.test.name);
|
|
729
|
-
}
|
|
730
|
-
break;
|
|
731
|
-
}
|
|
732
|
-
case "test:pass": {
|
|
733
|
-
if (this.#isFileViewExpanded) {
|
|
734
|
-
this.#fileView.addTest("pass", payload.result.test.name);
|
|
735
|
-
}
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
case "expect:error":
|
|
739
|
-
case "expect:fail": {
|
|
740
|
-
for (const diagnostic of payload.diagnostics) {
|
|
741
|
-
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
742
|
-
}
|
|
743
|
-
break;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
class SetupReporter {
|
|
750
|
-
#outputService;
|
|
751
|
-
constructor(outputService) {
|
|
752
|
-
this.#outputService = outputService;
|
|
753
|
-
}
|
|
754
|
-
handleEvent([eventName, payload]) {
|
|
755
|
-
if (eventName === "store:info") {
|
|
756
|
-
this.#outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if ("diagnostics" in payload) {
|
|
760
|
-
for (const diagnostic of payload.diagnostics) {
|
|
761
|
-
switch (diagnostic.category) {
|
|
762
|
-
case "error": {
|
|
763
|
-
this.#outputService.writeError(diagnosticText(diagnostic));
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
case "warning": {
|
|
767
|
-
this.#outputService.writeWarning(diagnosticText(diagnostic));
|
|
768
|
-
break;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
class SummaryReporter {
|
|
777
|
-
#outputService;
|
|
778
|
-
constructor(outputService) {
|
|
779
|
-
this.#outputService = outputService;
|
|
780
|
-
}
|
|
781
|
-
handleEvent([eventName, payload]) {
|
|
782
|
-
switch (eventName) {
|
|
783
|
-
case "run:end": {
|
|
784
|
-
this.#outputService.writeMessage(summaryText({
|
|
785
|
-
duration: payload.result.timing.duration,
|
|
786
|
-
expectCount: payload.result.expectCount,
|
|
787
|
-
fileCount: payload.result.fileCount,
|
|
788
|
-
onlyMatch: payload.result.resolvedConfig.only,
|
|
789
|
-
pathMatch: payload.result.resolvedConfig.pathMatch,
|
|
790
|
-
skipMatch: payload.result.resolvedConfig.skip,
|
|
791
|
-
targetCount: payload.result.targetCount,
|
|
792
|
-
testCount: payload.result.testCount,
|
|
793
|
-
}));
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
class WatchReporter {
|
|
801
|
-
#outputService;
|
|
802
|
-
constructor(outputService) {
|
|
803
|
-
this.#outputService = outputService;
|
|
804
|
-
}
|
|
805
|
-
handleEvent([eventName, payload]) {
|
|
806
|
-
switch (eventName) {
|
|
807
|
-
case "run:start": {
|
|
808
|
-
this.#outputService.clearTerminal();
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
case "run:end": {
|
|
812
|
-
this.#outputService.writeMessage(watchUsageText());
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
case "watch:error": {
|
|
816
|
-
this.#outputService.clearTerminal();
|
|
817
|
-
for (const diagnostic of payload.diagnostics) {
|
|
818
|
-
this.#outputService.writeError(diagnosticText(diagnostic));
|
|
819
|
-
}
|
|
820
|
-
this.#outputService.writeMessage(waitingForFileChangesText());
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
class ResultTiming {
|
|
828
|
-
end = Number.NaN;
|
|
829
|
-
start = Number.NaN;
|
|
830
|
-
get duration() {
|
|
831
|
-
return this.end - this.start;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
class DescribeResult {
|
|
836
|
-
describe;
|
|
837
|
-
parent;
|
|
838
|
-
results = [];
|
|
839
|
-
timing = new ResultTiming();
|
|
840
|
-
constructor(describe, parent) {
|
|
841
|
-
this.describe = describe;
|
|
842
|
-
this.parent = parent;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
var ResultStatus;
|
|
847
|
-
(function (ResultStatus) {
|
|
848
|
-
ResultStatus["Runs"] = "runs";
|
|
849
|
-
ResultStatus["Passed"] = "passed";
|
|
850
|
-
ResultStatus["Failed"] = "failed";
|
|
851
|
-
ResultStatus["Skipped"] = "skipped";
|
|
852
|
-
ResultStatus["Todo"] = "todo";
|
|
853
|
-
})(ResultStatus || (ResultStatus = {}));
|
|
854
|
-
|
|
855
|
-
class ExpectResult {
|
|
856
|
-
assertion;
|
|
857
|
-
diagnostics = [];
|
|
858
|
-
parent;
|
|
859
|
-
status = "runs";
|
|
860
|
-
timing = new ResultTiming();
|
|
861
|
-
constructor(assertion, parent) {
|
|
862
|
-
this.assertion = assertion;
|
|
863
|
-
this.parent = parent;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
class ResultCount {
|
|
868
|
-
failed = 0;
|
|
869
|
-
passed = 0;
|
|
870
|
-
skipped = 0;
|
|
871
|
-
todo = 0;
|
|
872
|
-
get total() {
|
|
873
|
-
return this.failed + this.passed + this.skipped + this.todo;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
class FileResult {
|
|
878
|
-
diagnostics = [];
|
|
879
|
-
expectCount = new ResultCount();
|
|
880
|
-
results = [];
|
|
881
|
-
status = "runs";
|
|
882
|
-
testCount = new ResultCount();
|
|
883
|
-
testFile;
|
|
884
|
-
timing = new ResultTiming();
|
|
885
|
-
constructor(testFile) {
|
|
886
|
-
this.testFile = testFile;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
class ProjectResult {
|
|
891
|
-
compilerVersion;
|
|
892
|
-
diagnostics = [];
|
|
893
|
-
projectConfigFilePath;
|
|
894
|
-
results = [];
|
|
895
|
-
constructor(compilerVersion, projectConfigFilePath) {
|
|
896
|
-
this.compilerVersion = compilerVersion;
|
|
897
|
-
this.projectConfigFilePath = projectConfigFilePath;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
class Result {
|
|
902
|
-
expectCount = new ResultCount();
|
|
903
|
-
fileCount = new ResultCount();
|
|
904
|
-
resolvedConfig;
|
|
905
|
-
results = [];
|
|
906
|
-
targetCount = new ResultCount();
|
|
907
|
-
testCount = new ResultCount();
|
|
908
|
-
testFiles;
|
|
909
|
-
timing = new ResultTiming();
|
|
910
|
-
constructor(resolvedConfig, testFiles) {
|
|
911
|
-
this.resolvedConfig = resolvedConfig;
|
|
912
|
-
this.testFiles = testFiles;
|
|
913
|
-
}
|
|
215
|
+
class TestResult {
|
|
216
|
+
diagnostics = [];
|
|
217
|
+
expectCount = new ResultCount();
|
|
218
|
+
parent;
|
|
219
|
+
results = [];
|
|
220
|
+
status = "runs";
|
|
221
|
+
test;
|
|
222
|
+
timing = new ResultTiming();
|
|
223
|
+
constructor(test, parent) {
|
|
224
|
+
this.test = test;
|
|
225
|
+
this.parent = parent;
|
|
226
|
+
}
|
|
914
227
|
}
|
|
915
228
|
|
|
916
229
|
class ResultHandler {
|
|
@@ -1129,783 +442,883 @@ class ResultHandler {
|
|
|
1129
442
|
}
|
|
1130
443
|
}
|
|
1131
444
|
|
|
1132
|
-
class
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
445
|
+
class Environment {
|
|
446
|
+
static #isCi = Environment.#resolveIsCi();
|
|
447
|
+
static #noColor = Environment.#resolveNoColor();
|
|
448
|
+
static #noInteractive = Environment.#resolveNoInteractive();
|
|
449
|
+
static #storePath = Environment.#resolveStorePath();
|
|
450
|
+
static #timeout = Environment.#resolveTimeout();
|
|
451
|
+
static #typescriptPath = Environment.#resolveTypeScriptPath();
|
|
452
|
+
static get isCi() {
|
|
453
|
+
return Environment.#isCi;
|
|
454
|
+
}
|
|
455
|
+
static get noColor() {
|
|
456
|
+
return Environment.#noColor;
|
|
457
|
+
}
|
|
458
|
+
static get noInteractive() {
|
|
459
|
+
return Environment.#noInteractive;
|
|
460
|
+
}
|
|
461
|
+
static get storePath() {
|
|
462
|
+
return Environment.#storePath;
|
|
463
|
+
}
|
|
464
|
+
static get timeout() {
|
|
465
|
+
return Environment.#timeout;
|
|
466
|
+
}
|
|
467
|
+
static get typescriptPath() {
|
|
468
|
+
return Environment.#typescriptPath;
|
|
469
|
+
}
|
|
470
|
+
static #resolveIsCi() {
|
|
471
|
+
if (process.env["CI"] != null) {
|
|
472
|
+
return process.env["CI"] !== "";
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
static #resolveNoColor() {
|
|
477
|
+
if (process.env["TSTYCHE_NO_COLOR"] != null) {
|
|
478
|
+
return process.env["TSTYCHE_NO_COLOR"] !== "";
|
|
479
|
+
}
|
|
480
|
+
if (process.env["NO_COLOR"] != null) {
|
|
481
|
+
return process.env["NO_COLOR"] !== "";
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
static #resolveNoInteractive() {
|
|
486
|
+
if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) {
|
|
487
|
+
return process.env["TSTYCHE_NO_INTERACTIVE"] !== "";
|
|
488
|
+
}
|
|
489
|
+
return !process.stdout.isTTY;
|
|
490
|
+
}
|
|
491
|
+
static #resolveStorePath() {
|
|
492
|
+
if (process.env["TSTYCHE_STORE_PATH"] != null) {
|
|
493
|
+
return Path.resolve(process.env["TSTYCHE_STORE_PATH"]);
|
|
494
|
+
}
|
|
495
|
+
if (process.platform === "darwin") {
|
|
496
|
+
return Path.resolve(os.homedir(), "Library", "TSTyche");
|
|
497
|
+
}
|
|
498
|
+
if (process.env["LocalAppData"] != null) {
|
|
499
|
+
return Path.resolve(process.env["LocalAppData"], "TSTyche");
|
|
500
|
+
}
|
|
501
|
+
if (process.env["XDG_DATA_HOME"] != null) {
|
|
502
|
+
return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche");
|
|
503
|
+
}
|
|
504
|
+
return Path.resolve(os.homedir(), ".local", "share", "TSTyche");
|
|
505
|
+
}
|
|
506
|
+
static #resolveTimeout() {
|
|
507
|
+
if (process.env["TSTYCHE_TIMEOUT"] != null) {
|
|
508
|
+
return Number(process.env["TSTYCHE_TIMEOUT"]);
|
|
509
|
+
}
|
|
510
|
+
return 30;
|
|
511
|
+
}
|
|
512
|
+
static #resolveTypeScriptPath() {
|
|
513
|
+
let moduleId = "typescript";
|
|
514
|
+
if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) {
|
|
515
|
+
moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"];
|
|
516
|
+
}
|
|
517
|
+
let resolvedPath;
|
|
518
|
+
try {
|
|
519
|
+
resolvedPath = Path.normalizeSlashes(createRequire(import.meta.url).resolve(moduleId));
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
}
|
|
523
|
+
return resolvedPath;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function jsx(type, props) {
|
|
528
|
+
return { props, type };
|
|
1142
529
|
}
|
|
1143
530
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
531
|
+
var Color;
|
|
532
|
+
(function (Color) {
|
|
533
|
+
Color["Reset"] = "0";
|
|
534
|
+
Color["Red"] = "31";
|
|
535
|
+
Color["Green"] = "32";
|
|
536
|
+
Color["Yellow"] = "33";
|
|
537
|
+
Color["Blue"] = "34";
|
|
538
|
+
Color["Magenta"] = "35";
|
|
539
|
+
Color["Cyan"] = "36";
|
|
540
|
+
Color["Gray"] = "90";
|
|
541
|
+
})(Color || (Color = {}));
|
|
542
|
+
|
|
543
|
+
function Text({ children, color, indent }) {
|
|
544
|
+
const ansiEscapes = [];
|
|
545
|
+
if (color != null) {
|
|
546
|
+
ansiEscapes.push(color);
|
|
1155
547
|
}
|
|
548
|
+
return (jsx("text", { indent: indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: "0" }) : undefined] }));
|
|
1156
549
|
}
|
|
1157
550
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
551
|
+
function Line({ children, color, indent }) {
|
|
552
|
+
return (jsx(Text, { color: color, indent: indent, children: [children, jsx("newLine", {})] }));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
class Scribbler {
|
|
556
|
+
#indentStep = " ";
|
|
557
|
+
#newLine;
|
|
558
|
+
#noColor;
|
|
559
|
+
#notEmptyLineRegex = /^(?!$)/gm;
|
|
560
|
+
constructor(options) {
|
|
561
|
+
this.#newLine = options?.newLine ?? "\n";
|
|
562
|
+
this.#noColor = options?.noColor ?? false;
|
|
1164
563
|
}
|
|
1165
|
-
|
|
1166
|
-
return
|
|
564
|
+
#escapeSequence(attributes) {
|
|
565
|
+
return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
|
|
1167
566
|
}
|
|
1168
|
-
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1171
|
-
handler(reason);
|
|
1172
|
-
}
|
|
1173
|
-
this.#isCancelled = true;
|
|
1174
|
-
this.#reason = reason;
|
|
567
|
+
#indentEachLine(lines, level) {
|
|
568
|
+
if (level === 0) {
|
|
569
|
+
return lines;
|
|
1175
570
|
}
|
|
571
|
+
return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
|
|
1176
572
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
573
|
+
render(element) {
|
|
574
|
+
if (typeof element.type === "function") {
|
|
575
|
+
return this.render(element.type({ ...element.props }));
|
|
576
|
+
}
|
|
577
|
+
if (element.type === "ansi" && !this.#noColor) {
|
|
578
|
+
return this.#escapeSequence(element.props.escapes);
|
|
579
|
+
}
|
|
580
|
+
if (element.type === "newLine") {
|
|
581
|
+
return this.#newLine;
|
|
582
|
+
}
|
|
583
|
+
if (element.type === "text") {
|
|
584
|
+
const text = this.#visitChildren(element.props.children);
|
|
585
|
+
return this.#indentEachLine(text, element.props.indent);
|
|
586
|
+
}
|
|
587
|
+
return "";
|
|
1179
588
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
589
|
+
#visitChildren(children) {
|
|
590
|
+
const text = [];
|
|
591
|
+
for (const child of children) {
|
|
592
|
+
if (typeof child === "string") {
|
|
593
|
+
text.push(child);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (Array.isArray(child)) {
|
|
597
|
+
text.push(this.#visitChildren(child));
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (child != null && typeof child === "object") {
|
|
601
|
+
text.push(this.render(child));
|
|
602
|
+
}
|
|
1184
603
|
}
|
|
604
|
+
return text.join("");
|
|
1185
605
|
}
|
|
1186
606
|
}
|
|
1187
607
|
|
|
1188
|
-
|
|
1189
|
-
(
|
|
1190
|
-
|
|
1191
|
-
CancellationReason["ConfigError"] = "configError";
|
|
1192
|
-
CancellationReason["FailFast"] = "failFast";
|
|
1193
|
-
})(CancellationReason || (CancellationReason = {}));
|
|
608
|
+
function addsPackageStepText(compilerVersion, installationPath) {
|
|
609
|
+
return (jsx(Line, { children: [jsx(Text, { color: "90", children: "adds" }), " TypeScript ", compilerVersion, jsx(Text, { color: "90", children: [" to ", installationPath] })] }));
|
|
610
|
+
}
|
|
1194
611
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
612
|
+
function describeNameText(name, indent = 0) {
|
|
613
|
+
return jsx(Line, { indent: indent + 1, children: name });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function CodeSpanText(diagnosticOrigin) {
|
|
617
|
+
const lastLineInFile = diagnosticOrigin.sourceFile.getLineAndCharacterOfPosition(diagnosticOrigin.sourceFile.text.length).line;
|
|
618
|
+
const { character: markedCharacter, line: markedLine } = diagnosticOrigin.sourceFile.getLineAndCharacterOfPosition(diagnosticOrigin.start);
|
|
619
|
+
const firstLine = Math.max(markedLine - 2, 0);
|
|
620
|
+
const lastLine = Math.min(firstLine + 5, lastLineInFile);
|
|
621
|
+
const lineNumberMaxWidth = String(lastLine + 1).length;
|
|
622
|
+
const codeSpan = [];
|
|
623
|
+
for (let index = firstLine; index <= lastLine; index++) {
|
|
624
|
+
const lineStart = diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index, 0);
|
|
625
|
+
const lineEnd = index === lastLineInFile
|
|
626
|
+
? diagnosticOrigin.sourceFile.text.length
|
|
627
|
+
: diagnosticOrigin.sourceFile.getPositionOfLineAndCharacter(index + 1, 0);
|
|
628
|
+
const lineNumberText = String(index + 1);
|
|
629
|
+
const lineText = diagnosticOrigin.sourceFile.text.slice(lineStart, lineEnd).trimEnd().replace(/\t/g, " ");
|
|
630
|
+
if (index === markedLine) {
|
|
631
|
+
codeSpan.push(jsx(Line, { children: [jsx(Text, { color: "31", children: ">" }), jsx(Text, { children: " " }), lineNumberText.padStart(lineNumberMaxWidth), jsx(Text, { children: " " }), jsx(Text, { color: "90", children: "|" }), " ", lineText] }), jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 3), jsx(Text, { color: "90", children: "|" }), " ".repeat(markedCharacter + 1), jsx(Text, { color: "31", children: "^" })] }));
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
codeSpan.push(jsx(Line, { children: [" ".repeat(2), jsx(Text, { color: "90", children: [lineNumberText.padStart(lineNumberMaxWidth), " | ", lineText || ""] })] }));
|
|
635
|
+
}
|
|
1210
636
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
637
|
+
const breadcrumbs = diagnosticOrigin.breadcrumbs?.flatMap((ancestor) => [
|
|
638
|
+
jsx(Text, { color: "90", children: " ❭ " }),
|
|
639
|
+
jsx(Text, { children: ancestor }),
|
|
640
|
+
]);
|
|
641
|
+
const location = (jsx(Line, { children: [" ".repeat(lineNumberMaxWidth + 5), jsx(Text, { color: "90", children: "at" }), jsx(Text, { children: " " }), jsx(Text, { color: "36", children: Path.relative("", diagnosticOrigin.sourceFile.fileName) }), jsx(Text, { color: "90", children: [":", String(markedLine + 1), ":", String(markedCharacter + 1)] }), breadcrumbs] }));
|
|
642
|
+
return (jsx(Text, { children: [codeSpan, jsx(Line, {}), location] }));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function DiagnosticText({ diagnostic }) {
|
|
646
|
+
const code = typeof diagnostic.code === "string" ? jsx(Text, { color: "90", children: [" ", diagnostic.code] }) : undefined;
|
|
647
|
+
const text = Array.isArray(diagnostic.text) ? diagnostic.text : [diagnostic.text];
|
|
648
|
+
const message = text.map((text, index) => (jsx(Text, { children: [index === 1 ? jsx(Line, {}) : undefined, jsx(Line, { children: [text, code] })] })));
|
|
649
|
+
const related = diagnostic.related?.map((relatedDiagnostic) => jsx(DiagnosticText, { diagnostic: relatedDiagnostic }));
|
|
650
|
+
const codeSpan = diagnostic.origin ? (jsx(Text, { children: [jsx(Line, {}), jsx(CodeSpanText, { ...diagnostic.origin })] })) : undefined;
|
|
651
|
+
return (jsx(Text, { children: [message, codeSpan, jsx(Line, {}), jsx(Text, { indent: 2, children: related })] }));
|
|
652
|
+
}
|
|
653
|
+
function diagnosticText(diagnostic) {
|
|
654
|
+
let prefix;
|
|
655
|
+
switch (diagnostic.category) {
|
|
656
|
+
case "error": {
|
|
657
|
+
prefix = jsx(Text, { color: "31", children: "Error: " });
|
|
658
|
+
break;
|
|
1225
659
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
660
|
+
case "warning": {
|
|
661
|
+
prefix = jsx(Text, { color: "33", children: "Warning: " });
|
|
662
|
+
break;
|
|
1228
663
|
}
|
|
1229
664
|
}
|
|
665
|
+
return (jsx(Text, { children: [prefix, jsx(DiagnosticText, { diagnostic: diagnostic })] }));
|
|
1230
666
|
}
|
|
1231
667
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
668
|
+
function FileNameText({ filePath }) {
|
|
669
|
+
const relativePath = Path.relative("", filePath);
|
|
670
|
+
const lastPathSeparator = relativePath.lastIndexOf("/");
|
|
671
|
+
const directoryNameText = relativePath.slice(0, lastPathSeparator + 1);
|
|
672
|
+
const fileNameText = relativePath.slice(lastPathSeparator + 1);
|
|
673
|
+
return (jsx(Text, { children: [jsx(Text, { color: "90", children: directoryNameText }), fileNameText] }));
|
|
674
|
+
}
|
|
675
|
+
function fileStatusText(status, testFile) {
|
|
676
|
+
let statusColor;
|
|
677
|
+
let statusText;
|
|
678
|
+
switch (status) {
|
|
679
|
+
case "runs": {
|
|
680
|
+
statusColor = "33";
|
|
681
|
+
statusText = "runs";
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case "passed": {
|
|
685
|
+
statusColor = "32";
|
|
686
|
+
statusText = "pass";
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "failed": {
|
|
690
|
+
statusColor = "31";
|
|
691
|
+
statusText = "fail";
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
1240
694
|
}
|
|
695
|
+
return (jsx(Line, { children: [jsx(Text, { color: statusColor, children: statusText }), " ", jsx(FileNameText, { filePath: testFile.path })] }));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function fileViewText(lines, addEmptyFinalLine) {
|
|
699
|
+
return (jsx(Text, { children: [[...lines], addEmptyFinalLine ? jsx(Line, {}) : undefined] }));
|
|
1241
700
|
}
|
|
1242
701
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
sourceFile;
|
|
1247
|
-
start;
|
|
1248
|
-
constructor(start, end, sourceFile, breadcrumbs) {
|
|
1249
|
-
this.start = start;
|
|
1250
|
-
this.end = end;
|
|
1251
|
-
this.sourceFile = sourceFile;
|
|
1252
|
-
this.breadcrumbs = breadcrumbs;
|
|
702
|
+
function formattedText(input) {
|
|
703
|
+
if (typeof input === "string") {
|
|
704
|
+
return jsx(Line, { children: input });
|
|
1253
705
|
}
|
|
1254
|
-
|
|
1255
|
-
return
|
|
706
|
+
if (Array.isArray(input)) {
|
|
707
|
+
return jsx(Line, { children: JSON.stringify(input, null, 2) });
|
|
1256
708
|
}
|
|
1257
|
-
|
|
1258
|
-
return
|
|
709
|
+
function sortObject(target) {
|
|
710
|
+
return Object.keys(target)
|
|
711
|
+
.sort()
|
|
712
|
+
.reduce((result, key) => {
|
|
713
|
+
result[key] = target[key];
|
|
714
|
+
return result;
|
|
715
|
+
}, {});
|
|
1259
716
|
}
|
|
717
|
+
return jsx(Line, { children: JSON.stringify(sortObject(input), null, 2) });
|
|
1260
718
|
}
|
|
1261
719
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
add(options) {
|
|
1274
|
-
if (options.code != null) {
|
|
1275
|
-
this.code = options.code;
|
|
1276
|
-
}
|
|
1277
|
-
if (options.origin != null) {
|
|
1278
|
-
this.origin = options.origin;
|
|
1279
|
-
}
|
|
1280
|
-
if (options.related != null) {
|
|
1281
|
-
this.related = options.related;
|
|
1282
|
-
}
|
|
1283
|
-
return this;
|
|
1284
|
-
}
|
|
1285
|
-
static error(text, origin) {
|
|
1286
|
-
return new Diagnostic(text, "error", origin);
|
|
720
|
+
function HintText({ children }) {
|
|
721
|
+
return (jsx(Text, { indent: 1, color: "90", children: children }));
|
|
722
|
+
}
|
|
723
|
+
function HelpHeaderText({ tstycheVersion }) {
|
|
724
|
+
return (jsx(Line, { children: ["The TSTyche Type Test Runner", jsx(HintText, { children: tstycheVersion })] }));
|
|
725
|
+
}
|
|
726
|
+
function CommandText({ hint, text }) {
|
|
727
|
+
let hintText;
|
|
728
|
+
if (hint != null) {
|
|
729
|
+
hintText = jsx(HintText, { children: hint });
|
|
1287
730
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
731
|
+
return (jsx(Line, { indent: 1, children: [jsx(Text, { color: "34", children: text }), hintText] }));
|
|
732
|
+
}
|
|
733
|
+
function OptionDescriptionText({ text }) {
|
|
734
|
+
return jsx(Line, { indent: 1, children: text });
|
|
735
|
+
}
|
|
736
|
+
function CommandLineUsageText() {
|
|
737
|
+
const usage = [
|
|
738
|
+
["tstyche", "Run all tests."],
|
|
739
|
+
["tstyche path/to/first.test.ts", "Only run the test files with matching path."],
|
|
740
|
+
["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."],
|
|
741
|
+
];
|
|
742
|
+
const usageText = usage.map(([commandText, descriptionText]) => (jsx(Line, { children: [jsx(CommandText, { text: commandText }), jsx(OptionDescriptionText, { text: descriptionText })] })));
|
|
743
|
+
return jsx(Text, { children: usageText });
|
|
744
|
+
}
|
|
745
|
+
function CommandLineOptionNameText({ text }) {
|
|
746
|
+
return jsx(Text, { children: ["--", text] });
|
|
747
|
+
}
|
|
748
|
+
function CommandLineOptionHintText({ definition }) {
|
|
749
|
+
if (definition.brand === "list") {
|
|
750
|
+
return (jsx(Text, { children: [definition.brand, " of ", definition.items.brand, "s"] }));
|
|
1299
751
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
752
|
+
return jsx(Text, { children: definition.brand });
|
|
753
|
+
}
|
|
754
|
+
function CommandLineOptionsText({ optionDefinitions }) {
|
|
755
|
+
const definitions = [...optionDefinitions.values()];
|
|
756
|
+
const optionsText = definitions.map((definition) => {
|
|
757
|
+
let hint;
|
|
758
|
+
if (definition.brand !== "bareTrue") {
|
|
759
|
+
hint = jsx(CommandLineOptionHintText, { definition: definition });
|
|
1308
760
|
}
|
|
1309
|
-
return
|
|
761
|
+
return (jsx(Text, { children: [jsx(CommandText, { text: jsx(CommandLineOptionNameText, { text: definition.name }), hint: hint }), jsx(OptionDescriptionText, { text: definition.description }), jsx(Line, {})] }));
|
|
762
|
+
});
|
|
763
|
+
return (jsx(Text, { children: [jsx(Line, { children: "Command Line Options" }), jsx(Line, {}), optionsText] }));
|
|
764
|
+
}
|
|
765
|
+
function HelpFooterText() {
|
|
766
|
+
return jsx(Line, { children: "To learn more, visit https://tstyche.org" });
|
|
767
|
+
}
|
|
768
|
+
function helpText(optionDefinitions, tstycheVersion) {
|
|
769
|
+
return (jsx(Text, { children: [jsx(HelpHeaderText, { tstycheVersion: tstycheVersion }), jsx(Line, {}), jsx(CommandLineUsageText, {}), jsx(Line, {}), jsx(CommandLineOptionsText, { optionDefinitions: optionDefinitions }), jsx(Line, {}), jsx(HelpFooterText, {}), jsx(Line, {})] }));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
class OutputService {
|
|
773
|
+
#isClear = false;
|
|
774
|
+
#noColor;
|
|
775
|
+
#scribbler;
|
|
776
|
+
#stderr;
|
|
777
|
+
#stdout;
|
|
778
|
+
constructor(options) {
|
|
779
|
+
this.#noColor = options?.noColor ?? Environment.noColor;
|
|
780
|
+
this.#stderr = options?.stderr ?? process.stderr;
|
|
781
|
+
this.#stdout = options?.stdout ?? process.stdout;
|
|
782
|
+
this.#scribbler = new Scribbler({ noColor: this.#noColor });
|
|
1310
783
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
784
|
+
clearTerminal() {
|
|
785
|
+
if (!this.#isClear) {
|
|
786
|
+
this.#stdout.write("\u001B[2J\u001B[3J\u001B[H");
|
|
787
|
+
this.#isClear = true;
|
|
788
|
+
}
|
|
1313
789
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
790
|
+
eraseLastLine() {
|
|
791
|
+
this.#stdout.write("\u001B[1A\u001B[0K");
|
|
1316
792
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
(
|
|
1321
|
-
DiagnosticCategory["Error"] = "error";
|
|
1322
|
-
DiagnosticCategory["Warning"] = "warning";
|
|
1323
|
-
})(DiagnosticCategory || (DiagnosticCategory = {}));
|
|
1324
|
-
|
|
1325
|
-
class OptionDefinitionsMap {
|
|
1326
|
-
static #definitions = [
|
|
1327
|
-
{
|
|
1328
|
-
brand: "string",
|
|
1329
|
-
description: "The path to a TSTyche configuration file.",
|
|
1330
|
-
group: 2,
|
|
1331
|
-
name: "config",
|
|
1332
|
-
},
|
|
1333
|
-
{
|
|
1334
|
-
brand: "boolean",
|
|
1335
|
-
description: "Stop running tests after the first failed assertion.",
|
|
1336
|
-
group: 4 | 2,
|
|
1337
|
-
name: "failFast",
|
|
1338
|
-
},
|
|
1339
|
-
{
|
|
1340
|
-
brand: "bareTrue",
|
|
1341
|
-
description: "Print the list of command line options with brief descriptions and exit.",
|
|
1342
|
-
group: 2,
|
|
1343
|
-
name: "help",
|
|
1344
|
-
},
|
|
1345
|
-
{
|
|
1346
|
-
brand: "bareTrue",
|
|
1347
|
-
description: "Install specified versions of the 'typescript' package and exit.",
|
|
1348
|
-
group: 2,
|
|
1349
|
-
name: "install",
|
|
1350
|
-
},
|
|
1351
|
-
{
|
|
1352
|
-
brand: "bareTrue",
|
|
1353
|
-
description: "Print the list of the selected test files and exit.",
|
|
1354
|
-
group: 2,
|
|
1355
|
-
name: "listFiles",
|
|
1356
|
-
},
|
|
1357
|
-
{
|
|
1358
|
-
brand: "string",
|
|
1359
|
-
description: "Only run tests with matching name.",
|
|
1360
|
-
group: 2,
|
|
1361
|
-
name: "only",
|
|
1362
|
-
},
|
|
1363
|
-
{
|
|
1364
|
-
brand: "bareTrue",
|
|
1365
|
-
description: "Remove all installed versions of the 'typescript' package and exit.",
|
|
1366
|
-
group: 2,
|
|
1367
|
-
name: "prune",
|
|
1368
|
-
},
|
|
1369
|
-
{
|
|
1370
|
-
brand: "string",
|
|
1371
|
-
description: "The path to a directory containing files of a test project.",
|
|
1372
|
-
group: 4,
|
|
1373
|
-
name: "rootPath",
|
|
1374
|
-
},
|
|
1375
|
-
{
|
|
1376
|
-
brand: "bareTrue",
|
|
1377
|
-
description: "Print the resolved configuration and exit.",
|
|
1378
|
-
group: 2,
|
|
1379
|
-
name: "showConfig",
|
|
1380
|
-
},
|
|
1381
|
-
{
|
|
1382
|
-
brand: "string",
|
|
1383
|
-
description: "Skip tests with matching name.",
|
|
1384
|
-
group: 2,
|
|
1385
|
-
name: "skip",
|
|
1386
|
-
},
|
|
1387
|
-
{
|
|
1388
|
-
brand: "list",
|
|
1389
|
-
description: "The list of TypeScript versions to be tested on.",
|
|
1390
|
-
group: 2 | 4,
|
|
1391
|
-
items: {
|
|
1392
|
-
brand: "string",
|
|
1393
|
-
name: "target",
|
|
1394
|
-
pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
|
|
1395
|
-
},
|
|
1396
|
-
name: "target",
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
brand: "list",
|
|
1400
|
-
description: "The list of glob patterns matching the test files.",
|
|
1401
|
-
group: 4,
|
|
1402
|
-
items: {
|
|
1403
|
-
brand: "string",
|
|
1404
|
-
name: "testFileMatch",
|
|
1405
|
-
},
|
|
1406
|
-
name: "testFileMatch",
|
|
1407
|
-
},
|
|
1408
|
-
{
|
|
1409
|
-
brand: "bareTrue",
|
|
1410
|
-
description: "Fetch the 'typescript' package metadata from the registry and exit.",
|
|
1411
|
-
group: 2,
|
|
1412
|
-
name: "update",
|
|
1413
|
-
},
|
|
1414
|
-
{
|
|
1415
|
-
brand: "bareTrue",
|
|
1416
|
-
description: "Print the version number and exit.",
|
|
1417
|
-
group: 2,
|
|
1418
|
-
name: "version",
|
|
1419
|
-
},
|
|
1420
|
-
{
|
|
1421
|
-
brand: "bareTrue",
|
|
1422
|
-
description: "Watch for changes and rerun related test files.",
|
|
1423
|
-
group: 2,
|
|
1424
|
-
name: "watch",
|
|
1425
|
-
},
|
|
1426
|
-
];
|
|
1427
|
-
static for(optionGroup) {
|
|
1428
|
-
const definitionMap = new Map();
|
|
1429
|
-
for (const definition of OptionDefinitionsMap.#definitions) {
|
|
1430
|
-
if (definition.group & optionGroup) {
|
|
1431
|
-
definitionMap.set(definition.name, definition);
|
|
1432
|
-
}
|
|
793
|
+
#write(stream, body) {
|
|
794
|
+
const elements = Array.isArray(body) ? body : [body];
|
|
795
|
+
for (const element of elements) {
|
|
796
|
+
stream.write(this.#scribbler.render(element));
|
|
1433
797
|
}
|
|
1434
|
-
|
|
798
|
+
this.#isClear = false;
|
|
799
|
+
}
|
|
800
|
+
writeError(body) {
|
|
801
|
+
this.#write(this.#stderr, body);
|
|
802
|
+
}
|
|
803
|
+
writeMessage(body) {
|
|
804
|
+
this.#write(this.#stdout, body);
|
|
805
|
+
}
|
|
806
|
+
writeWarning(body) {
|
|
807
|
+
this.#write(this.#stderr, body);
|
|
1435
808
|
}
|
|
1436
809
|
}
|
|
1437
810
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
811
|
+
function RowText({ label, text }) {
|
|
812
|
+
return (jsx(Line, { children: [`${label}:`.padEnd(12), text] }));
|
|
813
|
+
}
|
|
814
|
+
function CountText({ failed, passed, skipped, todo, total }) {
|
|
815
|
+
return (jsx(Text, { children: [failed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "31", children: [String(failed), " failed"] }), jsx(Text, { children: ", " })] })) : undefined, skipped > 0 ? (jsx(Text, { children: [jsx(Text, { color: "33", children: [String(skipped), " skipped"] }), jsx(Text, { children: ", " })] })) : undefined, todo > 0 ? (jsx(Text, { children: [jsx(Text, { color: "35", children: [String(todo), " todo"] }), jsx(Text, { children: ", " })] })) : undefined, passed > 0 ? (jsx(Text, { children: [jsx(Text, { color: "32", children: [String(passed), " passed"] }), jsx(Text, { children: ", " })] })) : undefined, jsx(Text, { children: [String(total), jsx(Text, { children: " total" })] })] }));
|
|
816
|
+
}
|
|
817
|
+
function DurationText({ duration }) {
|
|
818
|
+
const minutes = Math.floor(duration / 60);
|
|
819
|
+
const seconds = duration % 60;
|
|
820
|
+
return (jsx(Text, { children: [minutes > 0 ? `${String(minutes)}m ` : undefined, `${String(Math.round(seconds * 10) / 10)}s`] }));
|
|
821
|
+
}
|
|
822
|
+
function MatchText({ text }) {
|
|
823
|
+
if (typeof text === "string") {
|
|
824
|
+
return jsx(Text, { children: ["'", text, "'"] });
|
|
1441
825
|
}
|
|
1442
|
-
|
|
1443
|
-
return
|
|
826
|
+
if (text.length <= 1) {
|
|
827
|
+
return jsx(Text, { children: ["'", ...text, "'"] });
|
|
1444
828
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
829
|
+
const lastItem = text.pop();
|
|
830
|
+
return (jsx(Text, { children: [text.map((match, index, list) => (jsx(Text, { children: ["'", match, "'", index === list.length - 1 ? jsx(Text, { children: " " }) : jsx(Text, { color: "90", children: ", " })] }))), jsx(Text, { color: "90", children: "or" }), " '", lastItem, "'"] }));
|
|
831
|
+
}
|
|
832
|
+
function RanFilesText({ onlyMatch, pathMatch, skipMatch }) {
|
|
833
|
+
const testNameMatchText = [];
|
|
834
|
+
if (onlyMatch != null) {
|
|
835
|
+
testNameMatchText.push(jsx(Text, { children: [jsx(Text, { color: "90", children: "matching " }), jsx(MatchText, { text: onlyMatch })] }));
|
|
1448
836
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
837
|
+
if (skipMatch != null) {
|
|
838
|
+
testNameMatchText.push(jsx(Text, { children: [onlyMatch == null ? undefined : jsx(Text, { color: "90", children: " and " }), jsx(Text, { color: "90", children: "not matching " }), jsx(MatchText, { text: skipMatch })] }));
|
|
1451
839
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
`Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
|
|
1456
|
-
];
|
|
1457
|
-
if (resolvedConfig.pathMatch.length > 0) {
|
|
1458
|
-
text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
|
|
1459
|
-
}
|
|
1460
|
-
return text;
|
|
840
|
+
let pathMatchText;
|
|
841
|
+
if (pathMatch.length > 0) {
|
|
842
|
+
pathMatchText = (jsx(Text, { children: [jsx(Text, { color: "90", children: "test files matching " }), jsx(MatchText, { text: pathMatch }), jsx(Text, { color: "90", children: "." })] }));
|
|
1461
843
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
"No test files were left to run using current configuration.",
|
|
1465
|
-
...OptionDiagnosticText.#pathSelectOptions(resolvedConfig),
|
|
1466
|
-
];
|
|
844
|
+
else {
|
|
845
|
+
pathMatchText = jsx(Text, { color: "90", children: "all test files." });
|
|
1467
846
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
847
|
+
return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Ran " }), testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: "tests " }) : undefined, testNameMatchText, testNameMatchText.length > 0 ? jsx(Text, { color: "90", children: " in " }) : undefined, pathMatchText] }));
|
|
848
|
+
}
|
|
849
|
+
function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
|
|
850
|
+
const targetCountText = (jsx(RowText, { label: "Targets", text: jsx(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
|
|
851
|
+
const fileCountText = (jsx(RowText, { label: "Test files", text: jsx(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
|
|
852
|
+
const testCountText = (jsx(RowText, { label: "Tests", text: jsx(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
|
|
853
|
+
const assertionCountText = (jsx(RowText, { label: "Assertions", text: jsx(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
|
|
854
|
+
return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { duration: duration / 1000 }) }), jsx(Line, {}), jsx(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })] }));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function StatusText({ status }) {
|
|
858
|
+
switch (status) {
|
|
859
|
+
case "fail":
|
|
860
|
+
return jsx(Text, { color: "31", children: "\u00D7" });
|
|
861
|
+
case "pass":
|
|
862
|
+
return jsx(Text, { color: "32", children: "+" });
|
|
863
|
+
case "skip":
|
|
864
|
+
return jsx(Text, { color: "33", children: "- skip" });
|
|
865
|
+
case "todo":
|
|
866
|
+
return jsx(Text, { color: "35", children: "- todo" });
|
|
1473
867
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
868
|
+
}
|
|
869
|
+
function testNameText(status, name, indent = 0) {
|
|
870
|
+
return (jsx(Line, { indent: indent + 1, children: [jsx(StatusText, { status: status }), " ", jsx(Text, { color: "90", children: name })] }));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) {
|
|
874
|
+
let projectPathText;
|
|
875
|
+
if (tsconfigFilePath != null) {
|
|
876
|
+
projectPathText = (jsx(Text, { color: "90", children: [" with ", Path.relative("", tsconfigFilePath)] }));
|
|
1481
877
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
878
|
+
return (jsx(Text, { children: [options?.prependEmptyLine === true ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectPathText] }), jsx(Line, {})] }));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function waitingForFileChangesText() {
|
|
882
|
+
return jsx(Line, { children: "Waiting for file changes." });
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function watchUsageText() {
|
|
886
|
+
const usage = [
|
|
887
|
+
["a", "to run all tests."],
|
|
888
|
+
["x", "to exit."],
|
|
889
|
+
];
|
|
890
|
+
const usageText = usage.map(([keyText, actionText]) => {
|
|
891
|
+
return (jsx(Line, { children: [jsx(Text, { color: "90", children: "Press" }), jsx(Text, { children: ` ${keyText} ` }), jsx(Text, { color: "90", children: actionText })] }));
|
|
892
|
+
});
|
|
893
|
+
return jsx(Text, { children: usageText });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
class FileViewService {
|
|
897
|
+
#indent = 0;
|
|
898
|
+
#lines = [];
|
|
899
|
+
#messages = [];
|
|
900
|
+
get hasErrors() {
|
|
901
|
+
return this.#messages.length > 0;
|
|
1487
902
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
return `Option '${optionName}' requires a value of type ${optionBrand}.`;
|
|
903
|
+
addMessage(message) {
|
|
904
|
+
this.#messages.push(message);
|
|
1491
905
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
906
|
+
addTest(status, name) {
|
|
907
|
+
this.#lines.push(testNameText(status, name, this.#indent));
|
|
1494
908
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
return `TypeScript version '${value}' is not supported.`;
|
|
909
|
+
beginDescribe(name) {
|
|
910
|
+
this.#lines.push(describeNameText(name, this.#indent));
|
|
911
|
+
this.#indent++;
|
|
1500
912
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
913
|
+
clear() {
|
|
914
|
+
this.#indent = 0;
|
|
915
|
+
this.#lines = [];
|
|
916
|
+
this.#messages = [];
|
|
917
|
+
}
|
|
918
|
+
endDescribe() {
|
|
919
|
+
this.#indent--;
|
|
920
|
+
}
|
|
921
|
+
getMessages() {
|
|
922
|
+
return this.#messages;
|
|
1503
923
|
}
|
|
1504
|
-
|
|
1505
|
-
return
|
|
924
|
+
getViewText(options) {
|
|
925
|
+
return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
|
|
1506
926
|
}
|
|
1507
927
|
}
|
|
1508
928
|
|
|
1509
|
-
class
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
this.#optionGroup = optionGroup;
|
|
1514
|
-
this.#storeService = storeService;
|
|
929
|
+
class Reporter {
|
|
930
|
+
outputService;
|
|
931
|
+
constructor(outputService) {
|
|
932
|
+
this.outputService = outputService;
|
|
1515
933
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
class RunReporter extends Reporter {
|
|
937
|
+
#currentCompilerVersion;
|
|
938
|
+
#currentProjectConfigFilePath;
|
|
939
|
+
#fileCount = 0;
|
|
940
|
+
#fileView = new FileViewService();
|
|
941
|
+
#hasReportedAdds = false;
|
|
942
|
+
#hasReportedError = false;
|
|
943
|
+
#isFileViewExpanded = false;
|
|
944
|
+
#resolvedConfig;
|
|
945
|
+
#seenDeprecations = new Set();
|
|
946
|
+
constructor(resolvedConfig, outputService) {
|
|
947
|
+
super(outputService);
|
|
948
|
+
this.#resolvedConfig = resolvedConfig;
|
|
949
|
+
}
|
|
950
|
+
get #isLastFile() {
|
|
951
|
+
return this.#fileCount === 0;
|
|
952
|
+
}
|
|
953
|
+
handleEvent([eventName, payload]) {
|
|
954
|
+
switch (eventName) {
|
|
955
|
+
case "deprecation:info": {
|
|
956
|
+
for (const diagnostic of payload.diagnostics) {
|
|
957
|
+
if (!this.#seenDeprecations.has(diagnostic.text.toString())) {
|
|
958
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
959
|
+
this.#seenDeprecations.add(diagnostic.text.toString());
|
|
1530
960
|
}
|
|
1531
961
|
}
|
|
1532
962
|
break;
|
|
1533
963
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
964
|
+
case "run:start": {
|
|
965
|
+
this.#isFileViewExpanded = payload.result.testFiles.length === 1 && this.#resolvedConfig.watch !== true;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
case "store:info": {
|
|
969
|
+
this.outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
970
|
+
this.#hasReportedAdds = true;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case "store:error": {
|
|
974
|
+
for (const diagnostic of payload.diagnostics) {
|
|
975
|
+
this.outputService.writeError(diagnosticText(diagnostic));
|
|
976
|
+
}
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
case "target:start": {
|
|
980
|
+
this.#fileCount = payload.result.testFiles.length;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
case "target:end": {
|
|
984
|
+
this.#currentCompilerVersion = undefined;
|
|
985
|
+
this.#currentProjectConfigFilePath = undefined;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
case "project:info": {
|
|
989
|
+
if (this.#currentCompilerVersion !== payload.compilerVersion ||
|
|
990
|
+
this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) {
|
|
991
|
+
this.outputService.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, {
|
|
992
|
+
prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError,
|
|
993
|
+
}));
|
|
994
|
+
this.#hasReportedAdds = false;
|
|
995
|
+
this.#currentCompilerVersion = payload.compilerVersion;
|
|
996
|
+
this.#currentProjectConfigFilePath = payload.projectConfigFilePath;
|
|
1558
997
|
}
|
|
1559
998
|
break;
|
|
1560
999
|
}
|
|
1561
|
-
case "
|
|
1562
|
-
|
|
1563
|
-
this
|
|
1564
|
-
OptionDiagnosticText.versionIsNotSupported(optionValue),
|
|
1565
|
-
...(await this.#optionUsageText.get(optionName, optionBrand)),
|
|
1566
|
-
], origin));
|
|
1000
|
+
case "project:error": {
|
|
1001
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1002
|
+
this.outputService.writeError(diagnosticText(diagnostic));
|
|
1567
1003
|
}
|
|
1568
1004
|
break;
|
|
1569
1005
|
}
|
|
1570
|
-
case "
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.testFileMatchCannotStartWith(segment), origin));
|
|
1574
|
-
}
|
|
1006
|
+
case "file:start": {
|
|
1007
|
+
if (!Environment.noInteractive) {
|
|
1008
|
+
this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
1575
1009
|
}
|
|
1010
|
+
this.#fileCount--;
|
|
1011
|
+
this.#hasReportedError = false;
|
|
1576
1012
|
break;
|
|
1577
1013
|
}
|
|
1578
|
-
case "
|
|
1579
|
-
|
|
1580
|
-
this.#
|
|
1014
|
+
case "file:error": {
|
|
1015
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1016
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
1581
1017
|
}
|
|
1582
1018
|
break;
|
|
1583
1019
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
class CommandLineOptionsWorker {
|
|
1589
|
-
#commandLineOptionDefinitions;
|
|
1590
|
-
#commandLineOptions;
|
|
1591
|
-
#onDiagnostic;
|
|
1592
|
-
#optionGroup = 2;
|
|
1593
|
-
#optionUsageText;
|
|
1594
|
-
#optionValidator;
|
|
1595
|
-
#pathMatch;
|
|
1596
|
-
#storeService;
|
|
1597
|
-
constructor(commandLineOptions, pathMatch, storeService, onDiagnostic) {
|
|
1598
|
-
this.#commandLineOptions = commandLineOptions;
|
|
1599
|
-
this.#pathMatch = pathMatch;
|
|
1600
|
-
this.#storeService = storeService;
|
|
1601
|
-
this.#onDiagnostic = onDiagnostic;
|
|
1602
|
-
this.#commandLineOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
|
|
1603
|
-
this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
|
|
1604
|
-
this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
|
|
1605
|
-
}
|
|
1606
|
-
async #onExpectsValue(optionDefinition) {
|
|
1607
|
-
const text = [
|
|
1608
|
-
OptionDiagnosticText.expectsValue(optionDefinition.name, this.#optionGroup),
|
|
1609
|
-
...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
|
|
1610
|
-
];
|
|
1611
|
-
this.#onDiagnostic(Diagnostic.error(text));
|
|
1612
|
-
}
|
|
1613
|
-
async parse(commandLineArgs) {
|
|
1614
|
-
let index = 0;
|
|
1615
|
-
let arg = commandLineArgs[index];
|
|
1616
|
-
while (arg != null) {
|
|
1617
|
-
index++;
|
|
1618
|
-
if (arg.startsWith("--")) {
|
|
1619
|
-
const optionName = arg.slice(2);
|
|
1620
|
-
const optionDefinition = this.#commandLineOptionDefinitions.get(optionName);
|
|
1621
|
-
if (optionDefinition) {
|
|
1622
|
-
index = await this.#parseOptionValue(commandLineArgs, index, optionDefinition);
|
|
1020
|
+
case "file:end": {
|
|
1021
|
+
if (!Environment.noInteractive) {
|
|
1022
|
+
this.outputService.eraseLastLine();
|
|
1623
1023
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1024
|
+
this.outputService.writeMessage(fileStatusText(payload.result.status, payload.result.testFile));
|
|
1025
|
+
this.outputService.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile }));
|
|
1026
|
+
if (this.#fileView.hasErrors) {
|
|
1027
|
+
this.outputService.writeError(this.#fileView.getMessages());
|
|
1028
|
+
this.#hasReportedError = true;
|
|
1626
1029
|
}
|
|
1030
|
+
this.#fileView.clear();
|
|
1031
|
+
this.#seenDeprecations.clear();
|
|
1032
|
+
break;
|
|
1627
1033
|
}
|
|
1628
|
-
|
|
1629
|
-
this.#
|
|
1034
|
+
case "describe:start": {
|
|
1035
|
+
if (this.#isFileViewExpanded) {
|
|
1036
|
+
this.#fileView.beginDescribe(payload.result.describe.name);
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1630
1039
|
}
|
|
1631
|
-
|
|
1632
|
-
this.#
|
|
1040
|
+
case "describe:end": {
|
|
1041
|
+
if (this.#isFileViewExpanded) {
|
|
1042
|
+
this.#fileView.endDescribe();
|
|
1043
|
+
}
|
|
1044
|
+
break;
|
|
1633
1045
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
let optionValue = this.#resolveOptionValue(commandLineArgs[index]);
|
|
1639
|
-
switch (optionDefinition.brand) {
|
|
1640
|
-
case "bareTrue": {
|
|
1641
|
-
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
1642
|
-
this.#commandLineOptions[optionDefinition.name] = true;
|
|
1046
|
+
case "test:skip": {
|
|
1047
|
+
if (this.#isFileViewExpanded) {
|
|
1048
|
+
this.#fileView.addTest("skip", payload.result.test.name);
|
|
1049
|
+
}
|
|
1643
1050
|
break;
|
|
1644
1051
|
}
|
|
1645
|
-
case "
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
if (optionValue === "false" || optionValue === "true") {
|
|
1649
|
-
index++;
|
|
1052
|
+
case "test:todo": {
|
|
1053
|
+
if (this.#isFileViewExpanded) {
|
|
1054
|
+
this.#fileView.addTest("todo", payload.result.test.name);
|
|
1650
1055
|
}
|
|
1651
1056
|
break;
|
|
1652
1057
|
}
|
|
1653
|
-
case "
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
for (const optionValue of optionValues) {
|
|
1660
|
-
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
1661
|
-
}
|
|
1662
|
-
this.#commandLineOptions[optionDefinition.name] = optionValues;
|
|
1663
|
-
index++;
|
|
1664
|
-
break;
|
|
1058
|
+
case "test:error": {
|
|
1059
|
+
if (this.#isFileViewExpanded) {
|
|
1060
|
+
this.#fileView.addTest("fail", payload.result.test.name);
|
|
1061
|
+
}
|
|
1062
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1063
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
1665
1064
|
}
|
|
1666
|
-
await this.#onExpectsValue(optionDefinition);
|
|
1667
1065
|
break;
|
|
1668
1066
|
}
|
|
1669
|
-
case "
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1067
|
+
case "test:fail": {
|
|
1068
|
+
if (this.#isFileViewExpanded) {
|
|
1069
|
+
this.#fileView.addTest("fail", payload.result.test.name);
|
|
1070
|
+
}
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "test:pass": {
|
|
1074
|
+
if (this.#isFileViewExpanded) {
|
|
1075
|
+
this.#fileView.addTest("pass", payload.result.test.name);
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case "expect:error":
|
|
1080
|
+
case "expect:fail": {
|
|
1081
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1082
|
+
this.#fileView.addMessage(diagnosticText(diagnostic));
|
|
1678
1083
|
}
|
|
1679
|
-
await this.#onExpectsValue(optionDefinition);
|
|
1680
1084
|
break;
|
|
1681
1085
|
}
|
|
1682
1086
|
}
|
|
1683
|
-
return index;
|
|
1684
|
-
}
|
|
1685
|
-
#resolveOptionValue(target = "") {
|
|
1686
|
-
return target.startsWith("-") ? "" : target;
|
|
1687
1087
|
}
|
|
1688
1088
|
}
|
|
1689
1089
|
|
|
1690
|
-
class
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
#configFilePath;
|
|
1695
|
-
#onDiagnostic;
|
|
1696
|
-
#optionGroup = 4;
|
|
1697
|
-
#optionValidator;
|
|
1698
|
-
#storeService;
|
|
1699
|
-
constructor(compiler, configFileOptions, configFilePath, storeService, onDiagnostic) {
|
|
1700
|
-
this.#compiler = compiler;
|
|
1701
|
-
this.#configFileOptions = configFileOptions;
|
|
1702
|
-
this.#configFilePath = configFilePath;
|
|
1703
|
-
this.#storeService = storeService;
|
|
1704
|
-
this.#onDiagnostic = onDiagnostic;
|
|
1705
|
-
this.#configFileOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
|
|
1706
|
-
this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
|
|
1707
|
-
}
|
|
1708
|
-
#isDoubleQuotedString(node, sourceFile) {
|
|
1709
|
-
return (node.kind === this.#compiler.SyntaxKind.StringLiteral &&
|
|
1710
|
-
sourceFile.text.slice(this.#skipTrivia(node.pos, sourceFile), node.end).startsWith('"'));
|
|
1711
|
-
}
|
|
1712
|
-
async parse(sourceText) {
|
|
1713
|
-
const sourceFile = this.#compiler.parseJsonText(this.#configFilePath, sourceText);
|
|
1714
|
-
if (sourceFile.parseDiagnostics.length > 0) {
|
|
1715
|
-
for (const diagnostic of Diagnostic.fromDiagnostics(sourceFile.parseDiagnostics, this.#compiler)) {
|
|
1716
|
-
this.#onDiagnostic(diagnostic);
|
|
1717
|
-
}
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
const rootExpression = sourceFile.statements[0]?.expression;
|
|
1721
|
-
if (rootExpression == null || !this.#compiler.isObjectLiteralExpression(rootExpression)) {
|
|
1722
|
-
const origin = new DiagnosticOrigin(0, 0, sourceFile);
|
|
1723
|
-
this.#onDiagnostic(Diagnostic.error("The root value of a configuration file must be an object literal.", origin));
|
|
1090
|
+
class SetupReporter extends Reporter {
|
|
1091
|
+
handleEvent([eventName, payload]) {
|
|
1092
|
+
if (eventName === "store:info") {
|
|
1093
|
+
this.outputService.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath));
|
|
1724
1094
|
return;
|
|
1725
1095
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const optionDefinition = this.#configFileOptionDefinitions.get(optionName);
|
|
1738
|
-
if (optionDefinition) {
|
|
1739
|
-
this.#configFileOptions[optionDefinition.name] = await this.#parseOptionValue(sourceFile, property.initializer, optionDefinition);
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
|
|
1743
|
-
this.#onDiagnostic(Diagnostic.error(OptionDiagnosticText.unknownOption(optionName), origin));
|
|
1096
|
+
if ("diagnostics" in payload) {
|
|
1097
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1098
|
+
switch (diagnostic.category) {
|
|
1099
|
+
case "error": {
|
|
1100
|
+
this.outputService.writeError(diagnosticText(diagnostic));
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case "warning": {
|
|
1104
|
+
this.outputService.writeWarning(diagnosticText(diagnostic));
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1744
1107
|
}
|
|
1745
1108
|
}
|
|
1746
1109
|
}
|
|
1747
|
-
return;
|
|
1748
1110
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
class SummaryReporter extends Reporter {
|
|
1114
|
+
handleEvent([eventName, payload]) {
|
|
1115
|
+
switch (eventName) {
|
|
1116
|
+
case "run:end": {
|
|
1117
|
+
this.outputService.writeMessage(summaryText({
|
|
1118
|
+
duration: payload.result.timing.duration,
|
|
1119
|
+
expectCount: payload.result.expectCount,
|
|
1120
|
+
fileCount: payload.result.fileCount,
|
|
1121
|
+
onlyMatch: payload.result.resolvedConfig.only,
|
|
1122
|
+
pathMatch: payload.result.resolvedConfig.pathMatch,
|
|
1123
|
+
skipMatch: payload.result.resolvedConfig.skip,
|
|
1124
|
+
targetCount: payload.result.targetCount,
|
|
1125
|
+
testCount: payload.result.testCount,
|
|
1126
|
+
}));
|
|
1761
1127
|
break;
|
|
1762
1128
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
value = Path.resolve(Path.dirname(this.#configFilePath), value);
|
|
1773
|
-
}
|
|
1774
|
-
const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
|
|
1775
|
-
await this.#optionValidator.check(optionDefinition.name, value, optionDefinition.brand, origin);
|
|
1776
|
-
return value;
|
|
1777
|
-
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
class WatchReporter extends Reporter {
|
|
1134
|
+
handleEvent([eventName, payload]) {
|
|
1135
|
+
switch (eventName) {
|
|
1136
|
+
case "run:start": {
|
|
1137
|
+
this.outputService.clearTerminal();
|
|
1778
1138
|
break;
|
|
1779
1139
|
}
|
|
1780
|
-
case
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1140
|
+
case "run:end": {
|
|
1141
|
+
this.outputService.writeMessage(watchUsageText());
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
case "watch:error": {
|
|
1145
|
+
this.outputService.clearTerminal();
|
|
1146
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1147
|
+
this.outputService.writeError(diagnosticText(diagnostic));
|
|
1787
1148
|
}
|
|
1149
|
+
this.outputService.writeMessage(waitingForFileChangesText());
|
|
1788
1150
|
break;
|
|
1789
1151
|
}
|
|
1790
1152
|
}
|
|
1791
|
-
const text = isListItem
|
|
1792
|
-
? OptionDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
|
|
1793
|
-
: OptionDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand, this.#optionGroup);
|
|
1794
|
-
const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
|
|
1795
|
-
this.#onDiagnostic(Diagnostic.error(text, origin));
|
|
1796
|
-
return;
|
|
1797
1153
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
class CancellationToken {
|
|
1157
|
+
#isCancelled = false;
|
|
1158
|
+
#handlers = new Set();
|
|
1159
|
+
#reason;
|
|
1160
|
+
get isCancellationRequested() {
|
|
1161
|
+
return this.#isCancelled;
|
|
1803
1162
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1163
|
+
get reason() {
|
|
1164
|
+
return this.#reason;
|
|
1165
|
+
}
|
|
1166
|
+
cancel(reason) {
|
|
1167
|
+
if (!this.#isCancelled) {
|
|
1168
|
+
for (const handler of this.#handlers) {
|
|
1169
|
+
handler(reason);
|
|
1810
1170
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1171
|
+
this.#isCancelled = true;
|
|
1172
|
+
this.#reason = reason;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
onCancellationRequested(handler) {
|
|
1176
|
+
this.#handlers.add(handler);
|
|
1177
|
+
}
|
|
1178
|
+
reset() {
|
|
1179
|
+
if (this.#isCancelled) {
|
|
1180
|
+
this.#isCancelled = false;
|
|
1181
|
+
this.#reason = undefined;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
var CancellationReason;
|
|
1187
|
+
(function (CancellationReason) {
|
|
1188
|
+
CancellationReason["ConfigChange"] = "configChange";
|
|
1189
|
+
CancellationReason["ConfigError"] = "configError";
|
|
1190
|
+
CancellationReason["FailFast"] = "failFast";
|
|
1191
|
+
})(CancellationReason || (CancellationReason = {}));
|
|
1192
|
+
|
|
1193
|
+
class Watcher {
|
|
1194
|
+
#abortController = new AbortController();
|
|
1195
|
+
#onChanged;
|
|
1196
|
+
#onRemoved;
|
|
1197
|
+
#recursive;
|
|
1198
|
+
#targetPath;
|
|
1199
|
+
#watcher;
|
|
1200
|
+
constructor(targetPath, onChanged, onRemoved, options) {
|
|
1201
|
+
this.#targetPath = targetPath;
|
|
1202
|
+
this.#onChanged = onChanged;
|
|
1203
|
+
this.#onRemoved = onRemoved ?? onChanged;
|
|
1204
|
+
this.#recursive = options?.recursive;
|
|
1205
|
+
}
|
|
1206
|
+
close() {
|
|
1207
|
+
this.#abortController.abort();
|
|
1208
|
+
}
|
|
1209
|
+
async watch() {
|
|
1210
|
+
this.#watcher = fs.watch(this.#targetPath, { recursive: this.#recursive, signal: this.#abortController.signal });
|
|
1211
|
+
try {
|
|
1212
|
+
for await (const event of this.#watcher) {
|
|
1213
|
+
if (event.filename != null) {
|
|
1214
|
+
const filePath = Path.resolve(this.#targetPath, event.filename);
|
|
1215
|
+
if (existsSync(filePath)) {
|
|
1216
|
+
await this.#onChanged(filePath);
|
|
1819
1217
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
if (text.charAt(position + 1) === "*") {
|
|
1823
|
-
position += 2;
|
|
1824
|
-
while (position < text.length) {
|
|
1825
|
-
if (text.charAt(position) === "*" && text.charAt(position + 1) === "/") {
|
|
1826
|
-
position += 2;
|
|
1827
|
-
break;
|
|
1828
|
-
}
|
|
1829
|
-
position++;
|
|
1218
|
+
else {
|
|
1219
|
+
await this.#onRemoved(filePath);
|
|
1830
1220
|
}
|
|
1831
|
-
continue;
|
|
1832
1221
|
}
|
|
1833
|
-
position++;
|
|
1834
|
-
continue;
|
|
1835
1222
|
}
|
|
1836
|
-
break;
|
|
1837
1223
|
}
|
|
1838
|
-
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
if (error instanceof Error && error.name === "AbortError") ;
|
|
1226
|
+
}
|
|
1839
1227
|
}
|
|
1840
1228
|
}
|
|
1841
1229
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
#compiler;
|
|
1851
|
-
#configFileOptions = {};
|
|
1852
|
-
#configFilePath = Path.resolve(defaultOptions.rootPath, "./tstyche.config.json");
|
|
1853
|
-
#pathMatch = [];
|
|
1854
|
-
#storeService;
|
|
1855
|
-
constructor(compiler, storeService) {
|
|
1856
|
-
this.#compiler = compiler;
|
|
1857
|
-
this.#storeService = storeService;
|
|
1230
|
+
class FileWatcher extends Watcher {
|
|
1231
|
+
constructor(targetPath, onChanged) {
|
|
1232
|
+
const onChangedFile = async (filePath) => {
|
|
1233
|
+
if (filePath === targetPath) {
|
|
1234
|
+
await onChanged();
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
super(Path.dirname(targetPath), onChangedFile);
|
|
1858
1238
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
class DiagnosticOrigin {
|
|
1242
|
+
breadcrumbs;
|
|
1243
|
+
end;
|
|
1244
|
+
sourceFile;
|
|
1245
|
+
start;
|
|
1246
|
+
constructor(start, end, sourceFile, breadcrumbs) {
|
|
1247
|
+
this.start = start;
|
|
1248
|
+
this.end = end;
|
|
1249
|
+
this.sourceFile = sourceFile;
|
|
1250
|
+
this.breadcrumbs = breadcrumbs;
|
|
1861
1251
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
this.#pathMatch = [];
|
|
1865
|
-
const commandLineWorker = new CommandLineOptionsWorker(this.#commandLineOptions, this.#pathMatch, this.#storeService, this.#onDiagnostic);
|
|
1866
|
-
await commandLineWorker.parse(commandLineArgs);
|
|
1867
|
-
if (this.#commandLineOptions.config != null) {
|
|
1868
|
-
this.#configFilePath = this.#commandLineOptions.config;
|
|
1869
|
-
delete this.#commandLineOptions.config;
|
|
1870
|
-
}
|
|
1252
|
+
static fromJsonNode(node, sourceFile, skipTrivia) {
|
|
1253
|
+
return new DiagnosticOrigin(skipTrivia(node.pos, sourceFile), node.end, sourceFile);
|
|
1871
1254
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1255
|
+
static fromNode(node, breadcrumbs) {
|
|
1256
|
+
return new DiagnosticOrigin(node.getStart(), node.getEnd(), node.getSourceFile(), breadcrumbs);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
class Diagnostic {
|
|
1261
|
+
category;
|
|
1262
|
+
code;
|
|
1263
|
+
related;
|
|
1264
|
+
origin;
|
|
1265
|
+
text;
|
|
1266
|
+
constructor(text, category, origin) {
|
|
1267
|
+
this.text = text;
|
|
1268
|
+
this.category = category;
|
|
1269
|
+
this.origin = origin;
|
|
1270
|
+
}
|
|
1271
|
+
add(options) {
|
|
1272
|
+
if (options.code != null) {
|
|
1273
|
+
this.code = options.code;
|
|
1878
1274
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1275
|
+
if (options.origin != null) {
|
|
1276
|
+
this.origin = options.origin;
|
|
1277
|
+
}
|
|
1278
|
+
if (options.related != null) {
|
|
1279
|
+
this.related = options.related;
|
|
1280
|
+
}
|
|
1281
|
+
return this;
|
|
1282
|
+
}
|
|
1283
|
+
static error(text, origin) {
|
|
1284
|
+
return new Diagnostic(text, "error", origin);
|
|
1285
|
+
}
|
|
1286
|
+
static fromDiagnostics(diagnostics, compiler) {
|
|
1287
|
+
return diagnostics.map((diagnostic) => {
|
|
1288
|
+
const category = "error";
|
|
1289
|
+
const code = `ts(${String(diagnostic.code)})`;
|
|
1290
|
+
let origin;
|
|
1291
|
+
const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
1292
|
+
if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) {
|
|
1293
|
+
origin = new DiagnosticOrigin(diagnostic.start, diagnostic.start + diagnostic.length, diagnostic.file);
|
|
1294
|
+
}
|
|
1295
|
+
return new Diagnostic(text, category, origin).add({ code });
|
|
1881
1296
|
});
|
|
1882
|
-
const configFileWorker = new ConfigFileOptionsWorker(this.#compiler, this.#configFileOptions, this.#configFilePath, this.#storeService, this.#onDiagnostic);
|
|
1883
|
-
await configFileWorker.parse(configFileText);
|
|
1884
1297
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1298
|
+
static fromError(text, error) {
|
|
1299
|
+
const messageText = Array.isArray(text) ? text : [text];
|
|
1300
|
+
if (error instanceof Error && error.stack != null) {
|
|
1301
|
+
if (messageText.length > 1) {
|
|
1302
|
+
messageText.push("");
|
|
1303
|
+
}
|
|
1304
|
+
const stackLines = error.stack.split("\n").map((line) => line.trimStart());
|
|
1305
|
+
messageText.push(...stackLines);
|
|
1306
|
+
}
|
|
1307
|
+
return Diagnostic.error(messageText);
|
|
1308
|
+
}
|
|
1309
|
+
static isTsDiagnosticWithLocation(diagnostic) {
|
|
1310
|
+
return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null;
|
|
1311
|
+
}
|
|
1312
|
+
static warning(text, origin) {
|
|
1313
|
+
return new Diagnostic(text, "warning", origin);
|
|
1893
1314
|
}
|
|
1894
1315
|
}
|
|
1895
1316
|
|
|
1896
|
-
var
|
|
1897
|
-
(function (
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
OptionBrand["BareTrue"] = "bareTrue";
|
|
1902
|
-
OptionBrand["List"] = "list";
|
|
1903
|
-
})(OptionBrand || (OptionBrand = {}));
|
|
1904
|
-
var OptionGroup;
|
|
1905
|
-
(function (OptionGroup) {
|
|
1906
|
-
OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
|
|
1907
|
-
OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
|
|
1908
|
-
})(OptionGroup || (OptionGroup = {}));
|
|
1317
|
+
var DiagnosticCategory;
|
|
1318
|
+
(function (DiagnosticCategory) {
|
|
1319
|
+
DiagnosticCategory["Error"] = "error";
|
|
1320
|
+
DiagnosticCategory["Warning"] = "warning";
|
|
1321
|
+
})(DiagnosticCategory || (DiagnosticCategory = {}));
|
|
1909
1322
|
|
|
1910
1323
|
class InputService {
|
|
1911
1324
|
#onInput;
|
|
@@ -1914,6 +1327,7 @@ class InputService {
|
|
|
1914
1327
|
this.#onInput = onInput;
|
|
1915
1328
|
this.#stdin = options?.stdin ?? process.stdin;
|
|
1916
1329
|
this.#stdin.setRawMode?.(true);
|
|
1330
|
+
this.#stdin.setEncoding("utf8");
|
|
1917
1331
|
this.#stdin.unref();
|
|
1918
1332
|
this.#stdin.addListener("data", this.#onInput);
|
|
1919
1333
|
}
|
|
@@ -1923,6 +1337,142 @@ class InputService {
|
|
|
1923
1337
|
}
|
|
1924
1338
|
}
|
|
1925
1339
|
|
|
1340
|
+
class SelectDiagnosticText {
|
|
1341
|
+
static #pathSelectOptions(resolvedConfig) {
|
|
1342
|
+
const text = [
|
|
1343
|
+
`Root path: ${resolvedConfig.rootPath}`,
|
|
1344
|
+
`Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
|
|
1345
|
+
];
|
|
1346
|
+
if (resolvedConfig.pathMatch.length > 0) {
|
|
1347
|
+
text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
|
|
1348
|
+
}
|
|
1349
|
+
return text;
|
|
1350
|
+
}
|
|
1351
|
+
static noTestFilesWereLeft(resolvedConfig) {
|
|
1352
|
+
return [
|
|
1353
|
+
"No test files were left to run using current configuration.",
|
|
1354
|
+
...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
|
|
1355
|
+
];
|
|
1356
|
+
}
|
|
1357
|
+
static noTestFilesWereSelected(resolvedConfig) {
|
|
1358
|
+
return [
|
|
1359
|
+
"No test files were selected using current configuration.",
|
|
1360
|
+
...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
|
|
1361
|
+
];
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
class GlobPattern {
|
|
1366
|
+
static #reservedCharacterRegex = /[^\w\s/]/g;
|
|
1367
|
+
static #parse(pattern, usageTarget) {
|
|
1368
|
+
const segments = pattern.split("/");
|
|
1369
|
+
let resultPattern = "\\.";
|
|
1370
|
+
let optionalSegmentCount = 0;
|
|
1371
|
+
for (const segment of segments) {
|
|
1372
|
+
if (segment === ".") {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (segment === "**") {
|
|
1376
|
+
resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (usageTarget === "directories") {
|
|
1380
|
+
resultPattern += "(";
|
|
1381
|
+
optionalSegmentCount++;
|
|
1382
|
+
}
|
|
1383
|
+
resultPattern += "\\/";
|
|
1384
|
+
const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
|
|
1385
|
+
if (segmentPattern !== segment) {
|
|
1386
|
+
resultPattern += "(?!(node_modules)(\\/|$))";
|
|
1387
|
+
}
|
|
1388
|
+
resultPattern += segmentPattern;
|
|
1389
|
+
}
|
|
1390
|
+
resultPattern += ")?".repeat(optionalSegmentCount);
|
|
1391
|
+
return resultPattern;
|
|
1392
|
+
}
|
|
1393
|
+
static #replaceReservedCharacter(match, offset) {
|
|
1394
|
+
switch (match) {
|
|
1395
|
+
case "*":
|
|
1396
|
+
return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
|
|
1397
|
+
case "?":
|
|
1398
|
+
return offset === 0 ? "[^./]" : "[^/]";
|
|
1399
|
+
default:
|
|
1400
|
+
return `\\${match}`;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
static toRegex(patterns, usageTarget) {
|
|
1404
|
+
const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, usageTarget)})`).join("|");
|
|
1405
|
+
return new RegExp(`^(${patternText})$`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
class SelectService {
|
|
1410
|
+
#includeDirectoryRegex;
|
|
1411
|
+
#includeFileRegex;
|
|
1412
|
+
#resolvedConfig;
|
|
1413
|
+
constructor(resolvedConfig) {
|
|
1414
|
+
this.#resolvedConfig = resolvedConfig;
|
|
1415
|
+
this.#includeDirectoryRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "directories");
|
|
1416
|
+
this.#includeFileRegex = GlobPattern.toRegex(resolvedConfig.testFileMatch, "files");
|
|
1417
|
+
}
|
|
1418
|
+
#isDirectoryIncluded(directoryPath) {
|
|
1419
|
+
return this.#includeDirectoryRegex.test(directoryPath);
|
|
1420
|
+
}
|
|
1421
|
+
#isFileIncluded(filePath) {
|
|
1422
|
+
if (this.#resolvedConfig.pathMatch.length > 0 &&
|
|
1423
|
+
!this.#resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
return this.#includeFileRegex.test(filePath);
|
|
1427
|
+
}
|
|
1428
|
+
isTestFile(filePath) {
|
|
1429
|
+
return this.#isFileIncluded(Path.relative(this.#resolvedConfig.rootPath, filePath));
|
|
1430
|
+
}
|
|
1431
|
+
#onDiagnostic(diagnostic) {
|
|
1432
|
+
EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
|
|
1433
|
+
}
|
|
1434
|
+
async selectFiles() {
|
|
1435
|
+
const currentPath = ".";
|
|
1436
|
+
const testFilePaths = [];
|
|
1437
|
+
await this.#visitDirectory(currentPath, testFilePaths);
|
|
1438
|
+
if (testFilePaths.length === 0) {
|
|
1439
|
+
this.#onDiagnostic(Diagnostic.error(SelectDiagnosticText.noTestFilesWereSelected(this.#resolvedConfig)));
|
|
1440
|
+
}
|
|
1441
|
+
return testFilePaths.sort();
|
|
1442
|
+
}
|
|
1443
|
+
async #visitDirectory(currentPath, testFilePaths) {
|
|
1444
|
+
const targetPath = Path.join(this.#resolvedConfig.rootPath, currentPath);
|
|
1445
|
+
let entries = [];
|
|
1446
|
+
try {
|
|
1447
|
+
entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
}
|
|
1451
|
+
for (const entry of entries) {
|
|
1452
|
+
let entryMeta;
|
|
1453
|
+
if (entry.isSymbolicLink()) {
|
|
1454
|
+
try {
|
|
1455
|
+
entryMeta = await fs.stat([targetPath, entry.name].join("/"));
|
|
1456
|
+
}
|
|
1457
|
+
catch {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
entryMeta = entry;
|
|
1463
|
+
}
|
|
1464
|
+
const entryPath = [currentPath, entry.name].join("/");
|
|
1465
|
+
if (entryMeta.isDirectory() && this.#isDirectoryIncluded(entryPath)) {
|
|
1466
|
+
await this.#visitDirectory(entryPath, testFilePaths);
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (entryMeta.isFile() && this.#isFileIncluded(entryPath)) {
|
|
1470
|
+
testFilePaths.push([targetPath, entry.name].join("/"));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1926
1476
|
class Timer {
|
|
1927
1477
|
#timeout;
|
|
1928
1478
|
clear() {
|
|
@@ -1952,21 +1502,18 @@ class WatchService {
|
|
|
1952
1502
|
this.#selectService = selectService;
|
|
1953
1503
|
this.#watchedTestFiles = new Map(testFiles.map((testFile) => [testFile.path, testFile]));
|
|
1954
1504
|
const onInput = (chunk) => {
|
|
1955
|
-
switch (chunk.
|
|
1505
|
+
switch (chunk.toLowerCase()) {
|
|
1956
1506
|
case "\u0003":
|
|
1957
1507
|
case "\u0004":
|
|
1958
1508
|
case "\u001B":
|
|
1959
|
-
case "
|
|
1960
|
-
case "
|
|
1961
|
-
case "\u0058":
|
|
1962
|
-
case "\u0078": {
|
|
1509
|
+
case "q":
|
|
1510
|
+
case "x": {
|
|
1963
1511
|
this.close();
|
|
1964
1512
|
break;
|
|
1965
1513
|
}
|
|
1966
1514
|
case "\u000D":
|
|
1967
1515
|
case "\u0020":
|
|
1968
|
-
case "
|
|
1969
|
-
case "\u0061": {
|
|
1516
|
+
case "a": {
|
|
1970
1517
|
this.#runAll();
|
|
1971
1518
|
break;
|
|
1972
1519
|
}
|
|
@@ -2017,7 +1564,7 @@ class WatchService {
|
|
|
2017
1564
|
this.#changedTestFiles.delete(filePath);
|
|
2018
1565
|
this.#watchedTestFiles.delete(filePath);
|
|
2019
1566
|
if (this.#watchedTestFiles.size === 0) {
|
|
2020
|
-
this.#onDiagnostic(Diagnostic.error(
|
|
1567
|
+
this.#onDiagnostic(Diagnostic.error(SelectDiagnosticText.noTestFilesWereLeft(this.#resolvedConfig)));
|
|
2021
1568
|
}
|
|
2022
1569
|
};
|
|
2023
1570
|
this.#watchers.push(new Watcher(this.#resolvedConfig.rootPath, onChangedFile, onRemovedFile, { recursive: true }));
|
|
@@ -3093,294 +2640,743 @@ class TestTreeWorker {
|
|
|
3093
2640
|
if (testResult.expectCount.failed > 0) {
|
|
3094
2641
|
EventEmitter.dispatch(["test:fail", { result: testResult }]);
|
|
3095
2642
|
}
|
|
3096
|
-
else {
|
|
3097
|
-
EventEmitter.dispatch(["test:pass", { result: testResult }]);
|
|
2643
|
+
else {
|
|
2644
|
+
EventEmitter.dispatch(["test:pass", { result: testResult }]);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
class TestFileRunner {
|
|
2650
|
+
#compiler;
|
|
2651
|
+
#collectService;
|
|
2652
|
+
#resolvedConfig;
|
|
2653
|
+
#projectService;
|
|
2654
|
+
constructor(resolvedConfig, compiler) {
|
|
2655
|
+
this.#resolvedConfig = resolvedConfig;
|
|
2656
|
+
this.#compiler = compiler;
|
|
2657
|
+
this.#collectService = new CollectService(compiler);
|
|
2658
|
+
this.#projectService = new ProjectService(compiler);
|
|
2659
|
+
}
|
|
2660
|
+
run(testFile, cancellationToken) {
|
|
2661
|
+
if (cancellationToken?.isCancellationRequested === true) {
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
this.#projectService.openFile(testFile.path, undefined, this.#resolvedConfig.rootPath);
|
|
2665
|
+
const fileResult = new FileResult(testFile);
|
|
2666
|
+
EventEmitter.dispatch(["file:start", { result: fileResult }]);
|
|
2667
|
+
this.#runFile(testFile, fileResult, cancellationToken);
|
|
2668
|
+
EventEmitter.dispatch(["file:end", { result: fileResult }]);
|
|
2669
|
+
this.#projectService.closeFile(testFile.path);
|
|
2670
|
+
}
|
|
2671
|
+
#runFile(testFile, fileResult, cancellationToken) {
|
|
2672
|
+
const languageService = this.#projectService.getLanguageService(testFile.path);
|
|
2673
|
+
if (!languageService) {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
const syntacticDiagnostics = languageService.getSyntacticDiagnostics(testFile.path);
|
|
2677
|
+
if (syntacticDiagnostics.length > 0) {
|
|
2678
|
+
EventEmitter.dispatch([
|
|
2679
|
+
"file:error",
|
|
2680
|
+
{
|
|
2681
|
+
diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics, this.#compiler),
|
|
2682
|
+
result: fileResult,
|
|
2683
|
+
},
|
|
2684
|
+
]);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
const semanticDiagnostics = languageService.getSemanticDiagnostics(testFile.path);
|
|
2688
|
+
const program = languageService.getProgram();
|
|
2689
|
+
if (!program) {
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
const sourceFile = program.getSourceFile(testFile.path);
|
|
2693
|
+
if (!sourceFile) {
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
|
|
2697
|
+
if (testTree.diagnostics.size > 0) {
|
|
2698
|
+
EventEmitter.dispatch([
|
|
2699
|
+
"file:error",
|
|
2700
|
+
{
|
|
2701
|
+
diagnostics: Diagnostic.fromDiagnostics([...testTree.diagnostics], this.#compiler),
|
|
2702
|
+
result: fileResult,
|
|
2703
|
+
},
|
|
2704
|
+
]);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
const typeChecker = program.getTypeChecker();
|
|
2708
|
+
if (!Expect.assertTypeChecker(typeChecker)) {
|
|
2709
|
+
const text = "The required 'isTypeRelatedTo()' method is missing in the provided type checker.";
|
|
2710
|
+
EventEmitter.dispatch(["file:error", { diagnostics: [Diagnostic.error(text)], result: fileResult }]);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
const expect = new Expect(this.#compiler, typeChecker);
|
|
2714
|
+
const testTreeWorker = new TestTreeWorker(this.#resolvedConfig, this.#compiler, expect, {
|
|
2715
|
+
cancellationToken,
|
|
2716
|
+
fileResult,
|
|
2717
|
+
hasOnly: testTree.hasOnly,
|
|
2718
|
+
position: testFile.position,
|
|
2719
|
+
});
|
|
2720
|
+
testTreeWorker.visit(testTree.members, 0, undefined);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
class TaskRunner {
|
|
2725
|
+
#eventEmitter = new EventEmitter();
|
|
2726
|
+
#resolvedConfig;
|
|
2727
|
+
#selectService;
|
|
2728
|
+
#storeService;
|
|
2729
|
+
constructor(resolvedConfig, selectService, storeService) {
|
|
2730
|
+
this.#resolvedConfig = resolvedConfig;
|
|
2731
|
+
this.#selectService = selectService;
|
|
2732
|
+
this.#storeService = storeService;
|
|
2733
|
+
this.#eventEmitter.addHandler(new ResultHandler());
|
|
2734
|
+
}
|
|
2735
|
+
close() {
|
|
2736
|
+
this.#eventEmitter.removeHandlers();
|
|
2737
|
+
}
|
|
2738
|
+
async run(testFiles, cancellationToken = new CancellationToken()) {
|
|
2739
|
+
let cancellationHandler;
|
|
2740
|
+
if (this.#resolvedConfig.failFast) {
|
|
2741
|
+
cancellationHandler = new CancellationHandler(cancellationToken, "failFast");
|
|
2742
|
+
this.#eventEmitter.addHandler(cancellationHandler);
|
|
2743
|
+
}
|
|
2744
|
+
if (this.#resolvedConfig.watch === true) {
|
|
2745
|
+
await this.#watch(testFiles, cancellationToken);
|
|
2746
|
+
}
|
|
2747
|
+
else {
|
|
2748
|
+
await this.#run(testFiles, cancellationToken);
|
|
2749
|
+
}
|
|
2750
|
+
if (cancellationHandler != null) {
|
|
2751
|
+
this.#eventEmitter.removeHandler(cancellationHandler);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
async #run(testFiles, cancellationToken) {
|
|
2755
|
+
const result = new Result(this.#resolvedConfig, testFiles);
|
|
2756
|
+
EventEmitter.dispatch(["run:start", { result }]);
|
|
2757
|
+
for (const versionTag of this.#resolvedConfig.target) {
|
|
2758
|
+
const targetResult = new TargetResult(versionTag, testFiles);
|
|
2759
|
+
EventEmitter.dispatch(["target:start", { result: targetResult }]);
|
|
2760
|
+
const compiler = await this.#storeService.load(versionTag, cancellationToken);
|
|
2761
|
+
if (compiler) {
|
|
2762
|
+
const testFileRunner = new TestFileRunner(this.#resolvedConfig, compiler);
|
|
2763
|
+
for (const testFile of testFiles) {
|
|
2764
|
+
testFileRunner.run(testFile, cancellationToken);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
EventEmitter.dispatch(["target:end", { result: targetResult }]);
|
|
2768
|
+
}
|
|
2769
|
+
EventEmitter.dispatch(["run:end", { result }]);
|
|
2770
|
+
if (cancellationToken?.reason === "failFast") {
|
|
2771
|
+
cancellationToken.reset();
|
|
3098
2772
|
}
|
|
3099
2773
|
}
|
|
2774
|
+
async #watch(testFiles, cancellationToken) {
|
|
2775
|
+
await this.#run(testFiles, cancellationToken);
|
|
2776
|
+
const runCallback = async (testFiles) => {
|
|
2777
|
+
await this.#run(testFiles, cancellationToken);
|
|
2778
|
+
};
|
|
2779
|
+
const watchModeManager = new WatchService(this.#resolvedConfig, runCallback, this.#selectService, testFiles);
|
|
2780
|
+
cancellationToken?.onCancellationRequested((reason) => {
|
|
2781
|
+
if (reason !== "failFast") {
|
|
2782
|
+
watchModeManager.close();
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
await watchModeManager.watch(cancellationToken);
|
|
2786
|
+
}
|
|
3100
2787
|
}
|
|
3101
2788
|
|
|
3102
|
-
class
|
|
3103
|
-
#
|
|
3104
|
-
#
|
|
2789
|
+
class TSTyche {
|
|
2790
|
+
#eventEmitter = new EventEmitter();
|
|
2791
|
+
#outputService;
|
|
3105
2792
|
#resolvedConfig;
|
|
3106
|
-
#
|
|
3107
|
-
|
|
2793
|
+
#selectService;
|
|
2794
|
+
#storeService;
|
|
2795
|
+
#taskRunner;
|
|
2796
|
+
static version = "2.0.0";
|
|
2797
|
+
constructor(resolvedConfig, outputService, selectService, storeService) {
|
|
3108
2798
|
this.#resolvedConfig = resolvedConfig;
|
|
3109
|
-
this.#
|
|
3110
|
-
this.#
|
|
3111
|
-
this.#
|
|
2799
|
+
this.#outputService = outputService;
|
|
2800
|
+
this.#selectService = selectService;
|
|
2801
|
+
this.#storeService = storeService;
|
|
2802
|
+
this.#taskRunner = new TaskRunner(this.#resolvedConfig, this.#selectService, this.#storeService);
|
|
3112
2803
|
}
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
return;
|
|
3116
|
-
}
|
|
3117
|
-
this.#projectService.openFile(testFile.path, undefined, this.#resolvedConfig.rootPath);
|
|
3118
|
-
const fileResult = new FileResult(testFile);
|
|
3119
|
-
EventEmitter.dispatch(["file:start", { result: fileResult }]);
|
|
3120
|
-
this.#runFile(testFile, fileResult, cancellationToken);
|
|
3121
|
-
EventEmitter.dispatch(["file:end", { result: fileResult }]);
|
|
3122
|
-
this.#projectService.closeFile(testFile.path);
|
|
2804
|
+
close() {
|
|
2805
|
+
this.#taskRunner.close();
|
|
3123
2806
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
if (
|
|
3127
|
-
|
|
2807
|
+
async run(testFiles, cancellationToken = new CancellationToken()) {
|
|
2808
|
+
this.#eventEmitter.addHandler(new RunReporter(this.#resolvedConfig, this.#outputService));
|
|
2809
|
+
if (this.#resolvedConfig.watch === true) {
|
|
2810
|
+
this.#eventEmitter.addHandler(new WatchReporter(this.#outputService));
|
|
3128
2811
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
EventEmitter.dispatch([
|
|
3132
|
-
"file:error",
|
|
3133
|
-
{
|
|
3134
|
-
diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics, this.#compiler),
|
|
3135
|
-
result: fileResult,
|
|
3136
|
-
},
|
|
3137
|
-
]);
|
|
3138
|
-
return;
|
|
2812
|
+
else {
|
|
2813
|
+
this.#eventEmitter.addHandler(new SummaryReporter(this.#outputService));
|
|
3139
2814
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
2815
|
+
await this.#taskRunner.run(testFiles.map((testFile) => new TestFile(testFile)), cancellationToken);
|
|
2816
|
+
this.#eventEmitter.removeHandlers();
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
class ConfigDiagnosticText {
|
|
2821
|
+
static doubleQuotesExpected() {
|
|
2822
|
+
return "String literal with double quotes expected.";
|
|
2823
|
+
}
|
|
2824
|
+
static expectsListItemType(optionName, optionBrand) {
|
|
2825
|
+
return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
|
|
2826
|
+
}
|
|
2827
|
+
static expectsValue(optionName, optionGroup) {
|
|
2828
|
+
optionName = ConfigDiagnosticText.#optionName(optionName, optionGroup);
|
|
2829
|
+
return `Option '${optionName}' expects a value.`;
|
|
2830
|
+
}
|
|
2831
|
+
static fileDoesNotExist(filePath) {
|
|
2832
|
+
return `The specified path '${filePath}' does not exist.`;
|
|
2833
|
+
}
|
|
2834
|
+
static #optionName(optionName, optionGroup) {
|
|
2835
|
+
switch (optionGroup) {
|
|
2836
|
+
case 2:
|
|
2837
|
+
return `--${optionName}`;
|
|
2838
|
+
case 4:
|
|
2839
|
+
return optionName;
|
|
3144
2840
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
2841
|
+
}
|
|
2842
|
+
static testFileMatchCannotStartWith(segment) {
|
|
2843
|
+
return [
|
|
2844
|
+
`A test file match pattern cannot start with '${segment}'.`,
|
|
2845
|
+
"The test files are only collected within the 'rootPath' directory.",
|
|
2846
|
+
];
|
|
2847
|
+
}
|
|
2848
|
+
static requiresValueType(optionName, optionBrand, optionGroup) {
|
|
2849
|
+
optionName = ConfigDiagnosticText.#optionName(optionName, optionGroup);
|
|
2850
|
+
return `Option '${optionName}' requires a value of type ${optionBrand}.`;
|
|
2851
|
+
}
|
|
2852
|
+
static unknownOption(optionName) {
|
|
2853
|
+
return `Unknown option '${optionName}'.`;
|
|
2854
|
+
}
|
|
2855
|
+
static versionIsNotSupported(value) {
|
|
2856
|
+
if (value === "current") {
|
|
2857
|
+
return "Cannot use 'current' as a target. Failed to resolve the path to the currently installed TypeScript module.";
|
|
3148
2858
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
2859
|
+
return `TypeScript version '${value}' is not supported.`;
|
|
2860
|
+
}
|
|
2861
|
+
static watchCannotBeEnabled() {
|
|
2862
|
+
return "The watch mode cannot be enabled in a continuous integration environment.";
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
class OptionDefinitionsMap {
|
|
2867
|
+
static #definitions = [
|
|
2868
|
+
{
|
|
2869
|
+
brand: "string",
|
|
2870
|
+
description: "The path to a TSTyche configuration file.",
|
|
2871
|
+
group: 2,
|
|
2872
|
+
name: "config",
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
brand: "boolean",
|
|
2876
|
+
description: "Stop running tests after the first failed assertion.",
|
|
2877
|
+
group: 4 | 2,
|
|
2878
|
+
name: "failFast",
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
brand: "bareTrue",
|
|
2882
|
+
description: "Print the list of command line options with brief descriptions and exit.",
|
|
2883
|
+
group: 2,
|
|
2884
|
+
name: "help",
|
|
2885
|
+
},
|
|
2886
|
+
{
|
|
2887
|
+
brand: "bareTrue",
|
|
2888
|
+
description: "Install specified versions of the 'typescript' package and exit.",
|
|
2889
|
+
group: 2,
|
|
2890
|
+
name: "install",
|
|
2891
|
+
},
|
|
2892
|
+
{
|
|
2893
|
+
brand: "bareTrue",
|
|
2894
|
+
description: "Print the list of the selected test files and exit.",
|
|
2895
|
+
group: 2,
|
|
2896
|
+
name: "listFiles",
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
brand: "string",
|
|
2900
|
+
description: "Only run tests with matching name.",
|
|
2901
|
+
group: 2,
|
|
2902
|
+
name: "only",
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
brand: "bareTrue",
|
|
2906
|
+
description: "Remove all installed versions of the 'typescript' package and exit.",
|
|
2907
|
+
group: 2,
|
|
2908
|
+
name: "prune",
|
|
2909
|
+
},
|
|
2910
|
+
{
|
|
2911
|
+
brand: "string",
|
|
2912
|
+
description: "The path to a directory containing files of a test project.",
|
|
2913
|
+
group: 4,
|
|
2914
|
+
name: "rootPath",
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
brand: "bareTrue",
|
|
2918
|
+
description: "Print the resolved configuration and exit.",
|
|
2919
|
+
group: 2,
|
|
2920
|
+
name: "showConfig",
|
|
2921
|
+
},
|
|
2922
|
+
{
|
|
2923
|
+
brand: "string",
|
|
2924
|
+
description: "Skip tests with matching name.",
|
|
2925
|
+
group: 2,
|
|
2926
|
+
name: "skip",
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
brand: "list",
|
|
2930
|
+
description: "The list of TypeScript versions to be tested on.",
|
|
2931
|
+
group: 2 | 4,
|
|
2932
|
+
items: {
|
|
2933
|
+
brand: "string",
|
|
2934
|
+
name: "target",
|
|
2935
|
+
pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
|
|
2936
|
+
},
|
|
2937
|
+
name: "target",
|
|
2938
|
+
},
|
|
2939
|
+
{
|
|
2940
|
+
brand: "list",
|
|
2941
|
+
description: "The list of glob patterns matching the test files.",
|
|
2942
|
+
group: 4,
|
|
2943
|
+
items: {
|
|
2944
|
+
brand: "string",
|
|
2945
|
+
name: "testFileMatch",
|
|
2946
|
+
},
|
|
2947
|
+
name: "testFileMatch",
|
|
2948
|
+
},
|
|
2949
|
+
{
|
|
2950
|
+
brand: "bareTrue",
|
|
2951
|
+
description: "Fetch the 'typescript' package metadata from the registry and exit.",
|
|
2952
|
+
group: 2,
|
|
2953
|
+
name: "update",
|
|
2954
|
+
},
|
|
2955
|
+
{
|
|
2956
|
+
brand: "bareTrue",
|
|
2957
|
+
description: "Print the version number and exit.",
|
|
2958
|
+
group: 2,
|
|
2959
|
+
name: "version",
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
brand: "bareTrue",
|
|
2963
|
+
description: "Watch for changes and rerun related test files.",
|
|
2964
|
+
group: 2,
|
|
2965
|
+
name: "watch",
|
|
2966
|
+
},
|
|
2967
|
+
];
|
|
2968
|
+
static for(optionGroup) {
|
|
2969
|
+
const definitionMap = new Map();
|
|
2970
|
+
for (const definition of OptionDefinitionsMap.#definitions) {
|
|
2971
|
+
if (definition.group & optionGroup) {
|
|
2972
|
+
definitionMap.set(definition.name, definition);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
return definitionMap;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
class OptionUsageText {
|
|
2980
|
+
#optionGroup;
|
|
2981
|
+
#storeService;
|
|
2982
|
+
constructor(optionGroup, storeService) {
|
|
2983
|
+
this.#optionGroup = optionGroup;
|
|
2984
|
+
this.#storeService = storeService;
|
|
2985
|
+
}
|
|
2986
|
+
async get(optionName, optionBrand) {
|
|
2987
|
+
const usageText = [];
|
|
2988
|
+
switch (optionName) {
|
|
2989
|
+
case "target": {
|
|
2990
|
+
const supportedTags = await this.#storeService.getSupportedTags();
|
|
2991
|
+
const supportedTagsText = `Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`;
|
|
2992
|
+
switch (this.#optionGroup) {
|
|
2993
|
+
case 2: {
|
|
2994
|
+
usageText.push("Value for the '--target' option must be a single tag or a comma separated list.", "Usage examples: '--target 4.9', '--target latest', '--target 4.9,5.3.2,current'.", supportedTagsText);
|
|
2995
|
+
break;
|
|
2996
|
+
}
|
|
2997
|
+
case 4: {
|
|
2998
|
+
usageText.push("Item of the 'target' list must be a supported version tag.", supportedTagsText);
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
break;
|
|
3003
|
+
}
|
|
3004
|
+
default:
|
|
3005
|
+
usageText.push(ConfigDiagnosticText.requiresValueType(optionName, optionBrand, this.#optionGroup));
|
|
3159
3006
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3007
|
+
return usageText;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
class OptionValidator {
|
|
3012
|
+
#onDiagnostic;
|
|
3013
|
+
#optionGroup;
|
|
3014
|
+
#optionUsageText;
|
|
3015
|
+
#storeService;
|
|
3016
|
+
constructor(optionGroup, storeService, onDiagnostic) {
|
|
3017
|
+
this.#optionGroup = optionGroup;
|
|
3018
|
+
this.#storeService = storeService;
|
|
3019
|
+
this.#onDiagnostic = onDiagnostic;
|
|
3020
|
+
this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
|
|
3021
|
+
}
|
|
3022
|
+
async check(optionName, optionValue, optionBrand, origin) {
|
|
3023
|
+
switch (optionName) {
|
|
3024
|
+
case "config":
|
|
3025
|
+
case "rootPath": {
|
|
3026
|
+
if (!existsSync(optionValue)) {
|
|
3027
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.fileDoesNotExist(optionValue), origin));
|
|
3028
|
+
}
|
|
3029
|
+
break;
|
|
3030
|
+
}
|
|
3031
|
+
case "target": {
|
|
3032
|
+
if ((await this.#storeService.validateTag(optionValue)) === false) {
|
|
3033
|
+
this.#onDiagnostic(Diagnostic.error([
|
|
3034
|
+
ConfigDiagnosticText.versionIsNotSupported(optionValue),
|
|
3035
|
+
...(await this.#optionUsageText.get(optionName, optionBrand)),
|
|
3036
|
+
], origin));
|
|
3037
|
+
}
|
|
3038
|
+
break;
|
|
3039
|
+
}
|
|
3040
|
+
case "testFileMatch": {
|
|
3041
|
+
for (const segment of ["/", "../"]) {
|
|
3042
|
+
if (optionValue.startsWith(segment)) {
|
|
3043
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.testFileMatchCannotStartWith(segment), origin));
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
break;
|
|
3047
|
+
}
|
|
3048
|
+
case "watch": {
|
|
3049
|
+
if (Environment.isCi) {
|
|
3050
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.watchCannotBeEnabled(), origin));
|
|
3051
|
+
}
|
|
3052
|
+
break;
|
|
3053
|
+
}
|
|
3165
3054
|
}
|
|
3166
|
-
const expect = new Expect(this.#compiler, typeChecker);
|
|
3167
|
-
const testTreeWorker = new TestTreeWorker(this.#resolvedConfig, this.#compiler, expect, {
|
|
3168
|
-
cancellationToken,
|
|
3169
|
-
fileResult,
|
|
3170
|
-
hasOnly: testTree.hasOnly,
|
|
3171
|
-
position: testFile.position,
|
|
3172
|
-
});
|
|
3173
|
-
testTreeWorker.visit(testTree.members, 0, undefined);
|
|
3174
3055
|
}
|
|
3175
3056
|
}
|
|
3176
3057
|
|
|
3177
|
-
class
|
|
3178
|
-
#
|
|
3179
|
-
#
|
|
3180
|
-
#
|
|
3058
|
+
class CommandLineOptionsWorker {
|
|
3059
|
+
#commandLineOptionDefinitions;
|
|
3060
|
+
#commandLineOptions;
|
|
3061
|
+
#onDiagnostic;
|
|
3062
|
+
#optionGroup = 2;
|
|
3063
|
+
#optionUsageText;
|
|
3064
|
+
#optionValidator;
|
|
3065
|
+
#pathMatch;
|
|
3181
3066
|
#storeService;
|
|
3182
|
-
constructor(
|
|
3183
|
-
this.#
|
|
3184
|
-
this.#
|
|
3067
|
+
constructor(commandLineOptions, pathMatch, storeService, onDiagnostic) {
|
|
3068
|
+
this.#commandLineOptions = commandLineOptions;
|
|
3069
|
+
this.#pathMatch = pathMatch;
|
|
3185
3070
|
this.#storeService = storeService;
|
|
3186
|
-
this.#
|
|
3071
|
+
this.#onDiagnostic = onDiagnostic;
|
|
3072
|
+
this.#commandLineOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
|
|
3073
|
+
this.#optionUsageText = new OptionUsageText(this.#optionGroup, this.#storeService);
|
|
3074
|
+
this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
|
|
3187
3075
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3076
|
+
async #onExpectsValue(optionDefinition) {
|
|
3077
|
+
const text = [
|
|
3078
|
+
ConfigDiagnosticText.expectsValue(optionDefinition.name, this.#optionGroup),
|
|
3079
|
+
...(await this.#optionUsageText.get(optionDefinition.name, optionDefinition.brand)),
|
|
3080
|
+
];
|
|
3081
|
+
this.#onDiagnostic(Diagnostic.error(text));
|
|
3190
3082
|
}
|
|
3191
|
-
async
|
|
3192
|
-
let
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3083
|
+
async parse(commandLineArgs) {
|
|
3084
|
+
let index = 0;
|
|
3085
|
+
let arg = commandLineArgs[index];
|
|
3086
|
+
while (arg != null) {
|
|
3087
|
+
index++;
|
|
3088
|
+
if (arg.startsWith("--")) {
|
|
3089
|
+
const optionName = arg.slice(2);
|
|
3090
|
+
const optionDefinition = this.#commandLineOptionDefinitions.get(optionName);
|
|
3091
|
+
if (optionDefinition) {
|
|
3092
|
+
index = await this.#parseOptionValue(commandLineArgs, index, optionDefinition);
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(arg)));
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
else if (arg.startsWith("-")) {
|
|
3099
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(arg)));
|
|
3100
|
+
}
|
|
3101
|
+
else {
|
|
3102
|
+
this.#pathMatch.push(Path.normalizeSlashes(arg));
|
|
3103
|
+
}
|
|
3104
|
+
arg = commandLineArgs[index];
|
|
3205
3105
|
}
|
|
3206
3106
|
}
|
|
3207
|
-
async #
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3107
|
+
async #parseOptionValue(commandLineArgs, index, optionDefinition) {
|
|
3108
|
+
let optionValue = this.#resolveOptionValue(commandLineArgs[index]);
|
|
3109
|
+
switch (optionDefinition.brand) {
|
|
3110
|
+
case "bareTrue": {
|
|
3111
|
+
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
3112
|
+
this.#commandLineOptions[optionDefinition.name] = true;
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3115
|
+
case "boolean": {
|
|
3116
|
+
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
3117
|
+
this.#commandLineOptions[optionDefinition.name] = optionValue !== "false";
|
|
3118
|
+
if (optionValue === "false" || optionValue === "true") {
|
|
3119
|
+
index++;
|
|
3218
3120
|
}
|
|
3121
|
+
break;
|
|
3122
|
+
}
|
|
3123
|
+
case "list": {
|
|
3124
|
+
if (optionValue !== "") {
|
|
3125
|
+
const optionValues = optionValue
|
|
3126
|
+
.split(",")
|
|
3127
|
+
.map((value) => value.trim())
|
|
3128
|
+
.filter((value) => value !== "");
|
|
3129
|
+
for (const optionValue of optionValues) {
|
|
3130
|
+
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
3131
|
+
}
|
|
3132
|
+
this.#commandLineOptions[optionDefinition.name] = optionValues;
|
|
3133
|
+
index++;
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
3136
|
+
await this.#onExpectsValue(optionDefinition);
|
|
3137
|
+
break;
|
|
3138
|
+
}
|
|
3139
|
+
case "string": {
|
|
3140
|
+
if (optionValue !== "") {
|
|
3141
|
+
if (optionDefinition.name === "config") {
|
|
3142
|
+
optionValue = Path.resolve(optionValue);
|
|
3143
|
+
}
|
|
3144
|
+
await this.#optionValidator.check(optionDefinition.name, optionValue, optionDefinition.brand);
|
|
3145
|
+
this.#commandLineOptions[optionDefinition.name] = optionValue;
|
|
3146
|
+
index++;
|
|
3147
|
+
break;
|
|
3148
|
+
}
|
|
3149
|
+
await this.#onExpectsValue(optionDefinition);
|
|
3150
|
+
break;
|
|
3219
3151
|
}
|
|
3220
|
-
EventEmitter.dispatch(["target:end", { result: targetResult }]);
|
|
3221
|
-
}
|
|
3222
|
-
EventEmitter.dispatch(["run:end", { result }]);
|
|
3223
|
-
if (cancellationToken?.reason === "failFast") {
|
|
3224
|
-
cancellationToken.reset();
|
|
3225
3152
|
}
|
|
3153
|
+
return index;
|
|
3226
3154
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
const runCallback = async (testFiles) => {
|
|
3230
|
-
await this.#run(testFiles, cancellationToken);
|
|
3231
|
-
};
|
|
3232
|
-
const watchModeManager = new WatchService(this.#resolvedConfig, runCallback, this.#selectService, testFiles);
|
|
3233
|
-
cancellationToken?.onCancellationRequested((reason) => {
|
|
3234
|
-
if (reason !== "failFast") {
|
|
3235
|
-
watchModeManager.close();
|
|
3236
|
-
}
|
|
3237
|
-
});
|
|
3238
|
-
await watchModeManager.watch(cancellationToken);
|
|
3155
|
+
#resolveOptionValue(target = "") {
|
|
3156
|
+
return target.startsWith("-") ? "" : target;
|
|
3239
3157
|
}
|
|
3240
3158
|
}
|
|
3241
3159
|
|
|
3242
|
-
class
|
|
3243
|
-
#
|
|
3244
|
-
#
|
|
3245
|
-
#
|
|
3246
|
-
#
|
|
3160
|
+
class ConfigFileOptionsWorker {
|
|
3161
|
+
#compiler;
|
|
3162
|
+
#configFileOptionDefinitions;
|
|
3163
|
+
#configFileOptions;
|
|
3164
|
+
#configFilePath;
|
|
3165
|
+
#onDiagnostic;
|
|
3166
|
+
#optionGroup = 4;
|
|
3167
|
+
#optionValidator;
|
|
3247
3168
|
#storeService;
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
this.#
|
|
3252
|
-
this.#outputService = outputService;
|
|
3253
|
-
this.#selectService = selectService;
|
|
3169
|
+
constructor(compiler, configFileOptions, configFilePath, storeService, onDiagnostic) {
|
|
3170
|
+
this.#compiler = compiler;
|
|
3171
|
+
this.#configFileOptions = configFileOptions;
|
|
3172
|
+
this.#configFilePath = configFilePath;
|
|
3254
3173
|
this.#storeService = storeService;
|
|
3255
|
-
this.#
|
|
3174
|
+
this.#onDiagnostic = onDiagnostic;
|
|
3175
|
+
this.#configFileOptionDefinitions = OptionDefinitionsMap.for(this.#optionGroup);
|
|
3176
|
+
this.#optionValidator = new OptionValidator(this.#optionGroup, this.#storeService, this.#onDiagnostic);
|
|
3256
3177
|
}
|
|
3257
|
-
|
|
3258
|
-
this.#
|
|
3178
|
+
#isDoubleQuotedString(node, sourceFile) {
|
|
3179
|
+
return (node.kind === this.#compiler.SyntaxKind.StringLiteral &&
|
|
3180
|
+
sourceFile.text.slice(this.#skipTrivia(node.pos, sourceFile), node.end).startsWith('"'));
|
|
3259
3181
|
}
|
|
3260
|
-
async
|
|
3261
|
-
this.#
|
|
3262
|
-
if (
|
|
3263
|
-
|
|
3182
|
+
async parse(sourceText) {
|
|
3183
|
+
const sourceFile = this.#compiler.parseJsonText(this.#configFilePath, sourceText);
|
|
3184
|
+
if (sourceFile.parseDiagnostics.length > 0) {
|
|
3185
|
+
for (const diagnostic of Diagnostic.fromDiagnostics(sourceFile.parseDiagnostics, this.#compiler)) {
|
|
3186
|
+
this.#onDiagnostic(diagnostic);
|
|
3187
|
+
}
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const rootExpression = sourceFile.statements[0]?.expression;
|
|
3191
|
+
if (rootExpression == null || !this.#compiler.isObjectLiteralExpression(rootExpression)) {
|
|
3192
|
+
const origin = new DiagnosticOrigin(0, 0, sourceFile);
|
|
3193
|
+
this.#onDiagnostic(Diagnostic.error("The root value of a configuration file must be an object literal.", origin));
|
|
3194
|
+
return;
|
|
3264
3195
|
}
|
|
3265
|
-
|
|
3266
|
-
this.#
|
|
3196
|
+
for (const property of rootExpression.properties) {
|
|
3197
|
+
if (this.#compiler.isPropertyAssignment(property)) {
|
|
3198
|
+
if (!this.#isDoubleQuotedString(property.name, sourceFile)) {
|
|
3199
|
+
const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
|
|
3200
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.doubleQuotesExpected(), origin));
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
const optionName = this.#resolvePropertyName(property);
|
|
3204
|
+
if (optionName === "$schema") {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
const optionDefinition = this.#configFileOptionDefinitions.get(optionName);
|
|
3208
|
+
if (optionDefinition) {
|
|
3209
|
+
this.#configFileOptions[optionDefinition.name] = await this.#parseOptionValue(sourceFile, property.initializer, optionDefinition);
|
|
3210
|
+
}
|
|
3211
|
+
else {
|
|
3212
|
+
const origin = DiagnosticOrigin.fromJsonNode(property, sourceFile, this.#skipTrivia);
|
|
3213
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.unknownOption(optionName), origin));
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3267
3216
|
}
|
|
3268
|
-
|
|
3269
|
-
this.#eventEmitter.removeHandlers();
|
|
3217
|
+
return;
|
|
3270
3218
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
let optionalSegmentCount = 0;
|
|
3279
|
-
for (const segment of segments) {
|
|
3280
|
-
if (segment === ".") {
|
|
3281
|
-
continue;
|
|
3219
|
+
async #parseOptionValue(sourceFile, valueExpression, optionDefinition, isListItem = false) {
|
|
3220
|
+
switch (valueExpression.kind) {
|
|
3221
|
+
case this.#compiler.SyntaxKind.TrueKeyword: {
|
|
3222
|
+
if (optionDefinition.brand === "boolean") {
|
|
3223
|
+
return true;
|
|
3224
|
+
}
|
|
3225
|
+
break;
|
|
3282
3226
|
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3227
|
+
case this.#compiler.SyntaxKind.FalseKeyword: {
|
|
3228
|
+
if (optionDefinition.brand === "boolean") {
|
|
3229
|
+
return false;
|
|
3230
|
+
}
|
|
3231
|
+
break;
|
|
3286
3232
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3233
|
+
case this.#compiler.SyntaxKind.StringLiteral: {
|
|
3234
|
+
if (!this.#isDoubleQuotedString(valueExpression, sourceFile)) {
|
|
3235
|
+
const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
|
|
3236
|
+
this.#onDiagnostic(Diagnostic.error(ConfigDiagnosticText.doubleQuotesExpected(), origin));
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
if (optionDefinition.brand === "string") {
|
|
3240
|
+
let value = valueExpression.text;
|
|
3241
|
+
if (optionDefinition.name === "rootPath") {
|
|
3242
|
+
value = Path.resolve(Path.dirname(this.#configFilePath), value);
|
|
3243
|
+
}
|
|
3244
|
+
const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
|
|
3245
|
+
await this.#optionValidator.check(optionDefinition.name, value, optionDefinition.brand, origin);
|
|
3246
|
+
return value;
|
|
3247
|
+
}
|
|
3248
|
+
break;
|
|
3290
3249
|
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3250
|
+
case this.#compiler.SyntaxKind.ArrayLiteralExpression: {
|
|
3251
|
+
if (optionDefinition.brand === "list") {
|
|
3252
|
+
const value = [];
|
|
3253
|
+
for (const element of valueExpression.elements) {
|
|
3254
|
+
value.push(await this.#parseOptionValue(sourceFile, element, optionDefinition.items, true));
|
|
3255
|
+
}
|
|
3256
|
+
return value;
|
|
3257
|
+
}
|
|
3258
|
+
break;
|
|
3295
3259
|
}
|
|
3296
|
-
resultPattern += segmentPattern;
|
|
3297
3260
|
}
|
|
3298
|
-
|
|
3299
|
-
|
|
3261
|
+
const text = isListItem
|
|
3262
|
+
? ConfigDiagnosticText.expectsListItemType(optionDefinition.name, optionDefinition.brand)
|
|
3263
|
+
: ConfigDiagnosticText.requiresValueType(optionDefinition.name, optionDefinition.brand, this.#optionGroup);
|
|
3264
|
+
const origin = DiagnosticOrigin.fromJsonNode(valueExpression, sourceFile, this.#skipTrivia);
|
|
3265
|
+
this.#onDiagnostic(Diagnostic.error(text, origin));
|
|
3266
|
+
return;
|
|
3300
3267
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
|
|
3305
|
-
case "?":
|
|
3306
|
-
return offset === 0 ? "[^./]" : "[^/]";
|
|
3307
|
-
default:
|
|
3308
|
-
return `\\${match}`;
|
|
3268
|
+
#resolvePropertyName({ name }) {
|
|
3269
|
+
if ("text" in name) {
|
|
3270
|
+
return name.text;
|
|
3309
3271
|
}
|
|
3272
|
+
return "";
|
|
3310
3273
|
}
|
|
3311
|
-
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3274
|
+
#skipTrivia(position, sourceFile) {
|
|
3275
|
+
const { text } = sourceFile.getSourceFile();
|
|
3276
|
+
while (position < text.length) {
|
|
3277
|
+
if (/\s/.test(text.charAt(position))) {
|
|
3278
|
+
position++;
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
if (text.charAt(position) === "/") {
|
|
3282
|
+
if (text.charAt(position + 1) === "/") {
|
|
3283
|
+
position += 2;
|
|
3284
|
+
while (position < text.length) {
|
|
3285
|
+
if (text.charAt(position) === "\n") {
|
|
3286
|
+
break;
|
|
3287
|
+
}
|
|
3288
|
+
position++;
|
|
3289
|
+
}
|
|
3290
|
+
continue;
|
|
3291
|
+
}
|
|
3292
|
+
if (text.charAt(position + 1) === "*") {
|
|
3293
|
+
position += 2;
|
|
3294
|
+
while (position < text.length) {
|
|
3295
|
+
if (text.charAt(position) === "*" && text.charAt(position + 1) === "/") {
|
|
3296
|
+
position += 2;
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
position++;
|
|
3300
|
+
}
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
position++;
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
break;
|
|
3307
|
+
}
|
|
3308
|
+
return position;
|
|
3314
3309
|
}
|
|
3315
3310
|
}
|
|
3316
3311
|
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
#
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
#
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
}
|
|
3334
|
-
return this.#includeFileRegex.test(filePath);
|
|
3335
|
-
}
|
|
3336
|
-
isTestFile(filePath) {
|
|
3337
|
-
return this.#isFileIncluded(Path.relative(this.#resolvedConfig.rootPath, filePath));
|
|
3312
|
+
const defaultOptions = {
|
|
3313
|
+
failFast: false,
|
|
3314
|
+
rootPath: "./",
|
|
3315
|
+
target: [Environment.typescriptPath == null ? "latest" : "current"],
|
|
3316
|
+
testFileMatch: ["**/*.tst.*", "**/__typetests__/*.test.*", "**/typetests/*.test.*"],
|
|
3317
|
+
};
|
|
3318
|
+
class ConfigService {
|
|
3319
|
+
#commandLineOptions = {};
|
|
3320
|
+
#compiler;
|
|
3321
|
+
#configFileOptions = {};
|
|
3322
|
+
#configFilePath = Path.resolve(defaultOptions.rootPath, "./tstyche.config.json");
|
|
3323
|
+
#pathMatch = [];
|
|
3324
|
+
#storeService;
|
|
3325
|
+
constructor(compiler, storeService) {
|
|
3326
|
+
this.#compiler = compiler;
|
|
3327
|
+
this.#storeService = storeService;
|
|
3338
3328
|
}
|
|
3339
3329
|
#onDiagnostic(diagnostic) {
|
|
3340
|
-
EventEmitter.dispatch(["
|
|
3330
|
+
EventEmitter.dispatch(["config:error", { diagnostics: [diagnostic] }]);
|
|
3341
3331
|
}
|
|
3342
|
-
async
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3332
|
+
async parseCommandLine(commandLineArgs) {
|
|
3333
|
+
this.#commandLineOptions = {};
|
|
3334
|
+
this.#pathMatch = [];
|
|
3335
|
+
const commandLineWorker = new CommandLineOptionsWorker(this.#commandLineOptions, this.#pathMatch, this.#storeService, this.#onDiagnostic);
|
|
3336
|
+
await commandLineWorker.parse(commandLineArgs);
|
|
3337
|
+
if (this.#commandLineOptions.config != null) {
|
|
3338
|
+
this.#configFilePath = this.#commandLineOptions.config;
|
|
3339
|
+
delete this.#commandLineOptions.config;
|
|
3348
3340
|
}
|
|
3349
|
-
return testFilePaths.sort();
|
|
3350
3341
|
}
|
|
3351
|
-
async
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
catch {
|
|
3358
|
-
}
|
|
3359
|
-
for (const entry of entries) {
|
|
3360
|
-
let entryMeta;
|
|
3361
|
-
if (entry.isSymbolicLink()) {
|
|
3362
|
-
try {
|
|
3363
|
-
entryMeta = await fs.stat([targetPath, entry.name].join("/"));
|
|
3364
|
-
}
|
|
3365
|
-
catch {
|
|
3366
|
-
continue;
|
|
3367
|
-
}
|
|
3368
|
-
}
|
|
3369
|
-
else {
|
|
3370
|
-
entryMeta = entry;
|
|
3371
|
-
}
|
|
3372
|
-
const entryPath = [currentPath, entry.name].join("/");
|
|
3373
|
-
if (entryMeta.isDirectory() && this.#isDirectoryIncluded(entryPath)) {
|
|
3374
|
-
await this.#visitDirectory(entryPath, testFilePaths);
|
|
3375
|
-
continue;
|
|
3376
|
-
}
|
|
3377
|
-
if (entryMeta.isFile() && this.#isFileIncluded(entryPath)) {
|
|
3378
|
-
testFilePaths.push([targetPath, entry.name].join("/"));
|
|
3379
|
-
}
|
|
3342
|
+
async readConfigFile() {
|
|
3343
|
+
this.#configFileOptions = {
|
|
3344
|
+
rootPath: Path.dirname(this.#configFilePath),
|
|
3345
|
+
};
|
|
3346
|
+
if (!existsSync(this.#configFilePath)) {
|
|
3347
|
+
return;
|
|
3380
3348
|
}
|
|
3349
|
+
const configFileText = await fs.readFile(this.#configFilePath, {
|
|
3350
|
+
encoding: "utf8",
|
|
3351
|
+
});
|
|
3352
|
+
const configFileWorker = new ConfigFileOptionsWorker(this.#compiler, this.#configFileOptions, this.#configFilePath, this.#storeService, this.#onDiagnostic);
|
|
3353
|
+
await configFileWorker.parse(configFileText);
|
|
3354
|
+
}
|
|
3355
|
+
resolveConfig() {
|
|
3356
|
+
return {
|
|
3357
|
+
...defaultOptions,
|
|
3358
|
+
...this.#configFileOptions,
|
|
3359
|
+
...this.#commandLineOptions,
|
|
3360
|
+
configFilePath: this.#configFilePath,
|
|
3361
|
+
pathMatch: this.#pathMatch,
|
|
3362
|
+
};
|
|
3381
3363
|
}
|
|
3382
3364
|
}
|
|
3383
3365
|
|
|
3366
|
+
var OptionBrand;
|
|
3367
|
+
(function (OptionBrand) {
|
|
3368
|
+
OptionBrand["String"] = "string";
|
|
3369
|
+
OptionBrand["Number"] = "number";
|
|
3370
|
+
OptionBrand["Boolean"] = "boolean";
|
|
3371
|
+
OptionBrand["BareTrue"] = "bareTrue";
|
|
3372
|
+
OptionBrand["List"] = "list";
|
|
3373
|
+
})(OptionBrand || (OptionBrand = {}));
|
|
3374
|
+
var OptionGroup;
|
|
3375
|
+
(function (OptionGroup) {
|
|
3376
|
+
OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
|
|
3377
|
+
OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
|
|
3378
|
+
})(OptionGroup || (OptionGroup = {}));
|
|
3379
|
+
|
|
3384
3380
|
class StoreDiagnosticText {
|
|
3385
3381
|
static failedToFetchMetadata(registryUrl) {
|
|
3386
3382
|
return `Failed to fetch metadata of the 'typescript' package from '${registryUrl.toString()}'.`;
|
|
@@ -3883,4 +3879,4 @@ class Cli {
|
|
|
3883
3879
|
}
|
|
3884
3880
|
}
|
|
3885
3881
|
|
|
3886
|
-
export { Assertion, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Environment, EventEmitter, ExitCodeHandler, Expect, ExpectResult, FileResult, FileWatcher, InputService, Line, OptionBrand, OptionDefinitionsMap,
|
|
3882
|
+
export { Assertion, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, ConfigDiagnosticText, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Environment, EventEmitter, ExitCodeHandler, Expect, ExpectResult, FileResult, FileWatcher, InputService, Line, OptionBrand, OptionDefinitionsMap, OptionGroup, OutputService, Path, ProjectResult, ProjectService, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, RunReporter, Scribbler, SelectDiagnosticText, SelectService, SetupReporter, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestFile, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, Version, WatchReporter, WatchService, Watcher, addsPackageStepText, defaultOptions, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText, waitingForFileChangesText, watchUsageText };
|