@eeue56/bach
Version:
A simple TypeScript test runner inspired by Pytest.
452 lines (451 loc) • 19.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runner = exports.centerAndPadding = void 0;
const baner_1 = require("@eeue56/baner");
const assert_1 = __importDefault(require("assert"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const fs_1 = require("fs");
const json5_1 = __importDefault(require("json5"));
const path = __importStar(require("path"));
const perf_hooks_1 = require("perf_hooks");
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
const chalk = {
red: function (text) {
return `\x1b[31m${text}\x1b[0m`;
},
green: function (text) {
return `\x1b[32m\x1b[1m${text}\x1b[0m`;
},
};
function widestSingleFileCellSizes(results) {
const cellSizes = {
functionName: " Function name ".length,
passed: " Passed? ".length,
};
for (const result of results) {
if (cellSizes.functionName < result.functionName.length + 2) {
cellSizes.functionName = result.functionName.length + 2;
}
if (cellSizes.passed < `${result.passed}`.length + 2) {
cellSizes.passed = `${result.passed}`.length + 2;
}
}
return {
functionName: cellSizes.functionName % 2 === 0
? cellSizes.functionName
: cellSizes.functionName + 1,
passed: cellSizes.passed % 2 === 0
? cellSizes.passed
: cellSizes.passed + 1,
};
}
function widestMultipleFileCellSizes(results) {
const cellSizes = {
fileName: " Function name ".length,
passed: " Passed ".length,
failed: " Failed ".length,
};
for (const result of results) {
if (cellSizes.fileName < result.fileName.length + 2) {
cellSizes.fileName = result.fileName.length + 2;
}
if (cellSizes.passed < `${result.passed}`.length + 2) {
cellSizes.passed = `${result.passed}`.length + 2;
}
if (cellSizes.failed < `${result.failed}`.length + 2) {
cellSizes.failed = `${result.failed}`.length + 2;
}
}
return {
fileName: cellSizes.fileName % 2 === 0
? cellSizes.fileName
: cellSizes.fileName + 1,
passed: cellSizes.passed % 2 === 0
? cellSizes.passed
: cellSizes.passed + 1,
failed: cellSizes.failed % 2 === 0
? cellSizes.failed
: cellSizes.failed + 1,
};
}
const horizontal = "─";
const vertical = "│";
const leftJoin = "├";
const middleJoin = "┼";
const rightJoin = "┤";
const leftCorner = "┌";
const rightCorner = "┐";
const bottomLeftCorner = "└";
const bottomRightCorner = "┘";
function centerAndPadding(cellSize, text) {
const length = text.length;
const eitherSide = Math.floor((cellSize - length) / 2);
let extraLeft = 0;
if (text.length % 2 === 1) {
extraLeft = 1;
}
return " ".repeat(eitherSide + extraLeft) + text + " ".repeat(eitherSide);
}
exports.centerAndPadding = centerAndPadding;
function viewSingleFileResult(result, tableWidth) {
console.log(leftJoin +
horizontal.repeat(tableWidth.functionName) +
middleJoin +
horizontal.repeat(tableWidth.passed) +
rightJoin);
const colouredFunctionName = result.passed
? chalk.green(centerAndPadding(tableWidth.functionName, `${result.functionName}`))
: chalk.red(centerAndPadding(tableWidth.functionName, `${result.functionName}`));
const colouredPassed = result.passed
? chalk.green(centerAndPadding(tableWidth.passed, `${result.passed}`))
: chalk.red(centerAndPadding(tableWidth.passed, `${result.passed}`));
console.log(vertical + colouredFunctionName + vertical + colouredPassed + vertical);
}
function viewSingleFileResults(results) {
const cellSizes = widestSingleFileCellSizes(results);
const headers = vertical +
centerAndPadding(cellSizes.functionName, "Function name") +
vertical +
centerAndPadding(cellSizes.passed, "Passed?") +
vertical;
console.log(leftCorner +
horizontal.repeat(cellSizes.functionName + cellSizes.passed + 1) +
rightCorner);
console.log(headers);
for (const result of results) {
viewSingleFileResult(result, cellSizes);
}
console.log(bottomLeftCorner +
horizontal.repeat(cellSizes.functionName) +
horizontal.repeat(cellSizes.passed + 1) +
bottomRightCorner);
}
function viewMultipleFileResult(result, tableWidth) {
console.log(leftJoin +
horizontal.repeat(tableWidth.fileName) +
middleJoin +
horizontal.repeat(tableWidth.passed) +
middleJoin +
horizontal.repeat(tableWidth.failed) +
rightJoin);
const colouredFileName = result.failed === 0
? chalk.green(centerAndPadding(tableWidth.fileName, `${result.fileName}`))
: chalk.red(centerAndPadding(tableWidth.fileName, `${result.fileName}`));
const colouredPassed = result.passed > 0
? chalk.green(centerAndPadding(tableWidth.passed, `${result.passed}`))
: chalk.red(centerAndPadding(tableWidth.passed, `${result.passed}`));
const colouredFailed = result.failed === 0
? chalk.green(centerAndPadding(tableWidth.failed, `${result.failed}`))
: chalk.red(centerAndPadding(tableWidth.failed, `${result.failed}`));
console.log(vertical +
colouredFileName +
vertical +
colouredPassed +
vertical +
colouredFailed +
vertical);
}
function viewMultipleFileResults(results) {
const cellSizes = widestMultipleFileCellSizes(results);
const headers = vertical +
centerAndPadding(cellSizes.fileName, "File name") +
vertical +
centerAndPadding(cellSizes.passed, "Passed") +
vertical +
centerAndPadding(cellSizes.failed, "Failed") +
vertical;
console.log(leftCorner +
horizontal.repeat(cellSizes.fileName + cellSizes.passed + cellSizes.failed + 2) +
rightCorner);
console.log(headers);
for (const result of results) {
viewMultipleFileResult(result, cellSizes);
}
console.log(bottomLeftCorner +
horizontal.repeat(cellSizes.fileName + cellSizes.passed + cellSizes.failed + 2) +
bottomRightCorner);
}
function isAsyncFunction(func) {
return Object.getPrototypeOf(func).constructor === AsyncFunction;
}
function getSnapshotFileName(config, fileName, functionName) {
return path.join(config.include[0].split("/")[0], `__snapshots__`, path
.relative(process.cwd(), fileName)
.split(".")
.slice(0, -1)
.join("."), functionName + ".ts");
}
async function updateSnapshots(config, program, filesToProcess, functionNamesToRun) {
let totalSnapshotsUpdated = 0;
await Promise.all(filesToProcess.map(async (fileName) => {
return new Promise(async (resolve, reject) => {
fileName =
program.flags.file.arguments.kind === "ok"
? path.join(process.cwd(), fileName)
: fileName;
const baseFileName = path
.basename(fileName)
.split(".")
.slice(0, -1)
.join(".");
const extension = path.extname(fileName);
const isValidExtension = extension === ".js" || extension === ".ts";
if (!baseFileName.endsWith("test") || !isValidExtension) {
return resolve(null);
}
console.log(`Found ${fileName}`);
const imported = await Promise.resolve().then(() => __importStar(require(fileName)));
for (const functionName of Object.keys(imported)) {
if (!functionName.startsWith("snapshot")) {
continue;
}
if (functionNamesToRun &&
functionNamesToRun.indexOf(functionName) === -1)
continue;
const func = imported[functionName];
const isAsync = isAsyncFunction(func);
totalSnapshotsUpdated += 1;
console.log(`Running ${functionName}`);
let computedSnapshot;
if (isAsync) {
computedSnapshot = await func();
}
else {
computedSnapshot = func();
}
const snapshotFileName = getSnapshotFileName(config, fileName, functionName);
console.log("writing to ", snapshotFileName);
let fileContents = computedSnapshot;
const strToWrite = `
export const ${functionName} = ${JSON.stringify(fileContents, null, 4)};
`.trim();
await fs_1.promises.mkdir(path.dirname(snapshotFileName), {
recursive: true,
});
await fs_1.promises.writeFile(snapshotFileName, strToWrite);
}
resolve(null);
});
}));
console.log(`Updated ${totalSnapshotsUpdated} snapshots.`);
}
async function runner() {
const cliParser = (0, baner_1.parser)([
(0, baner_1.longFlag)("function", "Run a specific function", (0, baner_1.variableList)((0, baner_1.string)())),
(0, baner_1.longFlag)("file", "Run a specific file", (0, baner_1.variableList)((0, baner_1.string)())),
(0, baner_1.longFlag)("clean-exit", "Don't use process.exit even if tests fail", (0, baner_1.empty)()),
(0, baner_1.longFlag)("only-fails", "Only show the tests that fail", (0, baner_1.empty)()),
(0, baner_1.longFlag)("in-chunks", "Run tests in chunks of N files (suitable for lower memory impact)", (0, baner_1.number)()),
(0, baner_1.longFlag)("chunk-start", "Start running chunk at N", (0, baner_1.number)()),
(0, baner_1.bothFlag)("u", "update-snapshots", "Update the snapshots and exit", (0, baner_1.empty)()),
(0, baner_1.bothFlag)("h", "help", "Displays help message", (0, baner_1.empty)()),
]);
const program = (0, baner_1.parse)(cliParser, process.argv);
if (program.flags["h/help"].isPresent) {
console.log((0, baner_1.help)(cliParser));
return;
}
const onlyFails = program.flags["only-fails"].isPresent;
const functionNamesToRun = program.flags.function.arguments.kind === "ok"
? program.flags.function.arguments.value
: null;
const fileNamesToRun = program.flags.file.arguments.kind === "ok"
? program.flags.file.arguments.value
: null;
console.log("Looking for tsconfig...");
const strConfig = (await fs_1.promises.readFile("./tsconfig.json")).toString();
const config = json5_1.default.parse(strConfig);
if (!fileNamesToRun) {
console.log(`Looking for tests in ${config.include}...`);
if (!config.include) {
console.error("include was not set in tsconfig, not sure where to look for tests");
console.error("Quitting...");
return;
}
}
else {
console.log("Running provided filenames...");
}
const files = fileNamesToRun
? fileNamesToRun
: await (0, fast_glob_1.default)(config.include, { absolute: true });
const results = {};
let passedTests = 0;
let totalTests = 0;
const chunks = program.flags["in-chunks"].isPresent
? program.flags["in-chunks"].arguments.value
: files.length;
const chunkStart = program.flags["chunk-start"].isPresent
? program.flags["chunk-start"].arguments.value
: 0;
const startTime = perf_hooks_1.performance.now();
const filesToProcess = files.slice(chunkStart, chunkStart + chunks);
if (program.flags["u/update-snapshots"].isPresent) {
await updateSnapshots(config, program, filesToProcess, functionNamesToRun);
return;
}
await Promise.all(filesToProcess.map(async (fileName) => {
return new Promise(async (resolve, reject) => {
fileName =
program.flags.file.arguments.kind === "ok"
? path.join(process.cwd(), fileName)
: fileName;
const baseFileName = path
.basename(fileName)
.split(".")
.slice(0, -1)
.join(".");
const extension = path.extname(fileName);
const isValidExtension = extension === ".js" || extension === ".ts";
if (!baseFileName.endsWith("test") || !isValidExtension) {
return resolve(null);
}
results[fileName] = {};
console.log(`Found ${fileName}`);
const imported = await Promise.resolve().then(() => __importStar(require(fileName)));
for (const functionName of Object.keys(imported)) {
if (functionName.startsWith("test")) {
if (functionNamesToRun &&
functionNamesToRun.indexOf(functionName) === -1)
continue;
const func = imported[functionName];
const isAsync = isAsyncFunction(func);
totalTests += 1;
if (!onlyFails)
console.log(`Running ${functionName}`);
try {
if (isAsync) {
await func();
}
else {
func();
}
results[fileName][functionName] = true;
passedTests += 1;
}
catch (e) {
results[fileName][functionName] = false;
console.error(chalk.red(`${fileName} ${functionName} failed.`));
console.error(e);
}
}
else if (functionName.startsWith("snapshot")) {
if (functionNamesToRun &&
functionNamesToRun.indexOf(functionName) === -1)
continue;
const func = imported[functionName];
const isAsync = isAsyncFunction(func);
totalTests += 1;
if (!onlyFails)
console.log(`Running ${functionName}`);
try {
let computedSnapshot;
if (isAsync) {
computedSnapshot = await func();
}
else {
computedSnapshot = func();
}
const snapshotFileName = getSnapshotFileName(config, fileName, functionName);
let fileContents = computedSnapshot;
try {
fileContents = (await Promise.resolve().then(() => __importStar(require(path.join(process.cwd(), snapshotFileName)))))[functionName];
}
catch (e) {
console.log("Creating snapshot for the first time...");
const strToWrite = `
export const ${functionName} = ${JSON.stringify(fileContents, null, 4)};
`.trim();
await fs_1.promises.mkdir(path.dirname(snapshotFileName), { recursive: true });
await fs_1.promises.writeFile(snapshotFileName, strToWrite);
}
assert_1.default.deepStrictEqual(computedSnapshot, fileContents);
results[fileName][functionName] = true;
passedTests += 1;
}
catch (e) {
results[fileName][functionName] = false;
console.error(chalk.red(`${fileName} ${functionName} failed.`));
console.error(e);
}
}
}
resolve(null);
});
}));
if (filesToProcess.length < files.length) {
console.log(`Ran ${chunks} files, starting at ${chunkStart}, out of ${files.length} total. New start should be ${chunkStart + chunks}`);
}
const endTime = perf_hooks_1.performance.now();
if (program.flags["file"].isPresent) {
const formattedResults = [];
for (const fileName of Object.keys(results)) {
const functions = results[fileName];
for (const functionName of Object.keys(functions)) {
formattedResults.push({
functionName,
passed: functions[functionName],
});
}
}
if (onlyFails) {
viewSingleFileResults(formattedResults.filter((result) => result.passed === false));
}
else {
viewSingleFileResults(formattedResults);
}
}
else {
const formattedResults = Object.entries(results).map(([fileName, functions]) => {
let passed = 0;
Object.entries(functions).forEach(([functionName, didPass]) => {
if (didPass)
passed += 1;
});
const failed = Object.keys(functions).length - passed;
return {
fileName,
passed,
failed,
};
});
if (onlyFails) {
viewMultipleFileResults(formattedResults.filter((result) => result.failed > 0));
}
else {
viewMultipleFileResults(formattedResults);
}
}
console.log(`Ran ${totalTests} tests in ${Math.floor(endTime - startTime)}ms. ${passedTests} tests passed, ${totalTests - passedTests} failed`);
if (totalTests - passedTests > 0 &&
!program.flags["clean-exit"].isPresent) {
process.exit(1);
}
}
exports.runner = runner;
if (require.main === module) {
runner();
}