feat(test-utils): add runtime detection predicates and cross-engine assertion helpers - #9492
Conversation
…ssertion helpers Adds runtime environment predicates (isNode, isBun, isDeno, getRuntime, getNodeMajorVersion), Array subclass equality helpers (requiresStrictArrayPrototypeEquality, assertArraySubclassStrictEqual), and cross-engine TypeError predicates (isReadOnlyPropertyError, isNullOrUndefinedPropertyError) to google-test-utils. [Generated-by: AI]
There was a problem hiding this comment.
Code Review
This pull request introduces runtime detection and assertion utilities in core/test-utils to handle differences across Node.js, Bun, and Deno environments, including utilities for checking read-only property errors and asserting array subclass equality. The review feedback highlights that the current implementation of assertArraySubclassStrictEqual only performs a shallow copy, which fails to normalize nested array subclasses (such as arrays of rows) on runtimes requiring strict prototype equality. It is recommended to recursively normalize array subclasses to plain arrays and add corresponding test coverage for nested structures.
| /** | ||
| * Asserts `deepStrictEqual` between an `Array` (or `Array` subclass instance such as | ||
| * Spanner's `RowImpl`) and an expected array, normalizing the `actual` array via | ||
| * spread (`[...actual]`) only on runtimes that enforce strict prototype equality | ||
| * for `Array` subclasses. | ||
| */ | ||
| export function assertArraySubclassStrictEqual<T>( | ||
| actual: ReadonlyArray<T>, | ||
| expected: ReadonlyArray<T>, | ||
| message?: string | Error, | ||
| versions?: RuntimeVersions, | ||
| ): void { | ||
| if (requiresStrictArrayPrototypeEquality(versions)) { | ||
| assert.deepStrictEqual([...actual], expected, message); | ||
| } else { | ||
| assert.deepStrictEqual(actual, expected, message); | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of assertArraySubclassStrictEqual uses a shallow copy ([...actual]) to normalize the array. However, if the array contains nested array subclasses (for example, an array of Spanner RowImpl instances like [row1, row2]), the shallow copy will not normalize the nested elements. On runtimes that require strict prototype equality (such as Node.js < 20 and Bun), assert.deepStrictEqual will still fail when comparing these nested subclass instances against plain arrays.
To support nested array subclasses and arrays of rows, we should recursively normalize any array subclasses to plain arrays.
function toPlainArray(val: unknown): unknown {
return Array.isArray(val) ? Array.from(val, toPlainArray) : val;
}
/**
* Asserts `deepStrictEqual` between an `Array` (or `Array` subclass instance such as
* Spanner's `RowImpl`) and an expected array, recursively normalizing any `Array`
* subclasses to plain arrays only on runtimes that enforce strict prototype equality.
*/
export function assertArraySubclassStrictEqual<T>(
actual: ReadonlyArray<T>,
expected: ReadonlyArray<T>,
message?: string | Error,
versions?: RuntimeVersions,
): void {
if (requiresStrictArrayPrototypeEquality(versions)) {
assert.deepStrictEqual(toPlainArray(actual), toPlainArray(expected), message);
} else {
assert.deepStrictEqual(actual, expected, message);
}
}References
- Prefer using Node version-based branching for temporary test workarounds (such as handling prototype equality differences in assert.deepStrictEqual) instead of permanently simplifying or weakening the assertion (e.g., by spreading an array). This keeps strict assertions active for newer Node versions and makes the workaround easy to identify and remove when older Node versions are deprecated.
There was a problem hiding this comment.
Updated in c453644 to recursively normalize nested Array subclasses via toPlainArray on runtimes requiring strict prototype equality, and guarded message !== undefined before passing the 3rd argument to assert.deepStrictEqual for Node.js 26 compatibility.
| it('compares Array subclass instances across runtimes using assertArraySubclassStrictEqual', () => { | ||
| const row = new CustomRow('a', 'b'); | ||
| expect(() => | ||
| assertArraySubclassStrictEqual(row, ['a', 'b'], undefined, { | ||
| node: '18.20.0', | ||
| }), | ||
| ).not.toThrow(); | ||
| expect(() => | ||
| assertArraySubclassStrictEqual(row, ['a', 'b'], undefined, { | ||
| node: '22.14.0', | ||
| }), | ||
| ).not.toThrow(); | ||
| expect(() => | ||
| assertArraySubclassStrictEqual(row, ['a', 'c'], undefined, { | ||
| node: '22.6.0', | ||
| bun: '1.2.4', | ||
| }), | ||
| ).toThrow(assert.AssertionError); | ||
| }); |
There was a problem hiding this comment.
Add a test case to verify that assertArraySubclassStrictEqual correctly handles nested array subclasses (such as an array of rows) on runtimes requiring strict prototype equality.
it('compares Array subclass instances across runtimes using assertArraySubclassStrictEqual', () => {
const row = new CustomRow('a', 'b');
expect(() =>
assertArraySubclassStrictEqual(row, ['a', 'b'], undefined, {
node: '18.20.0',
}),
).not.toThrow();
expect(() =>
assertArraySubclassStrictEqual(row, ['a', 'b'], undefined, {
node: '22.14.0',
}),
).not.toThrow();
expect(() =>
assertArraySubclassStrictEqual(row, ['a', 'c'], undefined, {
node: '22.6.0',
bun: '1.2.4',
}),
).toThrow(assert.AssertionError);
// Test nested array subclasses (e.g., array of rows)
const nestedRows = [new CustomRow('a', 'b')];
expect(() =>
assertArraySubclassStrictEqual(nestedRows, [['a', 'b']], undefined, {
node: '18.20.0',
}),
).not.toThrow();
});There was a problem hiding this comment.
Added test coverage for nested Array subclasses ([new CustomRow('a', 'b')]) and custom assertion messages in c453644.
Use assertArraySubclassStrictEqual, isReadOnlyPropertyError, and isNullOrUndefinedPropertyError from google-test-utils in Spanner unit tests to address PR #9460 review feedback. [Generated-by: AI]
…e workspace deps in cloudbuild
- Recursively normalize nested Array subclasses in assertArraySubclassStrictEqual via toPlainArray and avoid passing undefined message to assert.deepStrictEqual (fixes Node 26 TypeError).
- Add unit tests for nested Array subclasses and custom assertion messages.
- Update Spanner cloudbuild.yaml and cloudbuild-regular-sessions.yaml pnpm filter to ...{./handwritten/spanner}... so upstream workspace dependencies like google-test-utils are built.
[Generated-by: AI]
Summary
Follows up on review feedback from @feywind on #9460 by adding the shared runtime detection predicates and cross-engine assertion helpers to
google-test-utils(core/test-utils) and adopting them in the@google-cloud/spanner(handwritten/spanner) unit tests she commented on:isNode,isBun,getNodeMajorVersion,RuntimeVersions): Centralizes detection of Node.js vs. Bun so tests do not need ad-hocprocess.versionsparsing (discussion_r4149070743).requiresStrictArrayPrototypeEquality,assertArraySubclassStrictEqual): Centralizes the workaround for Node.js < 20 and Bun whereassert.deepStrictEqualstrictly enforces prototype equality when comparing anArraysubclass (such as Spanner'sRowImpl) against a plainArrayliteral (discussion_r4149239787).TypeErrormatcher (READONLY_PROPERTY_ERROR_REGEX,isReadOnlyPropertyError): Centralizes matching for read-only property assignment errors across V8 (Node.js) and JavaScriptCore (Bun) (discussion_r4149233766).handwritten/spanner):handwritten/spanner/test/helper.ts: Replaces the inline V8/JSC read-only property regex withisReadOnlyPropertyError(err, 'name').handwritten/spanner/test/partial-result-stream.ts: Replaces the two inlineprocess.versionschecks withassertArraySubclassStrictEqual(row, EXPECTED_ROW).Testing
core/test-utils/test/runtime.test.tscovering all exported functions (100% statement/function/line coverage).pnpm testandpnpm run lintincore/test-utils.helper,partial-result-stream) and ESLint/TypeScript checks inhandwritten/spanner.