summaryrefslogtreecommitdiff
path: root/std/testing
diff options
context:
space:
mode:
Diffstat (limited to 'std/testing')
-rw-r--r--std/testing/README.md223
-rw-r--r--std/testing/asserts.ts359
-rw-r--r--std/testing/asserts_test.ts253
-rw-r--r--std/testing/bench.ts179
-rw-r--r--std/testing/bench_example.ts29
-rw-r--r--std/testing/bench_test.ts60
-rw-r--r--std/testing/diff.ts220
-rw-r--r--std/testing/diff_test.ts111
-rw-r--r--std/testing/format.ts538
-rw-r--r--std/testing/format_test.ts805
-rw-r--r--std/testing/mod.ts422
-rwxr-xr-xstd/testing/runner.ts236
-rw-r--r--std/testing/runner_test.ts95
-rw-r--r--std/testing/test.ts263
-rw-r--r--std/testing/testdata/bar.js1
-rw-r--r--std/testing/testdata/bar_test.js1
-rw-r--r--std/testing/testdata/foo.ts1
-rw-r--r--std/testing/testdata/foo_test.ts1
-rw-r--r--std/testing/testdata/subdir/bar.js1
-rw-r--r--std/testing/testdata/subdir/bar_test.js1
-rw-r--r--std/testing/testdata/subdir/foo.ts1
-rw-r--r--std/testing/testdata/subdir/foo_test.ts1
-rw-r--r--std/testing/testdata/subdir/test.js1
-rw-r--r--std/testing/testdata/subdir/test.ts1
-rw-r--r--std/testing/testdata/test.js1
-rw-r--r--std/testing/testdata/test.ts1
-rw-r--r--std/testing/testing_bench.ts18
27 files changed, 3823 insertions, 0 deletions
diff --git a/std/testing/README.md b/std/testing/README.md
new file mode 100644
index 000000000..88923c1c9
--- /dev/null
+++ b/std/testing/README.md
@@ -0,0 +1,223 @@
+# Testing
+
+This module provides a few basic utilities to make testing easier and
+consistent in Deno.
+
+## Usage
+
+The module exports a `test` function which is the test harness in Deno. It
+accepts either a function (including async functions) or an object which
+contains a `name` property and a `fn` property. When running tests and
+outputting the results, the name of the past function is used, or if the
+object is passed, the `name` property is used to identify the test. If the assertion is false an `AssertionError` will be thrown.
+
+Asserts are exposed in `testing/asserts.ts` module.
+
+- `equal()` - Deep comparison function, where `actual` and `expected` are
+ compared deeply, and if they vary, `equal` returns `false`.
+- `assert()` - Expects a boolean value, throws if the value is `false`.
+- `assertEquals()` - Uses the `equal` comparison and throws if the `actual` and
+ `expected` are not equal.
+- `assertNotEquals()` - Uses the `equal` comparison and throws if the `actual` and
+ `expected` are equal.
+- `assertStrictEq()` - Compares `actual` and `expected` strictly, therefore
+ for non-primitives the values must reference the same instance.
+- `assertStrContains()` - Make an assertion that `actual` contains `expected`.
+- `assertMatch()` - Make an assertion that `actual` match RegExp `expected`.
+- `assertArrayContains()` - Make an assertion that `actual` array contains the `expected` values.
+- `assertThrows()` - Expects the passed `fn` to throw. If `fn` does not throw,
+ this function does. Also compares any errors thrown to an optional expected
+ `Error` class and checks that the error `.message` includes an optional
+ string.
+- `assertThrowsAsync()` - Expects the passed `fn` to be async and throw (or
+ return a `Promise` that rejects). If the `fn` does not throw or reject, this
+ function will throw asynchronously. Also compares any errors thrown to an
+ optional expected `Error` class and checks that the error `.message` includes
+ an optional string.
+- `unimplemented()` - Use this to stub out methods that will throw when invoked
+- `unreachable()` - Used to assert unreachable code
+
+`runTests()` executes the declared tests. It accepts a `RunOptions` parameter:
+
+- parallel : Execute tests in a parallel way.
+- exitOnFail : if one test fails, test will throw an error and stop the tests. If not all tests will be processed.
+
+Basic usage:
+
+```ts
+import { runTests, test } from "https://deno.land/std/testing/mod.ts";
+import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+
+test({
+ name: "testing example",
+ fn(): void {
+ assertEquals("world", "world");
+ assertEquals({ hello: "world" }, { hello: "world" });
+ }
+});
+
+runTests();
+```
+
+Short syntax (named function instead of object):
+
+```ts
+test(function example(): void {
+ assertEquals("world", "world");
+ assertEquals({ hello: "world" }, { hello: "world" });
+});
+```
+
+Using `assertStrictEq()`:
+
+```ts
+test(function isStrictlyEqual(): void {
+ const a = {};
+ const b = a;
+ assertStrictEq(a, b);
+});
+
+// This test fails
+test(function isNotStrictlyEqual(): void {
+ const a = {};
+ const b = {};
+ assertStrictEq(a, b);
+});
+```
+
+Using `assertThrows()`:
+
+```ts
+test(function doesThrow(): void {
+ assertThrows((): void => {
+ throw new TypeError("hello world!");
+ });
+ assertThrows((): void => {
+ throw new TypeError("hello world!");
+ }, TypeError);
+ assertThrows(
+ (): void => {
+ throw new TypeError("hello world!");
+ },
+ TypeError,
+ "hello"
+ );
+});
+
+// This test will not pass
+test(function fails(): void {
+ assertThrows((): void => {
+ console.log("Hello world");
+ });
+});
+```
+
+Using `assertThrowsAsync()`:
+
+```ts
+test(async function doesThrow(): Promise<void> {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ throw new TypeError("hello world!");
+ }
+ );
+ await assertThrowsAsync(async (): Promise<void> => {
+ throw new TypeError("hello world!");
+ }, TypeError);
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ throw new TypeError("hello world!");
+ },
+ TypeError,
+ "hello"
+ );
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ return Promise.reject(new Error());
+ }
+ );
+});
+
+// This test will not pass
+test(async function fails(): Promise<void> {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ console.log("Hello world");
+ }
+ );
+});
+```
+
+### Benching Usage
+
+Basic usage:
+
+```ts
+import { runBenchmarks, bench } from "https://deno.land/std/testing/bench.ts";
+
+bench(function forIncrementX1e9(b): void {
+ b.start();
+ for (let i = 0; i < 1e9; i++);
+ b.stop();
+});
+
+runBenchmarks();
+```
+
+Averaging execution time over multiple runs:
+
+```ts
+bench({
+ name: "runs100ForIncrementX1e6",
+ runs: 100,
+ func(b): void {
+ b.start();
+ for (let i = 0; i < 1e6; i++);
+ b.stop();
+ }
+});
+```
+
+#### Benching API
+
+##### `bench(benchmark: BenchmarkDefinition | BenchmarkFunction): void`
+
+Registers a benchmark that will be run once `runBenchmarks` is called.
+
+##### `runBenchmarks(opts?: BenchmarkRunOptions): Promise<void>`
+
+Runs all registered benchmarks serially. Filtering can be applied by setting
+`BenchmarkRunOptions.only` and/or `BenchmarkRunOptions.skip` to regular expressions matching benchmark names.
+
+##### `runIfMain(meta: ImportMeta, opts?: BenchmarkRunOptions): Promise<void>`
+
+Runs specified benchmarks if the enclosing script is main.
+
+##### Other exports
+
+```ts
+/** Provides methods for starting and stopping a benchmark clock. */
+export interface BenchmarkTimer {
+ start: () => void;
+ stop: () => void;
+}
+
+/** Defines a benchmark through a named function. */
+export interface BenchmarkFunction {
+ (b: BenchmarkTimer): void | Promise<void>;
+ name: string;
+}
+
+/** Defines a benchmark definition with configurable runs. */
+export interface BenchmarkDefinition {
+ func: BenchmarkFunction;
+ name: string;
+ runs?: number;
+}
+
+/** Defines runBenchmark's run constraints by matching benchmark names. */
+export interface BenchmarkRunOptions {
+ only?: RegExp;
+ skip?: RegExp;
+}
+```
diff --git a/std/testing/asserts.ts b/std/testing/asserts.ts
new file mode 100644
index 000000000..230e2c8d2
--- /dev/null
+++ b/std/testing/asserts.ts
@@ -0,0 +1,359 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { red, green, white, gray, bold } from "../fmt/colors.ts";
+import diff, { DiffType, DiffResult } from "./diff.ts";
+import { format } from "./format.ts";
+
+const CAN_NOT_DISPLAY = "[Cannot display]";
+
+interface Constructor {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ new (...args: any[]): any;
+}
+
+export class AssertionError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "AssertionError";
+ }
+}
+
+function createStr(v: unknown): string {
+ try {
+ return format(v);
+ } catch (e) {
+ return red(CAN_NOT_DISPLAY);
+ }
+}
+
+function createColor(diffType: DiffType): (s: string) => string {
+ switch (diffType) {
+ case DiffType.added:
+ return (s: string): string => green(bold(s));
+ case DiffType.removed:
+ return (s: string): string => red(bold(s));
+ default:
+ return white;
+ }
+}
+
+function createSign(diffType: DiffType): string {
+ switch (diffType) {
+ case DiffType.added:
+ return "+ ";
+ case DiffType.removed:
+ return "- ";
+ default:
+ return " ";
+ }
+}
+
+function buildMessage(diffResult: ReadonlyArray<DiffResult<string>>): string[] {
+ const messages: string[] = [];
+ messages.push("");
+ messages.push("");
+ messages.push(
+ ` ${gray(bold("[Diff]"))} ${red(bold("Left"))} / ${green(bold("Right"))}`
+ );
+ messages.push("");
+ messages.push("");
+ diffResult.forEach((result: DiffResult<string>): void => {
+ const c = createColor(result.type);
+ messages.push(c(`${createSign(result.type)}${result.value}`));
+ });
+ messages.push("");
+
+ return messages;
+}
+
+export function equal(c: unknown, d: unknown): boolean {
+ const seen = new Map();
+ return (function compare(a: unknown, b: unknown): boolean {
+ if (a && a instanceof Set && b && b instanceof Set) {
+ if (a.size !== b.size) {
+ return false;
+ }
+ for (const item of b) {
+ if (!a.has(item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ // Have to render RegExp & Date for string comparison
+ // unless it's mistreated as object
+ if (
+ a &&
+ b &&
+ ((a instanceof RegExp && b instanceof RegExp) ||
+ (a instanceof Date && b instanceof Date))
+ ) {
+ return String(a) === String(b);
+ }
+ if (Object.is(a, b)) {
+ return true;
+ }
+ if (a && typeof a === "object" && b && typeof b === "object") {
+ if (seen.get(a) === b) {
+ return true;
+ }
+ if (Object.keys(a || {}).length !== Object.keys(b || {}).length) {
+ return false;
+ }
+ const merged = { ...a, ...b };
+ for (const key in merged) {
+ type Key = keyof typeof merged;
+ if (!compare(a && a[key as Key], b && b[key as Key])) {
+ return false;
+ }
+ }
+ seen.set(a, b);
+ return true;
+ }
+ return false;
+ })(c, d);
+}
+
+/** Make an assertion, if not `true`, then throw. */
+export function assert(expr: boolean, msg = ""): void {
+ if (!expr) {
+ throw new AssertionError(msg);
+ }
+}
+
+/**
+ * Make an assertion that `actual` and `expected` are equal, deeply. If not
+ * deeply equal, then throw.
+ */
+export function assertEquals(
+ actual: unknown,
+ expected: unknown,
+ msg?: string
+): void {
+ if (equal(actual, expected)) {
+ return;
+ }
+ let message = "";
+ const actualString = createStr(actual);
+ const expectedString = createStr(expected);
+ try {
+ const diffResult = diff(
+ actualString.split("\n"),
+ expectedString.split("\n")
+ );
+ message = buildMessage(diffResult).join("\n");
+ } catch (e) {
+ message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`;
+ }
+ if (msg) {
+ message = msg;
+ }
+ throw new AssertionError(message);
+}
+
+/**
+ * Make an assertion that `actual` and `expected` are not equal, deeply.
+ * If not then throw.
+ */
+export function assertNotEquals(
+ actual: unknown,
+ expected: unknown,
+ msg?: string
+): void {
+ if (!equal(actual, expected)) {
+ return;
+ }
+ let actualString: string;
+ let expectedString: string;
+ try {
+ actualString = String(actual);
+ } catch (e) {
+ actualString = "[Cannot display]";
+ }
+ try {
+ expectedString = String(expected);
+ } catch (e) {
+ expectedString = "[Cannot display]";
+ }
+ if (!msg) {
+ msg = `actual: ${actualString} expected: ${expectedString}`;
+ }
+ throw new AssertionError(msg);
+}
+
+/**
+ * Make an assertion that `actual` and `expected` are strictly equal. If
+ * not then throw.
+ */
+export function assertStrictEq(
+ actual: unknown,
+ expected: unknown,
+ msg?: string
+): void {
+ if (actual !== expected) {
+ let actualString: string;
+ let expectedString: string;
+ try {
+ actualString = String(actual);
+ } catch (e) {
+ actualString = "[Cannot display]";
+ }
+ try {
+ expectedString = String(expected);
+ } catch (e) {
+ expectedString = "[Cannot display]";
+ }
+ if (!msg) {
+ msg = `actual: ${actualString} expected: ${expectedString}`;
+ }
+ throw new AssertionError(msg);
+ }
+}
+
+/**
+ * Make an assertion that actual contains expected. If not
+ * then thrown.
+ */
+export function assertStrContains(
+ actual: string,
+ expected: string,
+ msg?: string
+): void {
+ if (!actual.includes(expected)) {
+ if (!msg) {
+ msg = `actual: "${actual}" expected to contains: "${expected}"`;
+ }
+ throw new AssertionError(msg);
+ }
+}
+
+/**
+ * Make an assertion that `actual` contains the `expected` values
+ * If not then thrown.
+ */
+export function assertArrayContains(
+ actual: unknown[],
+ expected: unknown[],
+ msg?: string
+): void {
+ const missing: unknown[] = [];
+ for (let i = 0; i < expected.length; i++) {
+ let found = false;
+ for (let j = 0; j < actual.length; j++) {
+ if (equal(expected[i], actual[j])) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ missing.push(expected[i]);
+ }
+ }
+ if (missing.length === 0) {
+ return;
+ }
+ if (!msg) {
+ msg = `actual: "${actual}" expected to contains: "${expected}"`;
+ msg += "\n";
+ msg += `missing: ${missing}`;
+ }
+ throw new AssertionError(msg);
+}
+
+/**
+ * Make an assertion that `actual` match RegExp `expected`. If not
+ * then thrown
+ */
+export function assertMatch(
+ actual: string,
+ expected: RegExp,
+ msg?: string
+): void {
+ if (!expected.test(actual)) {
+ if (!msg) {
+ msg = `actual: "${actual}" expected to match: "${expected}"`;
+ }
+ throw new AssertionError(msg);
+ }
+}
+
+/**
+ * Forcefully throws a failed assertion
+ */
+export function fail(msg?: string): void {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`);
+}
+
+/** Executes a function, expecting it to throw. If it does not, then it
+ * throws. An error class and a string that should be included in the
+ * error message can also be asserted.
+ */
+export function assertThrows(
+ fn: () => void,
+ ErrorClass?: Constructor,
+ msgIncludes = "",
+ msg?: string
+): void {
+ let doesThrow = false;
+ try {
+ fn();
+ } catch (e) {
+ if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) {
+ msg = `Expected error to be instance of "${ErrorClass.name}"${
+ msg ? `: ${msg}` : "."
+ }`;
+ throw new AssertionError(msg);
+ }
+ if (msgIncludes && !e.message.includes(msgIncludes)) {
+ msg = `Expected error message to include "${msgIncludes}", but got "${
+ e.message
+ }"${msg ? `: ${msg}` : "."}`;
+ throw new AssertionError(msg);
+ }
+ doesThrow = true;
+ }
+ if (!doesThrow) {
+ msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
+ throw new AssertionError(msg);
+ }
+}
+
+export async function assertThrowsAsync(
+ fn: () => Promise<void>,
+ ErrorClass?: Constructor,
+ msgIncludes = "",
+ msg?: string
+): Promise<void> {
+ let doesThrow = false;
+ try {
+ await fn();
+ } catch (e) {
+ if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) {
+ msg = `Expected error to be instance of "${ErrorClass.name}"${
+ msg ? `: ${msg}` : "."
+ }`;
+ throw new AssertionError(msg);
+ }
+ if (msgIncludes && !e.message.includes(msgIncludes)) {
+ msg = `Expected error message to include "${msgIncludes}", but got "${
+ e.message
+ }"${msg ? `: ${msg}` : "."}`;
+ throw new AssertionError(msg);
+ }
+ doesThrow = true;
+ }
+ if (!doesThrow) {
+ msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
+ throw new AssertionError(msg);
+ }
+}
+
+/** Use this to stub out methods that will throw when invoked. */
+export function unimplemented(msg?: string): never {
+ throw new AssertionError(msg || "unimplemented");
+}
+
+/** Use this to assert unreachable code. */
+export function unreachable(): never {
+ throw new AssertionError("unreachable");
+}
diff --git a/std/testing/asserts_test.ts b/std/testing/asserts_test.ts
new file mode 100644
index 000000000..b480fe7c9
--- /dev/null
+++ b/std/testing/asserts_test.ts
@@ -0,0 +1,253 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assert,
+ assertNotEquals,
+ assertStrContains,
+ assertArrayContains,
+ assertMatch,
+ assertEquals,
+ assertThrows,
+ AssertionError,
+ equal,
+ fail,
+ unimplemented,
+ unreachable
+} from "./asserts.ts";
+import { test } from "./mod.ts";
+import { red, green, white, gray, bold } from "../fmt/colors.ts";
+
+test(function testingEqual(): void {
+ assert(equal("world", "world"));
+ assert(!equal("hello", "world"));
+ assert(equal(5, 5));
+ assert(!equal(5, 6));
+ assert(equal(NaN, NaN));
+ assert(equal({ hello: "world" }, { hello: "world" }));
+ assert(!equal({ world: "hello" }, { hello: "world" }));
+ assert(
+ equal(
+ { hello: "world", hi: { there: "everyone" } },
+ { hello: "world", hi: { there: "everyone" } }
+ )
+ );
+ assert(
+ !equal(
+ { hello: "world", hi: { there: "everyone" } },
+ { hello: "world", hi: { there: "everyone else" } }
+ )
+ );
+ assert(equal(/deno/, /deno/));
+ assert(!equal(/deno/, /node/));
+ assert(equal(new Date(2019, 0, 3), new Date(2019, 0, 3)));
+ assert(!equal(new Date(2019, 0, 3), new Date(2019, 1, 3)));
+ assert(equal(new Set([1]), new Set([1])));
+ assert(!equal(new Set([1]), new Set([2])));
+ assert(equal(new Set([1, 2, 3]), new Set([3, 2, 1])));
+ assert(!equal(new Set([1, 2]), new Set([3, 2, 1])));
+ assert(!equal(new Set([1, 2, 3]), new Set([4, 5, 6])));
+ assert(equal(new Set("denosaurus"), new Set("denosaurussss")));
+});
+
+test(function testingNotEquals(): void {
+ const a = { foo: "bar" };
+ const b = { bar: "foo" };
+ assertNotEquals(a, b);
+ assertNotEquals("Denosaurus", "Tyrannosaurus");
+ let didThrow;
+ try {
+ assertNotEquals("Raptor", "Raptor");
+ didThrow = false;
+ } catch (e) {
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assertEquals(didThrow, true);
+});
+
+test(function testingAssertStringContains(): void {
+ assertStrContains("Denosaurus", "saur");
+ assertStrContains("Denosaurus", "Deno");
+ assertStrContains("Denosaurus", "rus");
+ let didThrow;
+ try {
+ assertStrContains("Denosaurus", "Raptor");
+ didThrow = false;
+ } catch (e) {
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assertEquals(didThrow, true);
+});
+
+test(function testingArrayContains(): void {
+ const fixture = ["deno", "iz", "luv"];
+ const fixtureObject = [{ deno: "luv" }, { deno: "Js" }];
+ assertArrayContains(fixture, ["deno"]);
+ assertArrayContains(fixtureObject, [{ deno: "luv" }]);
+ let didThrow;
+ try {
+ assertArrayContains(fixtureObject, [{ deno: "node" }]);
+ didThrow = false;
+ } catch (e) {
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assertEquals(didThrow, true);
+});
+
+test(function testingAssertStringContainsThrow(): void {
+ let didThrow = false;
+ try {
+ assertStrContains("Denosaurus from Jurassic", "Raptor");
+ } catch (e) {
+ assert(
+ e.message ===
+ `actual: "Denosaurus from Jurassic" expected to contains: "Raptor"`
+ );
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertStringMatching(): void {
+ assertMatch("foobar@deno.com", RegExp(/[a-zA-Z]+@[a-zA-Z]+.com/));
+});
+
+test(function testingAssertStringMatchingThrows(): void {
+ let didThrow = false;
+ try {
+ assertMatch("Denosaurus from Jurassic", RegExp(/Raptor/));
+ } catch (e) {
+ assert(
+ e.message ===
+ `actual: "Denosaurus from Jurassic" expected to match: "/Raptor/"`
+ );
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertsUnimplemented(): void {
+ let didThrow = false;
+ try {
+ unimplemented();
+ } catch (e) {
+ assert(e.message === "unimplemented");
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertsUnreachable(): void {
+ let didThrow = false;
+ try {
+ unreachable();
+ } catch (e) {
+ assert(e.message === "unreachable");
+ assert(e instanceof AssertionError);
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertFail(): void {
+ assertThrows(fail, AssertionError, "Failed assertion.");
+ assertThrows(
+ (): void => {
+ fail("foo");
+ },
+ AssertionError,
+ "Failed assertion: foo"
+ );
+});
+
+const createHeader = (): string[] => [
+ "",
+ "",
+ ` ${gray(bold("[Diff]"))} ${red(bold("Left"))} / ${green(bold("Right"))}`,
+ "",
+ ""
+];
+
+const added: (s: string) => string = (s: string): string => green(bold(s));
+const removed: (s: string) => string = (s: string): string => red(bold(s));
+
+test({
+ name: "pass case",
+ fn(): void {
+ assertEquals({ a: 10 }, { a: 10 });
+ assertEquals(true, true);
+ assertEquals(10, 10);
+ assertEquals("abc", "abc");
+ assertEquals({ a: 10, b: { c: "1" } }, { a: 10, b: { c: "1" } });
+ }
+});
+
+test({
+ name: "failed with number",
+ fn(): void {
+ assertThrows(
+ (): void => assertEquals(1, 2),
+ AssertionError,
+ [...createHeader(), removed(`- 1`), added(`+ 2`), ""].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with number vs string",
+ fn(): void {
+ assertThrows(
+ (): void => assertEquals(1, "1"),
+ AssertionError,
+ [...createHeader(), removed(`- 1`), added(`+ "1"`)].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with array",
+ fn(): void {
+ assertThrows(
+ (): void => assertEquals([1, "2", 3], ["1", "2", 3]),
+ AssertionError,
+ [
+ ...createHeader(),
+ white(" Array ["),
+ removed(`- 1,`),
+ added(`+ "1",`),
+ white(' "2",'),
+ white(" 3,"),
+ white(" ]"),
+ ""
+ ].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with object",
+ fn(): void {
+ assertThrows(
+ (): void => assertEquals({ a: 1, b: "2", c: 3 }, { a: 1, b: 2, c: [3] }),
+ AssertionError,
+ [
+ ...createHeader(),
+ white(" Object {"),
+ white(` "a": 1,`),
+ added(`+ "b": 2,`),
+ added(`+ "c": Array [`),
+ added(`+ 3,`),
+ added(`+ ],`),
+ removed(`- "b": "2",`),
+ removed(`- "c": 3,`),
+ white(" }"),
+ ""
+ ].join("\n")
+ );
+ }
+});
diff --git a/std/testing/bench.ts b/std/testing/bench.ts
new file mode 100644
index 000000000..3bb62526d
--- /dev/null
+++ b/std/testing/bench.ts
@@ -0,0 +1,179 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+const { exit, noColor } = Deno;
+
+interface BenchmarkClock {
+ start: number;
+ stop: number;
+}
+
+/** Provides methods for starting and stopping a benchmark clock. */
+export interface BenchmarkTimer {
+ start: () => void;
+ stop: () => void;
+}
+
+/** Defines a benchmark through a named function. */
+export interface BenchmarkFunction {
+ (b: BenchmarkTimer): void | Promise<void>;
+ name: string;
+}
+
+/** Defines a benchmark definition with configurable runs. */
+export interface BenchmarkDefinition {
+ func: BenchmarkFunction;
+ name: string;
+ runs?: number;
+}
+
+/** Defines runBenchmark's run constraints by matching benchmark names. */
+export interface BenchmarkRunOptions {
+ only?: RegExp;
+ skip?: RegExp;
+}
+
+function red(text: string): string {
+ return noColor ? text : `\x1b[31m${text}\x1b[0m`;
+}
+
+function blue(text: string): string {
+ return noColor ? text : `\x1b[34m${text}\x1b[0m`;
+}
+
+function verifyOr1Run(runs?: number): number {
+ return runs && runs >= 1 && runs !== Infinity ? Math.floor(runs) : 1;
+}
+
+function assertTiming(clock: BenchmarkClock): void {
+ // NaN indicates that a benchmark has not been timed properly
+ if (!clock.stop) {
+ throw new Error("The benchmark timer's stop method must be called");
+ } else if (!clock.start) {
+ throw new Error("The benchmark timer's start method must be called");
+ } else if (clock.start > clock.stop) {
+ throw new Error(
+ "The benchmark timer's start method must be called before its " +
+ "stop method"
+ );
+ }
+}
+
+function createBenchmarkTimer(clock: BenchmarkClock): BenchmarkTimer {
+ return {
+ start(): void {
+ clock.start = performance.now();
+ },
+ stop(): void {
+ clock.stop = performance.now();
+ }
+ };
+}
+
+const candidates: BenchmarkDefinition[] = [];
+
+/** Registers a benchmark as a candidate for the runBenchmarks executor. */
+export function bench(
+ benchmark: BenchmarkDefinition | BenchmarkFunction
+): void {
+ if (!benchmark.name) {
+ throw new Error("The benchmark function must not be anonymous");
+ }
+ if (typeof benchmark === "function") {
+ candidates.push({ name: benchmark.name, runs: 1, func: benchmark });
+ } else {
+ candidates.push({
+ name: benchmark.name,
+ runs: verifyOr1Run(benchmark.runs),
+ func: benchmark.func
+ });
+ }
+}
+
+/** Runs all registered and non-skipped benchmarks serially. */
+export async function runBenchmarks({
+ only = /[^\s]/,
+ skip = /^\s*$/
+}: BenchmarkRunOptions = {}): Promise<void> {
+ // Filtering candidates by the "only" and "skip" constraint
+ const benchmarks: BenchmarkDefinition[] = candidates.filter(
+ ({ name }): boolean => only.test(name) && !skip.test(name)
+ );
+ // Init main counters and error flag
+ const filtered = candidates.length - benchmarks.length;
+ let measured = 0;
+ let failed = false;
+ // Setting up a shared benchmark clock and timer
+ const clock: BenchmarkClock = { start: NaN, stop: NaN };
+ const b = createBenchmarkTimer(clock);
+ // Iterating given benchmark definitions (await-in-loop)
+ console.log(
+ "running",
+ benchmarks.length,
+ `benchmark${benchmarks.length === 1 ? " ..." : "s ..."}`
+ );
+ for (const { name, runs = 0, func } of benchmarks) {
+ // See https://github.com/denoland/deno/pull/1452 about groupCollapsed
+ console.groupCollapsed(`benchmark ${name} ... `);
+ // Trying benchmark.func
+ let result = "";
+ try {
+ if (runs === 1) {
+ // b is a benchmark timer interfacing an unset (NaN) benchmark clock
+ await func(b);
+ // Making sure the benchmark was started/stopped properly
+ assertTiming(clock);
+ result = `${clock.stop - clock.start}ms`;
+ } else if (runs > 1) {
+ // Averaging runs
+ let pendingRuns = runs;
+ let totalMs = 0;
+ // Would be better 2 not run these serially
+ while (true) {
+ // b is a benchmark timer interfacing an unset (NaN) benchmark clock
+ await func(b);
+ // Making sure the benchmark was started/stopped properly
+ assertTiming(clock);
+ // Summing up
+ totalMs += clock.stop - clock.start;
+ // Resetting the benchmark clock
+ clock.start = clock.stop = NaN;
+ // Once all ran
+ if (!--pendingRuns) {
+ result = `${runs} runs avg: ${totalMs / runs}ms`;
+ break;
+ }
+ }
+ }
+ } catch (err) {
+ failed = true;
+ console.groupEnd();
+ console.error(red(err.stack));
+ break;
+ }
+ // Reporting
+ console.log(blue(result));
+ console.groupEnd();
+ measured++;
+ // Resetting the benchmark clock
+ clock.start = clock.stop = NaN;
+ }
+ // Closing results
+ console.log(
+ `benchmark result: ${failed ? red("FAIL") : blue("DONE")}. ` +
+ `${measured} measured; ${filtered} filtered`
+ );
+ // Making sure the program exit code is not zero in case of failure
+ if (failed) {
+ setTimeout((): void => exit(1), 0);
+ }
+}
+
+/** Runs specified benchmarks if the enclosing script is main. */
+export async function runIfMain(
+ meta: ImportMeta,
+ opts?: BenchmarkRunOptions
+): Promise<void> {
+ if (meta.main) {
+ return runBenchmarks(opts);
+ }
+}
diff --git a/std/testing/bench_example.ts b/std/testing/bench_example.ts
new file mode 100644
index 000000000..d27fb97e8
--- /dev/null
+++ b/std/testing/bench_example.ts
@@ -0,0 +1,29 @@
+// https://deno.land/std/testing/bench.ts
+import { BenchmarkTimer, bench, runIfMain } from "./bench.ts";
+
+// Basic
+bench(function forIncrementX1e9(b: BenchmarkTimer): void {
+ b.start();
+ for (let i = 0; i < 1e9; i++);
+ b.stop();
+});
+
+// Reporting average measured time for $runs runs of func
+bench({
+ name: "runs100ForIncrementX1e6",
+ runs: 100,
+ func(b): void {
+ b.start();
+ for (let i = 0; i < 1e6; i++);
+ b.stop();
+ }
+});
+
+// Itsabug
+bench(function throwing(b): void {
+ b.start();
+ // Throws bc the timer's stop method is never called
+});
+
+// Bench control
+runIfMain(import.meta, { skip: /throw/ });
diff --git a/std/testing/bench_test.ts b/std/testing/bench_test.ts
new file mode 100644
index 000000000..8af2f0d6d
--- /dev/null
+++ b/std/testing/bench_test.ts
@@ -0,0 +1,60 @@
+import { test, runIfMain } from "./mod.ts";
+import { bench, runBenchmarks } from "./bench.ts";
+
+import "./bench_example.ts";
+
+test(async function benching(): Promise<void> {
+ bench(function forIncrementX1e9(b): void {
+ b.start();
+ for (let i = 0; i < 1e9; i++);
+ b.stop();
+ });
+
+ bench(function forDecrementX1e9(b): void {
+ b.start();
+ for (let i = 1e9; i > 0; i--);
+ b.stop();
+ });
+
+ bench(async function forAwaitFetchDenolandX10(b): Promise<void> {
+ b.start();
+ for (let i = 0; i < 10; i++) {
+ const r = await fetch("https://deno.land/");
+ await r.text();
+ }
+ b.stop();
+ });
+
+ bench(async function promiseAllFetchDenolandX10(b): Promise<void> {
+ const urls = new Array(10).fill("https://deno.land/");
+ b.start();
+ await Promise.all(
+ urls.map(
+ async (denoland: string): Promise<void> => {
+ const r = await fetch(denoland);
+ await r.text();
+ }
+ )
+ );
+ b.stop();
+ });
+
+ bench({
+ name: "runs100ForIncrementX1e6",
+ runs: 100,
+ func(b): void {
+ b.start();
+ for (let i = 0; i < 1e6; i++);
+ b.stop();
+ }
+ });
+
+ bench(function throwing(b): void {
+ b.start();
+ // Throws bc the timer's stop method is never called
+ });
+
+ await runBenchmarks({ skip: /throw/ });
+});
+
+runIfMain(import.meta);
diff --git a/std/testing/diff.ts b/std/testing/diff.ts
new file mode 100644
index 000000000..dd544ac24
--- /dev/null
+++ b/std/testing/diff.ts
@@ -0,0 +1,220 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+interface FarthestPoint {
+ y: number;
+ id: number;
+}
+
+export enum DiffType {
+ removed = "removed",
+ common = "common",
+ added = "added"
+}
+
+export interface DiffResult<T> {
+ type: DiffType;
+ value: T;
+}
+
+const REMOVED = 1;
+const COMMON = 2;
+const ADDED = 3;
+
+function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
+ const common = [];
+ if (A.length === 0 || B.length === 0) return [];
+ for (let i = 0; i < Math.min(A.length, B.length); i += 1) {
+ if (
+ A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i]
+ ) {
+ common.push(A[reverse ? A.length - i - 1 : i]);
+ } else {
+ return common;
+ }
+ }
+ return common;
+}
+
+export default function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
+ const prefixCommon = createCommon(A, B);
+ const suffixCommon = createCommon(
+ A.slice(prefixCommon.length),
+ B.slice(prefixCommon.length),
+ true
+ ).reverse();
+ A = suffixCommon.length
+ ? A.slice(prefixCommon.length, -suffixCommon.length)
+ : A.slice(prefixCommon.length);
+ B = suffixCommon.length
+ ? B.slice(prefixCommon.length, -suffixCommon.length)
+ : B.slice(prefixCommon.length);
+ const swapped = B.length > A.length;
+ [A, B] = swapped ? [B, A] : [A, B];
+ const M = A.length;
+ const N = B.length;
+ if (!M && !N && !suffixCommon.length && !prefixCommon.length) return [];
+ if (!N) {
+ return [
+ ...prefixCommon.map(
+ (c): DiffResult<typeof c> => ({ type: DiffType.common, value: c })
+ ),
+ ...A.map(
+ (a): DiffResult<typeof a> => ({
+ type: swapped ? DiffType.added : DiffType.removed,
+ value: a
+ })
+ ),
+ ...suffixCommon.map(
+ (c): DiffResult<typeof c> => ({ type: DiffType.common, value: c })
+ )
+ ];
+ }
+ const offset = N;
+ const delta = M - N;
+ const size = M + N + 1;
+ const fp = new Array(size).fill({ y: -1 });
+ /**
+ * INFO:
+ * This buffer is used to save memory and improve performance.
+ * The first half is used to save route and last half is used to save diff
+ * type.
+ * This is because, when I kept new uint8array area to save type,performance
+ * worsened.
+ */
+ const routes = new Uint32Array((M * N + size + 1) * 2);
+ const diffTypesPtrOffset = routes.length / 2;
+ let ptr = 0;
+ let p = -1;
+
+ function backTrace<T>(
+ A: T[],
+ B: T[],
+ current: FarthestPoint,
+ swapped: boolean
+ ): Array<{
+ type: DiffType;
+ value: T;
+ }> {
+ const M = A.length;
+ const N = B.length;
+ const result = [];
+ let a = M - 1;
+ let b = N - 1;
+ let j = routes[current.id];
+ let type = routes[current.id + diffTypesPtrOffset];
+ while (true) {
+ if (!j && !type) break;
+ const prev = j;
+ if (type === REMOVED) {
+ result.unshift({
+ type: swapped ? DiffType.removed : DiffType.added,
+ value: B[b]
+ });
+ b -= 1;
+ } else if (type === ADDED) {
+ result.unshift({
+ type: swapped ? DiffType.added : DiffType.removed,
+ value: A[a]
+ });
+ a -= 1;
+ } else {
+ result.unshift({ type: DiffType.common, value: A[a] });
+ a -= 1;
+ b -= 1;
+ }
+ j = routes[prev];
+ type = routes[prev + diffTypesPtrOffset];
+ }
+ return result;
+ }
+
+ function createFP(
+ slide: FarthestPoint,
+ down: FarthestPoint,
+ k: number,
+ M: number
+ ): FarthestPoint {
+ if (slide && slide.y === -1 && (down && down.y === -1))
+ return { y: 0, id: 0 };
+ if (
+ (down && down.y === -1) ||
+ k === M ||
+ (slide && slide.y) > (down && down.y) + 1
+ ) {
+ const prev = slide.id;
+ ptr++;
+ routes[ptr] = prev;
+ routes[ptr + diffTypesPtrOffset] = ADDED;
+ return { y: slide.y, id: ptr };
+ } else {
+ const prev = down.id;
+ ptr++;
+ routes[ptr] = prev;
+ routes[ptr + diffTypesPtrOffset] = REMOVED;
+ return { y: down.y + 1, id: ptr };
+ }
+ }
+
+ function snake<T>(
+ k: number,
+ slide: FarthestPoint,
+ down: FarthestPoint,
+ _offset: number,
+ A: T[],
+ B: T[]
+ ): FarthestPoint {
+ const M = A.length;
+ const N = B.length;
+ if (k < -N || M < k) return { y: -1, id: -1 };
+ const fp = createFP(slide, down, k, M);
+ while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) {
+ const prev = fp.id;
+ ptr++;
+ fp.id = ptr;
+ fp.y += 1;
+ routes[ptr] = prev;
+ routes[ptr + diffTypesPtrOffset] = COMMON;
+ }
+ return fp;
+ }
+
+ while (fp[delta + offset].y < N) {
+ p = p + 1;
+ for (let k = -p; k < delta; ++k) {
+ fp[k + offset] = snake(
+ k,
+ fp[k - 1 + offset],
+ fp[k + 1 + offset],
+ offset,
+ A,
+ B
+ );
+ }
+ for (let k = delta + p; k > delta; --k) {
+ fp[k + offset] = snake(
+ k,
+ fp[k - 1 + offset],
+ fp[k + 1 + offset],
+ offset,
+ A,
+ B
+ );
+ }
+ fp[delta + offset] = snake(
+ delta,
+ fp[delta - 1 + offset],
+ fp[delta + 1 + offset],
+ offset,
+ A,
+ B
+ );
+ }
+ return [
+ ...prefixCommon.map(
+ (c): DiffResult<typeof c> => ({ type: DiffType.common, value: c })
+ ),
+ ...backTrace(A, B, fp[delta + offset], swapped),
+ ...suffixCommon.map(
+ (c): DiffResult<typeof c> => ({ type: DiffType.common, value: c })
+ )
+ ];
+}
diff --git a/std/testing/diff_test.ts b/std/testing/diff_test.ts
new file mode 100644
index 000000000..d9fbdb956
--- /dev/null
+++ b/std/testing/diff_test.ts
@@ -0,0 +1,111 @@
+import diff from "./diff.ts";
+import { assertEquals } from "../testing/asserts.ts";
+import { test } from "./mod.ts";
+
+test({
+ name: "empty",
+ fn(): void {
+ assertEquals(diff([], []), []);
+ }
+});
+
+test({
+ name: '"a" vs "b"',
+ fn(): void {
+ assertEquals(diff(["a"], ["b"]), [
+ { type: "removed", value: "a" },
+ { type: "added", value: "b" }
+ ]);
+ }
+});
+
+test({
+ name: '"a" vs "a"',
+ fn(): void {
+ assertEquals(diff(["a"], ["a"]), [{ type: "common", value: "a" }]);
+ }
+});
+
+test({
+ name: '"a" vs ""',
+ fn(): void {
+ assertEquals(diff(["a"], []), [{ type: "removed", value: "a" }]);
+ }
+});
+
+test({
+ name: '"" vs "a"',
+ fn(): void {
+ assertEquals(diff([], ["a"]), [{ type: "added", value: "a" }]);
+ }
+});
+
+test({
+ name: '"a" vs "a, b"',
+ fn(): void {
+ assertEquals(diff(["a"], ["a", "b"]), [
+ { type: "common", value: "a" },
+ { type: "added", value: "b" }
+ ]);
+ }
+});
+
+test({
+ name: '"strength" vs "string"',
+ fn(): void {
+ assertEquals(diff(Array.from("strength"), Array.from("string")), [
+ { type: "common", value: "s" },
+ { type: "common", value: "t" },
+ { type: "common", value: "r" },
+ { type: "removed", value: "e" },
+ { type: "added", value: "i" },
+ { type: "common", value: "n" },
+ { type: "common", value: "g" },
+ { type: "removed", value: "t" },
+ { type: "removed", value: "h" }
+ ]);
+ }
+});
+
+test({
+ name: '"strength" vs ""',
+ fn(): void {
+ assertEquals(diff(Array.from("strength"), Array.from("")), [
+ { type: "removed", value: "s" },
+ { type: "removed", value: "t" },
+ { type: "removed", value: "r" },
+ { type: "removed", value: "e" },
+ { type: "removed", value: "n" },
+ { type: "removed", value: "g" },
+ { type: "removed", value: "t" },
+ { type: "removed", value: "h" }
+ ]);
+ }
+});
+
+test({
+ name: '"" vs "strength"',
+ fn(): void {
+ assertEquals(diff(Array.from(""), Array.from("strength")), [
+ { type: "added", value: "s" },
+ { type: "added", value: "t" },
+ { type: "added", value: "r" },
+ { type: "added", value: "e" },
+ { type: "added", value: "n" },
+ { type: "added", value: "g" },
+ { type: "added", value: "t" },
+ { type: "added", value: "h" }
+ ]);
+ }
+});
+
+test({
+ name: '"abc", "c" vs "abc", "bcd", "c"',
+ fn(): void {
+ assertEquals(diff(["abc", "c"], ["abc", "bcd", "c"]), [
+ { type: "common", value: "abc" },
+ { type: "added", value: "bcd" },
+ { type: "common", value: "c" }
+ ]);
+ }
+});
diff --git a/std/testing/format.ts b/std/testing/format.ts
new file mode 100644
index 000000000..953347c27
--- /dev/null
+++ b/std/testing/format.ts
@@ -0,0 +1,538 @@
+// This file is ported from pretty-format@24.0.0
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type Refs = any[];
+export type Optional<T> = { [K in keyof T]?: T[K] };
+
+export interface Options {
+ callToJSON: boolean;
+ escapeRegex: boolean;
+ escapeString: boolean;
+ indent: number;
+ maxDepth: number;
+ min: boolean;
+ printFunctionName: boolean;
+}
+
+export interface Config {
+ callToJSON: boolean;
+ escapeRegex: boolean;
+ escapeString: boolean;
+ indent: string;
+ maxDepth: number;
+ min: boolean;
+ printFunctionName: boolean;
+ spacingInner: string;
+ spacingOuter: string;
+}
+
+export type Printer = (
+ val: unknown,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ hasCalledToJSON?: boolean
+) => string;
+
+const toString = Object.prototype.toString;
+const toISOString = Date.prototype.toISOString;
+const errorToString = Error.prototype.toString;
+const regExpToString = RegExp.prototype.toString;
+const symbolToString = Symbol.prototype.toString;
+
+const DEFAULT_OPTIONS: Options = {
+ callToJSON: true,
+ escapeRegex: false,
+ escapeString: true,
+ indent: 2,
+ maxDepth: Infinity,
+ min: false,
+ printFunctionName: true
+};
+
+interface BasicValueOptions {
+ printFunctionName: boolean;
+ escapeRegex: boolean;
+ escapeString: boolean;
+}
+
+/**
+ * Explicitly comparing typeof constructor to function avoids undefined as name
+ * when mock identity-obj-proxy returns the key as the value for any key.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const getConstructorName = (val: new (...args: any[]) => any): string =>
+ (typeof val.constructor === "function" && val.constructor.name) || "Object";
+
+/* global window */
+/** Is val is equal to global window object?
+ * Works even if it does not exist :)
+ * */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const isWindow = (val: any): val is Window =>
+ typeof window !== "undefined" && val === window;
+
+const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/;
+
+function isToStringedArrayType(toStringed: string): boolean {
+ return (
+ toStringed === "[object Array]" ||
+ toStringed === "[object ArrayBuffer]" ||
+ toStringed === "[object DataView]" ||
+ toStringed === "[object Float32Array]" ||
+ toStringed === "[object Float64Array]" ||
+ toStringed === "[object Int8Array]" ||
+ toStringed === "[object Int16Array]" ||
+ toStringed === "[object Int32Array]" ||
+ toStringed === "[object Uint8Array]" ||
+ toStringed === "[object Uint8ClampedArray]" ||
+ toStringed === "[object Uint16Array]" ||
+ toStringed === "[object Uint32Array]"
+ );
+}
+
+function printNumber(val: number): string {
+ return Object.is(val, -0) ? "-0" : String(val);
+}
+
+function printFunction(val: () => void, printFunctionName: boolean): string {
+ if (!printFunctionName) {
+ return "[Function]";
+ }
+ return "[Function " + (val.name || "anonymous") + "]";
+}
+
+function printSymbol(val: symbol): string {
+ return symbolToString.call(val).replace(SYMBOL_REGEXP, "Symbol($1)");
+}
+
+function printError(val: Error): string {
+ return "[" + errorToString.call(val) + "]";
+}
+
+/**
+ * The first port of call for printing an object, handles most of the
+ * data-types in JS.
+ */
+function printBasicValue(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ val: any,
+ { printFunctionName, escapeRegex, escapeString }: BasicValueOptions
+): string | null {
+ if (val === true || val === false) {
+ return String(val);
+ }
+ if (val === undefined) {
+ return "undefined";
+ }
+ if (val === null) {
+ return "null";
+ }
+
+ const typeOf = typeof val;
+
+ if (typeOf === "number") {
+ return printNumber(val);
+ }
+ if (typeOf === "string") {
+ if (escapeString) {
+ return `"${val.replace(/"|\\/g, "\\$&")}"`;
+ }
+ return `"${val}"`;
+ }
+ if (typeOf === "function") {
+ return printFunction(val, printFunctionName);
+ }
+ if (typeOf === "symbol") {
+ return printSymbol(val);
+ }
+
+ const toStringed = toString.call(val);
+
+ if (toStringed === "[object WeakMap]") {
+ return "WeakMap {}";
+ }
+ if (toStringed === "[object WeakSet]") {
+ return "WeakSet {}";
+ }
+ if (
+ toStringed === "[object Function]" ||
+ toStringed === "[object GeneratorFunction]"
+ ) {
+ return printFunction(val, printFunctionName);
+ }
+ if (toStringed === "[object Symbol]") {
+ return printSymbol(val);
+ }
+ if (toStringed === "[object Date]") {
+ return isNaN(+val) ? "Date { NaN }" : toISOString.call(val);
+ }
+ if (toStringed === "[object Error]") {
+ return printError(val);
+ }
+ if (toStringed === "[object RegExp]") {
+ if (escapeRegex) {
+ // https://github.com/benjamingr/RegExp.escape/blob/master/polyfill.js
+ return regExpToString.call(val).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
+ }
+ return regExpToString.call(val);
+ }
+
+ if (val instanceof Error) {
+ return printError(val);
+ }
+
+ return null;
+}
+
+function printer(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ val: any,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ hasCalledToJSON?: boolean
+): string {
+ const basicResult = printBasicValue(val, config);
+ if (basicResult !== null) {
+ return basicResult;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ return printComplexValue(
+ val,
+ config,
+ indentation,
+ depth,
+ refs,
+ hasCalledToJSON
+ );
+}
+
+/**
+ * Return items (for example, of an array)
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (for example, brackets)
+ */
+function printListItems(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ list: any,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer
+): string {
+ let result = "";
+
+ if (list.length) {
+ result += config.spacingOuter;
+
+ const indentationNext = indentation + config.indent;
+
+ for (let i = 0; i < list.length; i++) {
+ result +=
+ indentationNext +
+ printer(list[i], config, indentationNext, depth, refs);
+
+ if (i < list.length - 1) {
+ result += "," + config.spacingInner;
+ } else if (!config.min) {
+ result += ",";
+ }
+ }
+
+ result += config.spacingOuter + indentation;
+ }
+
+ return result;
+}
+
+/**
+ * Return entries (for example, of a map)
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (for example, braces)
+ */
+function printIteratorEntries(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ iterator: any,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer,
+ // Too bad, so sad that separator for ECMAScript Map has been ' => '
+ // What a distracting diff if you change a data structure to/from
+ // ECMAScript Object or Immutable.Map/OrderedMap which use the default.
+ separator = ": "
+): string {
+ let result = "";
+ let current = iterator.next();
+
+ if (!current.done) {
+ result += config.spacingOuter;
+
+ const indentationNext = indentation + config.indent;
+
+ while (!current.done) {
+ const name = printer(
+ current.value[0],
+ config,
+ indentationNext,
+ depth,
+ refs
+ );
+ const value = printer(
+ current.value[1],
+ config,
+ indentationNext,
+ depth,
+ refs
+ );
+
+ result += indentationNext + name + separator + value;
+
+ current = iterator.next();
+
+ if (!current.done) {
+ result += "," + config.spacingInner;
+ } else if (!config.min) {
+ result += ",";
+ }
+ }
+
+ result += config.spacingOuter + indentation;
+ }
+
+ return result;
+}
+
+/**
+ * Return values (for example, of a set)
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (braces or brackets)
+ */
+function printIteratorValues(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ iterator: Iterator<any>,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer
+): string {
+ let result = "";
+ let current = iterator.next();
+
+ if (!current.done) {
+ result += config.spacingOuter;
+
+ const indentationNext = indentation + config.indent;
+
+ while (!current.done) {
+ result +=
+ indentationNext +
+ printer(current.value, config, indentationNext, depth, refs);
+
+ current = iterator.next();
+
+ if (!current.done) {
+ result += "," + config.spacingInner;
+ } else if (!config.min) {
+ result += ",";
+ }
+ }
+
+ result += config.spacingOuter + indentation;
+ }
+
+ return result;
+}
+
+const getKeysOfEnumerableProperties = (object: {}): Array<string | symbol> => {
+ const keys: Array<string | symbol> = Object.keys(object).sort();
+
+ if (Object.getOwnPropertySymbols) {
+ Object.getOwnPropertySymbols(object).forEach((symbol): void => {
+ if (Object.getOwnPropertyDescriptor(object, symbol)!.enumerable) {
+ keys.push(symbol);
+ }
+ });
+ }
+
+ return keys;
+};
+
+/**
+ * Return properties of an object
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (for example, braces)
+ */
+function printObjectProperties(
+ val: {},
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer
+): string {
+ let result = "";
+ const keys = getKeysOfEnumerableProperties(val);
+
+ if (keys.length) {
+ result += config.spacingOuter;
+
+ const indentationNext = indentation + config.indent;
+
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ const name = printer(key, config, indentationNext, depth, refs);
+ const value = printer(
+ val[key as keyof typeof val],
+ config,
+ indentationNext,
+ depth,
+ refs
+ );
+
+ result += indentationNext + name + ": " + value;
+
+ if (i < keys.length - 1) {
+ result += "," + config.spacingInner;
+ } else if (!config.min) {
+ result += ",";
+ }
+ }
+
+ result += config.spacingOuter + indentation;
+ }
+
+ return result;
+}
+
+/**
+ * Handles more complex objects ( such as objects with circular references.
+ * maps and sets etc )
+ */
+function printComplexValue(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ val: any,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ hasCalledToJSON?: boolean
+): string {
+ if (refs.indexOf(val) !== -1) {
+ return "[Circular]";
+ }
+ refs = refs.slice();
+ refs.push(val);
+
+ const hitMaxDepth = ++depth > config.maxDepth;
+ const { min, callToJSON } = config;
+
+ if (
+ callToJSON &&
+ !hitMaxDepth &&
+ val.toJSON &&
+ typeof val.toJSON === "function" &&
+ !hasCalledToJSON
+ ) {
+ return printer(val.toJSON(), config, indentation, depth, refs, true);
+ }
+
+ const toStringed = toString.call(val);
+ if (toStringed === "[object Arguments]") {
+ return hitMaxDepth
+ ? "[Arguments]"
+ : (min ? "" : "Arguments ") +
+ "[" +
+ printListItems(val, config, indentation, depth, refs, printer) +
+ "]";
+ }
+ if (isToStringedArrayType(toStringed)) {
+ return hitMaxDepth
+ ? `[${val.constructor.name}]`
+ : (min ? "" : `${val.constructor.name} `) +
+ "[" +
+ printListItems(val, config, indentation, depth, refs, printer) +
+ "]";
+ }
+ if (toStringed === "[object Map]") {
+ return hitMaxDepth
+ ? "[Map]"
+ : "Map {" +
+ printIteratorEntries(
+ val.entries(),
+ config,
+ indentation,
+ depth,
+ refs,
+ printer,
+ " => "
+ ) +
+ "}";
+ }
+ if (toStringed === "[object Set]") {
+ return hitMaxDepth
+ ? "[Set]"
+ : "Set {" +
+ printIteratorValues(
+ val.values(),
+ config,
+ indentation,
+ depth,
+ refs,
+ printer
+ ) +
+ "}";
+ }
+
+ // Avoid failure to serialize global window object in jsdom test environment.
+ // For example, not even relevant if window is prop of React element.
+ return hitMaxDepth || isWindow(val)
+ ? "[" + getConstructorName(val) + "]"
+ : (min ? "" : getConstructorName(val) + " ") +
+ "{" +
+ printObjectProperties(val, config, indentation, depth, refs, printer) +
+ "}";
+}
+
+// TODO this is better done with `.padStart()`
+function createIndent(indent: number): string {
+ return new Array(indent + 1).join(" ");
+}
+
+const getConfig = (options: Options): Config => ({
+ ...options,
+ indent: options.min ? "" : createIndent(options.indent),
+ spacingInner: options.min ? " " : "\n",
+ spacingOuter: options.min ? "" : "\n"
+});
+
+/**
+ * Returns a presentation string of your `val` object
+ * @param val any potential JavaScript object
+ * @param options Custom settings
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function format(val: any, options: Optional<Options> = {}): string {
+ const opts: Options = {
+ ...DEFAULT_OPTIONS,
+ ...options
+ };
+ const basicResult = printBasicValue(val, opts);
+ if (basicResult !== null) {
+ return basicResult;
+ }
+
+ return printComplexValue(val, getConfig(opts), "", 0, []);
+}
diff --git a/std/testing/format_test.ts b/std/testing/format_test.ts
new file mode 100644
index 000000000..e2bb8df23
--- /dev/null
+++ b/std/testing/format_test.ts
@@ -0,0 +1,805 @@
+// This file is ported from pretty-format@24.0.0
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+import { test } from "./mod.ts";
+import { assertEquals } from "../testing/asserts.ts";
+import { format } from "./format.ts";
+
+// eslint-disable-next-line max-len
+// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-explicit-any
+function returnArguments(...args: any[]): IArguments {
+ // eslint-disable-next-line prefer-rest-params
+ return arguments;
+}
+
+function MyObject(value: unknown): void {
+ // @ts-ignore
+ this.name = value;
+}
+
+class MyArray<T> extends Array<T> {}
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const createVal = () => [
+ {
+ id: "8658c1d0-9eda-4a90-95e1-8001e8eb6036",
+ text: "Add alternative serialize API for pretty-format plugins",
+ type: "ADD_TODO"
+ },
+ {
+ id: "8658c1d0-9eda-4a90-95e1-8001e8eb6036",
+ type: "TOGGLE_TODO"
+ }
+];
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const createExpected = () =>
+ [
+ "Array [",
+ " Object {",
+ ' "id": "8658c1d0-9eda-4a90-95e1-8001e8eb6036",',
+ ' "text": "Add alternative serialize API for pretty-format plugins",',
+ ' "type": "ADD_TODO",',
+ " },",
+ " Object {",
+ ' "id": "8658c1d0-9eda-4a90-95e1-8001e8eb6036",',
+ ' "type": "TOGGLE_TODO",',
+ " },",
+ "]"
+ ].join("\n");
+
+test({
+ name: "prints empty arguments",
+ fn(): void {
+ const val = returnArguments();
+ assertEquals(format(val), "Arguments []");
+ }
+});
+
+test({
+ name: "prints an empty array",
+ fn(): void {
+ const val: unknown[] = [];
+ assertEquals(format(val), "Array []");
+ }
+});
+
+test({
+ name: "prints an array with items",
+ fn(): void {
+ const val = [1, 2, 3];
+ assertEquals(format(val), "Array [\n 1,\n 2,\n 3,\n]");
+ }
+});
+
+test({
+ name: "prints a empty typed array",
+ fn(): void {
+ const val = new Uint32Array(0);
+ assertEquals(format(val), "Uint32Array []");
+ }
+});
+
+test({
+ name: "prints a typed array with items",
+ fn(): void {
+ const val = new Uint32Array(3);
+ assertEquals(format(val), "Uint32Array [\n 0,\n 0,\n 0,\n]");
+ }
+});
+
+test({
+ name: "prints an array buffer",
+ fn(): void {
+ const val = new ArrayBuffer(3);
+ assertEquals(format(val), "ArrayBuffer []");
+ }
+});
+
+test({
+ name: "prints a nested array",
+ fn(): void {
+ const val = [[1, 2, 3]];
+ assertEquals(
+ format(val),
+ "Array [\n Array [\n 1,\n 2,\n 3,\n ],\n]"
+ );
+ }
+});
+
+test({
+ name: "prints true",
+ fn(): void {
+ const val = true;
+ assertEquals(format(val), "true");
+ }
+});
+
+test({
+ name: "prints false",
+ fn(): void {
+ const val = false;
+ assertEquals(format(val), "false");
+ }
+});
+
+test({
+ name: "prints an error",
+ fn(): void {
+ const val = new Error();
+ assertEquals(format(val), "[Error]");
+ }
+});
+
+test({
+ name: "prints a typed error with a message",
+ fn(): void {
+ const val = new TypeError("message");
+ assertEquals(format(val), "[TypeError: message]");
+ }
+});
+
+test({
+ name: "prints a function constructor",
+ fn(): void {
+ // tslint:disable-next-line:function-constructor
+ const val = new Function();
+ assertEquals(format(val), "[Function anonymous]");
+ }
+});
+
+test({
+ name: "prints an anonymous callback function",
+ fn(): void {
+ let val;
+ function f(cb: () => void): void {
+ val = cb;
+ }
+ // tslint:disable-next-line:no-empty
+ f((): void => {});
+ assertEquals(format(val), "[Function anonymous]");
+ }
+});
+
+test({
+ name: "prints an anonymous assigned function",
+ fn(): void {
+ // tslint:disable-next-line:no-empty
+ const val = (): void => {};
+ const formatted = format(val);
+ assertEquals(
+ formatted === "[Function anonymous]" || formatted === "[Function val]",
+ true
+ );
+ }
+});
+
+test({
+ name: "prints a named function",
+ fn(): void {
+ // tslint:disable-next-line:no-empty
+ const val = function named(): void {};
+ assertEquals(format(val), "[Function named]");
+ }
+});
+
+test({
+ name: "prints a named generator function",
+ fn(): void {
+ const val = function* generate(): IterableIterator<number> {
+ yield 1;
+ yield 2;
+ yield 3;
+ };
+ assertEquals(format(val), "[Function generate]");
+ }
+});
+
+test({
+ name: "can customize function names",
+ fn(): void {
+ // tslint:disable-next-line:no-empty
+ const val = function named(): void {};
+ assertEquals(
+ format(val, {
+ printFunctionName: false
+ }),
+ "[Function]"
+ );
+ }
+});
+
+test({
+ name: "prints Infinity",
+ fn(): void {
+ const val = Infinity;
+ assertEquals(format(val), "Infinity");
+ }
+});
+
+test({
+ name: "prints -Infinity",
+ fn(): void {
+ const val = -Infinity;
+ assertEquals(format(val), "-Infinity");
+ }
+});
+
+test({
+ name: "prints an empty map",
+ fn(): void {
+ const val = new Map();
+ assertEquals(format(val), "Map {}");
+ }
+});
+
+test({
+ name: "prints a map with values",
+ fn(): void {
+ const val = new Map();
+ val.set("prop1", "value1");
+ val.set("prop2", "value2");
+ assertEquals(
+ format(val),
+ 'Map {\n "prop1" => "value1",\n "prop2" => "value2",\n}'
+ );
+ }
+});
+
+test({
+ name: "prints a map with non-string keys",
+ fn(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const val = new Map<any, any>([
+ [false, "boolean"],
+ ["false", "string"],
+ [0, "number"],
+ ["0", "string"],
+ [null, "null"],
+ ["null", "string"],
+ [undefined, "undefined"],
+ ["undefined", "string"],
+ [Symbol("description"), "symbol"],
+ ["Symbol(description)", "string"],
+ [["array", "key"], "array"],
+ [{ key: "value" }, "object"]
+ ]);
+ const expected = [
+ "Map {",
+ ' false => "boolean",',
+ ' "false" => "string",',
+ ' 0 => "number",',
+ ' "0" => "string",',
+ ' null => "null",',
+ ' "null" => "string",',
+ ' undefined => "undefined",',
+ ' "undefined" => "string",',
+ ' Symbol(description) => "symbol",',
+ ' "Symbol(description)" => "string",',
+ " Array [",
+ ' "array",',
+ ' "key",',
+ ' ] => "array",',
+ " Object {",
+ ' "key": "value",',
+ ' } => "object",',
+ "}"
+ ].join("\n");
+ assertEquals(format(val), expected);
+ }
+});
+
+test({
+ name: "prints NaN",
+ fn(): void {
+ const val = NaN;
+ assertEquals(format(val), "NaN");
+ }
+});
+
+test({
+ name: "prints null",
+ fn(): void {
+ const val = null;
+ assertEquals(format(val), "null");
+ }
+});
+
+test({
+ name: "prints a positive number",
+ fn(): void {
+ const val = 123;
+ assertEquals(format(val), "123");
+ }
+});
+
+test({
+ name: "prints a negative number",
+ fn(): void {
+ const val = -123;
+ assertEquals(format(val), "-123");
+ }
+});
+
+test({
+ name: "prints zero",
+ fn(): void {
+ const val = 0;
+ assertEquals(format(val), "0");
+ }
+});
+
+test({
+ name: "prints negative zero",
+ fn(): void {
+ const val = -0;
+ assertEquals(format(val), "-0");
+ }
+});
+
+test({
+ name: "prints a date",
+ fn(): void {
+ const val = new Date(10e11);
+ assertEquals(format(val), "2001-09-09T01:46:40.000Z");
+ }
+});
+
+test({
+ name: "prints an invalid date",
+ fn(): void {
+ const val = new Date(Infinity);
+ assertEquals(format(val), "Date { NaN }");
+ }
+});
+
+test({
+ name: "prints an empty object",
+ fn(): void {
+ const val = {};
+ assertEquals(format(val), "Object {}");
+ }
+});
+
+test({
+ name: "prints an object with properties",
+ fn(): void {
+ const val = { prop1: "value1", prop2: "value2" };
+ assertEquals(
+ format(val),
+ 'Object {\n "prop1": "value1",\n "prop2": "value2",\n}'
+ );
+ }
+});
+
+test({
+ name: "prints an object with properties and symbols",
+ fn(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const val: any = {};
+ val[Symbol("symbol1")] = "value2";
+ val[Symbol("symbol2")] = "value3";
+ val.prop = "value1";
+ assertEquals(
+ format(val),
+ 'Object {\n "prop": "value1",\n Symbol(symbol1): "value2",\n ' +
+ 'Symbol(symbol2): "value3",\n}'
+ );
+ }
+});
+
+test({
+ name:
+ "prints an object without non-enumerable properties which have string key",
+ fn(): void {
+ const val = {
+ enumerable: true
+ };
+ const key = "non-enumerable";
+ Object.defineProperty(val, key, {
+ enumerable: false,
+ value: false
+ });
+ assertEquals(format(val), 'Object {\n "enumerable": true,\n}');
+ }
+});
+
+test({
+ name:
+ "prints an object without non-enumerable properties which have symbol key",
+ fn(): void {
+ const val = {
+ enumerable: true
+ };
+ const key = Symbol("non-enumerable");
+ Object.defineProperty(val, key, {
+ enumerable: false,
+ value: false
+ });
+ assertEquals(format(val), 'Object {\n "enumerable": true,\n}');
+ }
+});
+
+test({
+ name: "prints an object with sorted properties",
+ fn(): void {
+ const val = { b: 1, a: 2 };
+ assertEquals(format(val), 'Object {\n "a": 2,\n "b": 1,\n}');
+ }
+});
+
+test({
+ name: "prints regular expressions from constructors",
+ fn(): void {
+ const val = new RegExp("regexp");
+ assertEquals(format(val), "/regexp/");
+ }
+});
+
+test({
+ name: "prints regular expressions from literals",
+ fn(): void {
+ const val = /regexp/gi;
+ assertEquals(format(val), "/regexp/gi");
+ }
+});
+
+test({
+ name: "prints regular expressions {escapeRegex: false}",
+ fn(): void {
+ const val = /regexp\d/gi;
+ assertEquals(format(val), "/regexp\\d/gi");
+ }
+});
+
+test({
+ name: "prints regular expressions {escapeRegex: true}",
+ fn(): void {
+ const val = /regexp\d/gi;
+ assertEquals(format(val, { escapeRegex: true }), "/regexp\\\\d/gi");
+ }
+});
+
+test({
+ name: "escapes regular expressions nested inside object",
+ fn(): void {
+ const obj = { test: /regexp\d/gi };
+ assertEquals(
+ format(obj, { escapeRegex: true }),
+ 'Object {\n "test": /regexp\\\\d/gi,\n}'
+ );
+ }
+});
+
+test({
+ name: "prints an empty set",
+ fn(): void {
+ const val = new Set();
+ assertEquals(format(val), "Set {}");
+ }
+});
+
+test({
+ name: "prints a set with values",
+ fn(): void {
+ const val = new Set();
+ val.add("value1");
+ val.add("value2");
+ assertEquals(format(val), 'Set {\n "value1",\n "value2",\n}');
+ }
+});
+
+test({
+ name: "prints a string",
+ fn(): void {
+ const val = "string";
+ assertEquals(format(val), '"string"');
+ }
+});
+
+test({
+ name: "prints and escape a string",
+ fn(): void {
+ const val = "\"'\\";
+ assertEquals(format(val), '"\\"\'\\\\"');
+ }
+});
+
+test({
+ name: "doesn't escape string with {excapeString: false}",
+ fn(): void {
+ const val = "\"'\\n";
+ assertEquals(format(val, { escapeString: false }), '""\'\\n"');
+ }
+});
+
+test({
+ name: "prints a string with escapes",
+ fn(): void {
+ assertEquals(format('"-"'), '"\\"-\\""');
+ assertEquals(format("\\ \\\\"), '"\\\\ \\\\\\\\"');
+ }
+});
+
+test({
+ name: "prints a multiline string",
+ fn(): void {
+ const val = ["line 1", "line 2", "line 3"].join("\n");
+ assertEquals(format(val), '"' + val + '"');
+ }
+});
+
+test({
+ name: "prints a multiline string as value of object property",
+ fn(): void {
+ const polyline = {
+ props: {
+ id: "J",
+ points: ["0.5,0.460", "0.5,0.875", "0.25,0.875"].join("\n")
+ },
+ type: "polyline"
+ };
+ const val = {
+ props: {
+ children: polyline
+ },
+ type: "svg"
+ };
+ assertEquals(
+ format(val),
+ [
+ "Object {",
+ ' "props": Object {',
+ ' "children": Object {',
+ ' "props": Object {',
+ ' "id": "J",',
+ ' "points": "0.5,0.460',
+ "0.5,0.875",
+ '0.25,0.875",',
+ " },",
+ ' "type": "polyline",',
+ " },",
+ " },",
+ ' "type": "svg",',
+ "}"
+ ].join("\n")
+ );
+ }
+});
+
+test({
+ name: "prints a symbol",
+ fn(): void {
+ const val = Symbol("symbol");
+ assertEquals(format(val), "Symbol(symbol)");
+ }
+});
+
+test({
+ name: "prints undefined",
+ fn(): void {
+ const val = undefined;
+ assertEquals(format(val), "undefined");
+ }
+});
+
+test({
+ name: "prints a WeakMap",
+ fn(): void {
+ const val = new WeakMap();
+ assertEquals(format(val), "WeakMap {}");
+ }
+});
+
+test({
+ name: "prints a WeakSet",
+ fn(): void {
+ const val = new WeakSet();
+ assertEquals(format(val), "WeakSet {}");
+ }
+});
+
+test({
+ name: "prints deeply nested objects",
+ fn(): void {
+ const val = { prop: { prop: { prop: "value" } } };
+ assertEquals(
+ format(val),
+ 'Object {\n "prop": Object {\n "prop": Object {\n "prop": ' +
+ '"value",\n },\n },\n}'
+ );
+ }
+});
+
+test({
+ name: "prints circular references",
+ fn(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const val: any = {};
+ val.prop = val;
+ assertEquals(format(val), 'Object {\n "prop": [Circular],\n}');
+ }
+});
+
+test({
+ name: "prints parallel references",
+ fn(): void {
+ const inner = {};
+ const val = { prop1: inner, prop2: inner };
+ assertEquals(
+ format(val),
+ 'Object {\n "prop1": Object {},\n "prop2": Object {},\n}'
+ );
+ }
+});
+
+test({
+ name: "default implicit: 2 spaces",
+ fn(): void {
+ assertEquals(format(createVal()), createExpected());
+ }
+});
+
+test({
+ name: "default explicit: 2 spaces",
+ fn(): void {
+ assertEquals(format(createVal(), { indent: 2 }), createExpected());
+ }
+});
+
+// Tests assume that no strings in val contain multiple adjacent spaces!
+test({
+ name: "non-default: 0 spaces",
+ fn(): void {
+ const indent = 0;
+ assertEquals(
+ format(createVal(), { indent }),
+ createExpected().replace(/ {2}/g, " ".repeat(indent))
+ );
+ }
+});
+
+test({
+ name: "non-default: 4 spaces",
+ fn(): void {
+ const indent = 4;
+ assertEquals(
+ format(createVal(), { indent }),
+ createExpected().replace(/ {2}/g, " ".repeat(indent))
+ );
+ }
+});
+
+test({
+ name: "can customize the max depth",
+ fn(): void {
+ const v = [
+ {
+ "arguments empty": returnArguments(),
+ "arguments non-empty": returnArguments("arg"),
+ "array literal empty": [],
+ "array literal non-empty": ["item"],
+ "extended array empty": new MyArray(),
+ "map empty": new Map(),
+ "map non-empty": new Map([["name", "value"]]),
+ "object literal empty": {},
+ "object literal non-empty": { name: "value" },
+ // @ts-ignore
+ "object with constructor": new MyObject("value"),
+ "object without constructor": Object.create(null),
+ "set empty": new Set(),
+ "set non-empty": new Set(["value"])
+ }
+ ];
+ assertEquals(
+ format(v, { maxDepth: 2 }),
+ [
+ "Array [",
+ " Object {",
+ ' "arguments empty": [Arguments],',
+ ' "arguments non-empty": [Arguments],',
+ ' "array literal empty": [Array],',
+ ' "array literal non-empty": [Array],',
+ ' "extended array empty": [MyArray],',
+ ' "map empty": [Map],',
+ ' "map non-empty": [Map],',
+ ' "object literal empty": [Object],',
+ ' "object literal non-empty": [Object],',
+ ' "object with constructor": [MyObject],',
+ ' "object without constructor": [Object],',
+ ' "set empty": [Set],',
+ ' "set non-empty": [Set],',
+ " },",
+ "]"
+ ].join("\n")
+ );
+ }
+});
+
+test({
+ name: "prints objects with no constructor",
+ fn(): void {
+ assertEquals(format(Object.create(null)), "Object {}");
+ }
+});
+
+test({
+ name: "prints identity-obj-proxy with string constructor",
+ fn(): void {
+ const obj = Object.create(null);
+ obj.constructor = "constructor";
+ const expected = [
+ "Object {", // Object instead of undefined
+ ' "constructor": "constructor",',
+ "}"
+ ].join("\n");
+ assertEquals(format(obj), expected);
+ }
+});
+
+test({
+ name: "calls toJSON and prints its return value",
+ fn(): void {
+ assertEquals(
+ format({
+ toJSON: (): unknown => ({ value: false }),
+ value: true
+ }),
+ 'Object {\n "value": false,\n}'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON and prints an internal representation.",
+ fn(): void {
+ assertEquals(
+ format({
+ toJSON: (): string => "[Internal Object]",
+ value: true
+ }),
+ '"[Internal Object]"'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON only on functions",
+ fn(): void {
+ assertEquals(
+ format({
+ toJSON: false,
+ value: true
+ }),
+ 'Object {\n "toJSON": false,\n "value": true,\n}'
+ );
+ }
+});
+
+test({
+ name: "does not call toJSON recursively",
+ fn(): void {
+ assertEquals(
+ format({
+ toJSON: (): unknown => ({ toJSON: (): unknown => ({ value: true }) }),
+ value: false
+ }),
+ 'Object {\n "toJSON": [Function toJSON],\n}'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON on Sets",
+ fn(): void {
+ const set = new Set([1]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (set as any).toJSON = (): string => "map";
+ assertEquals(format(set), '"map"');
+ }
+});
diff --git a/std/testing/mod.ts b/std/testing/mod.ts
new file mode 100644
index 000000000..3b6386d5b
--- /dev/null
+++ b/std/testing/mod.ts
@@ -0,0 +1,422 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import {
+ bgRed,
+ white,
+ bold,
+ green,
+ red,
+ gray,
+ yellow,
+ italic
+} from "../fmt/colors.ts";
+export type TestFunction = () => void | Promise<void>;
+
+export interface TestDefinition {
+ fn: TestFunction;
+ name: string;
+}
+
+// Replacement of the global `console` function to be in silent mode
+const noop = function(): void {};
+
+// Clear the current line of the console.
+// see: http://ascii-table.com/ansi-escape-sequences-vt-100.php
+const CLEAR_LINE = "\x1b[2K\r";
+
+// Save Object of the global `console` in case of silent mode
+type Console = typeof window.console;
+// ref https://console.spec.whatwg.org/#console-namespace
+// For historical web-compatibility reasons, the namespace object for
+// console must have as its [[Prototype]] an empty object, created as if
+// by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%.
+const disabledConsole = Object.create({}) as Console;
+Object.assign(disabledConsole, {
+ log: noop,
+ debug: noop,
+ info: noop,
+ dir: noop,
+ warn: noop,
+ error: noop,
+ assert: noop,
+ count: noop,
+ countReset: noop,
+ table: noop,
+ time: noop,
+ timeLog: noop,
+ timeEnd: noop,
+ group: noop,
+ groupCollapsed: noop,
+ groupEnd: noop,
+ clear: noop
+});
+
+const originalConsole = window.console;
+
+function enableConsole(): void {
+ window.console = originalConsole;
+}
+
+function disableConsole(): void {
+ window.console = disabledConsole;
+}
+
+const encoder = new TextEncoder();
+function print(txt: string, newline = true): void {
+ if (newline) {
+ txt += "\n";
+ }
+ Deno.stdout.writeSync(encoder.encode(`${txt}`));
+}
+
+declare global {
+ interface Window {
+ /**
+ * A global property to collect all registered test cases.
+ *
+ * It is required because user's code can import multiple versions
+ * of `testing` module.
+ *
+ * If test cases aren't registered in a globally shared
+ * object, then imports from different versions would register test cases
+ * to registry from it's respective version of `testing` module.
+ */
+ __DENO_TEST_REGISTRY: TestDefinition[];
+ }
+}
+
+let candidates: TestDefinition[] = [];
+if (window["__DENO_TEST_REGISTRY"]) {
+ candidates = window.__DENO_TEST_REGISTRY as TestDefinition[];
+} else {
+ window["__DENO_TEST_REGISTRY"] = candidates;
+}
+let filterRegExp: RegExp | null;
+let filtered = 0;
+
+// Must be called before any test() that needs to be filtered.
+export function setFilter(s: string): void {
+ filterRegExp = new RegExp(s, "i");
+}
+
+function filter(name: string): boolean {
+ if (filterRegExp) {
+ return filterRegExp.test(name);
+ } else {
+ return true;
+ }
+}
+
+export function test(t: TestDefinition): void;
+export function test(fn: TestFunction): void;
+export function test(name: string, fn: TestFunction): void;
+export function test(
+ t: string | TestDefinition | TestFunction,
+ fn?: TestFunction
+): void {
+ let name: string;
+
+ if (typeof t === "string") {
+ if (!fn) {
+ throw new Error("Missing test function");
+ }
+ name = t;
+ } else {
+ fn = typeof t === "function" ? t : t.fn;
+ name = t.name;
+ }
+
+ if (!name) {
+ throw new Error("Test function may not be anonymous");
+ }
+ if (filter(name)) {
+ candidates.push({ fn, name });
+ } else {
+ filtered++;
+ }
+}
+
+const RED_FAILED = red("FAILED");
+const GREEN_OK = green("OK");
+const RED_BG_FAIL = bgRed(" FAIL ");
+
+interface TestStats {
+ filtered: number;
+ ignored: number;
+ measured: number;
+ passed: number;
+ failed: number;
+}
+
+interface TestResult {
+ timeElapsed?: number;
+ name: string;
+ error?: Error;
+ ok: boolean;
+ printed: boolean;
+}
+
+interface TestResults {
+ keys: Map<string, number>;
+ cases: Map<number, TestResult>;
+}
+
+function createTestResults(tests: TestDefinition[]): TestResults {
+ return tests.reduce(
+ (acc: TestResults, { name }: TestDefinition, i: number): TestResults => {
+ acc.keys.set(name, i);
+ acc.cases.set(i, { name, printed: false, ok: false, error: undefined });
+ return acc;
+ },
+ { cases: new Map(), keys: new Map() }
+ );
+}
+
+function formatTestTime(time = 0): string {
+ return `${time.toFixed(2)}ms`;
+}
+
+function promptTestTime(time = 0, displayWarning = false): string {
+ // if time > 5s we display a warning
+ // only for test time, not the full runtime
+ if (displayWarning && time >= 5000) {
+ return bgRed(white(bold(`(${formatTestTime(time)})`)));
+ } else {
+ return gray(italic(`(${formatTestTime(time)})`));
+ }
+}
+
+function report(result: TestResult): void {
+ if (result.ok) {
+ print(
+ `${GREEN_OK} ${result.name} ${promptTestTime(
+ result.timeElapsed,
+ true
+ )}`
+ );
+ } else if (result.error) {
+ print(`${RED_FAILED} ${result.name}\n${result.error.stack}`);
+ } else {
+ print(`test ${result.name} ... unresolved`);
+ }
+ result.printed = true;
+}
+
+function printFailedSummary(results: TestResults): void {
+ results.cases.forEach((v): void => {
+ if (!v.ok) {
+ console.error(`${RED_BG_FAIL} ${red(v.name)}`);
+ console.error(v.error);
+ }
+ });
+}
+
+function printResults(
+ stats: TestStats,
+ results: TestResults,
+ flush: boolean,
+ exitOnFail: boolean,
+ timeElapsed: number
+): void {
+ if (flush) {
+ for (const result of results.cases.values()) {
+ if (!result.printed) {
+ report(result);
+ if (result.error && exitOnFail) {
+ break;
+ }
+ }
+ }
+ }
+ // Attempting to match the output of Rust's test runner.
+ print(
+ `\ntest result: ${stats.failed ? RED_BG_FAIL : GREEN_OK} ` +
+ `${stats.passed} passed; ${stats.failed} failed; ` +
+ `${stats.ignored} ignored; ${stats.measured} measured; ` +
+ `${stats.filtered} filtered out ` +
+ `${promptTestTime(timeElapsed)}\n`
+ );
+}
+
+function previousPrinted(name: string, results: TestResults): boolean {
+ const curIndex: number = results.keys.get(name)!;
+ if (curIndex === 0) {
+ return true;
+ }
+ return results.cases.get(curIndex - 1)!.printed;
+}
+
+async function createTestCase(
+ stats: TestStats,
+ results: TestResults,
+ exitOnFail: boolean,
+ { fn, name }: TestDefinition
+): Promise<void> {
+ const result: TestResult = results.cases.get(results.keys.get(name)!)!;
+ try {
+ const start = performance.now();
+ await fn();
+ const end = performance.now();
+ stats.passed++;
+ result.ok = true;
+ result.timeElapsed = end - start;
+ } catch (err) {
+ stats.failed++;
+ result.error = err;
+ if (exitOnFail) {
+ throw err;
+ }
+ }
+ if (previousPrinted(name, results)) {
+ report(result);
+ }
+}
+
+function initTestCases(
+ stats: TestStats,
+ results: TestResults,
+ tests: TestDefinition[],
+ exitOnFail: boolean
+): Array<Promise<void>> {
+ return tests.map(createTestCase.bind(null, stats, results, exitOnFail));
+}
+
+async function runTestsParallel(
+ stats: TestStats,
+ results: TestResults,
+ tests: TestDefinition[],
+ exitOnFail: boolean
+): Promise<void> {
+ try {
+ await Promise.all(initTestCases(stats, results, tests, exitOnFail));
+ } catch (_) {
+ // The error was thrown to stop awaiting all promises if exitOnFail === true
+ // stats.failed has been incremented and the error stored in results
+ }
+}
+
+async function runTestsSerial(
+ stats: TestStats,
+ results: TestResults,
+ tests: TestDefinition[],
+ exitOnFail: boolean,
+ disableLog: boolean
+): Promise<void> {
+ for (const { fn, name } of tests) {
+ // Displaying the currently running test if silent mode
+ if (disableLog) {
+ print(`${yellow("RUNNING")} ${name}`, false);
+ }
+ try {
+ const start = performance.now();
+ await fn();
+ const end = performance.now();
+ if (disableLog) {
+ // Rewriting the current prompt line to erase `running ....`
+ print(CLEAR_LINE, false);
+ }
+ stats.passed++;
+ print(
+ GREEN_OK + " " + name + " " + promptTestTime(end - start, true)
+ );
+ results.cases.forEach((v): void => {
+ if (v.name === name) {
+ v.ok = true;
+ v.printed = true;
+ }
+ });
+ } catch (err) {
+ if (disableLog) {
+ print(CLEAR_LINE, false);
+ }
+ print(`${RED_FAILED} ${name}`);
+ print(err.stack);
+ stats.failed++;
+ results.cases.forEach((v): void => {
+ if (v.name === name) {
+ v.error = err;
+ v.ok = false;
+ v.printed = true;
+ }
+ });
+ if (exitOnFail) {
+ break;
+ }
+ }
+ }
+}
+
+/** Defines options for controlling execution details of a test suite. */
+export interface RunTestsOptions {
+ parallel?: boolean;
+ exitOnFail?: boolean;
+ only?: RegExp;
+ skip?: RegExp;
+ disableLog?: boolean;
+}
+
+/**
+ * Runs specified test cases.
+ * Parallel execution can be enabled via the boolean option; default: serial.
+ */
+// TODO: change return type to `Promise<boolean>` - ie. don't
+// exit but return value
+export async function runTests({
+ parallel = false,
+ exitOnFail = false,
+ only = /[^\s]/,
+ skip = /^\s*$/,
+ disableLog = false
+}: RunTestsOptions = {}): Promise<void> {
+ const tests: TestDefinition[] = candidates.filter(
+ ({ name }): boolean => only.test(name) && !skip.test(name)
+ );
+ const stats: TestStats = {
+ measured: 0,
+ ignored: candidates.length - tests.length,
+ filtered: filtered,
+ passed: 0,
+ failed: 0
+ };
+ const results: TestResults = createTestResults(tests);
+ print(`running ${tests.length} tests`);
+ const start = performance.now();
+ if (Deno.args.includes("--quiet")) {
+ disableLog = true;
+ }
+ if (disableLog) {
+ disableConsole();
+ }
+ if (parallel) {
+ await runTestsParallel(stats, results, tests, exitOnFail);
+ } else {
+ await runTestsSerial(stats, results, tests, exitOnFail, disableLog);
+ }
+ const end = performance.now();
+ if (disableLog) {
+ enableConsole();
+ }
+ printResults(stats, results, parallel, exitOnFail, end - start);
+ if (stats.failed) {
+ // Use setTimeout to avoid the error being ignored due to unhandled
+ // promise rejections being swallowed.
+ setTimeout((): void => {
+ console.error(`There were ${stats.failed} test failures.`);
+ printFailedSummary(results);
+ Deno.exit(1);
+ }, 0);
+ }
+}
+
+/**
+ * Runs specified test cases if the enclosing script is main.
+ * Execution mode is toggleable via opts.parallel, defaults to false.
+ */
+export async function runIfMain(
+ meta: ImportMeta,
+ opts?: RunTestsOptions
+): Promise<void> {
+ if (meta.main) {
+ return runTests(opts);
+ }
+}
diff --git a/std/testing/runner.ts b/std/testing/runner.ts
new file mode 100755
index 000000000..a78ed2b3a
--- /dev/null
+++ b/std/testing/runner.ts
@@ -0,0 +1,236 @@
+#!/usr/bin/env -S deno -A
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { parse } from "../flags/mod.ts";
+import { ExpandGlobOptions, expandGlob } from "../fs/mod.ts";
+import { isWindows } from "../fs/path/constants.ts";
+import { join } from "../fs/path/mod.ts";
+import { RunTestsOptions, runTests } from "./mod.ts";
+const { DenoError, ErrorKind, args, cwd, exit } = Deno;
+
+const DIR_GLOBS = [join("**", "?(*_)test.{js,ts}")];
+
+function showHelp(): void {
+ console.log(`Deno test runner
+
+USAGE:
+ deno -A https://deno.land/std/testing/runner.ts [OPTIONS] [MODULES...]
+
+OPTIONS:
+ -q, --quiet Don't show output from test cases
+ -f, --failfast Stop running tests on first error
+ -e, --exclude <MODULES...> List of comma-separated modules to exclude
+ --allow-none Exit with status 0 even when no test modules are
+ found
+
+ARGS:
+ [MODULES...] List of test modules to run.
+ A directory <dir> will expand to:
+ ${DIR_GLOBS.map((s: string): string => `${join("<dir>", s)}`)
+ .join(`
+ `)}
+ Defaults to "." when none are provided.
+
+Note that modules can refer to file paths or URLs. File paths support glob
+expansion.
+
+Examples:
+ deno test src/**/*_test.ts
+ deno test tests`);
+}
+
+function isRemoteUrl(url: string): boolean {
+ return /^https?:\/\//.test(url);
+}
+
+function partition(
+ arr: string[],
+ callback: (el: string) => boolean
+): [string[], string[]] {
+ return arr.reduce(
+ (paritioned: [string[], string[]], el: string): [string[], string[]] => {
+ paritioned[callback(el) ? 1 : 0].push(el);
+ return paritioned;
+ },
+ [[], []]
+ );
+}
+
+function filePathToUrl(path: string): string {
+ return `file://${isWindows ? "/" : ""}${path.replace(/\\/g, "/")}`;
+}
+
+/**
+ * Given a list of globs or URLs to include and exclude and a root directory
+ * from which to expand relative globs, yield a list of URLs
+ * (file: or remote) that should be imported for the test runner.
+ */
+export async function* findTestModules(
+ includeModules: string[],
+ excludeModules: string[],
+ root: string = cwd()
+): AsyncIterableIterator<string> {
+ const [includePaths, includeUrls] = partition(includeModules, isRemoteUrl);
+ const [excludePaths, excludeUrls] = partition(excludeModules, isRemoteUrl);
+
+ const expandGlobOpts: ExpandGlobOptions = {
+ root,
+ exclude: excludePaths,
+ includeDirs: true,
+ extended: true,
+ globstar: true
+ };
+
+ async function* expandDirectory(d: string): AsyncIterableIterator<string> {
+ for (const dirGlob of DIR_GLOBS) {
+ for await (const walkInfo of expandGlob(dirGlob, {
+ ...expandGlobOpts,
+ root: d,
+ includeDirs: false
+ })) {
+ yield filePathToUrl(walkInfo.filename);
+ }
+ }
+ }
+
+ for (const globString of includePaths) {
+ for await (const walkInfo of expandGlob(globString, expandGlobOpts)) {
+ if (walkInfo.info.isDirectory()) {
+ yield* expandDirectory(walkInfo.filename);
+ } else {
+ yield filePathToUrl(walkInfo.filename);
+ }
+ }
+ }
+
+ const excludeUrlPatterns = excludeUrls.map(
+ (url: string): RegExp => RegExp(url)
+ );
+ const shouldIncludeUrl = (url: string): boolean =>
+ !excludeUrlPatterns.some((p: RegExp): boolean => !!url.match(p));
+
+ yield* includeUrls.filter(shouldIncludeUrl);
+}
+
+export interface RunTestModulesOptions extends RunTestsOptions {
+ include?: string[];
+ exclude?: string[];
+ allowNone?: boolean;
+}
+
+/**
+ * Import the specified test modules and run their tests as a suite.
+ *
+ * Test modules are specified as an array of strings and can include local files
+ * or URLs.
+ *
+ * File matching and excluding support glob syntax - arguments recognized as
+ * globs will be expanded using `glob()` from the `fs` module.
+ *
+ * Example:
+ *
+ * runTestModules({ include: ["**\/*_test.ts", "**\/test.ts"] });
+ *
+ * Any matched directory `<dir>` will expand to:
+ * <dir>/**\/?(*_)test.{js,ts}
+ *
+ * So the above example is captured naturally by:
+ *
+ * runTestModules({ include: ["."] });
+ *
+ * Which is the default used for:
+ *
+ * runTestModules();
+ */
+// TODO: Change return type to `Promise<void>` once, `runTests` is updated
+// to return boolean instead of exiting.
+export async function runTestModules({
+ include = ["."],
+ exclude = [],
+ allowNone = false,
+ parallel = false,
+ exitOnFail = false,
+ only = /[^\s]/,
+ skip = /^\s*$/,
+ disableLog = false
+}: RunTestModulesOptions = {}): Promise<void> {
+ let moduleCount = 0;
+ for await (const testModule of findTestModules(include, exclude)) {
+ await import(testModule);
+ moduleCount++;
+ }
+
+ if (moduleCount == 0) {
+ const noneFoundMessage = "No matching test modules found.";
+ if (!allowNone) {
+ throw new DenoError(ErrorKind.NotFound, noneFoundMessage);
+ } else if (!disableLog) {
+ console.log(noneFoundMessage);
+ }
+ return;
+ }
+
+ if (!disableLog) {
+ console.log(`Found ${moduleCount} matching test modules.`);
+ }
+
+ await runTests({
+ parallel,
+ exitOnFail,
+ only,
+ skip,
+ disableLog
+ });
+}
+
+async function main(): Promise<void> {
+ const parsedArgs = parse(args.slice(1), {
+ boolean: ["allow-none", "failfast", "help", "quiet"],
+ string: ["exclude"],
+ alias: {
+ exclude: ["e"],
+ failfast: ["f"],
+ help: ["h"],
+ quiet: ["q"]
+ },
+ default: {
+ "allow-none": false,
+ failfast: false,
+ help: false,
+ quiet: false
+ }
+ });
+ if (parsedArgs.help) {
+ return showHelp();
+ }
+
+ const include =
+ parsedArgs._.length > 0
+ ? (parsedArgs._ as string[]).flatMap((fileGlob: string): string[] =>
+ fileGlob.split(",")
+ )
+ : ["."];
+ const exclude =
+ parsedArgs.exclude != null ? (parsedArgs.exclude as string).split(",") : [];
+ const allowNone = parsedArgs["allow-none"];
+ const exitOnFail = parsedArgs.failfast;
+ const disableLog = parsedArgs.quiet;
+
+ try {
+ await runTestModules({
+ include,
+ exclude,
+ allowNone,
+ exitOnFail,
+ disableLog
+ });
+ } catch (error) {
+ if (!disableLog) {
+ console.error(error.message);
+ }
+ exit(1);
+ }
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/std/testing/runner_test.ts b/std/testing/runner_test.ts
new file mode 100644
index 000000000..e2617b155
--- /dev/null
+++ b/std/testing/runner_test.ts
@@ -0,0 +1,95 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test } from "./mod.ts";
+import { findTestModules } from "./runner.ts";
+import { isWindows } from "../fs/path/constants.ts";
+import { assertEquals } from "../testing/asserts.ts";
+const { cwd } = Deno;
+
+function urlToFilePath(url: URL): string {
+ // Since `new URL('file:///C:/a').pathname` is `/C:/a`, remove leading slash.
+ return url.pathname.slice(url.protocol == "file:" && isWindows ? 1 : 0);
+}
+
+async function findTestModulesArray(
+ include: string[],
+ exclude: string[],
+ root: string = cwd()
+): Promise<string[]> {
+ const result = [];
+ for await (const testModule of findTestModules(include, exclude, root)) {
+ result.push(testModule);
+ }
+ return result;
+}
+
+const TEST_DATA_URL = new URL("testdata", import.meta.url);
+const TEST_DATA_PATH = urlToFilePath(TEST_DATA_URL);
+
+test(async function findTestModulesDir1(): Promise<void> {
+ const urls = await findTestModulesArray(["."], [], TEST_DATA_PATH);
+ assertEquals(urls.sort(), [
+ `${TEST_DATA_URL}/bar_test.js`,
+ `${TEST_DATA_URL}/foo_test.ts`,
+ `${TEST_DATA_URL}/subdir/bar_test.js`,
+ `${TEST_DATA_URL}/subdir/foo_test.ts`,
+ `${TEST_DATA_URL}/subdir/test.js`,
+ `${TEST_DATA_URL}/subdir/test.ts`,
+ `${TEST_DATA_URL}/test.js`,
+ `${TEST_DATA_URL}/test.ts`
+ ]);
+});
+
+test(async function findTestModulesDir2(): Promise<void> {
+ const urls = await findTestModulesArray(["subdir"], [], TEST_DATA_PATH);
+ assertEquals(urls.sort(), [
+ `${TEST_DATA_URL}/subdir/bar_test.js`,
+ `${TEST_DATA_URL}/subdir/foo_test.ts`,
+ `${TEST_DATA_URL}/subdir/test.js`,
+ `${TEST_DATA_URL}/subdir/test.ts`
+ ]);
+});
+
+test(async function findTestModulesGlob(): Promise<void> {
+ const urls = await findTestModulesArray(
+ ["**/*_test.{js,ts}"],
+ [],
+ TEST_DATA_PATH
+ );
+ assertEquals(urls.sort(), [
+ `${TEST_DATA_URL}/bar_test.js`,
+ `${TEST_DATA_URL}/foo_test.ts`,
+ `${TEST_DATA_URL}/subdir/bar_test.js`,
+ `${TEST_DATA_URL}/subdir/foo_test.ts`
+ ]);
+});
+
+test(async function findTestModulesExcludeDir(): Promise<void> {
+ const urls = await findTestModulesArray(["."], ["subdir"], TEST_DATA_PATH);
+ assertEquals(urls.sort(), [
+ `${TEST_DATA_URL}/bar_test.js`,
+ `${TEST_DATA_URL}/foo_test.ts`,
+ `${TEST_DATA_URL}/test.js`,
+ `${TEST_DATA_URL}/test.ts`
+ ]);
+});
+
+test(async function findTestModulesExcludeGlob(): Promise<void> {
+ const urls = await findTestModulesArray(["."], ["**/foo*"], TEST_DATA_PATH);
+ assertEquals(urls.sort(), [
+ `${TEST_DATA_URL}/bar_test.js`,
+ `${TEST_DATA_URL}/subdir/bar_test.js`,
+ `${TEST_DATA_URL}/subdir/test.js`,
+ `${TEST_DATA_URL}/subdir/test.ts`,
+ `${TEST_DATA_URL}/test.js`,
+ `${TEST_DATA_URL}/test.ts`
+ ]);
+});
+
+test(async function findTestModulesRemote(): Promise<void> {
+ const urls = [
+ "https://example.com/colors_test.ts",
+ "http://example.com/printf_test.ts"
+ ];
+ const matches = await findTestModulesArray(urls, []);
+ assertEquals(matches, urls);
+});
diff --git a/std/testing/test.ts b/std/testing/test.ts
new file mode 100644
index 000000000..dd6d772f6
--- /dev/null
+++ b/std/testing/test.ts
@@ -0,0 +1,263 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, runIfMain } from "./mod.ts";
+import {
+ assert,
+ assertEquals,
+ assertStrictEq,
+ assertThrows,
+ assertThrowsAsync
+} from "./asserts.ts";
+
+test(function testingAssertEqualActualUncoercable(): void {
+ let didThrow = false;
+ const a = Object.create(null);
+ try {
+ assertEquals(a, "bar");
+ } catch (e) {
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertEqualExpectedUncoercable(): void {
+ let didThrow = false;
+ const a = Object.create(null);
+ try {
+ assertStrictEq("bar", a);
+ } catch (e) {
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingAssertStrictEqual(): void {
+ const a = {};
+ const b = a;
+ assertStrictEq(a, b);
+});
+
+test(function testingAssertNotStrictEqual(): void {
+ let didThrow = false;
+ const a = {};
+ const b = {};
+ try {
+ assertStrictEq(a, b);
+ } catch (e) {
+ assert(e.message === "actual: [object Object] expected: [object Object]");
+ didThrow = true;
+ }
+ assert(didThrow);
+});
+
+test(function testingDoesThrow(): void {
+ let count = 0;
+ assertThrows((): void => {
+ count++;
+ throw new Error();
+ });
+ assert(count === 1);
+});
+
+test(function testingDoesNotThrow(): void {
+ let count = 0;
+ let didThrow = false;
+ try {
+ assertThrows((): void => {
+ count++;
+ console.log("Hello world");
+ });
+ } catch (e) {
+ assert(e.message === "Expected function to throw.");
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(function testingThrowsErrorType(): void {
+ let count = 0;
+ assertThrows((): void => {
+ count++;
+ throw new TypeError();
+ }, TypeError);
+ assert(count === 1);
+});
+
+test(function testingThrowsNotErrorType(): void {
+ let count = 0;
+ let didThrow = false;
+ try {
+ assertThrows((): void => {
+ count++;
+ throw new TypeError();
+ }, RangeError);
+ } catch (e) {
+ assert(e.message === `Expected error to be instance of "RangeError".`);
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(function testingThrowsMsgIncludes(): void {
+ let count = 0;
+ assertThrows(
+ (): void => {
+ count++;
+ throw new TypeError("Hello world!");
+ },
+ TypeError,
+ "world"
+ );
+ assert(count === 1);
+});
+
+test(function testingThrowsMsgNotIncludes(): void {
+ let count = 0;
+ let didThrow = false;
+ try {
+ assertThrows(
+ (): void => {
+ count++;
+ throw new TypeError("Hello world!");
+ },
+ TypeError,
+ "foobar"
+ );
+ } catch (e) {
+ assert(
+ e.message ===
+ `Expected error message to include "foobar", but got "Hello world!".`
+ );
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(async function testingDoesThrowAsync(): Promise<void> {
+ let count = 0;
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ count++;
+ throw new Error();
+ }
+ );
+ assert(count === 1);
+});
+
+test(async function testingDoesReject(): Promise<void> {
+ let count = 0;
+ await assertThrowsAsync(
+ (): Promise<never> => {
+ count++;
+ return Promise.reject(new Error());
+ }
+ );
+ assert(count === 1);
+});
+
+test(async function testingDoesNotThrowAsync(): Promise<void> {
+ let count = 0;
+ let didThrow = false;
+ try {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ count++;
+ console.log("Hello world");
+ }
+ );
+ } catch (e) {
+ assert(e.message === "Expected function to throw.");
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(async function testingDoesNotRejectAsync(): Promise<void> {
+ let count = 0;
+ let didThrow = false;
+ try {
+ await assertThrowsAsync(
+ (): Promise<void> => {
+ count++;
+ console.log("Hello world");
+ return Promise.resolve();
+ }
+ );
+ } catch (e) {
+ assert(e.message === "Expected function to throw.");
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(async function testingThrowsAsyncErrorType(): Promise<void> {
+ let count = 0;
+ await assertThrowsAsync((): Promise<void> => {
+ count++;
+ throw new TypeError();
+ }, TypeError);
+ assert(count === 1);
+});
+
+test(async function testingThrowsAsyncNotErrorType(): Promise<void> {
+ let count = 0;
+ let didThrow = false;
+ try {
+ await assertThrowsAsync(async (): Promise<void> => {
+ count++;
+ throw new TypeError();
+ }, RangeError);
+ } catch (e) {
+ assert(e.message === `Expected error to be instance of "RangeError".`);
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test(async function testingThrowsAsyncMsgIncludes(): Promise<void> {
+ let count = 0;
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ count++;
+ throw new TypeError("Hello world!");
+ },
+ TypeError,
+ "world"
+ );
+ assert(count === 1);
+});
+
+test(async function testingThrowsAsyncMsgNotIncludes(): Promise<void> {
+ let count = 0;
+ let didThrow = false;
+ try {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ count++;
+ throw new TypeError("Hello world!");
+ },
+ TypeError,
+ "foobar"
+ );
+ } catch (e) {
+ assert(
+ e.message ===
+ `Expected error message to include "foobar", but got "Hello world!".`
+ );
+ didThrow = true;
+ }
+ assert(count === 1);
+ assert(didThrow);
+});
+
+test("test fn overloading", (): void => {
+ // just verifying that you can use this test definition syntax
+ assert(true);
+});
+
+runIfMain(import.meta);
diff --git a/std/testing/testdata/bar.js b/std/testing/testdata/bar.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/bar.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/bar_test.js b/std/testing/testdata/bar_test.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/bar_test.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/foo.ts b/std/testing/testdata/foo.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/foo.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/foo_test.ts b/std/testing/testdata/foo_test.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/foo_test.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/bar.js b/std/testing/testdata/subdir/bar.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/bar.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/bar_test.js b/std/testing/testdata/subdir/bar_test.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/bar_test.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/foo.ts b/std/testing/testdata/subdir/foo.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/foo.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/foo_test.ts b/std/testing/testdata/subdir/foo_test.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/foo_test.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/test.js b/std/testing/testdata/subdir/test.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/test.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/subdir/test.ts b/std/testing/testdata/subdir/test.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/subdir/test.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/test.js b/std/testing/testdata/test.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/test.js
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testdata/test.ts b/std/testing/testdata/test.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/std/testing/testdata/test.ts
@@ -0,0 +1 @@
+export {};
diff --git a/std/testing/testing_bench.ts b/std/testing/testing_bench.ts
new file mode 100644
index 000000000..9033e3a72
--- /dev/null
+++ b/std/testing/testing_bench.ts
@@ -0,0 +1,18 @@
+import { bench, runIfMain } from "./bench.ts";
+import { runTests } from "./mod.ts";
+
+import "./asserts_test.ts";
+
+bench(async function testingSerial(b): Promise<void> {
+ b.start();
+ await runTests();
+ b.stop();
+});
+
+bench(async function testingParallel(b): Promise<void> {
+ b.start();
+ await runTests({ parallel: true });
+ b.stop();
+});
+
+runIfMain(import.meta);