1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

refactor(cli/js/testing): Reduce testing interfaces (#4451)

* Reduce "testing" interfaces
* Use a callback instead of a generator for Deno.runTests()
* Default RunTestsOptions::reportToConsole to true
* Compose TestMessage into a single interface
This commit is contained in:
Nayeem Rahman 2020-04-01 09:47:23 +01:00 committed by GitHub
parent 017a611131
commit 270e87d9db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 237 additions and 384 deletions

View file

@ -115,7 +115,13 @@ export { utimeSync, utime } from "./ops/fs/utime.ts";
export { version } from "./version.ts"; export { version } from "./version.ts";
export { writeFileSync, writeFile, WriteFileOptions } from "./write_file.ts"; export { writeFileSync, writeFile, WriteFileOptions } from "./write_file.ts";
export const args: string[] = []; export const args: string[] = [];
export { test, runTests, TestEvent, ConsoleTestReporter } from "./testing.ts"; export {
RunTestsOptions,
TestDefinition,
TestMessage,
runTests,
test,
} from "./testing.ts";
// These are internal Deno APIs. We are marking them as internal so they do not // These are internal Deno APIs. We are marking them as internal so they do not
// appear in the runtime type library. // appear in the runtime type library.

View file

@ -12,10 +12,8 @@ declare namespace Deno {
* See: https://no-color.org/ */ * See: https://no-color.org/ */
export let noColor: boolean; export let noColor: boolean;
export type TestFunction = () => void | Promise<void>;
export interface TestDefinition { export interface TestDefinition {
fn: TestFunction; fn: () => void | Promise<void>;
name: string; name: string;
ignore?: boolean; ignore?: boolean;
disableOpSanitizer?: boolean; disableOpSanitizer?: boolean;
@ -70,7 +68,7 @@ declare namespace Deno {
* assertEquals(decoder.decode(data), "Hello world") * assertEquals(decoder.decode(data), "Hello world")
* }); * });
**/ **/
export function test(fn: TestFunction): void; export function test(fn: () => void | Promise<void>): void;
/** Register a test which will be run when `deno test` is used on the command /** Register a test which will be run when `deno test` is used on the command
* line and the containing module looks like a test module, or explicitly * line and the containing module looks like a test module, or explicitly
@ -88,78 +86,37 @@ declare namespace Deno {
* assertEquals(decoder.decode(data), "Hello world") * assertEquals(decoder.decode(data), "Hello world")
* }); * });
* */ * */
export function test(name: string, fn: TestFunction): void; export function test(name: string, fn: () => void | Promise<void>): void;
enum TestStatus { export interface TestMessage {
Passed = "passed", start?: {
Failed = "failed", tests: TestDefinition[];
Ignored = "ignored", };
} testStart?: {
[P in keyof TestDefinition]: TestDefinition[P];
interface TestResult { };
name: string; testEnd?: {
status: TestStatus; name: string;
duration?: number; status: "passed" | "failed" | "ignored";
error?: Error; duration: number;
} error?: Error;
};
interface TestStats { end?: {
filtered: number; filtered: number;
ignored: number; ignored: number;
measured: number; measured: number;
passed: number; passed: number;
failed: number; failed: number;
} duration: number;
results: Array<TestMessage["testEnd"] & {}>;
export enum TestEvent { };
Start = "start",
TestStart = "testStart",
TestEnd = "testEnd",
End = "end",
}
interface TestEventStart {
kind: TestEvent.Start;
tests: number;
}
interface TestEventTestStart {
kind: TestEvent.TestStart;
name: string;
}
interface TestEventTestEnd {
kind: TestEvent.TestEnd;
result: TestResult;
}
interface TestEventEnd {
kind: TestEvent.End;
stats: TestStats;
duration: number;
results: TestResult[];
}
interface TestReporter {
start(event: TestEventStart): Promise<void>;
testStart(msg: TestEventTestStart): Promise<void>;
testEnd(msg: TestEventTestEnd): Promise<void>;
end(event: TestEventEnd): Promise<void>;
}
export class ConsoleTestReporter implements TestReporter {
constructor();
start(event: TestEventStart): Promise<void>;
testStart(msg: TestEventTestStart): Promise<void>;
testEnd(msg: TestEventTestEnd): Promise<void>;
end(event: TestEventEnd): Promise<void>;
} }
export interface RunTestsOptions { export interface RunTestsOptions {
/** If `true`, Deno will exit with status code 1 if there was /** If `true`, Deno will exit with status code 1 if there was
* test failure. Defaults to `true`. */ * test failure. Defaults to `true`. */
exitOnFail?: boolean; exitOnFail?: boolean;
/** If `true`, Deno will exit upon first test failure Defaults to `false`. */ /** If `true`, Deno will exit upon first test failure. Defaults to `false`. */
failFast?: boolean; failFast?: boolean;
/** String or RegExp used to filter test to run. Only test with names /** String or RegExp used to filter test to run. Only test with names
* matching provided `String` or `RegExp` will be run. */ * matching provided `String` or `RegExp` will be run. */
@ -169,8 +126,10 @@ declare namespace Deno {
skip?: string | RegExp; skip?: string | RegExp;
/** Disable logging of the results. Defaults to `false`. */ /** Disable logging of the results. Defaults to `false`. */
disableLog?: boolean; disableLog?: boolean;
/** Custom reporter class. If not provided uses console reporter. */ /** If true, report results to the console as is done for `deno test`. Defaults to `true`. */
reporter?: TestReporter; reportToConsole?: boolean;
/** Called for each message received from the test run. */
onMessage?: (message: TestMessage) => void | Promise<void>;
} }
/** Run any tests which have been registered via `Deno.test()`. Always resolves /** Run any tests which have been registered via `Deno.test()`. Always resolves
@ -193,11 +152,7 @@ declare namespace Deno {
*/ */
export function runTests( export function runTests(
opts?: RunTestsOptions opts?: RunTestsOptions
): Promise<{ ): Promise<TestMessage["end"]> & {};
results: TestResult[];
stats: TestStats;
duration: number;
}>;
/** Returns an array containing the 1, 5, and 15 minute load averages. The /** Returns an array containing the 1, 5, and 15 minute load averages. The
* load average is a measure of CPU and IO utilization of the last one, five, * load average is a measure of CPU and IO utilization of the last one, five,

View file

@ -3,6 +3,7 @@ import { gray, green, italic, red, yellow } from "./colors.ts";
import { exit } from "./ops/os.ts"; import { exit } from "./ops/os.ts";
import { Console, stringifyArgs } from "./web/console.ts"; import { Console, stringifyArgs } from "./web/console.ts";
import { stdout } from "./files.ts"; import { stdout } from "./files.ts";
import { exposeForTest } from "./internals.ts";
import { TextEncoder } from "./web/text_encoding.ts"; import { TextEncoder } from "./web/text_encoding.ts";
import { metrics } from "./ops/runtime.ts"; import { metrics } from "./ops/runtime.ts";
import { resources } from "./ops/resources.ts"; import { resources } from "./ops/resources.ts";
@ -18,12 +19,12 @@ function formatDuration(time = 0): string {
return gray(italic(timeStr)); return gray(italic(timeStr));
} }
// Wrap `TestFunction` in additional assertion that makes sure // Wrap test function in additional assertion that makes sure
// the test case does not leak async "ops" - ie. number of async // the test case does not leak async "ops" - ie. number of async
// completed ops after the test is the same as number of dispatched // completed ops after the test is the same as number of dispatched
// ops. Note that "unref" ops are ignored since in nature that are // ops. Note that "unref" ops are ignored since in nature that are
// optional. // optional.
function assertOps(fn: TestFunction): TestFunction { function assertOps(fn: () => void | Promise<void>): () => void | Promise<void> {
return async function asyncOpSanitizer(): Promise<void> { return async function asyncOpSanitizer(): Promise<void> {
const pre = metrics(); const pre = metrics();
await fn(); await fn();
@ -38,17 +39,19 @@ function assertOps(fn: TestFunction): TestFunction {
Before: Before:
- dispatched: ${pre.opsDispatchedAsync} - dispatched: ${pre.opsDispatchedAsync}
- completed: ${pre.opsCompletedAsync} - completed: ${pre.opsCompletedAsync}
After: After:
- dispatched: ${post.opsDispatchedAsync} - dispatched: ${post.opsDispatchedAsync}
- completed: ${post.opsCompletedAsync}` - completed: ${post.opsCompletedAsync}`
); );
}; };
} }
// Wrap `TestFunction` in additional assertion that makes sure // Wrap test function in additional assertion that makes sure
// the test case does not "leak" resources - ie. resource table after // the test case does not "leak" resources - ie. resource table after
// the test has exactly the same contents as before the test. // the test has exactly the same contents as before the test.
function assertResources(fn: TestFunction): TestFunction { function assertResources(
fn: () => void | Promise<void>
): () => void | Promise<void> {
return async function resourceSanitizer(): Promise<void> { return async function resourceSanitizer(): Promise<void> {
const pre = resources(); const pre = resources();
await fn(); await fn();
@ -63,10 +66,8 @@ After: ${postStr}`;
}; };
} }
export type TestFunction = () => void | Promise<void>;
export interface TestDefinition { export interface TestDefinition {
fn: TestFunction; fn: () => void | Promise<void>;
name: string; name: string;
ignore?: boolean; ignore?: boolean;
disableOpSanitizer?: boolean; disableOpSanitizer?: boolean;
@ -76,13 +77,13 @@ export interface TestDefinition {
const TEST_REGISTRY: TestDefinition[] = []; const TEST_REGISTRY: TestDefinition[] = [];
export function test(t: TestDefinition): void; export function test(t: TestDefinition): void;
export function test(fn: TestFunction): void; export function test(fn: () => void | Promise<void>): void;
export function test(name: string, fn: TestFunction): void; export function test(name: string, fn: () => void | Promise<void>): void;
// Main test function provided by Deno, as you can see it merely // Main test function provided by Deno, as you can see it merely
// creates a new object with "name" and "fn" fields. // creates a new object with "name" and "fn" fields.
export function test( export function test(
t: string | TestDefinition | TestFunction, t: string | TestDefinition | (() => void | Promise<void>),
fn?: TestFunction fn?: () => void | Promise<void>
): void { ): void {
let testDef: TestDefinition; let testDef: TestDefinition;
@ -93,7 +94,7 @@ export function test(
if (!t) { if (!t) {
throw new TypeError("The test name can't be empty"); throw new TypeError("The test name can't be empty");
} }
testDef = { fn: fn as TestFunction, name: t, ignore: false }; testDef = { fn: fn as () => void | Promise<void>, name: t, ignore: false };
} else if (typeof t === "function") { } else if (typeof t === "function") {
if (!t.name) { if (!t.name) {
throw new TypeError("The test function can't be anonymous"); throw new TypeError("The test function can't be anonymous");
@ -120,70 +121,98 @@ export function test(
TEST_REGISTRY.push(testDef); TEST_REGISTRY.push(testDef);
} }
interface TestStats { export interface TestMessage {
filtered: number; start?: {
ignored: number; tests: TestDefinition[];
measured: number; };
passed: number; // Must be extensible, avoiding `testStart?: TestDefinition;`.
failed: number; testStart?: {
[P in keyof TestDefinition]: TestDefinition[P];
};
testEnd?: {
name: string;
status: "passed" | "failed" | "ignored";
duration: number;
error?: Error;
};
end?: {
filtered: number;
ignored: number;
measured: number;
passed: number;
failed: number;
duration: number;
results: Array<TestMessage["testEnd"] & {}>;
};
} }
export interface RunTestsOptions { const encoder = new TextEncoder();
exitOnFail?: boolean;
failFast?: boolean; function log(msg: string, noNewLine = false): void {
only?: string | RegExp; if (!noNewLine) {
skip?: string | RegExp; msg += "\n";
disableLog?: boolean; }
reporter?: TestReporter;
// Using `stdout` here because it doesn't force new lines
// compared to `console.log`; `core.print` on the other hand
// is line-buffered and doesn't output message without newline
stdout.writeSync(encoder.encode(msg));
} }
enum TestStatus { function reportToConsole(message: TestMessage): void {
Passed = "passed", if (message.start != null) {
Failed = "failed", log(`running ${message.start.tests.length} tests`);
Ignored = "ignored", } else if (message.testStart != null) {
const { name } = message.testStart;
log(`test ${name} ... `, true);
return;
} else if (message.testEnd != null) {
switch (message.testEnd.status) {
case "passed":
log(`${GREEN_OK} ${formatDuration(message.testEnd.duration)}`);
break;
case "failed":
log(`${RED_FAILED} ${formatDuration(message.testEnd.duration)}`);
break;
case "ignored":
log(`${YELLOW_IGNORED} ${formatDuration(message.testEnd.duration)}`);
break;
}
} else if (message.end != null) {
const failures = message.end.results.filter((m) => m.error != null);
if (failures.length > 0) {
log(`\nfailures:\n`);
for (const { name, error } of failures) {
log(name);
log(stringifyArgs([error!]));
log("");
}
log(`failures:\n`);
for (const { name } of failures) {
log(`\t${name}`);
}
}
log(
`\ntest result: ${message.end.failed ? RED_FAILED : GREEN_OK}. ` +
`${message.end.passed} passed; ${message.end.failed} failed; ` +
`${message.end.ignored} ignored; ${message.end.measured} measured; ` +
`${message.end.filtered} filtered out ` +
`${formatDuration(message.end.duration)}\n`
);
}
} }
interface TestResult { exposeForTest("reportToConsole", reportToConsole);
name: string;
status: TestStatus;
duration: number;
error?: Error;
}
export enum TestEvent {
Start = "start",
TestStart = "testStart",
TestEnd = "testEnd",
End = "end",
}
interface TestEventStart {
kind: TestEvent.Start;
tests: number;
}
interface TestEventTestStart {
kind: TestEvent.TestStart;
name: string;
}
interface TestEventTestEnd {
kind: TestEvent.TestEnd;
result: TestResult;
}
interface TestEventEnd {
kind: TestEvent.End;
stats: TestStats;
duration: number;
results: TestResult[];
}
// TODO: already implements AsyncGenerator<RunTestsMessage>, but add as "implements to class" // TODO: already implements AsyncGenerator<RunTestsMessage>, but add as "implements to class"
// TODO: implements PromiseLike<TestsResult> // TODO: implements PromiseLike<RunTestsEndResult>
class TestApi { class TestApi {
readonly testsToRun: TestDefinition[]; readonly testsToRun: TestDefinition[];
readonly stats: TestStats = { readonly stats = {
filtered: 0, filtered: 0,
ignored: 0, ignored: 0,
measured: 0, measured: 0,
@ -200,51 +229,43 @@ class TestApi {
this.stats.filtered = tests.length - this.testsToRun.length; this.stats.filtered = tests.length - this.testsToRun.length;
} }
async *[Symbol.asyncIterator](): AsyncIterator< async *[Symbol.asyncIterator](): AsyncIterator<TestMessage> {
TestEventStart | TestEventTestStart | TestEventTestEnd | TestEventEnd yield { start: { tests: this.testsToRun } };
> {
yield {
kind: TestEvent.Start,
tests: this.testsToRun.length,
};
const results: TestResult[] = []; const results: Array<TestMessage["testEnd"] & {}> = [];
const suiteStart = +new Date(); const suiteStart = +new Date();
for (const { name, fn, ignore } of this.testsToRun) { for (const test of this.testsToRun) {
const result: Partial<TestResult> = { name, duration: 0 }; const endMessage: Partial<TestMessage["testEnd"] & {}> = {
yield { kind: TestEvent.TestStart, name }; name: test.name,
if (ignore) { duration: 0,
result.status = TestStatus.Ignored; };
yield { testStart: { ...test } };
if (test.ignore) {
endMessage.status = "ignored";
this.stats.ignored++; this.stats.ignored++;
} else { } else {
const start = +new Date(); const start = +new Date();
try { try {
await fn(); await test.fn();
result.status = TestStatus.Passed; endMessage.status = "passed";
this.stats.passed++; this.stats.passed++;
} catch (err) { } catch (err) {
result.status = TestStatus.Failed; endMessage.status = "failed";
result.error = err; endMessage.error = err;
this.stats.failed++; this.stats.failed++;
} finally {
result.duration = +new Date() - start;
} }
endMessage.duration = +new Date() - start;
} }
yield { kind: TestEvent.TestEnd, result: result as TestResult }; results.push(endMessage as TestMessage["testEnd"] & {});
results.push(result as TestResult); yield { testEnd: endMessage as TestMessage["testEnd"] };
if (this.failFast && result.error != null) { if (this.failFast && endMessage.error != null) {
break; break;
} }
} }
const duration = +new Date() - suiteStart; const duration = +new Date() - suiteStart;
yield { yield { end: { ...this.stats, duration, results } };
kind: TestEvent.End,
stats: this.stats,
results,
duration,
};
} }
} }
@ -275,95 +296,14 @@ function createFilterFn(
}; };
} }
interface TestReporter { export interface RunTestsOptions {
start(msg: TestEventStart): Promise<void>; exitOnFail?: boolean;
testStart(msg: TestEventTestStart): Promise<void>; failFast?: boolean;
testEnd(msg: TestEventTestEnd): Promise<void>; only?: string | RegExp;
end(msg: TestEventEnd): Promise<void>; skip?: string | RegExp;
} disableLog?: boolean;
reportToConsole?: boolean;
export class ConsoleTestReporter implements TestReporter { onMessage?: (message: TestMessage) => void | Promise<void>;
start(event: TestEventStart): Promise<void> {
ConsoleTestReporter.log(`running ${event.tests} tests`);
return Promise.resolve();
}
testStart(event: TestEventTestStart): Promise<void> {
const { name } = event;
ConsoleTestReporter.log(`test ${name} ... `, true);
return Promise.resolve();
}
testEnd(event: TestEventTestEnd): Promise<void> {
const { result } = event;
switch (result.status) {
case TestStatus.Passed:
ConsoleTestReporter.log(
`${GREEN_OK} ${formatDuration(result.duration)}`
);
break;
case TestStatus.Failed:
ConsoleTestReporter.log(
`${RED_FAILED} ${formatDuration(result.duration)}`
);
break;
case TestStatus.Ignored:
ConsoleTestReporter.log(
`${YELLOW_IGNORED} ${formatDuration(result.duration)}`
);
break;
}
return Promise.resolve();
}
end(event: TestEventEnd): Promise<void> {
const { stats, duration, results } = event;
// Attempting to match the output of Rust's test runner.
const failedTests = results.filter((r) => r.error);
if (failedTests.length > 0) {
ConsoleTestReporter.log(`\nfailures:\n`);
for (const result of failedTests) {
ConsoleTestReporter.log(`${result.name}`);
ConsoleTestReporter.log(`${stringifyArgs([result.error!])}`);
ConsoleTestReporter.log("");
}
ConsoleTestReporter.log(`failures:\n`);
for (const result of failedTests) {
ConsoleTestReporter.log(`\t${result.name}`);
}
}
ConsoleTestReporter.log(
`\ntest result: ${stats.failed ? RED_FAILED : GREEN_OK}. ` +
`${stats.passed} passed; ${stats.failed} failed; ` +
`${stats.ignored} ignored; ${stats.measured} measured; ` +
`${stats.filtered} filtered out ` +
`${formatDuration(duration)}\n`
);
return Promise.resolve();
}
static encoder = new TextEncoder();
static log(msg: string, noNewLine = false): Promise<void> {
if (!noNewLine) {
msg += "\n";
}
// Using `stdout` here because it doesn't force new lines
// compared to `console.log`; `core.print` on the other hand
// is line-buffered and doesn't output message without newline
stdout.writeSync(ConsoleTestReporter.encoder.encode(msg));
return Promise.resolve();
}
} }
export async function runTests({ export async function runTests({
@ -372,19 +312,12 @@ export async function runTests({
only = undefined, only = undefined,
skip = undefined, skip = undefined,
disableLog = false, disableLog = false,
reporter = undefined, reportToConsole: reportToConsole_ = true,
}: RunTestsOptions = {}): Promise<{ onMessage = undefined,
results: TestResult[]; }: RunTestsOptions = {}): Promise<TestMessage["end"] & {}> {
stats: TestStats;
duration: number;
}> {
const filterFn = createFilterFn(only, skip); const filterFn = createFilterFn(only, skip);
const testApi = new TestApi(TEST_REGISTRY, filterFn, failFast); const testApi = new TestApi(TEST_REGISTRY, filterFn, failFast);
if (!reporter) {
reporter = new ConsoleTestReporter();
}
// @ts-ignore // @ts-ignore
const originalConsole = globalThis.console; const originalConsole = globalThis.console;
@ -393,24 +326,17 @@ export async function runTests({
globalThis.console = disabledConsole; globalThis.console = disabledConsole;
} }
let endMsg: TestEventEnd; let endMsg: TestMessage["end"];
for await (const testMsg of testApi) { for await (const message of testApi) {
switch (testMsg.kind) { if (onMessage != null) {
case TestEvent.Start: await onMessage(message);
await reporter.start(testMsg); }
continue; if (reportToConsole_) {
case TestEvent.TestStart: reportToConsole(message);
await reporter.testStart(testMsg); }
continue; if (message.end != null) {
case TestEvent.TestEnd: endMsg = message.end;
await reporter.testEnd(testMsg);
continue;
case TestEvent.End:
endMsg = testMsg;
delete endMsg!.kind;
await reporter.end(testMsg);
continue;
} }
} }
@ -419,7 +345,7 @@ export async function runTests({
globalThis.console = originalConsole; globalThis.console = originalConsole;
} }
if (endMsg!.stats.failed > 0 && exitOnFail) { if (endMsg!.failed > 0 && exitOnFail) {
exit(1); exit(1);
} }

View file

@ -31,7 +31,3 @@ unitTest(function formatDiagnosticError() {
} }
assert(thrown); assert(thrown);
}); });
if (import.meta.main) {
Deno.runTests();
}

View file

@ -132,19 +132,21 @@ interface UnitTestDefinition extends Deno.TestDefinition {
perms: Permissions; perms: Permissions;
} }
type TestFunction = () => void | Promise<void>;
export const REGISTERED_UNIT_TESTS: UnitTestDefinition[] = []; export const REGISTERED_UNIT_TESTS: UnitTestDefinition[] = [];
export function unitTest(fn: Deno.TestFunction): void; export function unitTest(fn: TestFunction): void;
export function unitTest(options: UnitTestOptions, fn: Deno.TestFunction): void; export function unitTest(options: UnitTestOptions, fn: TestFunction): void;
export function unitTest( export function unitTest(
optionsOrFn: UnitTestOptions | Deno.TestFunction, optionsOrFn: UnitTestOptions | TestFunction,
maybeFn?: Deno.TestFunction maybeFn?: TestFunction
): void { ): void {
assert(optionsOrFn, "At least one argument is required"); assert(optionsOrFn, "At least one argument is required");
let options: UnitTestOptions; let options: UnitTestOptions;
let name: string; let name: string;
let fn: Deno.TestFunction; let fn: TestFunction;
if (typeof optionsOrFn === "function") { if (typeof optionsOrFn === "function") {
options = {}; options = {};
@ -196,44 +198,38 @@ export function createResolvable<T>(): Resolvable<T> {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
export class SocketReporter implements Deno.TestReporter { // Replace functions with null, errors with their stack strings, and JSONify.
#conn: Deno.Conn; // eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeTestMessage(message: Deno.TestMessage): string {
return JSON.stringify({
start: message.start && {
...message.start,
tests: message.start.tests.map((test) => ({ ...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,
results: message.end.results.map((result) => ({
...result,
error: result.error?.stack,
})),
},
});
}
constructor(conn: Deno.Conn) { export async function reportToConn(
this.#conn = conn; conn: Deno.Conn,
} message: Deno.TestMessage
): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const line = serializeTestMessage(message);
async write(msg: any): Promise<void> { const encodedMsg = encoder.encode(line + (message.end == null ? "\n" : ""));
const encodedMsg = encoder.encode(JSON.stringify(msg) + "\n"); await Deno.writeAll(conn, encodedMsg);
await Deno.writeAll(this.#conn, encodedMsg); if (message.end != null) {
} conn.closeWrite();
async start(msg: Deno.TestEventStart): Promise<void> {
await this.write(msg);
}
async testStart(msg: Deno.TestEventTestStart): Promise<void> {
await this.write(msg);
}
async testEnd(msg: Deno.TestEventTestEnd): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serializedMsg: any = { ...msg };
// Error is a JS object, so we need to turn it into string to
// send over socket.
if (serializedMsg.result.error) {
serializedMsg.result.error = String(serializedMsg.result.error.stack);
}
await this.write(serializedMsg);
}
async end(msg: Deno.TestEventEnd): Promise<void> {
const encodedMsg = encoder.encode(JSON.stringify(msg));
await Deno.writeAll(this.#conn, encodedMsg);
this.#conn.closeWrite();
} }
} }

View file

@ -6,18 +6,20 @@ import {
permissionCombinations, permissionCombinations,
Permissions, Permissions,
registerUnitTests, registerUnitTests,
SocketReporter,
fmtPerms, fmtPerms,
parseArgs, parseArgs,
reportToConn,
} from "./test_util.ts"; } from "./test_util.ts";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reportToConsole = (Deno as any)[Deno.symbols.internal]
.reportToConsole as (message: Deno.TestMessage) => void;
interface PermissionSetTestResult { interface PermissionSetTestResult {
perms: Permissions; perms: Permissions;
passed: boolean; passed: boolean;
stats: Deno.TestStats; endMessage: Deno.TestMessage["end"];
permsStr: string; permsStr: string;
duration: number;
results: Deno.TestResult[];
} }
const PERMISSIONS: Deno.PermissionName[] = [ const PERMISSIONS: Deno.PermissionName[] = [
@ -59,17 +61,16 @@ async function workerRunnerMain(
} }
// Setup reporter // Setup reporter
const conn = await Deno.connect(addr); const conn = await Deno.connect(addr);
const socketReporter = new SocketReporter(conn);
// Drop current process permissions to requested set // Drop current process permissions to requested set
await dropWorkerPermissions(perms); await dropWorkerPermissions(perms);
// Register unit tests that match process permissions // Register unit tests that match process permissions
await registerUnitTests(); await registerUnitTests();
// Execute tests // Execute tests
await Deno.runTests({ await Deno.runTests({
failFast: false,
exitOnFail: false, exitOnFail: false,
reporter: socketReporter,
only: filter, only: filter,
reportToConsole: false,
onMessage: reportToConn.bind(null, conn),
}); });
} }
@ -117,7 +118,6 @@ async function runTestsForPermissionSet(
listener: Deno.Listener, listener: Deno.Listener,
addrStr: string, addrStr: string,
verbose: boolean, verbose: boolean,
reporter: Deno.ConsoleTestReporter,
perms: Permissions, perms: Permissions,
filter?: string filter?: string
): Promise<PermissionSetTestResult> { ): Promise<PermissionSetTestResult> {
@ -128,22 +128,16 @@ async function runTestsForPermissionSet(
const conn = await listener.accept(); const conn = await listener.accept();
let expectedPassedTests; let expectedPassedTests;
let endEvent; let endMessage: Deno.TestMessage["end"];
try { try {
for await (const line of readLines(conn)) { for await (const line of readLines(conn)) {
const msg = JSON.parse(line); const message = JSON.parse(line) as Deno.TestMessage;
reportToConsole(message);
if (msg.kind === Deno.TestEvent.Start) { if (message.start != null) {
expectedPassedTests = msg.tests; expectedPassedTests = message.start.tests.length;
await reporter.start(msg); } else if (message.end != null) {
} else if (msg.kind === Deno.TestEvent.TestStart) { endMessage = message.end;
await reporter.testStart(msg);
} else if (msg.kind === Deno.TestEvent.TestEnd) {
await reporter.testEnd(msg);
} else {
endEvent = msg;
await reporter.end(msg);
} }
} }
} finally { } finally {
@ -151,11 +145,11 @@ async function runTestsForPermissionSet(
conn.close(); conn.close();
} }
if (expectedPassedTests === undefined) { if (expectedPassedTests == null) {
throw new Error("Worker runner didn't report start"); throw new Error("Worker runner didn't report start");
} }
if (endEvent === undefined) { if (endMessage == null) {
throw new Error("Worker runner didn't report end"); throw new Error("Worker runner didn't report end");
} }
@ -168,16 +162,13 @@ async function runTestsForPermissionSet(
workerProcess.close(); workerProcess.close();
const passed = const passed = expectedPassedTests === endMessage.passed + endMessage.ignored;
expectedPassedTests === endEvent.stats.passed + endEvent.stats.ignored;
return { return {
perms, perms,
passed, passed,
permsStr: permsFmt, permsStr: permsFmt,
duration: endEvent.duration, endMessage,
stats: endEvent.stats,
results: endEvent.results,
}; };
} }
@ -195,7 +186,6 @@ async function masterRunnerMain(
} }
const testResults = new Set<PermissionSetTestResult>(); const testResults = new Set<PermissionSetTestResult>();
const consoleReporter = new Deno.ConsoleTestReporter();
const addr = { hostname: "127.0.0.1", port: 4510 }; const addr = { hostname: "127.0.0.1", port: 4510 };
const addrStr = `${addr.hostname}:${addr.port}`; const addrStr = `${addr.hostname}:${addr.port}`;
const listener = Deno.listen(addr); const listener = Deno.listen(addr);
@ -205,7 +195,6 @@ async function masterRunnerMain(
listener, listener,
addrStr, addrStr,
verbose, verbose,
consoleReporter,
perms, perms,
filter filter
); );
@ -217,14 +206,9 @@ async function masterRunnerMain(
let testsPassed = true; let testsPassed = true;
for (const testResult of testResults) { for (const testResult of testResults) {
const { permsStr, stats, duration, results } = testResult; const { permsStr, endMessage } = testResult;
console.log(`Summary for ${permsStr}`); console.log(`Summary for ${permsStr}`);
await consoleReporter.end({ reportToConsole({ end: endMessage });
kind: Deno.TestEvent.End,
stats,
duration,
results,
});
testsPassed = testsPassed && testResult.passed; testsPassed = testsPassed && testResult.passed;
} }
@ -312,11 +296,7 @@ async function main(): Promise<void> {
// Running tests matching current process permissions // Running tests matching current process permissions
await registerUnitTests(); await registerUnitTests();
await Deno.runTests({ await Deno.runTests({ only: filter });
failFast: false,
exitOnFail: true,
only: filter,
});
} }
main(); main();

View file

@ -212,7 +212,3 @@ unitTest(function createBadUrl(): void {
new URL("0.0.0.0:8080"); new URL("0.0.0.0:8080");
}); });
}); });
if (import.meta.main) {
Deno.runTests();
}

View file

@ -7,7 +7,7 @@ const isWindows = Deno.build.os == "win";
export function testWalk( export function testWalk(
setup: (arg0: string) => void | Promise<void>, setup: (arg0: string) => void | Promise<void>,
t: Deno.TestFunction, t: () => void | Promise<void>,
ignore = false ignore = false
): void { ): void {
const name = t.name; const name = t.name;

View file

@ -46,8 +46,6 @@ Deno.test({
assertEquals({ hello: "world" }, { hello: "world" }); assertEquals({ hello: "world" }, { hello: "world" });
}, },
}); });
await Deno.runTests();
``` ```
Short syntax (named function instead of object): Short syntax (named function instead of object):