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:
parent
017a611131
commit
270e87d9db
9 changed files with 237 additions and 384 deletions
|
@ -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.
|
||||||
|
|
107
cli/js/lib.deno.ns.d.ts
vendored
107
cli/js/lib.deno.ns.d.ts
vendored
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,3 @@ unitTest(function formatDiagnosticError() {
|
||||||
}
|
}
|
||||||
assert(thrown);
|
assert(thrown);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (import.meta.main) {
|
|
||||||
Deno.runTests();
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue