Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions native-lib/node/tests/tck/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ function compareJson(actual: string, expected: string): CompareResult {
} catch (err) {
return fail(`expected is not valid JSON: ${err}`);
}
return deepEqual(a, e) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`);
// Normalize CRLF → LF within string values so platform line endings don't
// cause a mismatch — e.g. an embedded CSV value the writer emits with CRLF on
// Windows but LF elsewhere. Matches the CLI's AssertionHelper EOL handling.
return deepEqual(a, e, true) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`);
}

const xmlParser = new XMLParser({
Expand Down Expand Up @@ -141,15 +144,23 @@ function stripAllWhitespace(s: string): string {
return s.replace(/\s+/g, "");
}

/** Structural deep equality for JSON values (objects compared key-insensitively to order). */
export function deepEqual(a: unknown, b: unknown): boolean {
/**
* Structural deep equality for JSON values (objects compared key-insensitively
* to order). When `normEol` is true, string values are compared with CRLF
* normalized to LF so platform line endings inside a value don't cause a
* mismatch.
*/
export function deepEqual(a: unknown, b: unknown, normEol = false): boolean {
if (typeof a === "string" && typeof b === "string") {
return normEol ? a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n") : a === b;
}
if (a === b) return true;
if (a === null || b === null) return a === b;
if (typeof a !== typeof b) return false;

if (Array.isArray(a) || Array.isArray(b)) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
return a.every((v, i) => deepEqual(v, b[i]));
return a.every((v, i) => deepEqual(v, b[i], normEol));
}

if (typeof a === "object" && typeof b === "object") {
Expand All @@ -158,7 +169,7 @@ export function deepEqual(a: unknown, b: unknown): boolean {
const ak = Object.keys(ao);
const bk = Object.keys(bo);
if (ak.length !== bk.length) return false;
return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]));
return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k], normEol));
}

return false;
Expand Down
15 changes: 15 additions & 0 deletions native-lib/node/tests/unit/tck-compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ describe("compareOutput — json (structural)", () => {
expect(r.match).toBe(false);
expect(r.detail).toMatch(/actual is not valid JSON/);
});

it("ignores CRLF vs LF inside a string value (Windows line endings)", () => {
// Regression: a JSON output embedding CSV — the writer emits CRLF on Windows
// but the fixture has LF. The line breaks are escaped \r\n / \n inside the
// JSON string, so they must be normalized during value comparison.
const actual = buf(JSON.stringify({ csv: "a|b\r\nx|y\r\n" }));
const expected = buf(JSON.stringify({ csv: "a|b\nx|y\n" }));
expect(compareOutput("json", actual, expected).match).toBe(true);
});

it("still distinguishes genuinely different string values", () => {
const actual = buf(JSON.stringify({ v: "hello" }));
const expected = buf(JSON.stringify({ v: "world" }));
expect(compareOutput("json", actual, expected).match).toBe(false);
});
});

describe("compareOutput — xml (structural)", () => {
Expand Down
Loading