diff --git a/testing/diff.ts b/testing/diff.ts new file mode 100644 index 0000000000..a1385b88a0 --- /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 { + type: DiffType; + value: T; +} + +const REMOVED = 1; +const COMMON = 2; +const ADDED = 3; + +function createCommon(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(A: T[], B: T[]): DiffResult[] { + function backTrace( + 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( + 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 0000000000..d2259ba35c --- /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 0000000000..8434db1c2d --- /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 = { [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 = 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, + 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 = {}): 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 0000000000..a07426046c --- /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) { + return arguments; +} + +function MyObject(value: unknown) { + // @ts-ignore + this.name = value; +} + +class MyArray extends Array {} + +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([ + [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 0000000000..aa90f24693 --- /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>) { + 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) => { + 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 0000000000..f3b087aff9 --- /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 a27ffb2630..8c780fdf15 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"));