summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbokuweb <bokuweb@users.noreply.github.com>2019-02-16 01:11:55 +0900
committerRyan Dahl <ry@tinyclouds.org>2019-02-15 11:11:55 -0500
commit0eed9b30298e1ba83d8b21bad24ee77dff59942c (patch)
treefce6e4f43d573faf03e336cee5ad82e97e04e539
parent57f4e6a86448263c9f1c75934e829e048c76f572 (diff)
feat: Add pretty assert (denoland/deno_std#187)
Original: https://github.com/denoland/deno_std/commit/ddafcc6572b6574eb0566d650e5f9ca9f049a8d6
-rw-r--r--testing/diff.ts198
-rw-r--r--testing/diff_test.ts110
-rw-r--r--testing/format.ts529
-rw-r--r--testing/format_test.ts793
-rw-r--r--testing/pretty.ts75
-rw-r--r--testing/pretty_test.ts92
-rw-r--r--testing/test.ts3
7 files changed, 1800 insertions, 0 deletions
diff --git a/testing/diff.ts b/testing/diff.ts
new file mode 100644
index 000000000..a1385b88a
--- /dev/null
+++ b/testing/diff.ts
@@ -0,0 +1,198 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+interface FarthestPoint {
+ y: number;
+ id: number;
+}
+
+export type DiffType = "removed" | "common" | "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) {
+ 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[]): DiffResult<T>[] {
+ function backTrace<T>(
+ A: T[],
+ B: T[],
+ current: FarthestPoint,
+ swapped: boolean
+ ) {
+ 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 ? "removed" : "added") as DiffType,
+ value: B[b]
+ });
+ b -= 1;
+ } else if (type === ADDED) {
+ result.unshift({
+ type: (swapped ? "added" : "removed") as DiffType,
+ value: A[a]
+ });
+ a -= 1;
+ } else {
+ result.unshift({ type: "common" as DiffType, 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,
+ N: 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[]
+ ) {
+ const M = A.length;
+ const N = B.length;
+ if (k < -N || M < k) return { y: -1 };
+ const fp = createFP(slide, down, k, M, N);
+ 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;
+ }
+
+ 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 => ({ type: "common" as DiffType, value: c })),
+ ...A.map(a => ({
+ type: (swapped ? "added" : "removed") as DiffType,
+ value: a
+ })),
+ ...suffixCommon.map(c => ({ type: "common" as DiffType, 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;
+ 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 => ({ type: "common" as DiffType, value: c })),
+ ...backTrace(A, B, fp[delta + offset], swapped),
+ ...suffixCommon.map(c => ({ type: "common" as DiffType, value: c }))
+ ];
+}
diff --git a/testing/diff_test.ts b/testing/diff_test.ts
new file mode 100644
index 000000000..d2259ba35
--- /dev/null
+++ b/testing/diff_test.ts
@@ -0,0 +1,110 @@
+import diff from "./diff.ts";
+import { test, assertEqual } from "./mod.ts";
+
+test({
+ name: "empty",
+ fn() {
+ assertEqual(diff([], []), []);
+ }
+});
+
+test({
+ name: '"a" vs "b"',
+ fn() {
+ assertEqual(diff(["a"], ["b"]), [
+ { type: "removed", value: "a" },
+ { type: "added", value: "b" }
+ ]);
+ }
+});
+
+test({
+ name: '"a" vs "a"',
+ fn() {
+ assertEqual(diff(["a"], ["a"]), [{ type: "common", value: "a" }]);
+ }
+});
+
+test({
+ name: '"a" vs ""',
+ fn() {
+ assertEqual(diff(["a"], []), [{ type: "removed", value: "a" }]);
+ }
+});
+
+test({
+ name: '"" vs "a"',
+ fn() {
+ assertEqual(diff([], ["a"]), [{ type: "added", value: "a" }]);
+ }
+});
+
+test({
+ name: '"a" vs "a, b"',
+ fn() {
+ assertEqual(diff(["a"], ["a", "b"]), [
+ { type: "common", value: "a" },
+ { type: "added", value: "b" }
+ ]);
+ }
+});
+
+test({
+ name: '"strength" vs "string"',
+ fn() {
+ assertEqual(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() {
+ assertEqual(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() {
+ assertEqual(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() {
+ assertEqual(diff(["abc", "c"], ["abc", "bcd", "c"]), [
+ { type: "common", value: "abc" },
+ { type: "added", value: "bcd" },
+ { type: "common", value: "c" }
+ ]);
+ }
+});
diff --git a/testing/format.ts b/testing/format.ts
new file mode 100644
index 000000000..8434db1c2
--- /dev/null
+++ b/testing/format.ts
@@ -0,0 +1,529 @@
+// 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.
+ *
+ */
+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: any,
+ 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.
+ */
+const getConstructorName = (val: new (...args: any[]) => any) =>
+ (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 :) */
+const isWindow = (val: any) => 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(
+ val: any,
+ { printFunctionName, escapeRegex, escapeString }: BasicValueOptions
+): string | null {
+ if (val === true || val === false) {
+ return "" + 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;
+}
+
+/**
+ * Handles more complex objects ( such as objects with circular references.
+ * maps and sets etc )
+ */
+function printComplexValue(
+ 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) +
+ "}";
+}
+
+function printer(
+ val: any,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ hasCalledToJSON?: boolean
+): string {
+ const basicResult = printBasicValue(val, config);
+ if (basicResult !== null) {
+ return basicResult;
+ }
+ return printComplexValue(
+ val,
+ config,
+ indentation,
+ depth,
+ refs,
+ hasCalledToJSON
+ );
+}
+
+const getConfig = (options: Options): Config => ({
+ ...options,
+ indent: options.min ? "" : createIndent(options.indent),
+ spacingInner: options.min ? " " : "\n",
+ spacingOuter: options.min ? "" : "\n"
+});
+
+function createIndent(indent: number): string {
+ return new Array(indent + 1).join(" ");
+}
+
+const getKeysOfEnumerableProperties = (object: {}) => {
+ const keys: Array<string | symbol> = Object.keys(object).sort();
+
+ if (Object.getOwnPropertySymbols) {
+ Object.getOwnPropertySymbols(object).forEach(symbol => {
+ if (Object.getOwnPropertyDescriptor(object, symbol)!.enumerable) {
+ keys.push(symbol);
+ }
+ });
+ }
+
+ return keys;
+};
+
+/**
+ * Return entries (for example, of a map)
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (for example, braces)
+ */
+function printIteratorEntries(
+ 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 = ": "
+): 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(
+ 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;
+}
+
+/**
+ * Return items (for example, of an array)
+ * with spacing, indentation, and comma
+ * without surrounding punctuation (for example, brackets)
+ */
+function printListItems(
+ 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 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;
+}
+
+/**
+ * Returns a presentation string of your `val` object
+ * @param val any potential JavaScript object
+ * @param options Custom settings
+ */
+export function format(val: any, options: Optional<Options> = {}): string {
+ const opts = Object.keys(DEFAULT_OPTIONS).reduce(
+ (acc: Options, k: keyof Options) => {
+ const opt = options[k];
+ if (typeof opt === "undefined") {
+ return { ...acc, [k]: DEFAULT_OPTIONS[k] };
+ }
+ return { ...acc, [k]: opt };
+ },
+ {}
+ ) as Options;
+ const basicResult = printBasicValue(val, opts);
+ if (basicResult !== null) {
+ return basicResult;
+ }
+
+ return printComplexValue(val, getConfig(opts), "", 0, []);
+}
diff --git a/testing/format_test.ts b/testing/format_test.ts
new file mode 100644
index 000000000..a07426046
--- /dev/null
+++ b/testing/format_test.ts
@@ -0,0 +1,793 @@
+// 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, assertEqual } from "./mod.ts";
+import { format } from "./format.ts";
+
+function returnArguments(..._args: Array<unknown>) {
+ return arguments;
+}
+
+function MyObject(value: unknown) {
+ // @ts-ignore
+ this.name = value;
+}
+
+class MyArray<T> extends Array<T> {}
+
+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"
+ }
+];
+
+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() {
+ const val = returnArguments();
+ assertEqual(format(val), "Arguments []");
+ }
+});
+
+test({
+ name: "prints empty arguments",
+ fn() {
+ const val: any[] = [];
+ assertEqual(format(val), "Array []");
+ }
+});
+
+test({
+ name: "prints an array with items",
+ fn() {
+ const val = [1, 2, 3];
+ assertEqual(format(val), "Array [\n 1,\n 2,\n 3,\n]");
+ }
+});
+
+test({
+ name: "prints a empty typed array",
+ fn() {
+ const val = new Uint32Array(0);
+ assertEqual(format(val), "Uint32Array []");
+ }
+});
+
+test({
+ name: "prints a typed array with items",
+ fn() {
+ const val = new Uint32Array(3);
+ assertEqual(format(val), "Uint32Array [\n 0,\n 0,\n 0,\n]");
+ }
+});
+
+test({
+ name: "prints an array buffer",
+ fn() {
+ const val = new ArrayBuffer(3);
+ assertEqual(format(val), "ArrayBuffer []");
+ }
+});
+
+test({
+ name: "prints a nested array",
+ fn() {
+ const val = [[1, 2, 3]];
+ assertEqual(
+ format(val),
+ "Array [\n Array [\n 1,\n 2,\n 3,\n ],\n]"
+ );
+ }
+});
+
+test({
+ name: "prints true",
+ fn() {
+ const val = true;
+ assertEqual(format(val), "true");
+ }
+});
+
+test({
+ name: "prints false",
+ fn() {
+ const val = false;
+ assertEqual(format(val), "false");
+ }
+});
+
+test({
+ name: "prints an error",
+ fn() {
+ const val = new Error();
+ assertEqual(format(val), "[Error]");
+ }
+});
+
+test({
+ name: "prints a typed error with a message",
+ fn() {
+ const val = new TypeError("message");
+ assertEqual(format(val), "[TypeError: message]");
+ }
+});
+
+test({
+ name: "prints a function constructor",
+ fn() {
+ // tslint:disable-next-line:function-constructor
+ const val = new Function();
+ assertEqual(format(val), "[Function anonymous]");
+ }
+});
+
+test({
+ name: "prints an anonymous callback function",
+ fn() {
+ let val;
+ function f(cb: () => void) {
+ val = cb;
+ }
+ // tslint:disable-next-line:no-empty
+ f(() => {});
+ assertEqual(format(val), "[Function anonymous]");
+ }
+});
+
+test({
+ name: "prints an anonymous assigned function",
+ fn() {
+ // tslint:disable-next-line:no-empty
+ const val = () => {};
+ const formatted = format(val);
+ assertEqual(
+ formatted === "[Function anonymous]" || formatted === "[Function val]",
+ true
+ );
+ }
+});
+
+test({
+ name: "prints a named function",
+ fn() {
+ // tslint:disable-next-line:no-empty
+ const val = function named() {};
+ assertEqual(format(val), "[Function named]");
+ }
+});
+
+test({
+ name: "prints a named generator function",
+ fn() {
+ const val = function* generate() {
+ yield 1;
+ yield 2;
+ yield 3;
+ };
+ assertEqual(format(val), "[Function generate]");
+ }
+});
+
+test({
+ name: "can customize function names",
+ fn() {
+ // tslint:disable-next-line:no-empty
+ const val = function named() {};
+ assertEqual(
+ format(val, {
+ printFunctionName: false
+ }),
+ "[Function]"
+ );
+ }
+});
+
+test({
+ name: "prints Infinity",
+ fn() {
+ const val = Infinity;
+ assertEqual(format(val), "Infinity");
+ }
+});
+
+test({
+ name: "prints -Infinity",
+ fn() {
+ const val = -Infinity;
+ assertEqual(format(val), "-Infinity");
+ }
+});
+
+test({
+ name: "prints an empty map",
+ fn() {
+ const val = new Map();
+ assertEqual(format(val), "Map {}");
+ }
+});
+
+test({
+ name: "prints a map with values",
+ fn() {
+ const val = new Map();
+ val.set("prop1", "value1");
+ val.set("prop2", "value2");
+ assertEqual(
+ format(val),
+ 'Map {\n "prop1" => "value1",\n "prop2" => "value2",\n}'
+ );
+ }
+});
+
+test({
+ name: "prints a map with non-string keys",
+ fn() {
+ 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");
+ assertEqual(format(val), expected);
+ }
+});
+
+test({
+ name: "prints NaN",
+ fn() {
+ const val = NaN;
+ assertEqual(format(val), "NaN");
+ }
+});
+
+test({
+ name: "prints null",
+ fn() {
+ const val = null;
+ assertEqual(format(val), "null");
+ }
+});
+
+test({
+ name: "prints a positive number",
+ fn() {
+ const val = 123;
+ assertEqual(format(val), "123");
+ }
+});
+
+test({
+ name: "prints a negative number",
+ fn() {
+ const val = -123;
+ assertEqual(format(val), "-123");
+ }
+});
+
+test({
+ name: "prints zero",
+ fn() {
+ const val = 0;
+ assertEqual(format(val), "0");
+ }
+});
+
+test({
+ name: "prints negative zero",
+ fn() {
+ const val = -0;
+ assertEqual(format(val), "-0");
+ }
+});
+
+test({
+ name: "prints a date",
+ fn() {
+ const val = new Date(10e11);
+ assertEqual(format(val), "2001-09-09T01:46:40.000Z");
+ }
+});
+
+test({
+ name: "prints an invalid date",
+ fn() {
+ const val = new Date(Infinity);
+ assertEqual(format(val), "Date { NaN }");
+ }
+});
+
+test({
+ name: "prints an empty object",
+ fn() {
+ const val = {};
+ assertEqual(format(val), "Object {}");
+ }
+});
+
+test({
+ name: "prints an object with properties",
+ fn() {
+ const val = { prop1: "value1", prop2: "value2" };
+ assertEqual(
+ format(val),
+ 'Object {\n "prop1": "value1",\n "prop2": "value2",\n}'
+ );
+ }
+});
+
+test({
+ name: "prints an object with properties and symbols",
+ fn() {
+ const val: any = {};
+ val[Symbol("symbol1")] = "value2";
+ val[Symbol("symbol2")] = "value3";
+ val.prop = "value1";
+ assertEqual(
+ 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() {
+ const val: any = {
+ enumerable: true
+ };
+ const key = "non-enumerable";
+ Object.defineProperty(val, key, {
+ enumerable: false,
+ value: false
+ });
+ assertEqual(format(val), 'Object {\n "enumerable": true,\n}');
+ }
+});
+
+test({
+ name:
+ "prints an object without non-enumerable properties which have symbol key",
+ fn() {
+ const val: any = {
+ enumerable: true
+ };
+ const key = Symbol("non-enumerable");
+ Object.defineProperty(val, key, {
+ enumerable: false,
+ value: false
+ });
+ assertEqual(format(val), 'Object {\n "enumerable": true,\n}');
+ }
+});
+
+test({
+ name: "prints an object with sorted properties",
+ fn() {
+ const val = { b: 1, a: 2 };
+ assertEqual(format(val), 'Object {\n "a": 2,\n "b": 1,\n}');
+ }
+});
+
+test({
+ name: "prints regular expressions from constructors",
+ fn() {
+ const val = new RegExp("regexp");
+ assertEqual(format(val), "/regexp/");
+ }
+});
+
+test({
+ name: "prints regular expressions from literals",
+ fn() {
+ const val = /regexp/gi;
+ assertEqual(format(val), "/regexp/gi");
+ }
+});
+
+test({
+ name: "prints regular expressions {escapeRegex: false}",
+ fn() {
+ const val = /regexp\d/gi;
+ assertEqual(format(val), "/regexp\\d/gi");
+ }
+});
+
+test({
+ name: "prints regular expressions {escapeRegex: true}",
+ fn() {
+ const val = /regexp\d/gi;
+ assertEqual(format(val, { escapeRegex: true }), "/regexp\\\\d/gi");
+ }
+});
+
+test({
+ name: "escapes regular expressions nested inside object",
+ fn() {
+ const obj = { test: /regexp\d/gi };
+ assertEqual(
+ format(obj, { escapeRegex: true }),
+ 'Object {\n "test": /regexp\\\\d/gi,\n}'
+ );
+ }
+});
+
+test({
+ name: "prints an empty set",
+ fn() {
+ const val = new Set();
+ assertEqual(format(val), "Set {}");
+ }
+});
+
+test({
+ name: "prints a set with values",
+ fn() {
+ const val = new Set();
+ val.add("value1");
+ val.add("value2");
+ assertEqual(format(val), 'Set {\n "value1",\n "value2",\n}');
+ }
+});
+
+test({
+ name: "prints a string",
+ fn() {
+ const val = "string";
+ assertEqual(format(val), '"string"');
+ }
+});
+
+test({
+ name: "prints and escape a string",
+ fn() {
+ const val = "\"'\\";
+ assertEqual(format(val), '"\\"\'\\\\"');
+ }
+});
+
+test({
+ name: "doesn't escape string with {excapeString: false}",
+ fn() {
+ const val = "\"'\\n";
+ assertEqual(format(val, { escapeString: false }), '""\'\\n"');
+ }
+});
+
+test({
+ name: "prints a string with escapes",
+ fn() {
+ assertEqual(format('"-"'), '"\\"-\\""');
+ assertEqual(format("\\ \\\\"), '"\\\\ \\\\\\\\"');
+ }
+});
+
+test({
+ name: "prints a multiline string",
+ fn() {
+ const val = ["line 1", "line 2", "line 3"].join("\n");
+ assertEqual(format(val), '"' + val + '"');
+ }
+});
+
+test({
+ name: "prints a multiline string as value of object property",
+ fn() {
+ 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"
+ };
+ assertEqual(
+ 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() {
+ const val = Symbol("symbol");
+ assertEqual(format(val), "Symbol(symbol)");
+ }
+});
+
+test({
+ name: "prints undefined",
+ fn() {
+ const val = undefined;
+ assertEqual(format(val), "undefined");
+ }
+});
+
+test({
+ name: "prints a WeakMap",
+ fn() {
+ const val = new WeakMap();
+ assertEqual(format(val), "WeakMap {}");
+ }
+});
+
+test({
+ name: "prints a WeakSet",
+ fn() {
+ const val = new WeakSet();
+ assertEqual(format(val), "WeakSet {}");
+ }
+});
+
+test({
+ name: "prints deeply nested objects",
+ fn() {
+ const val = { prop: { prop: { prop: "value" } } };
+ assertEqual(
+ format(val),
+ 'Object {\n "prop": Object {\n "prop": Object {\n "prop": "value",\n },\n },\n}'
+ );
+ }
+});
+
+test({
+ name: "prints circular references",
+ fn() {
+ const val: any = {};
+ val.prop = val;
+ assertEqual(format(val), 'Object {\n "prop": [Circular],\n}');
+ }
+});
+
+test({
+ name: "prints parallel references",
+ fn() {
+ const inner = {};
+ const val = { prop1: inner, prop2: inner };
+ assertEqual(
+ format(val),
+ 'Object {\n "prop1": Object {},\n "prop2": Object {},\n}'
+ );
+ }
+});
+
+test({
+ name: "default implicit: 2 spaces",
+ fn() {
+ assertEqual(format(createVal()), createExpected());
+ }
+});
+
+test({
+ name: "default explicit: 2 spaces",
+ fn() {
+ assertEqual(format(createVal(), { indent: 2 }), createExpected());
+ }
+});
+
+// Tests assume that no strings in val contain multiple adjacent spaces!
+test({
+ name: "non-default: 0 spaces",
+ fn() {
+ const indent = 0;
+ assertEqual(
+ format(createVal(), { indent }),
+ createExpected().replace(/ {2}/g, " ".repeat(indent))
+ );
+ }
+});
+
+test({
+ name: "non-default: 4 spaces",
+ fn() {
+ const indent = 4;
+ assertEqual(
+ format(createVal(), { indent }),
+ createExpected().replace(/ {2}/g, " ".repeat(indent))
+ );
+ }
+});
+
+test({
+ name: "can customize the max depth",
+ fn() {
+ 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"])
+ }
+ ];
+ assertEqual(
+ 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() {
+ assertEqual(format(Object.create(null)), "Object {}");
+ }
+});
+
+test({
+ name: "prints identity-obj-proxy with string constructor",
+ fn() {
+ const obj = Object.create(null);
+ obj.constructor = "constructor";
+ const expected = [
+ "Object {", // Object instead of undefined
+ ' "constructor": "constructor",',
+ "}"
+ ].join("\n");
+ assertEqual(format(obj), expected);
+ }
+});
+
+test({
+ name: "calls toJSON and prints its return value",
+ fn() {
+ assertEqual(
+ format({
+ toJSON: () => ({ value: false }),
+ value: true
+ }),
+ 'Object {\n "value": false,\n}'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON and prints an internal representation.",
+ fn() {
+ assertEqual(
+ format({
+ toJSON: () => "[Internal Object]",
+ value: true
+ }),
+ '"[Internal Object]"'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON only on functions",
+ fn() {
+ assertEqual(
+ format({
+ toJSON: false,
+ value: true
+ }),
+ 'Object {\n "toJSON": false,\n "value": true,\n}'
+ );
+ }
+});
+
+test({
+ name: "does not call toJSON recursively",
+ fn() {
+ assertEqual(
+ format({
+ toJSON: () => ({ toJSON: () => ({ value: true }) }),
+ value: false
+ }),
+ 'Object {\n "toJSON": [Function toJSON],\n}'
+ );
+ }
+});
+
+test({
+ name: "calls toJSON on Sets",
+ fn() {
+ const set = new Set([1]);
+ (set as any).toJSON = () => "map";
+ assertEqual(format(set), '"map"');
+ }
+});
diff --git a/testing/pretty.ts b/testing/pretty.ts
new file mode 100644
index 000000000..aa90f2469
--- /dev/null
+++ b/testing/pretty.ts
@@ -0,0 +1,75 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import { equal } from "./mod.ts";
+import { red, green, white, gray, bold } from "../colors/mod.ts";
+import diff, { DiffType, DiffResult } from "./diff.ts";
+import { format } from "./format.ts";
+
+const CAN_NOT_DISPLAY = "[Cannot display]";
+
+function createStr(v: unknown): string {
+ try {
+ return format(v);
+ } catch (e) {
+ return red(CAN_NOT_DISPLAY);
+ }
+}
+
+function createColor(diffType: DiffType) {
+ switch (diffType) {
+ case "added":
+ return (s: string) => green(bold(s));
+ case "removed":
+ return (s: string) => red(bold(s));
+ default:
+ return white;
+ }
+}
+
+function createSign(diffType: DiffType) {
+ switch (diffType) {
+ case "added":
+ return "+ ";
+ case "removed":
+ return "- ";
+ default:
+ return " ";
+ }
+}
+
+function buildMessage(diffResult: ReadonlyArray<DiffResult<string>>) {
+ const messages = [];
+ messages.push("");
+ messages.push("");
+ messages.push(
+ ` ${gray(bold("[Diff]"))} ${red(bold("Left"))} / ${green(bold("Right"))}`
+ );
+ messages.push("");
+ messages.push("");
+ diffResult.forEach((result: DiffResult<string>) => {
+ const c = createColor(result.type);
+ messages.push(c(`${createSign(result.type)}${result.value}`));
+ });
+ messages.push("");
+
+ return messages;
+}
+
+export function assertEqual(actual: unknown, expected: unknown) {
+ 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`;
+ }
+ throw new Error(message);
+}
diff --git a/testing/pretty_test.ts b/testing/pretty_test.ts
new file mode 100644
index 000000000..f3b087aff
--- /dev/null
+++ b/testing/pretty_test.ts
@@ -0,0 +1,92 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import { test, assert } from "./mod.ts";
+import { red, green, white, gray, bold } from "../colors/mod.ts";
+import { assertEqual } from "./pretty.ts";
+
+const createHeader = () => [
+ "",
+ "",
+ ` ${gray(bold("[Diff]"))} ${red(bold("Left"))} / ${green(bold("Right"))}`,
+ "",
+ ""
+];
+
+const added = (s: string) => green(bold(s));
+const removed = (s: string) => red(bold(s));
+
+test({
+ name: "pass case",
+ fn() {
+ assertEqual({ a: 10 }, { a: 10 });
+ assertEqual(true, true);
+ assertEqual(10, 10);
+ assertEqual("abc", "abc");
+ assertEqual({ a: 10, b: { c: "1" } }, { a: 10, b: { c: "1" } });
+ }
+});
+
+test({
+ name: "failed with number",
+ fn() {
+ assert.throws(
+ () => assertEqual(1, 2),
+ Error,
+ [...createHeader(), removed(`- 1`), added(`+ 2`), ""].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with number vs string",
+ fn() {
+ assert.throws(
+ () => assertEqual(1, "1"),
+ Error,
+ [...createHeader(), removed(`- 1`), added(`+ "1"`)].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with array",
+ fn() {
+ assert.throws(
+ () => assertEqual([1, "2", 3], ["1", "2", 3]),
+ Error,
+ [
+ ...createHeader(),
+ white(" Array ["),
+ removed(`- 1,`),
+ added(`+ "1",`),
+ white(' "2",'),
+ white(" 3,"),
+ white(" ]"),
+ ""
+ ].join("\n")
+ );
+ }
+});
+
+test({
+ name: "failed with object",
+ fn() {
+ assert.throws(
+ () => assertEqual({ a: 1, b: "2", c: 3 }, { a: 1, b: 2, c: [3] }),
+ Error,
+ [
+ ...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/testing/test.ts b/testing/test.ts
index a27ffb263..8c780fdf1 100644
--- a/testing/test.ts
+++ b/testing/test.ts
@@ -1,6 +1,9 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test, assert, assertEqual, equal } from "./mod.ts";
+import "./format_test.ts";
+import "./diff_test.ts";
+import "./pretty_test.ts";
test(function testingEqual() {
assert(equal("world", "world"));