test(internal): improve test coverage (#4779)

* test(internal): improve test coverage

* test(internal): improve test coverage

* test(internal): implement review remarks

* test(internal): implement review remarks
This commit is contained in:
Michael Herzner 2024-05-21 14:54:22 +02:00 committed by GitHub
parent edfb539dad
commit a5bc643e8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 445 additions and 49 deletions

View File

@ -11,16 +11,26 @@ import type { DiffResult, DiffType } from "./types.ts";
* @param background If true, colors the background instead of the text. * @param background If true, colors the background instead of the text.
* *
* @returns A function that colors the input string. * @returns A function that colors the input string.
*
* @example Usage
* ```ts
* import { createColor } from "@std/internal";
* import { assertEquals } from "@std/assert/assert-equals";
* import { bold, green, red, white } from "@std/fmt/colors";
*
* assertEquals(createColor("added")("foo"), green(bold("foo")));
* assertEquals(createColor("removed")("foo"), red(bold("foo")));
* assertEquals(createColor("common")("foo"), white("foo"));
* ```
*/ */
function createColor( export function createColor(
diffType: DiffType, diffType: DiffType,
background = false,
): (s: string) => string {
/** /**
* TODO(@littledivy): Remove this when we can detect true color terminals. See * TODO(@littledivy): Remove this when we can detect true color terminals. See
* https://github.com/denoland/deno_std/issues/2575. * https://github.com/denoland/deno_std/issues/2575.
*/ */
background = false; background = false,
): (s: string) => string {
switch (diffType) { switch (diffType) {
case "added": case "added":
return (s) => background ? bgGreen(white(s)) : green(bold(s)); return (s) => background ? bgGreen(white(s)) : green(bold(s));
@ -37,8 +47,18 @@ function createColor(
* @param diffType Difference type, either added or removed * @param diffType Difference type, either added or removed
* *
* @returns A string representing the sign. * @returns A string representing the sign.
*
* @example Usage
* ```ts
* import { createSign } from "@std/internal";
* import { assertEquals } from "@std/assert/assert-equals";
*
* assertEquals(createSign("added"), "+ ");
* assertEquals(createSign("removed"), "- ");
* assertEquals(createSign("common"), " ");
* ```
*/ */
function createSign(diffType: DiffType): string { export function createSign(diffType: DiffType): string {
switch (diffType) { switch (diffType) {
case "added": case "added":
return "+ "; return "+ ";

View File

@ -0,0 +1,41 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "@std/assert";
import { bgGreen, bgRed, bold, gray, green, red, white } from "@std/fmt/colors";
import { buildMessage, createColor, createSign } from "./build_message.ts";
Deno.test("buildMessage()", () => {
const messages = [
"",
"",
` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${
green(bold("Expected"))
}`,
"",
"",
];
assertEquals(buildMessage([]), [...messages, ""]);
assertEquals(
buildMessage([{ type: "added", value: "foo" }, {
type: "removed",
value: "bar",
}]),
[...messages, green(bold("+ foo")), red(bold("- bar")), ""],
);
});
Deno.test("createColor()", () => {
assertEquals(createColor("added")("foo"), green(bold("foo")));
assertEquals(createColor("removed")("foo"), red(bold("foo")));
assertEquals(createColor("common")("foo"), white("foo"));
assertEquals(createColor("added", true)("foo"), bgGreen(white("foo")));
assertEquals(createColor("removed", true)("foo"), bgRed(white("foo")));
assertEquals(createColor("common", true)("foo"), white("foo"));
});
Deno.test("createSign()", () => {
assertEquals(createSign("added"), "+ ");
assertEquals(createSign("removed"), "- ");
assertEquals(createSign("common"), " ");
// deno-lint-ignore no-explicit-any
assertEquals(createSign("unknown" as any), " ");
});

View File

@ -3,8 +3,11 @@
import type { DiffResult, DiffType } from "./types.ts"; import type { DiffResult, DiffType } from "./types.ts";
interface FarthestPoint { /** Represents the farthest point in the diff algorithm. */
export interface FarthestPoint {
/** The y-coordinate of the point. */
y: number; y: number;
/** The id of the point. */
id: number; id: number;
} }
@ -21,8 +24,19 @@ const ADDED = 3;
* @param B The second array. * @param B The second array.
* *
* @returns An array containing the common elements between the two arrays. * @returns An array containing the common elements between the two arrays.
*
* @example Usage
* ```ts
* import { createCommon } from "@std/internal/diff";
* import { assertEquals } from "@std/assert/assert-equals";
*
* const a = [1, 2, 3];
* const b = [1, 2, 4];
*
* assertEquals(createCommon(a, b), [1, 2]);
* ```
*/ */
function createCommon<T>(A: T[], B: T[]): T[] { export function createCommon<T>(A: T[], B: T[]): T[] {
const common: T[] = []; const common: T[] = [];
if (A.length === 0 || B.length === 0) return []; if (A.length === 0 || B.length === 0) return [];
for (let i = 0; i < Math.min(A.length, B.length); i += 1) { for (let i = 0; i < Math.min(A.length, B.length); i += 1) {
@ -37,13 +51,62 @@ function createCommon<T>(A: T[], B: T[]): T[] {
return common; return common;
} }
function assertFp(value: unknown): asserts value is FarthestPoint { /**
if (value === undefined) { * Asserts that the value is a {@linkcode FarthestPoint}.
* If not, an error is thrown.
*
* @param value The value to check.
*
* @returns A void value that returns once the assertion completes.
*
* @example Usage
* ```ts
* import { assertFp } from "@std/internal/diff";
* import { assertThrows } from "@std/assert/assert-throws";
*
* assertFp({ y: 0, id: 0 });
* assertThrows(() => assertFp({ id: 0 }));
* assertThrows(() => assertFp({ y: 0 }));
* assertThrows(() => assertFp(undefined));
* ```
*/
export function assertFp(value: unknown): asserts value is FarthestPoint {
if (
value == null ||
typeof value !== "object" ||
typeof (value as FarthestPoint)?.y !== "number" ||
typeof (value as FarthestPoint)?.id !== "number"
) {
throw new Error("Unexpected missing FarthestPoint"); throw new Error("Unexpected missing FarthestPoint");
} }
} }
function backTrace<T>( /**
* Creates an array of backtraced differences.
*
* @typeParam T The type of elements in the arrays.
*
* @param A The first array.
* @param B The second array.
* @param current The current {@linkcode FarthestPoint}.
* @param swapped Boolean indicating if the arrays are swapped.
* @param routes The routes array.
* @param diffTypesPtrOffset The offset of the diff types in the routes array.
*
* @returns An array of backtraced differences.
*
* @example Usage
* ```ts
* import { backTrace } from "@std/internal/diff";
* import { assertEquals } from "@std/assert/assert-equals";
*
* assertEquals(
* backTrace([], [], { y: 0, id: 0 }, false, new Uint32Array(0), 0),
* [],
* );
* ```
*/
export function backTrace<T>(
A: T[], A: T[],
B: T[], B: T[],
current: FarthestPoint, current: FarthestPoint,
@ -87,7 +150,39 @@ function backTrace<T>(
return result; return result;
} }
function createFp( /**
* Creates a {@linkcode FarthestPoint}.
*
* @param k The current index.
* @param M The length of the first array.
* @param routes The routes array.
* @param diffTypesPtrOffset The offset of the diff types in the routes array.
* @param ptr The current pointer.
* @param slide The slide {@linkcode FarthestPoint}.
* @param down The down {@linkcode FarthestPoint}.
*
* @returns A {@linkcode FarthestPoint}.
*
* @example Usage
* ```ts
* import { createFp } from "@std/internal/diff";
* import { assertEquals } from "@std/assert/assert-equals";
*
* assertEquals(
* createFp(
* 0,
* 0,
* new Uint32Array(0),
* 0,
* 0,
* { y: -1, id: 0 },
* { y: 0, id: 0 },
* ),
* { y: -1, id: 1 },
* );
* ```
*/
export function createFp(
k: number, k: number,
M: number, M: number,
routes: Uint32Array, routes: Uint32Array,
@ -147,22 +242,17 @@ function createFp(
*/ */
export function diff<T>(A: T[], B: T[]): DiffResult<T>[] { export function diff<T>(A: T[], B: T[]): DiffResult<T>[] {
const prefixCommon = createCommon(A, B); const prefixCommon = createCommon(A, B);
const suffixCommon = createCommon( A = A.slice(prefixCommon.length);
A.slice(prefixCommon.length), B = B.slice(prefixCommon.length);
B.slice(prefixCommon.length),
);
A = A.slice(prefixCommon.length, -suffixCommon.length || undefined);
B = B.slice(prefixCommon.length, -suffixCommon.length || undefined);
const swapped = B.length > A.length; const swapped = B.length > A.length;
[A, B] = swapped ? [B, A] : [A, B]; [A, B] = swapped ? [B, A] : [A, B];
const M = A.length; const M = A.length;
const N = B.length; const N = B.length;
if (!M && !N && !suffixCommon.length && !prefixCommon.length) return []; if (!M && !N && !prefixCommon.length) return [];
if (!N) { if (!N) {
return [ return [
...prefixCommon.map((value) => ({ type: "common", value })), ...prefixCommon.map((value) => ({ type: "common", value })),
...A.map((value) => ({ type: swapped ? "added" : "removed", value })), ...A.map((value) => ({ type: swapped ? "added" : "removed", value })),
...suffixCommon.map((value) => ({ type: "common", value })),
] as DiffResult<T>[]; ] as DiffResult<T>[];
} }
const offset = N; const offset = N;
@ -187,7 +277,6 @@ export function diff<T>(A: T[], B: T[]): DiffResult<T>[] {
): FarthestPoint { ): FarthestPoint {
const M = A.length; const M = A.length;
const N = B.length; const N = B.length;
if (k < -N || M < k) return { y: -1, id: -1 };
const fp = createFp(k, M, routes, diffTypesPtrOffset, ptr, slide, down); const fp = createFp(k, M, routes, diffTypesPtrOffset, ptr, slide, down);
ptr = fp.id; ptr = fp.id;
while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) { while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) {
@ -222,6 +311,5 @@ export function diff<T>(A: T[], B: T[]): DiffResult<T>[] {
return [ return [
...prefixCommon.map((value) => ({ type: "common", value })), ...prefixCommon.map((value) => ({ type: "common", value })),
...backTrace(A, B, currentFp, swapped, routes, diffTypesPtrOffset), ...backTrace(A, B, currentFp, swapped, routes, diffTypesPtrOffset),
...suffixCommon.map((value) => ({ type: "common", value })),
] as DiffResult<T>[]; ] as DiffResult<T>[];
} }

View File

@ -10,8 +10,16 @@ import { diff } from "./diff.ts";
* @param string String to unescape. * @param string String to unescape.
* *
* @returns Unescaped string. * @returns Unescaped string.
*
* @example Usage
* ```ts
* import { unescape } from "@std/internal/diff-str";
* import { assertEquals } from "@std/assert/assert-equals";
*
* assertEquals(unescape("Hello\nWorld"), "Hello\\n\nWorld");
* ```
*/ */
function unescape(string: string): string { export function unescape(string: string): string {
return string return string
.replaceAll("\b", "\\b") .replaceAll("\b", "\\b")
.replaceAll("\f", "\\f") .replaceAll("\f", "\\f")
@ -25,8 +33,6 @@ function unescape(string: string): string {
} }
const WHITESPACE_SYMBOLS = /([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/; const WHITESPACE_SYMBOLS = /([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/;
const EXT_LATIN_CHARS =
/^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u;
/** /**
* Tokenizes a string into an array of tokens. * Tokenizes a string into an array of tokens.
@ -35,26 +41,20 @@ const EXT_LATIN_CHARS =
* @param wordDiff If true, performs word-based tokenization. Default is false. * @param wordDiff If true, performs word-based tokenization. Default is false.
* *
* @returns An array of tokens. * @returns An array of tokens.
*
* @example Usage
* ```ts
* import { tokenize } from "@std/internal/diff-str";
* import { assertEquals } from "@std/assert/assert-equals";
*
* assertEquals(tokenize("Hello\nWorld"), ["Hello\n", "World"]);
* ```
*/ */
function tokenize(string: string, wordDiff = false): string[] { export function tokenize(string: string, wordDiff = false): string[] {
if (wordDiff) { if (wordDiff) {
const tokens = string.split(WHITESPACE_SYMBOLS).filter((token) => token); return string
for (let i = 0; i < tokens.length - 1; i++) { .split(WHITESPACE_SYMBOLS)
const token = tokens[i]; .filter((token) => token);
const tokenPlusTwo = tokens[i + 2];
if (
!tokens[i + 1] &&
token &&
tokenPlusTwo &&
EXT_LATIN_CHARS.test(token) &&
EXT_LATIN_CHARS.test(tokenPlusTwo)
) {
tokens[i] += tokenPlusTwo;
tokens.splice(i + 1, 2);
i--;
}
}
return tokens;
} }
const tokens: string[] = []; const tokens: string[] = [];
const lines = string.split(/(\n|\r\n)/).filter((line) => line); const lines = string.split(/(\n|\r\n)/).filter((line) => line);
@ -77,11 +77,27 @@ function tokenize(string: string, wordDiff = false): string[] {
* @param tokens Word-diff tokens * @param tokens Word-diff tokens
* *
* @returns Array of diff results. * @returns Array of diff results.
*
* @example Usage
* ```ts
* import { createDetails } from "@std/internal/diff-str";
* import { assertEquals } from "@std/assert/assert-equals";
*
* const tokens = [
* { type: "added", value: "a" },
* { type: "removed", value: "b" },
* { type: "common", value: "c" },
* ] as const;
* assertEquals(
* createDetails({ type: "added", value: "a" }, [...tokens]),
* [{ type: "added", value: "a" }, { type: "common", value: "c" }]
* );
* ```
*/ */
function createDetails( export function createDetails(
line: DiffResult<string>, line: DiffResult<string>,
tokens: Array<DiffResult<string>>, tokens: DiffResult<string>[],
) { ): DiffResult<string>[] {
return tokens.filter(({ type }) => type === line.type || type === "common") return tokens.filter(({ type }) => type === line.type || type === "common")
.map((result, i, t) => { .map((result, i, t) => {
const token = t[i - 1]; const token = t[i - 1];
@ -163,7 +179,7 @@ export function diffStr(A: string, B: string): DiffResult<string>[] {
b = bLines.shift(); b = bLines.shift();
const tokenized = [ const tokenized = [
tokenize(a.value, true), tokenize(a.value, true),
tokenize(b?.value ?? "", true), tokenize(b!.value, true),
] as [string[], string[]]; ] as [string[], string[]];
if (hasMoreRemovedLines) tokenized.reverse(); if (hasMoreRemovedLines) tokenized.reverse();
tokens = diff(tokenized[0], tokenized[1]); tokens = diff(tokenized[0], tokenized[1]);

View File

@ -1,8 +1,33 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { diffStr } from "./diff_str.ts"; import { createDetails, diffStr, tokenize, unescape } from "./diff_str.ts";
import { assertEquals } from "@std/assert/assert-equals"; import { assertEquals } from "@std/assert/assert-equals";
Deno.test({
name: 'diff() "a" vs "b" (diffstr)',
fn() {
const diffResult = diffStr("a", "b");
assertEquals(diffResult, [
{
details: [
{ type: "removed", value: "a" },
{ type: "common", value: "\n" },
],
type: "removed",
value: "a\n",
},
{
details: [
{ type: "added", value: "b" },
{ type: "common", value: "\n" },
],
type: "added",
value: "b\n",
},
]);
},
});
Deno.test({ Deno.test({
name: 'diff() "a b c d" vs "a b x d e" (diffStr)', name: 'diff() "a b c d" vs "a b x d e" (diffStr)',
fn() { fn() {
@ -236,3 +261,47 @@ Deno.test({
]); ]);
}, },
}); });
Deno.test({
name: "createDetails()",
fn() {
const tokens = [
{ type: "added", value: "a" },
{ type: "removed", value: "b" },
{ type: "common", value: "c" },
] as const;
for (const token of tokens) {
assertEquals(
createDetails(token, [...tokens]),
tokens.filter(({ type }) => type === token.type || type === "common"),
);
}
},
});
Deno.test({
name: "tokenize()",
fn() {
assertEquals(tokenize("a\nb"), ["a\n", "b"]);
assertEquals(tokenize("a\r\nb"), ["a\r\n", "b"]);
assertEquals(tokenize("a\nb\n"), ["a\n", "b\n"]);
assertEquals(tokenize("a b"), ["a b"]);
assertEquals(tokenize("a b", true), ["a", " ", "b"]);
assertEquals(tokenize("abc bcd", true), ["abc", " ", "bcd"]);
assertEquals(tokenize("abc ", true), ["abc", " "]);
},
});
Deno.test({
name: "unescape()",
fn() {
assertEquals(unescape("Hello\nWorld"), "Hello\\n\nWorld");
assertEquals(unescape("a\b"), "a\\b");
assertEquals(unescape("a\f"), "a\\f");
assertEquals(unescape("a\t"), "a\\t");
assertEquals(unescape("a\v"), "a\\v");
assertEquals(unescape("a\r"), "a\\r");
assertEquals(unescape("a\n"), "a\\n\n");
assertEquals(unescape("a\r\n"), "a\\r\\n\r\n");
},
});

View File

@ -1,7 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { diff } from "./diff.ts"; import { assertFp, backTrace, createCommon, createFp, diff } from "./diff.ts";
import { assertEquals } from "@std/assert/assert-equals"; import { assertEquals, assertThrows } from "@std/assert";
Deno.test({ Deno.test({
name: "diff() with empty values", name: "diff() with empty values",
@ -51,6 +51,18 @@ Deno.test({
}, },
}); });
Deno.test({
name: 'diff() "a, x, c" vs "a, b, c"',
fn() {
assertEquals(diff(["a", "x", "c"], ["a", "b", "c"]), [
{ type: "common", value: "a" },
{ type: "removed", value: "x" },
{ type: "added", value: "b" },
{ type: "common", value: "c" },
]);
},
});
Deno.test({ Deno.test({
name: 'diff() "strength" vs "string"', name: 'diff() "strength" vs "string"',
fn() { fn() {
@ -110,3 +122,113 @@ Deno.test({
]); ]);
}, },
}); });
Deno.test({
name: "assertFp()",
fn() {
const fp = { y: 0, id: 0 };
assertEquals(assertFp(fp), undefined);
},
});
Deno.test({
name: "assertFp() throws",
fn() {
const error = "Unexpected missing FarthestPoint";
assertThrows(() => assertFp({ id: 0 }), Error, error);
assertThrows(() => assertFp({ y: 0 }), Error, error);
assertThrows(() => assertFp(undefined), Error, error);
assertThrows(() => assertFp(null), Error, error);
},
});
Deno.test({
name: "backTrace()",
fn() {
assertEquals(
backTrace([], [], { y: 0, id: 0 }, false, new Uint32Array(0), 0),
[],
);
assertEquals(
backTrace(["a"], ["b"], { y: 1, id: 3 }, false, new Uint32Array(10), 5),
[],
);
},
});
Deno.test({
name: "createCommon()",
fn() {
assertEquals(createCommon([], []), []);
assertEquals(createCommon([1], []), []);
assertEquals(createCommon([], [1]), []);
assertEquals(createCommon([1], [1]), [1]);
assertEquals(createCommon([1, 2], [1]), [1]);
assertEquals(createCommon([1], [1, 2]), [1]);
},
});
Deno.test({
name: "createFp()",
fn() {
assertEquals(
createFp(
0,
0,
new Uint32Array(0),
0,
0,
{ y: -1, id: 0 },
{ y: -1, id: 0 },
),
{ y: 0, id: 0 },
);
},
});
Deno.test({
name: 'createFp() "isAdding"',
fn() {
assertEquals(
createFp(
0,
0,
new Uint32Array(0),
0,
0,
{ y: 0, id: 0 },
{ y: -1, id: 0 },
),
{ y: 0, id: 1 },
);
},
});
Deno.test({
name: 'createFp() "!isAdding"',
fn() {
assertEquals(
createFp(
0,
0,
new Uint32Array(0),
0,
0,
{ y: -1, id: 0 },
{ y: 0, id: 0 },
),
{ y: -1, id: 1 },
);
},
});
Deno.test({
name: "createFp() throws",
fn() {
assertThrows(
() => createFp(0, 0, new Uint32Array(0), 0, 0),
Error,
"Unexpected missing FarthestPoint",
);
},
});

View File

@ -96,3 +96,16 @@ Deno.test("format() doesn't truncate long strings in object", () => {
}`, }`,
); );
}); });
Deno.test("format() has fallback to String if Deno.inspect is not available", () => {
// Simulates the environment where Deno.inspect is not available
const inspect = Deno.inspect;
// deno-lint-ignore no-explicit-any
delete (Deno as any).inspect;
try {
assertEquals(format([..."abcd"]), `"a,b,c,d"`);
assertEquals(format({ a: 1, b: 2 }), `"[object Object]"`);
} finally {
Deno.inspect = inspect;
}
});

View File

@ -51,3 +51,30 @@ Deno.test("stripAnsiCode()", function () {
"foofoo", "foofoo",
); );
}); });
Deno.test("noColor", async function () {
const fixtures = [
["true", "foo bar\n"],
["1", "foo bar\n"],
["", "foo bar\n"],
] as const;
const code = `
import * as c from "${import.meta.resolve("./styles.ts")}";
console.log(c.red("foo bar"));
`;
for await (const [fixture, expected] of fixtures) {
const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "--no-lock", code],
clearEnv: true,
env: {
NO_COLOR: fixture,
},
});
const { stdout } = await command.output();
const decoder = new TextDecoder();
const output = decoder.decode(stdout);
assertEquals(output, expected);
}
});