Files
renovate/lib/util/fingerprint.spec.ts
mado e4710c1df5 fix(util/fingerprint): incrementally hash input to avoid V8 max string length (#43605)
* fix(util/fingerprint): incrementally hash input to avoid V8 max string length

* test(util/fingerprint): cover backward-compat with safeStringify + hash

* style(util/fingerprint): satisfy prettier + oxlint (no-unnecessary-type-assertion)

* refactor(util/fingerprint): trim comments to WHY only

* fix(util/fingerprint): pre-check emittability to match safe-stable-stringify on toJSON-returns-undefined; satisfy biome import sort

* test(util/fingerprint): cover root function/symbol and toJSON-returns-undefined branches

* style(util/fingerprint): drop unnecessary non-null assertion

* perf(util/fingerprint): memoize isEmittable per object reference
2026-06-01 09:03:25 +00:00

106 lines
3.0 KiB
TypeScript

import { fingerprint } from './fingerprint.ts';
import { hash } from './hash.ts';
import { safeStringify } from './stringify.ts';
// Compute the fingerprint via the prior `safeStringify` + `hash` approach so
// the new incremental implementation can be asserted byte-identical for
// inputs that fit in a single V8 string (existing stored fingerprints stay
// valid across the upgrade).
function legacyFingerprint(input: unknown): string {
const s = safeStringify(input);
return s ? hash(s) : '';
}
describe('util/fingerprint', () => {
const obj: any = {
name: 'object',
type: 'object',
isObject: true,
};
const obj2: any = {
type: 'object',
name: 'object',
isObject: true,
};
it('returns empty string', () => {
const res = fingerprint(undefined);
expect(res).toBeEmptyString();
});
it('maintains deterministic order', () => {
const res = fingerprint(obj);
const res2 = fingerprint(obj2);
expect(res).not.toEqual(JSON.stringify(obj)); // shows that the canonical form differs from the original order
expect(res).toEqual(res2);
});
it.each([
null,
true,
false,
0,
42,
-1.5,
'',
'string with "quotes" and \\backslashes\n',
[],
[1, 2, 3, 'a', null, true],
{},
{ a: 1, b: [1, 2, { c: 'x' }] },
{ z: 1, a: 2, m: { y: 1, b: 2 } },
new Date(0),
])('matches legacy safeStringify+hash output for %p', (input) => {
expect(fingerprint(input)).toEqual(legacyFingerprint(input));
});
it('returns empty string for root function/symbol', () => {
expect(fingerprint(() => 1)).toBeEmptyString();
expect(fingerprint(Symbol('s'))).toBeEmptyString();
});
it('drops undefined/function/symbol object values like JSON.stringify', () => {
const sym = Symbol('s');
const input = {
keep: 1,
dropU: undefined,
dropF: () => 1,
dropS: sym,
};
expect(fingerprint(input)).toEqual(legacyFingerprint(input));
});
it('replaces undefined/function/symbol with null in arrays', () => {
const input = [1, undefined, () => 1, Symbol('s'), 'x'];
expect(fingerprint(input)).toEqual(legacyFingerprint(input));
});
it('drops object keys whose toJSON resolves to undefined', () => {
const input = { x: { toJSON: () => undefined }, y: 1 };
expect(fingerprint(input)).toEqual(legacyFingerprint(input));
});
it('renders array items whose toJSON resolves to undefined as null', () => {
const input = [{ toJSON: () => undefined }, 1];
expect(fingerprint(input)).toEqual(legacyFingerprint(input));
});
it('handles circular references', () => {
const a: any = { name: 'a' };
a.self = a;
const res1 = fingerprint(a);
const res2 = fingerprint(a);
expect(res1).toEqual(res2);
expect(res1).toEqual(legacyFingerprint(a));
});
it('handles many entries without stack overflow', () => {
const wide = Array.from({ length: 10_000 }, (_, i) => ({
id: i,
body: 'x'.repeat(100),
}));
expect(fingerprint(wide)).toEqual(legacyFingerprint(wide));
});
});