UNPKG

tstyche

Version:

The Essential Type Testing Tool.

1,391 lines (1,354 loc) 141 kB
import process from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import { existsSync, writeFileSync, rmSync } from 'node:fs'; import fs from 'node:fs/promises'; import vm from 'node:vm'; import https from 'node:https'; import { spawn } from 'node:child_process'; class EventEmitter { static #handlers = new Set(); static addHandler(handler) { EventEmitter.#handlers.add(handler); } static dispatch(event) { for (const handler of EventEmitter.#handlers) { handler(event); } } static removeHandler(handler) { EventEmitter.#handlers.delete(handler); } } class Path { static dirname(filePath) { return Path.normalizeSlashes(path.dirname(filePath)); } static join(...filePaths) { return Path.normalizeSlashes(path.join(...filePaths)); } static normalizeSlashes(filePath) { if (path.sep === "/") { return filePath; } return filePath.replaceAll("\\", "/"); } static relative(from, to) { let relativePath = path.relative(from, to); if (!relativePath.startsWith(".")) { relativePath = `./${relativePath}`; } return Path.normalizeSlashes(relativePath); } static resolve(...filePaths) { return Path.normalizeSlashes(path.resolve(...filePaths)); } } class Environment { static #noColor = Environment.#resolveNoColor(); static #noInteractive = Environment.#resolveNoInteractive(); static #storePath = Environment.#resolveStorePath(); static #timeout = Environment.#resolveTimeout(); static #typescriptPath = Environment.#resolveTypeScriptPath(); static get noColor() { return Environment.#noColor; } static get noInteractive() { return Environment.#noInteractive; } static get storePath() { return Environment.#storePath; } static get timeout() { return Environment.#timeout; } static get typescriptPath() { return Environment.#typescriptPath; } static #resolveNoColor() { if (process.env["TSTYCHE_NO_COLOR"] != null) { return process.env["TSTYCHE_NO_COLOR"] !== ""; } if (process.env["NO_COLOR"] != null) { return process.env["NO_COLOR"] !== ""; } return false; } static #resolveNoInteractive() { if (process.env["TSTYCHE_NO_INTERACTIVE"] != null) { return process.env["TSTYCHE_NO_INTERACTIVE"] !== ""; } return !process.stdout.isTTY; } static #resolveStorePath() { if (process.env["TSTYCHE_STORE_PATH"] != null) { return Path.resolve(process.env["TSTYCHE_STORE_PATH"]); } if (process.platform === "darwin") { return Path.resolve(os.homedir(), "Library", "TSTyche"); } if (process.env["LocalAppData"] != null) { return Path.resolve(process.env["LocalAppData"], "TSTyche"); } if (process.env["XDG_DATA_HOME"] != null) { return Path.resolve(process.env["XDG_DATA_HOME"], "TSTyche"); } return Path.resolve(os.homedir(), ".local", "share", "TSTyche"); } static #resolveTimeout() { if (process.env["TSTYCHE_TIMEOUT"] != null) { return Number(process.env["TSTYCHE_TIMEOUT"]); } return 30; } static #resolveTypeScriptPath() { let moduleId = "typescript"; if (process.env["TSTYCHE_TYPESCRIPT_PATH"] != null) { moduleId = process.env["TSTYCHE_TYPESCRIPT_PATH"]; } let resolvedPath; try { resolvedPath = createRequire(import.meta.url).resolve(moduleId); } catch { } return resolvedPath; } } var Color; (function (Color) { Color["Reset"] = "0"; Color["Red"] = "31"; Color["Green"] = "32"; Color["Yellow"] = "33"; Color["Blue"] = "34"; Color["Magenta"] = "35"; Color["Cyan"] = "36"; Color["Gray"] = "90"; })(Color || (Color = {})); class Scribbler { #newLine; #noColor; constructor(options) { this.#newLine = options?.newLine ?? "\n"; this.#noColor = options?.noColor ?? false; } static createElement(type, props, ...children) { return { $$typeof: Symbol.for("tstyche:scribbler"), children: children.length > 1 ? children : children[0], props, type, }; } #escapeSequence(attributes) { return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join(""); } #indentEachLine(lines, level) { const indentStep = " "; const notEmptyLineRegExp = /^(?!$)/gm; return lines.replaceAll(notEmptyLineRegExp, indentStep.repeat(level)); } render(element) { if (element != null) { if (typeof element.type === "function") { const instance = new element.type({ ...element.props, children: element.children, }); return this.render(instance.render()); } if (element.type === "ansi" && !this.#noColor) { const flags = typeof element.props?.["escapes"] === "string" || Array.isArray(element.props?.["escapes"]) ? element.props["escapes"] : undefined; if (flags != null) { return this.#escapeSequence(flags); } } if (element.type === "newLine") { return this.#newLine; } if (element.type === "text") { const indentLevel = typeof element.props?.["indent"] === "number" ? element.props["indent"] : 0; let text = this.#visitElementChildren(element.children); if (indentLevel > 0) { text = this.#indentEachLine(text, indentLevel); } return text; } } return ""; } #visitElementChildren(children) { if (typeof children === "string") { return children; } if (Array.isArray(children)) { const text = []; for (const child of children) { if (typeof child === "string") { text.push(child); continue; } if (Array.isArray(child)) { text.push(this.#visitElementChildren(child)); continue; } if (child != null && typeof child === "object") { text.push(this.render(child)); continue; } } return text.join(""); } if (children != null && typeof children === "object") { return this.render(children); } return ""; } } class Text { props; constructor(props) { this.props = props; } render() { const ansiEscapes = []; if (this.props.color != null) { ansiEscapes.push(this.props.color); } return (Scribbler.createElement("text", { indent: this.props.indent }, ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: ansiEscapes }) : undefined, this.props.children, ansiEscapes.length > 0 ? Scribbler.createElement("ansi", { escapes: "0" }) : undefined)); } } class Line { props; constructor(props) { this.props = props; } render() { return (Scribbler.createElement("text", null, Scribbler.createElement(Text, { color: this.props.color, indent: this.props.indent }, this.props.children), Scribbler.createElement("newLine", null))); } } class Logger { #noColor; #scribbler; #stderr; #stdout; constructor(options) { this.#noColor = options?.noColor ?? Environment.noColor; this.#stderr = options?.stderr ?? process.stderr; this.#stdout = options?.stdout ?? process.stdout; this.#scribbler = new Scribbler({ noColor: this.#noColor }); } eraseLastLine() { this.#stdout.write("\u001B[1A\u001B[0K"); } #write(stream, body) { const elements = Array.isArray(body) ? body : [body]; for (const element of elements) { if (element.$$typeof !== Symbol.for("tstyche:scribbler")) { return; } stream.write(this.#scribbler.render(element)); } } writeError(body) { this.#write(this.#stderr, body); } writeMessage(body) { this.#write(this.#stdout, body); } writeWarning(body) { this.#write(this.#stderr, body); } } class Reporter { resolvedConfig; logger; constructor(resolvedConfig) { this.resolvedConfig = resolvedConfig; this.logger = new Logger(); } } function addsPackageStepText(compilerVersion, installationPath) { return (Scribbler.createElement(Line, null, Scribbler.createElement(Text, { color: "90" }, "adds"), " TypeScript ", compilerVersion, Scribbler.createElement(Text, { color: "90" }, " to ", installationPath))); } function describeNameText(name, indent = 0) { return Scribbler.createElement(Line, { indent: indent + 1 }, name); } class CodeSpanText { props; constructor(props) { this.props = props; } render() { const lastLineInFile = this.props.file.getLineAndCharacterOfPosition(this.props.file.text.length).line; const { character: markedCharacter, line: markedLine } = this.props.file.getLineAndCharacterOfPosition(this.props.start); const firstLine = Math.max(markedLine - 2, 0); const lastLine = Math.min(firstLine + 5, lastLineInFile); const lineNumberMaxWidth = `${lastLine + 1}`.length; const codeSpan = []; for (let index = firstLine; index <= lastLine; index++) { const lineStart = this.props.file.getPositionOfLineAndCharacter(index, 0); const lineEnd = index === lastLineInFile ? this.props.file.text.length : this.props.file.getPositionOfLineAndCharacter(index + 1, 0); const lineNumberText = String(index + 1); const lineText = this.props.file.text.slice(lineStart, lineEnd).trimEnd().replaceAll("\t", " "); if (index === markedLine) { codeSpan.push(Scribbler.createElement(Line, null, Scribbler.createElement(Text, { color: "31" }, ">"), " ", lineNumberText.padStart(lineNumberMaxWidth), " ", Scribbler.createElement(Text, { color: "90" }, "|"), " ", lineText), Scribbler.createElement(Line, null, " ".repeat(lineNumberMaxWidth + 3), Scribbler.createElement(Text, { color: "90" }, "|"), " ".repeat(markedCharacter + 1), Scribbler.createElement(Text, { color: "31" }, "^"))); } else { codeSpan.push(Scribbler.createElement(Line, null, " ".repeat(2), Scribbler.createElement(Text, { color: "90" }, lineNumberText.padStart(lineNumberMaxWidth), " | ", lineText || ""))); } } const breadcrumbs = this.props.breadcrumbs?.flatMap((ancestor) => [ Scribbler.createElement(Text, { color: "90" }, " ❭ "), Scribbler.createElement(Text, null, ancestor), ]); const location = (Scribbler.createElement(Line, null, " ".repeat(lineNumberMaxWidth + 5), Scribbler.createElement(Text, { color: "90" }, "at"), " ", Scribbler.createElement(Text, { color: "36" }, Path.relative("", this.props.file.fileName)), Scribbler.createElement(Text, { color: "90" }, ":", String(markedLine + 1), ":", String(markedCharacter + 1)), breadcrumbs)); return (Scribbler.createElement(Text, null, codeSpan, Scribbler.createElement(Line, null), location)); } } class DiagnosticText { props; constructor(props) { this.props = props; } render() { const code = typeof this.props.diagnostic.code === "string" ? Scribbler.createElement(Text, { color: "90" }, " ", this.props.diagnostic.code) : undefined; const text = Array.isArray(this.props.diagnostic.text) ? this.props.diagnostic.text : [this.props.diagnostic.text]; const message = text.map((text, index) => (Scribbler.createElement(Text, null, index === 1 ? Scribbler.createElement(Line, null) : undefined, Scribbler.createElement(Line, null, text, code)))); const related = this.props.diagnostic.related?.map((relatedDiagnostic) => (Scribbler.createElement(DiagnosticText, { diagnostic: relatedDiagnostic }))); const codeSpan = this.props.diagnostic.origin ? (Scribbler.createElement(Text, null, Scribbler.createElement(Line, null), Scribbler.createElement(CodeSpanText, { ...this.props.diagnostic.origin }))) : undefined; return (Scribbler.createElement(Text, null, message, codeSpan, Scribbler.createElement(Line, null), Scribbler.createElement(Text, { indent: 2 }, related))); } } function diagnosticText(diagnostic) { let prefix; switch (diagnostic.category) { case "error": prefix = Scribbler.createElement(Text, { color: "31" }, "Error: "); break; case "warning": prefix = Scribbler.createElement(Text, { color: "33" }, "Warning: "); break; } return (Scribbler.createElement(Text, null, prefix, Scribbler.createElement(DiagnosticText, { diagnostic: diagnostic }))); } class FileNameText { props; constructor(props) { this.props = props; } render() { const relativePath = Path.relative("", this.props.filePath); const lastPathSeparator = relativePath.lastIndexOf("/"); const directoryNameText = relativePath.slice(0, lastPathSeparator + 1); const fileNameText = relativePath.slice(lastPathSeparator + 1); return (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "90" }, directoryNameText), fileNameText)); } } function fileStatusText(status, testFile) { let statusColor; let statusText; switch (status) { case "runs": statusColor = "33"; statusText = "runs"; break; case "passed": statusColor = "32"; statusText = "pass"; break; case "failed": statusColor = "31"; statusText = "fail"; break; } return (Scribbler.createElement(Line, null, Scribbler.createElement(Text, { color: statusColor }, statusText), " ", Scribbler.createElement(FileNameText, { filePath: fileURLToPath(testFile) }))); } function fileViewText(lines, addEmptyFinalLine) { return (Scribbler.createElement(Text, null, [...lines], addEmptyFinalLine ? Scribbler.createElement(Line, null) : undefined)); } class JsonText { props; constructor(props) { this.props = props; } render() { return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2)); } #sortObject(target) { if (Array.isArray(target)) { return target; } return Object.keys(target) .sort() .reduce((result, key) => { result[key] = target[key]; return result; }, {}); } } function formattedText(input) { if (typeof input === "string") { return Scribbler.createElement(Line, null, input); } return Scribbler.createElement(JsonText, { input: input }); } const usageExamples = [ ["tstyche", "Run all tests."], ["tstyche path/to/first.test.ts", "Only run the test files with matching path."], ["tstyche --target 4.9,5.3.2,current", "Test on all specified versions of TypeScript."], ]; class HintText { props; constructor(props) { this.props = props; } render() { return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children)); } } class HelpHeaderText { props; constructor(props) { this.props = props; } render() { const hint = (Scribbler.createElement(HintText, null, Scribbler.createElement(Text, null, this.props.tstycheVersion))); return (Scribbler.createElement(Line, null, Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"), hint)); } } class CommandText { props; constructor(props) { this.props = props; } render() { let hint; if (this.props.hint != null) { hint = Scribbler.createElement(HintText, null, this.props.hint); } return (Scribbler.createElement(Line, { indent: 1 }, Scribbler.createElement(Text, { color: "34" }, this.props.text), hint)); } } class OptionDescriptionText { props; constructor(props) { this.props = props; } render() { return Scribbler.createElement(Line, { indent: 1 }, this.props.text); } } class CommandLineUsageText { render() { const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null, Scribbler.createElement(CommandText, { text: commandText }), Scribbler.createElement(OptionDescriptionText, { text: descriptionText }), Scribbler.createElement(Line, null)))); return Scribbler.createElement(Text, null, usageText); } } class CommandLineOptionNameText { props; constructor(props) { this.props = props; } render() { return Scribbler.createElement(Text, null, "--", this.props.text); } } class CommandLineOptionHintText { props; constructor(props) { this.props = props; } render() { if (this.props.definition.brand === "list") { return (Scribbler.createElement(Text, null, this.props.definition.brand, " of ", this.props.definition.items.brand, "s")); } return Scribbler.createElement(Text, null, this.props.definition.brand); } } class CommandLineOptionsText { props; constructor(props) { this.props = props; } render() { const definitions = [...this.props.optionDefinitions.values()]; const optionsText = definitions.map((definition) => { let hint; if (definition.brand !== "true") { hint = Scribbler.createElement(CommandLineOptionHintText, { definition: definition }); } return (Scribbler.createElement(Text, null, Scribbler.createElement(CommandText, { text: Scribbler.createElement(CommandLineOptionNameText, { text: definition.name }), hint: hint }), Scribbler.createElement(OptionDescriptionText, { text: definition.description }), Scribbler.createElement(Line, null))); }); return (Scribbler.createElement(Text, null, Scribbler.createElement(Line, null, "Command Line Options"), Scribbler.createElement(Line, null), optionsText)); } } class HelpFooterText { render() { return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org"); } } function helpText(optionDefinitions, tstycheVersion) { return (Scribbler.createElement(Text, null, Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }), Scribbler.createElement(Line, null), Scribbler.createElement(CommandLineUsageText, null), Scribbler.createElement(Line, null), Scribbler.createElement(CommandLineOptionsText, { optionDefinitions: optionDefinitions }), Scribbler.createElement(Line, null), Scribbler.createElement(HelpFooterText, null), Scribbler.createElement(Line, null))); } class RowText { props; constructor(props) { this.props = props; } render() { return (Scribbler.createElement(Line, null, `${this.props.label}:`.padEnd(12), this.props.text)); } } class CountText { props; constructor(props) { this.props = props; } render() { return (Scribbler.createElement(Text, null, this.props.failed > 0 ? (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "31" }, String(this.props.failed), " failed"), Scribbler.createElement(Text, null, ", "))) : undefined, this.props.skipped > 0 ? (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "33" }, String(this.props.skipped), " skipped"), Scribbler.createElement(Text, null, ", "))) : undefined, this.props.todo > 0 ? (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "35" }, String(this.props.todo), " todo"), Scribbler.createElement(Text, null, ", "))) : undefined, this.props.passed > 0 ? (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "32" }, String(this.props.passed), " passed"), Scribbler.createElement(Text, null, ", "))) : undefined, Scribbler.createElement(Text, null, String(this.props.total), Scribbler.createElement(Text, null, " total")))); } } class DurationText { props; constructor(props) { this.props = props; } render() { const duration = this.props.duration / 1000; const minutes = Math.floor(duration / 60); const seconds = duration % 60; return (Scribbler.createElement(Text, null, minutes > 0 ? `${minutes}m ` : undefined, `${Math.round(seconds * 10) / 10}s`)); } } class MatchText { props; constructor(props) { this.props = props; } render() { if (typeof this.props.text === "string") { return Scribbler.createElement(Text, null, "'", this.props.text, "'"); } if (this.props.text.length <= 1) { return Scribbler.createElement(Text, null, "'", ...this.props.text, "'"); } const lastItem = this.props.text.pop(); return (Scribbler.createElement(Text, null, this.props.text.map((match, index, list) => (Scribbler.createElement(Text, null, "'", match, "'", index === list.length - 1 ? Scribbler.createElement(Text, null, " ") : Scribbler.createElement(Text, { color: "90" }, ", ")))), Scribbler.createElement(Text, { color: "90" }, "or"), " '", lastItem, "'")); } } class RanFilesText { props; constructor(props) { this.props = props; } render() { const testNameMatch = []; if (this.props.onlyMatch != null) { testNameMatch.push(Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "90" }, "matching "), Scribbler.createElement(MatchText, { text: this.props.onlyMatch }))); } if (this.props.skipMatch != null) { testNameMatch.push(Scribbler.createElement(Text, null, this.props.onlyMatch == null ? undefined : Scribbler.createElement(Text, { color: "90" }, " and "), Scribbler.createElement(Text, { color: "90" }, "not matching "), Scribbler.createElement(MatchText, { text: this.props.skipMatch }))); } let pathMatch; if (this.props.pathMatch.length > 0) { pathMatch = (Scribbler.createElement(Text, null, Scribbler.createElement(Text, { color: "90" }, "test files matching "), Scribbler.createElement(MatchText, { text: this.props.pathMatch }), Scribbler.createElement(Text, { color: "90" }, "."))); } else { pathMatch = Scribbler.createElement(Text, { color: "90" }, "all test files."); } return (Scribbler.createElement(Line, null, Scribbler.createElement(Text, { color: "90" }, "Ran "), testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, "tests ") : undefined, testNameMatch, testNameMatch.length > 0 ? Scribbler.createElement(Text, { color: "90" }, " in ") : undefined, pathMatch)); } } function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) { const targetCountText = (Scribbler.createElement(RowText, { label: "Targets", text: Scribbler.createElement(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) })); const fileCountText = (Scribbler.createElement(RowText, { label: "Test files", text: Scribbler.createElement(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) })); const testCountText = (Scribbler.createElement(RowText, { label: "Tests", text: Scribbler.createElement(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) })); const assertionCountText = (Scribbler.createElement(RowText, { label: "Assertions", text: Scribbler.createElement(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) })); return (Scribbler.createElement(Text, null, targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, Scribbler.createElement(RowText, { label: "Duration", text: Scribbler.createElement(DurationText, { duration: duration }) }), Scribbler.createElement(Line, null), Scribbler.createElement(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch }))); } class StatusText { props; constructor(props) { this.props = props; } render() { switch (this.props.status) { case "fail": return Scribbler.createElement(Text, { color: "31" }, "\u00D7"); case "pass": return Scribbler.createElement(Text, { color: "32" }, "+"); case "skip": return Scribbler.createElement(Text, { color: "33" }, "- skip"); case "todo": return Scribbler.createElement(Text, { color: "35" }, "- todo"); } } } function testNameText(status, name, indent = 0) { return (Scribbler.createElement(Line, { indent: indent + 1 }, Scribbler.createElement(StatusText, { status: status }), " ", Scribbler.createElement(Text, { color: "90" }, name))); } class ProjectNameText { props; constructor(props) { this.props = props; } render() { return (Scribbler.createElement(Text, { color: "90" }, " with ", Path.relative("", this.props.filePath))); } } function usesCompilerStepText(compilerVersion, tsconfigFilePath, options) { let projectPathText; if (tsconfigFilePath != null) { projectPathText = Scribbler.createElement(ProjectNameText, { filePath: tsconfigFilePath }); } return (Scribbler.createElement(Text, null, options?.prependEmptyLine === true ? Scribbler.createElement(Line, null) : undefined, Scribbler.createElement(Line, null, Scribbler.createElement(Text, { color: "34" }, "uses"), " TypeScript ", compilerVersion, projectPathText), Scribbler.createElement(Line, null))); } class SummaryReporter extends Reporter { handleEvent([eventName, payload]) { switch (eventName) { case "end": this.logger.writeMessage(summaryText({ duration: payload.result.timing.duration, expectCount: payload.result.expectCount, fileCount: payload.result.fileCount, onlyMatch: payload.result.resolvedConfig.only, pathMatch: payload.result.resolvedConfig.pathMatch, skipMatch: payload.result.resolvedConfig.skip, targetCount: payload.result.targetCount, testCount: payload.result.testCount, })); break; } } } class FileViewService { #indent = 0; #lines = []; #messages = []; get hasErrors() { return this.#messages.length > 0; } addMessage(message) { this.#messages.push(message); } addTest(status, name) { this.#lines.push(testNameText(status, name, this.#indent)); } beginDescribe(name) { this.#lines.push(describeNameText(name, this.#indent)); this.#indent++; } endDescribe() { this.#indent--; } getMessages() { return this.#messages; } getViewText(options) { return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors); } reset() { this.#indent = 0; this.#lines = []; this.#messages = []; } } class ThoroughReporter extends Reporter { #currentCompilerVersion; #currentProjectConfigFilePath; #fileCount = 0; #fileView = new FileViewService(); #hasReportedAdds = false; #hasReportedError = false; #isFileViewExpanded = false; get #isLastFile() { return this.#fileCount === 0; } handleEvent([eventName, payload]) { switch (eventName) { case "start": this.#isFileViewExpanded = payload.result.testFiles.length === 1; break; case "store:info": this.logger.writeMessage(addsPackageStepText(payload.compilerVersion, payload.installationPath)); this.#hasReportedAdds = true; break; case "store:error": for (const diagnostic of payload.diagnostics) { this.logger.writeError(diagnosticText(diagnostic)); } break; case "target:start": this.#fileCount = payload.result.testFiles.length; break; case "target:end": this.#currentCompilerVersion = undefined; this.#currentProjectConfigFilePath = undefined; break; case "project:info": if (this.#currentCompilerVersion !== payload.compilerVersion || this.#currentProjectConfigFilePath !== payload.projectConfigFilePath) { this.logger.writeMessage(usesCompilerStepText(payload.compilerVersion, payload.projectConfigFilePath, { prependEmptyLine: this.#currentCompilerVersion != null && !this.#hasReportedAdds && !this.#hasReportedError, })); this.#hasReportedAdds = false; this.#currentCompilerVersion = payload.compilerVersion; this.#currentProjectConfigFilePath = payload.projectConfigFilePath; } break; case "project:error": for (const diagnostic of payload.diagnostics) { this.logger.writeError(diagnosticText(diagnostic)); } break; case "file:start": if (!Environment.noInteractive) { this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile)); } this.#fileCount--; this.#hasReportedError = false; break; case "file:error": for (const diagnostic of payload.diagnostics) { this.#fileView.addMessage(diagnosticText(diagnostic)); } break; case "file:end": if (!Environment.noInteractive) { this.logger.eraseLastLine(); } this.logger.writeMessage(fileStatusText(payload.result.status, payload.result.testFile)); this.logger.writeMessage(this.#fileView.getViewText({ appendEmptyLine: this.#isLastFile })); if (this.#fileView.hasErrors) { this.logger.writeError(this.#fileView.getMessages()); this.#hasReportedError = true; } this.#fileView.reset(); break; case "describe:start": if (this.#isFileViewExpanded) { this.#fileView.beginDescribe(payload.result.describe.name); } break; case "describe:end": if (this.#isFileViewExpanded) { this.#fileView.endDescribe(); } break; case "test:skip": if (this.#isFileViewExpanded) { this.#fileView.addTest("skip", payload.result.test.name); } break; case "test:todo": if (this.#isFileViewExpanded) { this.#fileView.addTest("todo", payload.result.test.name); } break; case "test:error": if (this.#isFileViewExpanded) { this.#fileView.addTest("fail", payload.result.test.name); } for (const diagnostic of payload.diagnostics) { this.#fileView.addMessage(diagnosticText(diagnostic)); } break; case "test:fail": if (this.#isFileViewExpanded) { this.#fileView.addTest("fail", payload.result.test.name); } break; case "test:pass": if (this.#isFileViewExpanded) { this.#fileView.addTest("pass", payload.result.test.name); } break; case "expect:error": case "expect:fail": for (const diagnostic of payload.diagnostics) { this.#fileView.addMessage(diagnosticText(diagnostic)); } break; } } } class ResultTiming { end = Date.now(); start = Date.now(); get duration() { return this.end - this.start; } } class DescribeResult { describe; parent; results = []; timing = new ResultTiming(); constructor(describe, parent) { this.describe = describe; this.parent = parent; } } class ExpectResult { assertion; parent; diagnostics = []; status = "runs"; timing = new ResultTiming(); constructor(assertion, parent) { this.assertion = assertion; this.parent = parent; } } class ResultCount { failed = 0; passed = 0; skipped = 0; todo = 0; get total() { return this.failed + this.passed + this.skipped + this.todo; } } class FileResult { testFile; diagnostics = []; expectCount = new ResultCount(); results = []; status = "runs"; testCount = new ResultCount(); timing = new ResultTiming(); constructor(testFile) { this.testFile = testFile; } } class ProjectResult { compilerVersion; projectConfigFilePath; diagnostics = []; results = []; constructor(compilerVersion, projectConfigFilePath) { this.compilerVersion = compilerVersion; this.projectConfigFilePath = projectConfigFilePath; } } class Result { resolvedConfig; testFiles; expectCount = new ResultCount(); fileCount = new ResultCount(); results = []; targetCount = new ResultCount(); testCount = new ResultCount(); timing = new ResultTiming(); constructor(resolvedConfig, testFiles) { this.resolvedConfig = resolvedConfig; this.testFiles = testFiles; } } class ResultManager { #describeResult; #expectResult; #fileResult; #projectResult; #result; #targetResult; #testResult; handleEvent([eventName, payload]) { switch (eventName) { case "start": this.#result = payload.result; this.#result.timing.start = Date.now(); break; case "end": this.#result.timing.end = Date.now(); this.#result = undefined; break; case "target:start": this.#result.results.push(payload.result); this.#targetResult = payload.result; this.#targetResult.timing.start = Date.now(); break; case "target:end": if (this.#targetResult.status === "failed") { this.#result.targetCount.failed++; } else { this.#result.targetCount.passed++; this.#targetResult.status = "passed"; } this.#targetResult.timing.end = Date.now(); this.#targetResult = undefined; break; case "store:error": { if (payload.diagnostics.some(({ category }) => category === "error")) { this.#targetResult.status = "failed"; } break; } case "project:info": { let projectResult = this.#targetResult.results.get(payload.projectConfigFilePath); if (!projectResult) { projectResult = new ProjectResult(payload.compilerVersion, payload.projectConfigFilePath); this.#targetResult.results.set(payload.projectConfigFilePath, projectResult); } this.#projectResult = projectResult; } break; case "project:error": this.#targetResult.status = "failed"; this.#projectResult.diagnostics.push(...payload.diagnostics); break; case "file:start": this.#projectResult.results.push(payload.result); this.#fileResult = payload.result; this.#fileResult.timing.start = Date.now(); break; case "file:error": this.#targetResult.status = "failed"; this.#fileResult.status = "failed"; this.#fileResult.diagnostics.push(...payload.diagnostics); break; case "file:end": if (this.#fileResult.status === "failed" || this.#fileResult.expectCount.failed > 0 || this.#fileResult.testCount.failed > 0) { this.#result.fileCount.failed++; this.#targetResult.status = "failed"; this.#fileResult.status = "failed"; } else { this.#result.fileCount.passed++; this.#fileResult.status = "passed"; } this.#fileResult.timing.end = Date.now(); this.#fileResult = undefined; break; case "describe:start": if (this.#describeResult) { this.#describeResult.results.push(payload.result); } else { this.#fileResult.results.push(payload.result); } this.#describeResult = payload.result; this.#describeResult.timing.start = Date.now(); break; case "describe:end": this.#describeResult.timing.end = Date.now(); this.#describeResult = this.#describeResult.parent; break; case "test:start": if (this.#describeResult) { this.#describeResult.results.push(payload.result); } else { this.#fileResult.results.push(payload.result); } this.#testResult = payload.result; this.#testResult.timing.start = Date.now(); break; case "test:error": this.#result.testCount.failed++; this.#fileResult.testCount.failed++; this.#testResult.status = "failed"; this.#testResult.diagnostics.push(...payload.diagnostics); this.#testResult.timing.end = Date.now(); this.#testResult = undefined; break; case "test:fail": this.#result.testCount.failed++; this.#fileResult.testCount.failed++; this.#testResult.status = "failed"; this.#testResult.timing.end = Date.now(); this.#testResult = undefined; break; case "test:pass": this.#result.testCount.passed++; this.#fileResult.testCount.passed++; this.#testResult.status = "passed"; this.#testResult.timing.end = Date.now(); this.#testResult = undefined; break; case "test:skip": this.#result.testCount.skipped++; this.#fileResult.testCount.skipped++; this.#testResult.status = "skipped"; this.#testResult.timing.end = Date.now(); this.#testResult = undefined; break; case "test:todo": this.#result.testCount.todo++; this.#fileResult.testCount.todo++; this.#testResult.status = "todo"; this.#testResult.timing.end = Date.now(); this.#testResult = undefined; break; case "expect:start": if (this.#testResult) { this.#testResult.results.push(payload.result); } else { this.#fileResult.results.push(payload.result); } this.#expectResult = payload.result; this.#expectResult.timing.start = Date.now(); break; case "expect:error": this.#result.expectCount.failed++; this.#fileResult.expectCount.failed++; if (this.#testResult) { this.#testResult.expectCount.failed++; } this.#expectResult.status = "failed"; this.#expectResult.diagnostics.push(...payload.diagnostics); this.#expectResult.timing.end = Date.now(); this.#expectResult = undefined; break; case "expect:fail": this.#result.expectCount.failed++; this.#fileResult.expectCount.failed++; if (this.#testResult) { this.#testResult.expectCount.failed++; } this.#expectResult.status = "failed"; this.#expectResult.timing.end = Date.now(); this.#expectResult = undefined; break; case "expect:pass": this.#result.expectCount.passed++; this.#fileResult.expectCount.passed++; if (this.#testResult) { this.#testResult.expectCount.passed++; } this.#expectResult.status = "passed"; this.#expectResult.timing.end = Date.now(); this.#expectResult = undefined; break; case "expect:skip": this.#result.expectCount.skipped++; this.#fileResult.expectCount.skipped++; if (this.#testResult) { this.#testResult.expectCount.skipped++; } this.#expectResult.status = "skipped"; this.#expectResult.timing.end = Date.now(); this.#expectResult = undefined; break; } } } var ResultStatus; (function (ResultStatus) { ResultStatus["Runs"] = "runs"; ResultStatus["Passed"] = "passed"; ResultStatus["Failed"] = "failed"; ResultStatus["Skipped"] = "skipped"; ResultStatus["Todo"] = "todo"; })(ResultStatus || (ResultStatus = {})); class TargetResult { versionTag; testFiles; results = new Map(); status = "runs"; timing = new ResultTiming(); constructor(versionTag, testFiles) { this.versionTag = versionTag; this.testFiles = testFiles; } } class TestResult { test; parent; diagnostics = []; expectCount = new ResultCount(); results = []; status = "runs"; timing = new ResultTiming(); constructor(test, parent) { this.test = test; this.parent = parent; } } class Diagnostic { text; category; origin; code; related; constructor(text, category, origin) { this.text = text; this.category = category; this.origin = origin; } add(options) { if (options.code != null) { this.code = options.code; } if (options.origin != null) { this.origin = options.origin; } if (options.related != null) { this.related = options.related; } return this; } static error(text, origin) { return new Diagnostic(text, "error", origin); } static fromDiagnostics(diagnostics, compiler) { return diagnostics.map((diagnostic) => { const category = "error"; const code = `ts(${diagnostic.code})`; let origin; const text = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); if (Diagnostic.isTsDiagnosticWithLocation(diagnostic)) { origin = { end: diagnostic.start + diagnostic.length, file: diagnostic.file, start: diagnostic.start, }; } return new Diagnostic(text, category, origin).add({ code }); }); } static fromError(text, error) { const messageText = Array.isArray(text) ? text : [text]; if (error instanceof Error) { if (error.cause != null) { messageText.push(this.#normalizeMessage(String(error.cause))); } messageText.push(this.#normalizeMessage(String(error.message))); if (error.stack != null) { const stackLines = error.stack .split("\n") .slice(1) .map((line) => line.trimStart()); messageText.push(...stackLines); } } return Diagnostic.error(messageText); } static isTsDiagnosticWithLocation(diagnostic) { return diagnostic.file != null && diagnostic.start != null && diagnostic.length != null; } static #normalizeMessage(text) { if (text.endsWith(".")) { return text; } return `${text}.`; } static warning(text, origin) { return new Diagnostic(text, "warning", origin); } } var DiagnosticCategory; (function (DiagnosticCategory) { DiagnosticCategory["Error"] = "error"; DiagnosticCategory["Warning"] = "warning"; })(DiagnosticCategory || (DiagnosticCategory = {})); class TestMember { brand; node; parent; flags; compiler; diagnostics = new Set(); members = []; name = ""; constructor(brand, node, parent, flags) { this.brand = brand; this.node = node; this.parent = parent; this.flags = flags; this.compiler = parent.compiler; if (node.arguments[0] != null && this.compiler.isStringLiteralLike(node.arguments[0])) { this.name = node.arguments[0].text; } if (node.arguments[1] != null && parent.compiler.isFunctionLike(node.arguments[1]) && parent.