mirror of
https://github.com/denoland/deno.git
synced 2025-10-03 07:34:36 +00:00
reorg: move js runtime tests to cli/js/tests/ (#4250)
All Deno runtime test files were moved to cli/js/tests/ directory. It makes a clear distinction that cli/js/tests/ contains code that is run under Deno runtime as opposed to code in cli/js/ which is used to create bundle and snapshot with "deno_typescript".
This commit is contained in:
parent
dad8036766
commit
68119e1d7e
65 changed files with 76 additions and 18 deletions
|
@ -1,411 +0,0 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
//
|
||||
// We want to test many ops in deno which have different behavior depending on
|
||||
// the permissions set. These tests can specify which permissions they expect,
|
||||
// which appends a special string like "permW1N0" to the end of the test name.
|
||||
// Here we run several copies of deno with different permissions, filtering the
|
||||
// tests by the special string. permW1N0 means allow-write but not allow-net.
|
||||
// See tools/unit_tests.py for more details.
|
||||
|
||||
import { readLines } from "../../std/io/bufio.ts";
|
||||
import { assert, assertEquals } from "../../std/testing/asserts.ts";
|
||||
export {
|
||||
assert,
|
||||
assertThrows,
|
||||
assertEquals,
|
||||
assertMatch,
|
||||
assertNotEquals,
|
||||
assertStrictEq,
|
||||
assertStrContains,
|
||||
unreachable,
|
||||
fail
|
||||
} from "../../std/testing/asserts.ts";
|
||||
|
||||
interface TestPermissions {
|
||||
read?: boolean;
|
||||
write?: boolean;
|
||||
net?: boolean;
|
||||
env?: boolean;
|
||||
run?: boolean;
|
||||
plugin?: boolean;
|
||||
hrtime?: boolean;
|
||||
}
|
||||
|
||||
export interface Permissions {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
net: boolean;
|
||||
env: boolean;
|
||||
run: boolean;
|
||||
plugin: boolean;
|
||||
hrtime: boolean;
|
||||
}
|
||||
|
||||
const isGranted = async (name: Deno.PermissionName): Promise<boolean> =>
|
||||
(await Deno.permissions.query({ name })).state === "granted";
|
||||
|
||||
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")
|
||||
};
|
||||
}
|
||||
|
||||
const processPerms = await getProcessPermissions();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTestPermissions(perms: TestPermissions): Permissions {
|
||||
return {
|
||||
read: !!perms.read,
|
||||
write: !!perms.write,
|
||||
net: !!perms.net,
|
||||
run: !!perms.run,
|
||||
env: !!perms.env,
|
||||
plugin: !!perms.plugin,
|
||||
hrtime: !!perms.hrtime
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap `TestFunction` in additional assertion that makes sure
|
||||
// the test case does not leak async "ops" - ie. number of async
|
||||
// completed ops after the test is the same as number of dispatched
|
||||
// ops. Note that "unref" ops are ignored since in nature that are
|
||||
// optional.
|
||||
function assertOps(fn: Deno.TestFunction): Deno.TestFunction {
|
||||
return async function asyncOpSanitizer(): Promise<void> {
|
||||
const pre = Deno.metrics();
|
||||
await fn();
|
||||
const post = Deno.metrics();
|
||||
// We're checking diff because one might spawn HTTP server in the background
|
||||
// that will be a pending async op before test starts.
|
||||
assertEquals(
|
||||
post.opsDispatchedAsync - pre.opsDispatchedAsync,
|
||||
post.opsCompletedAsync - pre.opsCompletedAsync,
|
||||
`Test case is leaking async ops.
|
||||
Before:
|
||||
- dispatched: ${pre.opsDispatchedAsync}
|
||||
- completed: ${pre.opsCompletedAsync}
|
||||
After:
|
||||
- dispatched: ${post.opsDispatchedAsync}
|
||||
- completed: ${post.opsCompletedAsync}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap `TestFunction` in additional assertion that makes sure
|
||||
// the test case does not "leak" resources - ie. resource table after
|
||||
// the test has exactly the same contents as before the test.
|
||||
function assertResources(fn: Deno.TestFunction): Deno.TestFunction {
|
||||
return async function resourceSanitizer(): Promise<void> {
|
||||
const pre = Deno.resources();
|
||||
await fn();
|
||||
const post = Deno.resources();
|
||||
const msg = `Test case is leaking resources.
|
||||
Before: ${JSON.stringify(pre, null, 2)}
|
||||
After: ${JSON.stringify(post, null, 2)}`;
|
||||
assertEquals(pre, post, msg);
|
||||
};
|
||||
}
|
||||
|
||||
interface UnitTestOptions {
|
||||
skip?: boolean;
|
||||
perms?: TestPermissions;
|
||||
}
|
||||
|
||||
export function unitTest(fn: Deno.TestFunction): void;
|
||||
export function unitTest(options: UnitTestOptions, fn: Deno.TestFunction): void;
|
||||
export function unitTest(
|
||||
optionsOrFn: UnitTestOptions | Deno.TestFunction,
|
||||
maybeFn?: Deno.TestFunction
|
||||
): void {
|
||||
assert(optionsOrFn, "At least one argument is required");
|
||||
|
||||
let options: UnitTestOptions;
|
||||
let name: string;
|
||||
let fn: Deno.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");
|
||||
}
|
||||
|
||||
if (options.skip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPerms = normalizeTestPermissions(options.perms || {});
|
||||
registerPermCombination(normalizedPerms);
|
||||
if (!permissionsMatch(processPerms, normalizedPerms)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const testDefinition: Deno.TestDefinition = {
|
||||
name,
|
||||
fn: assertResources(assertOps(fn))
|
||||
};
|
||||
Deno.test(testDefinition);
|
||||
}
|
||||
|
||||
function extractNumber(re: RegExp, str: string): number | undefined {
|
||||
const match = str.match(re);
|
||||
|
||||
if (match) {
|
||||
return Number.parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseUnitTestOutput(
|
||||
reader: Deno.Reader,
|
||||
print: boolean
|
||||
): Promise<{ actual?: number; expected?: number; resultOutput?: string }> {
|
||||
let expected, actual, result;
|
||||
|
||||
for await (const line of readLines(reader)) {
|
||||
if (!expected) {
|
||||
// expect "running 30 tests"
|
||||
expected = extractNumber(/running (\d+) tests/, line);
|
||||
} else if (line.indexOf("test result:") !== -1) {
|
||||
result = line;
|
||||
}
|
||||
|
||||
if (print) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the number of expected tests equals what was reported at the
|
||||
// bottom.
|
||||
if (result) {
|
||||
// result should be a string like this:
|
||||
// "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; ..."
|
||||
actual = extractNumber(/(\d+) passed/, result);
|
||||
}
|
||||
|
||||
return { actual, expected, resultOutput: result };
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
unitTest(
|
||||
{ perms: { read: true } },
|
||||
async function parsingUnitTestOutput(): Promise<void> {
|
||||
const cwd = Deno.cwd();
|
||||
const testDataPath = `${cwd}/tools/testdata/`;
|
||||
|
||||
let result;
|
||||
|
||||
// This is an example of a successful unit test output.
|
||||
const f1 = await Deno.open(`${testDataPath}/unit_test_output1.txt`);
|
||||
result = await parseUnitTestOutput(f1, false);
|
||||
assertEquals(result.actual, 96);
|
||||
assertEquals(result.expected, 96);
|
||||
f1.close();
|
||||
|
||||
// This is an example of a silently dying unit test.
|
||||
const f2 = await Deno.open(`${testDataPath}/unit_test_output2.txt`);
|
||||
result = await parseUnitTestOutput(f2, false);
|
||||
assertEquals(result.actual, undefined);
|
||||
assertEquals(result.expected, 96);
|
||||
f2.close();
|
||||
|
||||
// This is an example of compiling before successful unit tests.
|
||||
const f3 = await Deno.open(`${testDataPath}/unit_test_output3.txt`);
|
||||
result = await parseUnitTestOutput(f3, false);
|
||||
assertEquals(result.actual, 96);
|
||||
assertEquals(result.expected, 96);
|
||||
f3.close();
|
||||
|
||||
// Check what happens on empty output.
|
||||
const f = new Deno.Buffer(new TextEncoder().encode("\n\n\n"));
|
||||
result = await parseUnitTestOutput(f, false);
|
||||
assertEquals(result.actual, undefined);
|
||||
assertEquals(result.expected, undefined);
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Ensure all unit test files (e.g. xxx_test.ts) are present as imports in
|
||||
* cli/js/unit_tests.ts as it is easy to miss this out
|
||||
*/
|
||||
unitTest(
|
||||
{ perms: { read: true } },
|
||||
async function assertAllUnitTestFilesImported(): Promise<void> {
|
||||
const directoryTestFiles = Deno.readdirSync("./cli/js")
|
||||
.map(k => k.name)
|
||||
.filter(file => file!.endsWith("_test.ts"));
|
||||
const unitTestsFile: Uint8Array = Deno.readFileSync(
|
||||
"./cli/js/unit_tests.ts"
|
||||
);
|
||||
const importLines = new TextDecoder("utf-8")
|
||||
.decode(unitTestsFile)
|
||||
.split("\n")
|
||||
.filter(line => line.startsWith("import") && line.includes("_test.ts"));
|
||||
const importedTestFiles = importLines.map(
|
||||
relativeFilePath => relativeFilePath.match(/\/([^\/]+)";/)![1]
|
||||
);
|
||||
|
||||
directoryTestFiles.forEach(dirFile => {
|
||||
if (!importedTestFiles.includes(dirFile!)) {
|
||||
throw new Error(
|
||||
"cil/js/unit_tests.ts is missing import of test file: cli/js/" +
|
||||
dirFile
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue