mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-02 02:29:51 +00:00
e4710c1df5
* 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
106 lines
3.0 KiB
TypeScript
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));
|
|
});
|
|
});
|