0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

test(cli): run unit tests using Deno.test (#10330)

Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
This commit is contained in:
Casper Beyer 2021-04-27 19:14:01 +08:00 committed by GitHub
parent baf7092ea2
commit 3a03084580
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 26 additions and 724 deletions

View file

@ -33,16 +33,17 @@ fn js_unit_tests_lint() {
#[test]
fn js_unit_tests() {
let _g = util::http_server();
let mut deno = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg("test")
.arg("--unstable")
.arg("--location=http://js-unit-tests/foo/bar")
.arg("-A")
.arg("cli/tests/unit/unit_test_runner.ts")
.arg("--master")
.arg("--verbose")
.arg("cli/tests/unit")
.spawn()
.expect("failed to spawn script");
let status = deno.wait().expect("failed to wait for the child process");
assert_eq!(Some(0), status.code());
assert!(status.success());

View file

@ -3,8 +3,8 @@
Files in this directory are unit tests for Deno runtime.
Testing Deno runtime code requires checking API under different runtime
permissions (ie. running with different `--allow-*` flags). To accomplish this
all tests exercised are created using `unitTest()` function.
permissions. To accomplish this all tests exercised are created using
`unitTest()` function.
```ts
import { unitTest } from "./test_util.ts";
@ -24,51 +24,16 @@ unitTest(
);
```
`unitTest` is a wrapper function that enhances `Deno.test()` API in several
ways:
- ability to conditionally skip tests using `UnitTestOptions.skip`.
- ability to register required set of permissions for given test case using
`UnitTestOptions.perms`.
- sanitization of resources - ensuring that tests close all opened resources
preventing interference between tests.
- sanitization of async ops - ensuring that tests don't leak async ops by
ensuring that all started async ops are done before test finishes.
## Running tests
`unit_test_runner.ts` is the main script used to run unit tests.
Runner discovers required permissions combinations by loading
`cli/tests/unit/unit_tests.ts` and going through all registered instances of
`unitTest`.
There are three ways to run `unit_test_runner.ts`:
```sh
# Run all tests. Spawns worker processes for each discovered permission
# combination:
target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --master
# Run all tests.
target/debug/deno test --allow-all --unstable cli/tests/unit/
# By default all output of worker processes is discarded; for debug purposes
# the --verbose flag preserves output from the worker.
target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --master --verbose
# Run subset of tests that don't require any permissions.
target/debug/deno run --unstable cli/tests/unit/unit_test_runner.ts
# Run subset tests that require "net" and "read" permissions.
target/debug/deno run --unstable --allow-net --allow-read cli/tests/unit/unit_test_runner.ts
# "worker" mode communicates with parent using TCP socket on provided address;
# after initial setup drops permissions to specified set. It shouldn't be used
# directly, only be "master" process.
target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --worker --addr=127.0.0.1:4500 --perms=net,write,run
# Run specific tests.
target/debug/deno run --unstable --allow-net cli/tests/unit/unit_test_runner.ts -- netTcpListenClose
RUST_BACKTRACE=1 cargo run -- run --unstable --allow-read --allow-write cli/tests/unit/unit_test_runner.ts -- netUnixDialListen
# Run a specific test module
target/debug/deno test --allow-all --unstable cli/tests/unit/files_test.ts
```
### Http server

View file

@ -25,105 +25,6 @@ export type { Deferred } from "../../../test_util/std/async/deferred.ts";
export { readLines } from "../../../test_util/std/io/bufio.ts";
export { parse as parseArgs } from "../../../test_util/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();
const onlyTests = REGISTERED_UNIT_TESTS.filter(({ only }) => only);
const unitTests = onlyTests.length > 0 ? onlyTests : REGISTERED_UNIT_TESTS;
for (const unitTestDefinition of unitTests) {
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;
@ -140,16 +41,8 @@ interface UnitTestOptions {
perms?: UnitTestPermissions;
}
interface UnitTestDefinition extends Deno.TestDefinition {
ignore: boolean;
only: 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(
@ -179,188 +72,25 @@ export function unitTest(
assert(name, "Missing test function name");
}
const normalizedPerms = normalizeTestPermissions(options.perms || {});
registerPermCombination(normalizedPerms);
const unitTestDefinition: UnitTestDefinition = {
const testDefinition: Deno.TestDefinition = {
name,
fn,
ignore: !!options.ignore,
only: !!options.only,
perms: normalizedPerms,
permissions: Object.assign({
read: false,
write: false,
net: false,
env: false,
run: false,
plugin: false,
hrtime: false,
}, options.perms),
};
REGISTERED_UNIT_TESTS.push(unitTestDefinition);
Deno.test(testDefinition);
}
const encoder = new TextEncoder();
// Replace functions with null, errors with their stack strings, and JSONify.
// deno-lint-ignore 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,
// deno-lint-ignore no-explicit-any
results: message.end.results.map((result: any) => ({
...result,
error: result.error?.stack,
})),
},
});
}
export async function reportToConn(
conn: Deno.Conn,
// deno-lint-ignore no-explicit-any
message: any,
): Promise<void> {
const line = serializeTestMessage(message);
const encodedMsg = encoder.encode(line + (message.end == null ? "\n" : ""));
// deno-lint-ignore no-deprecated-deno-api
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,
);
}
});
},
);
export function pathToAbsoluteFileUrl(path: string): URL {
path = resolve(path);

View file

@ -1,319 +0,0 @@
#!/usr/bin/env -S deno run --reload --allow-run
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import "./unit_tests.ts";
import {
colors,
fmtPerms,
parseArgs,
permissionCombinations,
Permissions,
readLines,
REGISTERED_UNIT_TESTS,
registerUnitTests,
reportToConn,
} from "./test_util.ts";
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
const internalObj = Deno[Deno.internal];
// deno-lint-ignore no-explicit-any
const reportToConsole = internalObj.reportToConsole as (message: any) => void;
// deno-lint-ignore no-explicit-any
const runTests = internalObj.runTests as (options: any) => Promise<any>;
interface PermissionSetTestResult {
perms: Permissions;
passed: boolean;
// deno-lint-ignore no-explicit-any
endMessage: any;
permsStr: string;
}
const PERMISSIONS: Deno.PermissionName[] = [
"read",
"write",
"net",
"env",
"run",
"plugin",
"hrtime",
];
/**
* Take a list of permissions and revoke missing permissions.
*/
async function dropWorkerPermissions(
requiredPermissions: Deno.PermissionName[],
): Promise<void> {
const permsToDrop = PERMISSIONS.filter((p): boolean => {
return !requiredPermissions.includes(p);
});
for (const perm of permsToDrop) {
await Deno.permissions.revoke({ name: perm });
}
}
async function workerRunnerMain(
addrStr: string,
permsStr: string,
filter?: string,
): Promise<void> {
const [hostname, port] = addrStr.split(":");
const addr = { hostname, port: Number(port) };
let perms: Deno.PermissionName[] = [];
if (permsStr.length > 0) {
perms = permsStr.split(",") as Deno.PermissionName[];
}
// Setup reporter
const conn = await Deno.connect(addr);
// Drop current process permissions to requested set
await dropWorkerPermissions(perms);
// Register unit tests that match process permissions
await registerUnitTests();
// Execute tests
await runTests({
exitOnFail: false,
filter,
reportToConsole: false,
onMessage: reportToConn.bind(null, conn),
});
}
function spawnWorkerRunner(
verbose: boolean,
addr: string,
perms: Permissions,
filter?: string,
): Deno.Process {
// run subsequent tests using same deno executable
const permStr = Object.keys(perms)
.filter((permName): boolean => {
return perms[permName as Deno.PermissionName] === true;
})
.join(",");
const cmd = [
Deno.execPath(),
"run",
"--unstable", // TODO(ry) be able to test stable vs unstable
"--location=http://js-unit-tests/foo/bar",
"-A",
"cli/tests/unit/unit_test_runner.ts",
"--worker",
`--addr=${addr}`,
`--perms=${permStr}`,
];
if (filter) {
cmd.push("--");
cmd.push(filter);
}
const ioMode = verbose ? "inherit" : "null";
const p = Deno.run({
cmd,
stdin: ioMode,
stdout: ioMode,
stderr: ioMode,
});
return p;
}
async function runTestsForPermissionSet(
listener: Deno.Listener,
addrStr: string,
verbose: boolean,
perms: Permissions,
filter?: string,
): Promise<PermissionSetTestResult> {
const permsFmt = fmtPerms(perms);
console.log(`Running tests for: ${permsFmt}`);
const workerProcess = spawnWorkerRunner(verbose, addrStr, perms, filter);
// Wait for worker subprocess to go online
const conn = await listener.accept();
let expectedPassedTests;
// deno-lint-ignore no-explicit-any
let endMessage: any;
try {
for await (const line of readLines(conn)) {
// deno-lint-ignore no-explicit-any
const message = JSON.parse(line) as any;
reportToConsole(message);
if (message.start != null) {
expectedPassedTests = message.start.tests.length;
} else if (message.end != null) {
endMessage = message.end;
}
}
} finally {
// Close socket to worker.
conn.close();
}
if (expectedPassedTests == null) {
throw new Error("Worker runner didn't report start");
}
if (endMessage == null) {
throw new Error("Worker runner didn't report end");
}
const workerStatus = await workerProcess.status();
if (!workerStatus.success) {
throw new Error(
`Worker runner exited with status code: ${workerStatus.code}`,
);
}
workerProcess.close();
const passed = expectedPassedTests === endMessage.passed + endMessage.ignored;
return {
perms,
passed,
permsStr: permsFmt,
endMessage,
};
}
async function masterRunnerMain(
verbose: boolean,
filter?: string,
): Promise<void> {
console.log(
"Discovered permission combinations for tests:",
permissionCombinations.size,
);
for (const perms of permissionCombinations.values()) {
console.log("\t" + fmtPerms(perms));
}
const testResults = new Set<PermissionSetTestResult>();
const addr = { hostname: "127.0.0.1", port: 4510 };
const addrStr = `${addr.hostname}:${addr.port}`;
const listener = Deno.listen(addr);
for (const perms of permissionCombinations.values()) {
const result = await runTestsForPermissionSet(
listener,
addrStr,
verbose,
perms,
filter,
);
testResults.add(result);
}
// if any run tests returned non-zero status then whole test
// run should fail
let testsPassed = true;
for (const testResult of testResults) {
const { permsStr, endMessage } = testResult;
console.log(`Summary for ${permsStr}`);
reportToConsole({ end: endMessage });
testsPassed = testsPassed && testResult.passed;
}
if (!testsPassed) {
console.error("Unit tests failed");
Deno.exit(1);
}
console.log("Unit tests passed");
if (REGISTERED_UNIT_TESTS.find(({ only }) => only)) {
console.error(
`\n${colors.red("FAILED")} because the "only" option was used`,
);
Deno.exit(1);
}
}
const HELP = `Unit test runner
Run tests matching current process permissions:
deno --allow-write unit_test_runner.ts
deno --allow-net --allow-hrtime unit_test_runner.ts
deno --allow-write unit_test_runner.ts -- testWriteFile
Run "master" process that creates "worker" processes
for each discovered permission combination:
deno -A unit_test_runner.ts --master
Run worker process for given permissions:
deno -A unit_test_runner.ts --worker --perms=net,read,write --addr=127.0.0.1:4500
OPTIONS:
--master
Run in master mode, spawning worker processes for
each discovered permission combination
--worker
Run in worker mode, requires "perms" and "addr" flags,
should be run with "-A" flag; after setup worker will
drop permissions to required set specified in "perms"
--perms=<perm_name>...
Set of permissions this process should run tests with,
--addr=<addr>
Address of TCP socket for reporting
ARGS:
-- <filter>...
Run only tests with names matching filter, must
be used after "--"
`;
function assertOrHelp(expr: unknown): asserts expr {
if (!expr) {
console.log(HELP);
Deno.exit(1);
}
}
async function main(): Promise<void> {
const args = parseArgs(Deno.args, {
boolean: ["master", "worker", "verbose"],
"--": true,
});
if (args.help) {
console.log(HELP);
return;
}
const filter = args["--"][0];
// Master mode
if (args.master) {
return masterRunnerMain(args.verbose, filter);
}
// Worker mode
if (args.worker) {
assertOrHelp(typeof args.addr === "string");
assertOrHelp(typeof args.perms === "string");
return workerRunnerMain(args.addr, args.perms, filter);
}
// Running tests matching current process permissions
await registerUnitTests();
await runTests({ filter });
}
main();

View file

@ -1,77 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// This test is executed as part of unit test suite.
//
// Test runner automatically spawns subprocesses for each required permissions combination.
import "./abort_controller_test.ts";
import "./blob_test.ts";
import "./body_test.ts";
import "./buffer_test.ts";
import "./build_test.ts";
import "./chmod_test.ts";
import "./chown_test.ts";
import "./console_test.ts";
import "./copy_file_test.ts";
import "./custom_event_test.ts";
import "./dir_test.ts";
import "./opcall_test.ts";
import "./error_stack_test.ts";
import "./event_test.ts";
import "./event_target_test.ts";
import "./fetch_test.ts";
import "./file_test.ts";
import "./filereader_test.ts";
import "./files_test.ts";
import "./filter_function_test.ts";
import "./format_error_test.ts";
import "./fs_events_test.ts";
import "./get_random_values_test.ts";
import "./globals_test.ts";
import "./headers_test.ts";
import "./http_test.ts";
import "./internals_test.ts";
import "./io_test.ts";
import "./link_test.ts";
import "./make_temp_test.ts";
import "./metrics_test.ts";
import "./mkdir_test.ts";
import "./net_test.ts";
import "./os_test.ts";
import "./permissions_test.ts";
import "./path_from_url_test.ts";
import "./process_test.ts";
import "./progressevent_test.ts";
import "./real_path_test.ts";
import "./read_dir_test.ts";
import "./read_text_file_test.ts";
import "./read_file_test.ts";
import "./read_link_test.ts";
import "./remove_test.ts";
import "./rename_test.ts";
import "./request_test.ts";
import "./resources_test.ts";
import "./response_test.ts";
import "./signal_test.ts";
import "./stat_test.ts";
import "./stdio_test.ts";
import "./streams_deprecated.ts";
import "./symlink_test.ts";
import "./sync_test.ts";
import "./text_encoding_test.ts";
import "./testing_test.ts";
import "./timers_test.ts";
import "./tls_test.ts";
import "./truncate_test.ts";
import "./tty_test.ts";
import "./umask_test.ts";
import "./url_test.ts";
import "./url_search_params_test.ts";
import "./utime_test.ts";
import "./worker_types.ts";
import "./write_file_test.ts";
import "./write_text_file_test.ts";
import "./performance_test.ts";
import "./version_test.ts";
import "./websocket_test.ts";
import "./webgpu_test.ts";

View file

@ -179,10 +179,11 @@ unitTest({
});
const encoder = device.createCommandEncoder();
const view = texture.createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: texture.createView(),
view,
storeOp: "store",
loadValue: [0, 1, 0, 1],
},
@ -204,7 +205,8 @@ unitTest({
dimensions,
);
device.queue.submit([encoder.finish()]);
const bundle = encoder.finish();
device.queue.submit([bundle]);
await outputBuffer.mapAsync(1);
const data = new Uint8Array(outputBuffer.getMappedRange());