diff --git a/assert/deno.json b/assert/deno.json index 2bc3dd3922d9..4360de6d5b0b 100644 --- a/assert/deno.json +++ b/assert/deno.json @@ -7,6 +7,7 @@ "./almost-equals": "./almost_equals.ts", "./array-includes": "./array_includes.ts", "./equals": "./equals.ts", + "./unstable-equals": "./unstable_equals.ts", "./exists": "./exists.ts", "./false": "./false.ts", "./greater": "./greater.ts", @@ -24,6 +25,7 @@ "./object-match": "./object_match.ts", "./rejects": "./rejects.ts", "./strict-equals": "./strict_equals.ts", + "./unstable-strict-equals": "./unstable_strict_equals.ts", "./string-includes": "./string_includes.ts", "./throws": "./throws.ts", "./assertion-error": "./assertion_error.ts", diff --git a/assert/equals.ts b/assert/equals.ts index f0e355590a5b..4ca43875ab09 100644 --- a/assert/equals.ts +++ b/assert/equals.ts @@ -59,7 +59,8 @@ export function assertEquals( const diffResult = stringDiff ? diffStr(actual as string, expected as string) : diff(actualString.split("\n"), expectedString.split("\n")); - const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); + const diffMsg = buildMessage(diffResult, { stringDiff }, arguments[3]) + .join("\n"); message = `${message}\n${diffMsg}`; throw new AssertionError(message); } diff --git a/assert/strict_equals.ts b/assert/strict_equals.ts index 197a5c708d88..724b7a2fe378 100644 --- a/assert/strict_equals.ts +++ b/assert/strict_equals.ts @@ -59,7 +59,8 @@ export function assertStrictEquals( const diffResult = stringDiff ? diffStr(actual as string, expected as string) : diff(actualString.split("\n"), expectedString.split("\n")); - const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); + const diffMsg = buildMessage(diffResult, { stringDiff }, arguments[3]) + .join("\n"); message = `Values are not strictly equal${msgSuffix}\n${diffMsg}`; } diff --git a/assert/unstable_equals.ts b/assert/unstable_equals.ts new file mode 100644 index 000000000000..e76f087926de --- /dev/null +++ b/assert/unstable_equals.ts @@ -0,0 +1,56 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { assertEquals as _assertEquals } from "./equals.ts"; +import { truncateDiff } from "@std/internal/truncate-build-message"; +// deno-lint-ignore no-unused-vars +import type { DIFF_CONTEXT_LENGTH } from "@std/internal/truncate-build-message"; + +/** + * Make an assertion that `actual` and `expected` are equal, deeply. If not + * deeply equal, then throw. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * Type parameter can be specified to ensure values under comparison have the + * same type. + * + * Note: When comparing `Blob` objects, you should first convert them to + * `Uint8Array` using the `Blob.bytes()` method and then compare their + * contents. + * + * The {@linkcode DIFF_CONTEXT_LENGTH} environment variable can be set to + * enable truncation of long diffs, in which case its value should be a + * positive integer representing the number of unchanged context lines to show + * around each changed part of the diff. By default, diffs are not truncated. + * + * @example Usage + * ```ts ignore + * import { assertEquals } from "@std/assert"; + * + * assertEquals("world", "world"); // Doesn't throw + * assertEquals("hello", "world"); // Throws + * ``` + * @example Compare `Blob` objects + * ```ts ignore + * import { assertEquals } from "@std/assert"; + * + * const bytes1 = await new Blob(["foo"]).bytes(); + * const bytes2 = await new Blob(["foo"]).bytes(); + * + * assertEquals(bytes1, bytes2); + * ``` + * + * @typeParam T The type of the values to compare. This is usually inferred. + * @param actual The actual value to compare. + * @param expected The expected value to compare. + * @param msg The optional message to display if the assertion fails. + */ +export function assertEquals( + actual: T, + expected: T, + msg?: string, +) { + const args: Parameters = [actual, expected, msg]; + // @ts-expect-error extra arg + _assertEquals(...args, truncateDiff); +} diff --git a/assert/unstable_equals_test.ts b/assert/unstable_equals_test.ts new file mode 100644 index 000000000000..ea7cbc502dca --- /dev/null +++ b/assert/unstable_equals_test.ts @@ -0,0 +1,257 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { AssertionError, assertThrows } from "./mod.ts"; +import { assertEquals } from "./unstable_equals.ts"; +import { DIFF_CONTEXT_LENGTH } from "@std/internal/truncate-build-message"; +import { stripAnsiCode } from "@std/internal/styles"; +import { dedent } from "@std/text/unstable-dedent"; +import { stub } from "@std/testing/mock"; +import { disposableStack } from "../internal/_testing.ts"; + +function assertDiffMessage(a: unknown, b: unknown, expected: string) { + const err = assertThrows(() => assertEquals(a, b), AssertionError); + // TODO(lionel-rowe): re-spell `fullExpectedMessage` indentation once https://github.com/denoland/std/issues/6830 + // is fixed + const fullExpectedMessage = dedent` + Values are not equal. + + + [Diff] Actual / Expected + + + ${expected} + `; + assertEquals( + // TODO(lionel-rowe): compare full messages without trimming once https://github.com/denoland/std/issues/6830 and + // https://github.com/denoland/std/issues/6831 are fixed + stripAnsiCode(err.message).trimEnd(), + fullExpectedMessage, + ); +} + +Deno.test(`assertEquals() truncates unchanged lines of large diffs when "${DIFF_CONTEXT_LENGTH}" is set`, async (t) => { + using stack = disposableStack(); + stack.use(stub(Deno.permissions, "querySync", (x) => { + if (x.name === "env") return { state: "granted" } as Deno.PermissionStatus; + throw new Error(`Unexpected permission descriptor: ${x.name}`); + })); + stack.use(stub(Deno.env, "get", (key) => { + if (key === DIFF_CONTEXT_LENGTH) return (10).toString(); + throw new Error(`Unexpected env var key: ${key}`); + })); + + const a = Array.from({ length: 1000 }, (_, i) => i); + const b = [...a]; + b[500] = -1; + + await t.step("array", () => { + assertDiffMessage( + a, + b, + dedent` + [ + ... 490 unchanged lines ... + 490, + 491, + 492, + 493, + 494, + 495, + 496, + 497, + 498, + 499, + - 500, + + -1, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 509, + 510, + ... 489 unchanged lines ... + ] + `, + ); + }); + + await t.step("object", () => { + assertDiffMessage( + Object.fromEntries(a.entries()), + Object.fromEntries(b.entries()), + dedent` + { + ... 437 unchanged lines ... + "492": 492, + "493": 493, + "494": 494, + "495": 495, + "496": 496, + "497": 497, + "498": 498, + "499": 499, + "5": 5, + "50": 50, + - "500": 500, + + "500": -1, + "501": 501, + "502": 502, + "503": 503, + "504": 504, + "505": 505, + "506": 506, + "507": 507, + "508": 508, + "509": 509, + "51": 51, + ... 542 unchanged lines ... + } + `, + ); + }); + + await t.step("string", () => { + assertDiffMessage( + a.join("\n"), + b.join("\n"), + dedent` + 0\\n + ... 489 unchanged lines ... + 490\\n + 491\\n + 492\\n + 493\\n + 494\\n + 495\\n + 496\\n + 497\\n + 498\\n + 499\\n + - 500\\n + + -1\\n + 501\\n + 502\\n + 503\\n + 504\\n + 505\\n + 506\\n + 507\\n + 508\\n + 509\\n + 510\\n + ... 488 unchanged lines ... + 999 + `, + ); + }); + + await t.step("Set", () => { + assertDiffMessage( + new Set(a), + new Set(b), + dedent` + Set(1000) { + + -1, + 0, + 1, + 10, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + ... 427 unchanged lines ... + 492, + 493, + 494, + 495, + 496, + 497, + 498, + 499, + 5, + 50, + - 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 509, + 51, + ... 542 unchanged lines ... + } + `, + ); + }); + + await t.step("diff near start", () => { + const a = Array.from({ length: 1000 }, (_, i) => i); + const b = [...a]; + b[3] = -1; + + assertDiffMessage( + a, + b, + dedent` + [ + 0, + 1, + 2, + - 3, + + -1, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + ... 986 unchanged lines ... + ] + `, + ); + }); + + await t.step("diff near end", () => { + const a = Array.from({ length: 1000 }, (_, i) => i); + const b = [...a]; + b[996] = -1; + + assertDiffMessage( + a, + b, + dedent` + [ + ... 986 unchanged lines ... + 986, + 987, + 988, + 989, + 990, + 991, + 992, + 993, + 994, + 995, + - 996, + + -1, + 997, + 998, + 999, + ] + `, + ); + }); +}); diff --git a/assert/unstable_strict_equals.ts b/assert/unstable_strict_equals.ts new file mode 100644 index 000000000000..e424a3dff7e9 --- /dev/null +++ b/assert/unstable_strict_equals.ts @@ -0,0 +1,45 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { assertStrictEquals as _assertStrictEquals } from "./strict_equals.ts"; +import { truncateDiff } from "@std/internal/truncate-build-message"; +// deno-lint-ignore no-unused-vars +import type { DIFF_CONTEXT_LENGTH } from "@std/internal/truncate-build-message"; + +/** + * Make an assertion that `actual` and `expected` are strictly equal, using + * {@linkcode Object.is} for equality comparison. If not, then throw. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * The {@linkcode DIFF_CONTEXT_LENGTH} environment variable can be set to + * enable truncation of long diffs, in which case its value should be a + * positive integer representing the number of unchanged context lines to show + * around each changed part of the diff. By default, diffs are not truncated. + * + * @example Usage + * ```ts ignore + * import { assertStrictEquals } from "@std/assert"; + * + * const a = {}; + * const b = a; + * assertStrictEquals(a, b); // Doesn't throw + * + * const c = {}; + * const d = {}; + * assertStrictEquals(c, d); // Throws + * ``` + * + * @typeParam T The type of the expected value. + * @param actual The actual value to compare. + * @param expected The expected value to compare. + * @param msg The optional message to display if the assertion fails. + */ +export function assertStrictEquals( + actual: unknown, + expected: T, + msg?: string, +): asserts actual is T { + const args: Parameters = [actual, expected, msg]; + // @ts-expect-error extra arg + _assertStrictEquals(...args, truncateDiff); +} diff --git a/internal/build_message.ts b/internal/build_message.ts index b45a8a9d5a09..413bf811a60f 100644 --- a/internal/build_message.ts +++ b/internal/build_message.ts @@ -36,6 +36,8 @@ export function createColor( return (s) => background ? bgGreen(white(s)) : green(bold(s)); case "removed": return (s) => background ? bgRed(white(s)) : red(bold(s)); + case "truncation": + return gray; default: return white; } @@ -73,7 +75,6 @@ export function createSign(diffType: DiffType): string { export interface BuildMessageOptions { /** * Whether to output the diff as a single string. - * * @default {false} */ stringDiff?: boolean; @@ -84,6 +85,7 @@ export interface BuildMessageOptions { * * @param diffResult The diff result array. * @param options Optional parameters for customizing the message. + * @param truncateDiff Function to truncate the diff (default is no truncation). * * @returns An array of strings representing the built message. * @@ -91,9 +93,7 @@ export interface BuildMessageOptions { * ```ts no-assert * import { diffStr, buildMessage } from "@std/internal"; * - * const diffResult = diffStr("Hello, world!", "Hello, world"); - * - * console.log(buildMessage(diffResult)); + * diffStr("Hello, world!", "Hello, world"); * // [ * // "", * // "", @@ -109,7 +109,16 @@ export interface BuildMessageOptions { export function buildMessage( diffResult: ReadonlyArray>, options: BuildMessageOptions = {}, + truncateDiff?: ( + diffResult: ReadonlyArray>, + stringDiff: boolean, + contextLength?: number | null, + ) => ReadonlyArray>, ): string[] { + if (truncateDiff != null) { + diffResult = truncateDiff(diffResult, options.stringDiff ?? false); + } + const { stringDiff = false } = options; const messages = [ "", @@ -122,11 +131,15 @@ export function buildMessage( ]; const diffMessages = diffResult.map((result) => { const color = createColor(result.type); - const line = result.details?.map((detail) => - detail.type !== "common" - ? createColor(detail.type, true)(detail.value) - : detail.value - ).join("") ?? result.value; + + const line = result.type === "added" || result.type === "removed" + ? result.details?.map((detail) => + detail.type !== "common" + ? createColor(detail.type, true)(detail.value) + : detail.value + ).join("") ?? result.value + : result.value; + return color(`${createSign(result.type)}${line}`); }); messages.push(...(stringDiff ? [diffMessages.join("")] : diffMessages), ""); diff --git a/internal/build_message_test.ts b/internal/build_message_test.ts index 0365006b25ae..0751e28027fb 100644 --- a/internal/build_message_test.ts +++ b/internal/build_message_test.ts @@ -2,9 +2,11 @@ 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"; +import { truncateDiff } from "@std/internal/truncate-build-message"; +import type { DiffResult } from "@std/internal/types"; -Deno.test("buildMessage()", () => { - const messages = [ +Deno.test("buildMessage()", async (t) => { + const prelude = [ "", "", ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ @@ -13,14 +15,62 @@ Deno.test("buildMessage()", () => { "", "", ]; - assertEquals(buildMessage([]), [...messages, ""]); - assertEquals( - buildMessage([{ type: "added", value: "foo" }, { - type: "removed", - value: "bar", - }]), - [...messages, green(bold("+ foo")), red(bold("- bar")), ""], - ); + + await t.step("basic", () => { + assertEquals(buildMessage([]), [...prelude, ""]); + assertEquals( + buildMessage([ + { type: "added", value: "foo" }, + { type: "removed", value: "bar" }, + ]), + [...prelude, green(bold("+ foo")), red(bold("- bar")), ""], + ); + }); + + await t.step("truncated", () => { + assertEquals( + buildMessage( + [ + { type: "added", value: "foo" }, + { type: "common", value: "bar 1" }, + { type: "common", value: "bar 2" }, + { type: "common", value: "bar 3" }, + { type: "common", value: "bar 4" }, + { type: "common", value: "bar 5" }, + { type: "added", value: "foo" }, + { type: "common", value: "bar 1" }, + { type: "common", value: "bar 2" }, + { type: "common", value: "bar 3" }, + { type: "common", value: "bar 4" }, + { type: "common", value: "bar 5" }, + { type: "common", value: "bar 6" }, + { type: "removed", value: "baz" }, + { type: "common", value: "bar" }, + ], + {}, + (diffResult: ReadonlyArray>, stringDiff: boolean) => + truncateDiff(diffResult, stringDiff, 2), + ), + [ + ...prelude, + green(bold("+ foo")), + white(" bar 1"), + white(" bar 2"), + white(" bar 3"), + white(" bar 4"), + white(" bar 5"), + green(bold("+ foo")), + white(" bar 1"), + white(" bar 2"), + gray(" ... 2 unchanged lines ..."), + white(" bar 5"), + white(" bar 6"), + red(bold("- baz")), + white(" bar"), + "", + ], + ); + }); }); Deno.test("createColor()", () => { diff --git a/internal/deno.json b/internal/deno.json index 018c6d37bf7f..4f0b73b0a22a 100644 --- a/internal/deno.json +++ b/internal/deno.json @@ -10,6 +10,7 @@ "./format": "./format.ts", "./os": "./os.ts", "./styles": "./styles.ts", + "./truncate-build-message": "./truncate_build_message.ts", "./types": "./types.ts" } } diff --git a/internal/diff_str.ts b/internal/diff_str.ts index fcf56512a091..862103fe7113 100644 --- a/internal/diff_str.ts +++ b/internal/diff_str.ts @@ -1,7 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. -import type { DiffResult } from "./types.ts"; +import type { ChangedDiffResult, DiffResult } from "./types.ts"; import { diff } from "./diff.ts"; /** @@ -179,7 +179,7 @@ export function diffStr(A: string, B: string): DiffResult[] { const bLines = hasMoreRemovedLines ? removed : added; for (const a of aLines) { let tokens = [] as Array>; - let b: undefined | DiffResult; + let b: undefined | ChangedDiffResult; // Search another diff line with at least one common token while (bLines.length) { b = bLines.shift(); diff --git a/internal/mod.ts b/internal/mod.ts index 3827151d9b34..aa62f2849c25 100644 --- a/internal/mod.ts +++ b/internal/mod.ts @@ -43,4 +43,5 @@ export * from "./diff_str.ts"; export * from "./format.ts"; export * from "./os.ts"; export * from "./styles.ts"; +export * from "./truncate_build_message.ts"; export * from "./types.ts"; diff --git a/internal/truncate_build_message.ts b/internal/truncate_build_message.ts new file mode 100644 index 000000000000..3955859d16cf --- /dev/null +++ b/internal/truncate_build_message.ts @@ -0,0 +1,164 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import type { CommonDiffResult, DiffResult } from "@std/internal/types"; + +/** The environment variable used for setting diff context length. */ +export const DIFF_CONTEXT_LENGTH = "DIFF_CONTEXT_LENGTH"; + +/** + * Get the truncation context length from the `DIFF_CONTEXT_LENGTH` + * environment variable. + * @returns The truncation context length, or `null` if not set or invalid. + * + * @example Usage + * ```ts no-assert ignore + * Deno.env.set("DIFF_CONTEXT_LENGTH", "10"); + * getTruncationContextLengthFromEnv(); // 10 + * ``` + */ +export function getTruncationContextLengthFromEnv(): number | null { + const envVar = getTruncationEnvVar(); + if (!envVar) return null; + const truncationContextLength = parseInt(envVar); + return Number.isFinite(truncationContextLength) && + truncationContextLength >= 0 + ? truncationContextLength + : null; +} + +function getTruncationEnvVar() { + // deno-lint-ignore no-explicit-any + const { Deno, process } = globalThis as any; + + if (typeof Deno === "object") { + const permissionStatus = Deno.permissions.querySync({ + name: "env", + variable: DIFF_CONTEXT_LENGTH, + }).state ?? "granted"; + + return permissionStatus === "granted" + ? Deno.env.get(DIFF_CONTEXT_LENGTH) ?? null + : null; + } + const nodeEnv = process?.getBuiltinModule?.("node:process")?.env as + | Partial> + | undefined; + return typeof nodeEnv === "object" + ? nodeEnv[DIFF_CONTEXT_LENGTH] ?? null + : null; +} + +/** + * Truncates a diff result by consolidating unchanged lines. + * + * @param diffResult The diff result to truncate. + * @param stringDiff Whether the diff is for strings. + * @param contextLength The number of unchanged context lines to show around + * each changed part of the diff. If not provided, it will be read from the + * `DIFF_CONTEXT_LENGTH` environment variable. If that is not set or invalid, + * no truncation will be performed. + * + * @returns The truncated diff result. + * + * @example Usage + * ```ts no-assert ignore + * truncateDiff(diffResult, false, 2); + * ``` + */ +export function truncateDiff( + diffResult: ReadonlyArray>, + stringDiff: boolean, + contextLength?: number | null, +): ReadonlyArray> { + contextLength ??= getTruncationContextLengthFromEnv(); + if (contextLength == null) return diffResult; + + const messages: DiffResult[] = []; + const commons: CommonDiffResult[] = []; + + for (let i = 0; i < diffResult.length; ++i) { + const result = diffResult[i]!; + + if (result.type === "common") { + commons.push(result as typeof result & { type: typeof result.type }); + } else { + messages.push( + ...consolidateCommon( + commons, + commons.length === i ? "start" : "middle", + stringDiff, + contextLength, + ), + ); + commons.length = 0; + messages.push(result); + } + } + + messages.push( + ...consolidateCommon(commons, "end", stringDiff, contextLength), + ); + + return messages; +} + +/** + * Consolidates a sequence of common diff results by truncating unchanged lines. + * + * @param commons The sequence of common diff results to consolidate. + * @param location The location of the common sequence in the overall diff: + * "start", "middle", or "end". + * @param stringDiff Whether the diff is for strings. + * @param contextLength The number of unchanged context lines to show around + * each changed part of the diff. + * @returns The consolidated sequence of common diff results. + * + * @example Usage + * ```ts no-assert ignore + * consolidateCommon(commons, "middle", false, 2); + * ``` + */ +export function consolidateCommon( + commons: ReadonlyArray>, + location: "start" | "middle" | "end", + stringDiff: boolean, + contextLength: number, +): ReadonlyArray> { + const beforeLength = location === "start" ? 1 : contextLength; + const afterLength = location === "end" ? 1 : contextLength; + + const omitLength = commons.length - beforeLength - afterLength; + + if (omitLength <= 1) return commons; + + const before = commons[beforeLength - 1]?.value ?? ""; + const after = commons[commons.length - afterLength]?.value ?? before; + const lineEnd = stringDiff ? "\n" : ""; + + const indent = location === "start" + ? getIndent(after) + : location === "end" + ? getIndent(before) + : commonIndent(before, after); + + const value = `${indent}... ${omitLength} unchanged lines ...${lineEnd}`; + + return [ + ...commons.slice(0, beforeLength), + { type: "truncation", value }, + ...commons.slice(commons.length - afterLength), + ]; +} + +function commonIndent(line1: string, line2: string): string { + const indent1 = getIndent(line1); + const indent2 = getIndent(line2); + return indent1.startsWith(indent2) + ? indent2 + : indent2.startsWith(indent1) + ? indent1 + : ""; +} + +function getIndent(line: string): string { + return line.match(/^\s+/g)?.[0] ?? ""; +} diff --git a/internal/truncate_build_message_test.ts b/internal/truncate_build_message_test.ts new file mode 100644 index 000000000000..01161372a2c1 --- /dev/null +++ b/internal/truncate_build_message_test.ts @@ -0,0 +1,89 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { assertEquals } from "@std/assert/equals"; +import { consolidateCommon, truncateDiff } from "./truncate_build_message.ts"; +import type { CommonDiffResult } from "@std/internal/types"; + +Deno.test("consolidateDiff()", () => { + assertEquals( + truncateDiff( + [ + { type: "added", value: "foo" }, + { type: "common", value: "[" }, + { type: "common", value: ' "bar-->",' }, + { type: "common", value: ' "bar",' }, + { type: "common", value: ' "bar",' }, + { type: "common", value: ' "<--bar",' }, + { type: "common", value: "]" }, + { type: "removed", value: "foo" }, + ], + false, + 2, + ), + [ + { type: "added", value: "foo" }, + { type: "common", value: "[" }, + { type: "common", value: ' "bar-->",' }, + { type: "truncation", value: " ... 2 unchanged lines ..." }, + { type: "common", value: ' "<--bar",' }, + { type: "common", value: "]" }, + { type: "removed", value: "foo" }, + ], + ); +}); + +Deno.test("consolidateCommon()", async (t) => { + const commons = [ + { type: "common", value: "[" }, + { type: "common", value: ' "bar-->",' }, + { type: "common", value: ' "bar",' }, + { type: "common", value: ' "bar",' }, + { type: "common", value: ' "bar",' }, + { type: "common", value: ' "<--bar",' }, + { type: "common", value: "]" }, + ] as const; + + const contextLength = 2; + + const cases: { + location: "start" | "middle" | "end"; + expected: ReadonlyArray>; + }[] = [ + { + location: "start", + expected: [ + { type: "common", value: "[" }, + { type: "truncation", value: " ... 4 unchanged lines ..." }, + { type: "common", value: ' "<--bar",' }, + { type: "common", value: "]" }, + ], + }, + { + location: "middle", + expected: [ + { type: "common", value: "[" }, + { type: "common", value: ' "bar-->",' }, + { type: "truncation", value: " ... 3 unchanged lines ..." }, + { type: "common", value: ' "<--bar",' }, + { type: "common", value: "]" }, + ], + }, + { + location: "end", + expected: [ + { type: "common", value: "[" }, + { type: "common", value: ' "bar-->",' }, + { type: "truncation", value: " ... 4 unchanged lines ..." }, + { type: "common", value: "]" }, + ], + }, + ]; + + for (const { location: extremity, expected } of cases) { + await t.step(extremity, () => { + assertEquals( + consolidateCommon(commons, extremity, false, contextLength), + expected, + ); + }); + } +}); diff --git a/internal/types.ts b/internal/types.ts index 6e7c5c113061..1d98f98b91ef 100644 --- a/internal/types.ts +++ b/internal/types.ts @@ -2,18 +2,29 @@ // This module is browser compatible. /** Ways that lines in a diff can be different. */ -export type DiffType = "removed" | "common" | "added"; +export type DiffType = DiffResult["type"]; /** * Represents the result of a diff operation. - * * @typeParam T The type of the value in the diff result. */ -export interface DiffResult { - /** The type of the diff. */ - type: DiffType; - /** The value of the diff. */ +export type DiffResult = ChangedDiffResult | CommonDiffResult; + +/** + * Represents the result of a common diff operation. + * @typeParam T The type of the value in the diff result. + */ +export type CommonDiffResult = { + type: "common" | "truncation"; + value: T; +}; + +/** + * Represents the result of a changed diff operation. + * @typeParam T The type of the value in the diff result. + */ +export type ChangedDiffResult = { + type: "removed" | "added"; value: T; - /** The details of the diff. */ details?: DiffResult[]; -} +};