diff options
Diffstat (limited to 'cli/tests/unit/test_util.ts')
-rw-r--r-- | cli/tests/unit/test_util.ts | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/cli/tests/unit/test_util.ts b/cli/tests/unit/test_util.ts new file mode 100644 index 000000000..1c5b6ff21 --- /dev/null +++ b/cli/tests/unit/test_util.ts @@ -0,0 +1,364 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "../../../std/testing/asserts.ts"; +export { + assert, + assertThrows, + assertEquals, + assertMatch, + assertNotEquals, + assertStrictEq, + assertStrContains, + unreachable, + fail, +} from "../../../std/testing/asserts.ts"; +export { readLines } from "../../../std/io/bufio.ts"; +export { parse as parseArgs } from "../../../std/flags/mod.ts"; + +export interface Permissions { + read: boolean; + write: boolean; + net: boolean; + env: boolean; + run: boolean; + plugin: boolean; + hrtime: boolean; +} + +export function fmtPerms(perms: Permissions): string { + const p = Object.keys(perms) + .filter((e): boolean => perms[e as keyof Permissions] === true) + .map((key) => `--allow-${key}`); + + if (p.length) { + return p.join(" "); + } + + return "<no permissions>"; +} + +const isGranted = async (name: Deno.PermissionName): Promise<boolean> => + (await Deno.permissions.query({ name })).state === "granted"; + +export async function getProcessPermissions(): Promise<Permissions> { + return { + run: await isGranted("run"), + read: await isGranted("read"), + write: await isGranted("write"), + net: await isGranted("net"), + env: await isGranted("env"), + plugin: await isGranted("plugin"), + hrtime: await isGranted("hrtime"), + }; +} + +export function permissionsMatch( + processPerms: Permissions, + requiredPerms: Permissions +): boolean { + for (const permName in processPerms) { + if ( + processPerms[permName as keyof Permissions] !== + requiredPerms[permName as keyof Permissions] + ) { + return false; + } + } + + return true; +} + +export const permissionCombinations: Map<string, Permissions> = new Map(); + +function permToString(perms: Permissions): string { + const r = perms.read ? 1 : 0; + const w = perms.write ? 1 : 0; + const n = perms.net ? 1 : 0; + const e = perms.env ? 1 : 0; + const u = perms.run ? 1 : 0; + const p = perms.plugin ? 1 : 0; + const h = perms.hrtime ? 1 : 0; + return `permR${r}W${w}N${n}E${e}U${u}P${p}H${h}`; +} + +function registerPermCombination(perms: Permissions): void { + const key = permToString(perms); + if (!permissionCombinations.has(key)) { + permissionCombinations.set(key, perms); + } +} + +export async function registerUnitTests(): Promise<void> { + const processPerms = await getProcessPermissions(); + + for (const unitTestDefinition of REGISTERED_UNIT_TESTS) { + if (!permissionsMatch(processPerms, unitTestDefinition.perms)) { + continue; + } + + Deno.test(unitTestDefinition); + } +} + +function normalizeTestPermissions(perms: UnitTestPermissions): Permissions { + return { + read: !!perms.read, + write: !!perms.write, + net: !!perms.net, + run: !!perms.run, + env: !!perms.env, + plugin: !!perms.plugin, + hrtime: !!perms.hrtime, + }; +} + +interface UnitTestPermissions { + read?: boolean; + write?: boolean; + net?: boolean; + env?: boolean; + run?: boolean; + plugin?: boolean; + hrtime?: boolean; +} + +interface UnitTestOptions { + ignore?: boolean; + perms?: UnitTestPermissions; +} + +interface UnitTestDefinition extends Deno.TestDefinition { + ignore: boolean; + perms: Permissions; +} + +type TestFunction = () => void | Promise<void>; + +export const REGISTERED_UNIT_TESTS: UnitTestDefinition[] = []; + +export function unitTest(fn: TestFunction): void; +export function unitTest(options: UnitTestOptions, fn: TestFunction): void; +export function unitTest( + optionsOrFn: UnitTestOptions | TestFunction, + maybeFn?: TestFunction +): void { + assert(optionsOrFn, "At least one argument is required"); + + let options: UnitTestOptions; + let name: string; + let fn: TestFunction; + + if (typeof optionsOrFn === "function") { + options = {}; + fn = optionsOrFn; + name = fn.name; + assert(name, "Missing test function name"); + } else { + options = optionsOrFn; + assert(maybeFn, "Missing test function definition"); + assert( + typeof maybeFn === "function", + "Second argument should be test function definition" + ); + fn = maybeFn; + name = fn.name; + assert(name, "Missing test function name"); + } + + const normalizedPerms = normalizeTestPermissions(options.perms || {}); + registerPermCombination(normalizedPerms); + + const unitTestDefinition: UnitTestDefinition = { + name, + fn, + ignore: !!options.ignore, + perms: normalizedPerms, + }; + + REGISTERED_UNIT_TESTS.push(unitTestDefinition); +} + +export interface ResolvableMethods<T> { + resolve: (value?: T | PromiseLike<T>) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void; +} + +export type Resolvable<T> = Promise<T> & ResolvableMethods<T>; + +export function createResolvable<T>(): Resolvable<T> { + let methods: ResolvableMethods<T>; + const promise = new Promise<T>((resolve, reject): void => { + methods = { resolve, reject }; + }); + // TypeScript doesn't know that the Promise callback occurs synchronously + // therefore use of not null assertion (`!`) + return Object.assign(promise, methods!) as Resolvable<T>; +} + +const encoder = new TextEncoder(); + +// Replace functions with null, errors with their stack strings, and JSONify. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function serializeTestMessage(message: any): string { + return JSON.stringify({ + start: message.start && { + ...message.start, + tests: message.start.tests.map((test: Deno.TestDefinition) => ({ + ...test, + fn: null, + })), + }, + testStart: message.testStart && { ...message.testStart, fn: null }, + testEnd: message.testEnd && { + ...message.testEnd, + error: String(message.testEnd.error?.stack), + }, + end: message.end && { + ...message.end, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + results: message.end.results.map((result: any) => ({ + ...result, + error: result.error?.stack, + })), + }, + }); +} + +export async function reportToConn( + conn: Deno.Conn, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: any +): Promise<void> { + const line = serializeTestMessage(message); + const encodedMsg = encoder.encode(line + (message.end == null ? "\n" : "")); + await Deno.writeAll(conn, encodedMsg); + if (message.end != null) { + conn.closeWrite(); + } +} + +unitTest(function permissionsMatches(): void { + assert( + permissionsMatch( + { + read: true, + write: false, + net: false, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({ read: true }) + ) + ); + + assert( + permissionsMatch( + { + read: false, + write: false, + net: false, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({}) + ) + ); + + assertEquals( + permissionsMatch( + { + read: false, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + }, + normalizeTestPermissions({ read: true }) + ), + false + ); + + assertEquals( + permissionsMatch( + { + read: true, + write: false, + net: true, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({ read: true }) + ), + false + ); + + assert( + permissionsMatch( + { + read: true, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + }, + { + read: true, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + } + ) + ); +}); + +/* + * Ensure all unit test files (e.g. xxx_test.ts) are present as imports in + * cli/tests/unit/unit_tests.ts as it is easy to miss this out + */ +unitTest( + { perms: { read: true } }, + function assertAllUnitTestFilesImported(): void { + const directoryTestFiles = [...Deno.readDirSync("./cli/tests/unit/")] + .map((k) => k.name) + .filter( + (file) => + file!.endsWith(".ts") && + !file!.endsWith("unit_tests.ts") && + !file!.endsWith("test_util.ts") && + !file!.endsWith("unit_test_runner.ts") + ); + const unitTestsFile: Uint8Array = Deno.readFileSync( + "./cli/tests/unit/unit_tests.ts" + ); + const importLines = new TextDecoder("utf-8") + .decode(unitTestsFile) + .split("\n") + .filter((line) => line.startsWith("import")); + const importedTestFiles = importLines.map( + (relativeFilePath) => relativeFilePath.match(/\/([^\/]+)";/)![1] + ); + + directoryTestFiles.forEach((dirFile) => { + if (!importedTestFiles.includes(dirFile!)) { + throw new Error( + "cil/tests/unit/unit_tests.ts is missing import of test file: cli/js/" + + dirFile + ); + } + }); + } +); |