Skip to content

Commit

Permalink
Add tests of canonicalStringify and helper lookupSortedKeys.
Browse files Browse the repository at this point in the history
The lookupSortedKeys function is not intended to be used directly, but
seemed worth unit testing nevertheless.
  • Loading branch information
benjamn committed Sep 28, 2023
1 parent 2311b8d commit 1b4aad3
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 2 deletions.
132 changes: 132 additions & 0 deletions src/utilities/common/__tests__/canonicalStringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
canonicalStringify,
lookupSortedKeys,
} from "../canonicalStringify";

function forEachPermutation(
keys: string[],
callback: (permutation: string[]) => void,
) {
if (keys.length <= 1) {
callback(keys);
return;
}
const first = keys[0];
const rest = keys.slice(1);
forEachPermutation(rest, (permutation) => {
for (let i = 0; i <= permutation.length; ++i) {
callback([
...permutation.slice(0, i),
first,
...permutation.slice(i),
]);
}
});
}

function allObjectPermutations<T extends Record<string, any>>(obj: T) {
const keys = Object.keys(obj);
const permutations: T[] = [];
forEachPermutation(keys, permutation => {
const permutationObj =
Object.create(Object.getPrototypeOf(obj));
permutation.forEach(key => {
permutationObj[key] = obj[key];
});
permutations.push(permutationObj);
});
return permutations;
}

describe("canonicalStringify", () => {
beforeEach(() => {
canonicalStringify.reset();
});

it("should not modify original object", () => {
const obj = { c: 3, a: 1, b: 2 };
expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}');
expect(Object.keys(obj)).toEqual(["c", "a", "b"]);
});

it("forEachPermutation should work", () => {
const permutations: string[][] = [];
forEachPermutation(["a", "b", "c"], (permutation) => {
permutations.push(permutation);
});
expect(permutations).toEqual([
["a", "b", "c"],
["b", "a", "c"],
["b", "c", "a"],
["a", "c", "b"],
["c", "a", "b"],
["c", "b", "a"],
]);
});

it("canonicalStringify should stably stringify all permutations of an object", () => {
const unstableStrings = new Set<string>();
const stableStrings = new Set<string>();

allObjectPermutations({
c: 3,
a: 1,
b: 2,
}).forEach(obj => {
unstableStrings.add(JSON.stringify(obj));
stableStrings.add(canonicalStringify(obj));

expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}');

allObjectPermutations({
z: "z",
y: ["y", obj, "why"],
x: "x",
}).forEach(parent => {
expect(canonicalStringify(parent)).toBe(
'{"x":"x","y":["y",{"a":1,"b":2,"c":3},"why"],"z":"z"}',
);
});
});

expect(unstableStrings.size).toBe(6);
expect(stableStrings.size).toBe(1);
});

it("lookupSortedKeys(keys, false) should reuse same sorted array for all permutations", () => {
const keys = ["z", "a", "c", "b"];
const sorted = lookupSortedKeys(["z", "a", "b", "c"], false);
expect(sorted).toEqual(["a", "b", "c", "z"]);
forEachPermutation(keys, permutation => {
expect(lookupSortedKeys(permutation, false)).toBe(sorted);
});
});

it("lookupSortedKeys(keys, true) should return same array if already sorted", () => {
const keys = ["a", "b", "c", "x", "y", "z"].sort();
const sorted = lookupSortedKeys(keys, true);
expect(sorted).toBe(keys);

forEachPermutation(keys, permutation => {
const sortedTrue = lookupSortedKeys(permutation, true);
const sortedFalse = lookupSortedKeys(permutation, false);

expect(sortedTrue).toEqual(sorted);
expect(sortedFalse).toEqual(sorted);

const wasPermutationSorted =
permutation.every((key, i) => key === keys[i]);

if (wasPermutationSorted) {
expect(sortedTrue).toBe(permutation);
expect(sortedTrue).not.toBe(sorted);
} else {
expect(sortedTrue).not.toBe(permutation);
expect(sortedTrue).toBe(sorted);
}

expect(sortedFalse).not.toBe(permutation);
expect(sortedFalse).toBe(sorted);
});
});
});
2 changes: 1 addition & 1 deletion src/utilities/common/canonicalStringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const sortingTrieRoot: SortingTrie = new Map;
// Sort the given keys using a lookup trie, with an option to return the same
// (===) array in case it was already sorted, so we can avoid always creating a
// new object in the replacer function above.
function lookupSortedKeys(
export function lookupSortedKeys(
keys: readonly string[],
returnKeysIfAlreadySorted: boolean,
): readonly string[] {
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export * from "./common/makeUniqueId.js";
export * from "./common/stringifyForDisplay.js";
export * from "./common/mergeOptions.js";
export * from "./common/incrementalResult.js";
export * from "./common/canonicalStringify.js";

export { canonicalStringify } from "./common/canonicalStringify.js";
export { omitDeep } from "./common/omitDeep.js";
export { stripTypename } from "./common/stripTypename.js";

Expand Down

0 comments on commit 1b4aad3

Please sign in to comment.