blob: ed71d85d35932e7459ebdb24e42467746b8d836b [file] [log] [blame]
// MAINTENANCE_TODO: The "checkThingTrue" naming is confusing; these must be used with `expectOK`
// or the result is dropped on the floor. Rename these to things like `typedArrayIsOK`(??) to
// make it clearer.
// MAINTENANCE_TODO: Also, audit to make sure we aren't dropping any on the floor. Consider a
// no-ignored-return lint check if we can find one that we can use.
import {
assert,
ErrorWithExtra,
iterRange,
range,
TypedArrayBufferView,
TypedArrayBufferViewConstructor,
} from '../../common/util/util.js';
import { Float16Array } from '../../external/petamoriken/float16/float16.js';
import { generatePrettyTable } from './pretty_diff_tables.js';
/** Generate an expected value at `index`, to test for equality with the actual value. */
export type CheckElementsGenerator = (index: number) => number;
/** Check whether the actual `value` at `index` is as expected. */
export type CheckElementsPredicate = (index: number, value: number | bigint) => boolean;
/**
* Provides a pretty-printing implementation for a particular CheckElementsPredicate.
* This is an array; each element provides info to print an additional row in the error message.
*/
export type CheckElementsSupplementalTableRows = Array<{
/** Row header. */
leftHeader: string;
/**
* Get the value for a cell in the table with element index `index`.
* May be a string or numeric (number | bigint); numerics will be formatted according to the TypedArray type used.
*/
getValueForCell: (index: number) => string | number | bigint;
}>;
/**
* Check whether two `TypedArray`s have equal contents.
* Returns `undefined` if the check passes, or an `Error` if not.
*/
export function checkElementsEqual(
actual: TypedArrayBufferView,
expected: TypedArrayBufferView
): ErrorWithExtra | undefined {
assert(actual.constructor === expected.constructor, 'TypedArray type mismatch');
assert(
actual.length === expected.length,
`length mismatch: expected ${expected.length} got ${actual.length}`
);
let failedElementsFirstMaybe: number | undefined = undefined;
/** Sparse array with `true` for elements that failed. */
const failedElements: (true | undefined)[] = [];
for (let i = 0; i < actual.length; ++i) {
if (actual[i] !== expected[i]) {
failedElementsFirstMaybe ??= i;
failedElements[i] = true;
}
}
if (failedElementsFirstMaybe === undefined) {
return undefined;
}
const failedElementsFirst = failedElementsFirstMaybe;
return failCheckElements({
actual,
failedElements,
failedElementsFirst,
predicatePrinter: [{ leftHeader: 'expected ==', getValueForCell: index => expected[index] }],
});
}
/**
* Check whether each value in a `TypedArray` is between the two corresponding "expected" values
* (either `a(i) <= actual[i] <= b(i)` or `a(i) >= actual[i] => b(i)`).
*/
export function checkElementsBetween(
actual: TypedArrayBufferView,
expected: readonly [CheckElementsGenerator, CheckElementsGenerator]
): ErrorWithExtra | undefined {
const error = checkElementsPassPredicate(
actual,
(index, value) =>
value >= Math.min(expected[0](index), expected[1](index)) &&
value <= Math.max(expected[0](index), expected[1](index)),
{
predicatePrinter: [
{ leftHeader: 'between', getValueForCell: index => expected[0](index) },
{ leftHeader: 'and', getValueForCell: index => expected[1](index) },
],
}
);
// If there was an error, extend it with additional extras.
return error ? new ErrorWithExtra(error, () => ({ expected })) : undefined;
}
/**
* Check whether each value in a `TypedArray` is equal to one of the two corresponding "expected"
* values (either `actual[i] === a[i]` or `actual[i] === b[i]`)
*/
export function checkElementsEqualEither(
actual: TypedArrayBufferView,
expected: readonly [TypedArrayBufferView, TypedArrayBufferView]
): ErrorWithExtra | undefined {
const error = checkElementsPassPredicate(
actual,
(index, value) => value === expected[0][index] || value === expected[1][index],
{
predicatePrinter: [
{ leftHeader: 'either', getValueForCell: index => expected[0][index] },
{ leftHeader: 'or', getValueForCell: index => expected[1][index] },
],
}
);
// If there was an error, extend it with additional extras.
return error ? new ErrorWithExtra(error, () => ({ expected })) : undefined;
}
/**
* Check whether a `TypedArray`'s contents equal the values produced by a generator function.
* Returns `undefined` if the check passes, or an `Error` if not.
*
* ```text
* Array had unexpected contents at indices 2 through 19.
* Starting at index 1:
* actual == 0x: 00 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00
* failed -> xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx
* expected == 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
* ```
*
* ```text
* Array had unexpected contents at indices 2 through 29.
* Starting at index 1:
* actual == 0.000 -2.000e+100 -1.000e+100 0.000 1.000e+100 2.000e+100 3.000e+100 4.000e+100 5.000e+100 6.000e+100 7.000e+100 ...
* failed -> xx xx xx xx xx xx xx xx xx ...
* expected == 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ...
* ```
*/
export function checkElementsEqualGenerated(
actual: TypedArrayBufferView,
generator: CheckElementsGenerator
): ErrorWithExtra | undefined {
let failedElementsFirstMaybe: number | undefined = undefined;
/** Sparse array with `true` for elements that failed. */
const failedElements: (true | undefined)[] = [];
for (let i = 0; i < actual.length; ++i) {
if (actual[i] !== generator(i)) {
failedElementsFirstMaybe ??= i;
failedElements[i] = true;
}
}
if (failedElementsFirstMaybe === undefined) {
return undefined;
}
const failedElementsFirst = failedElementsFirstMaybe;
const error = failCheckElements({
actual,
failedElements,
failedElementsFirst,
predicatePrinter: [{ leftHeader: 'expected ==', getValueForCell: index => generator(index) }],
});
// Add more extras to the error.
return new ErrorWithExtra(error, () => ({ generator }));
}
/**
* Check whether a `TypedArray`'s values pass the provided predicate function.
* Returns `undefined` if the check passes, or an `Error` if not.
*/
export function checkElementsPassPredicate(
actual: TypedArrayBufferView,
predicate: CheckElementsPredicate,
{ predicatePrinter }: { predicatePrinter?: CheckElementsSupplementalTableRows }
): ErrorWithExtra | undefined {
let failedElementsFirstMaybe: number | undefined = undefined;
/** Sparse array with `true` for elements that failed. */
const failedElements: (true | undefined)[] = [];
for (let i = 0; i < actual.length; ++i) {
if (!predicate(i, actual[i])) {
failedElementsFirstMaybe ??= i;
failedElements[i] = true;
}
}
if (failedElementsFirstMaybe === undefined) {
return undefined;
}
const failedElementsFirst = failedElementsFirstMaybe;
return failCheckElements({ actual, failedElements, failedElementsFirst, predicatePrinter });
}
interface CheckElementsFailOpts {
actual: TypedArrayBufferView;
failedElements: (true | undefined)[];
failedElementsFirst: number;
predicatePrinter?: CheckElementsSupplementalTableRows;
}
/**
* Implements the failure case of some checkElementsX helpers above. This allows those functions to
* implement their checks directly without too many function indirections in between.
*
* Note: Separating this into its own function significantly speeds up the non-error case in
* Chromium (though this may be V8-specific behavior).
*/
function failCheckElements({
actual,
failedElements,
failedElementsFirst,
predicatePrinter,
}: CheckElementsFailOpts): ErrorWithExtra {
const size = actual.length;
const ctor = actual.constructor as TypedArrayBufferViewConstructor;
const printAsFloat = ctor === Float16Array || ctor === Float32Array || ctor === Float64Array;
const failedElementsLast = failedElements.length - 1;
// Include one extra non-failed element at the beginning and end (if they exist), for context.
const printElementsStart = Math.max(0, failedElementsFirst - 1);
const printElementsEnd = Math.min(size, failedElementsLast + 2);
const printElementsCount = printElementsEnd - printElementsStart;
const numericToString = (val: number | bigint): string => {
if (typeof val === 'number' && printAsFloat) {
return val.toPrecision(4);
}
return intToPaddedHex(val, { byteLength: ctor.BYTES_PER_ELEMENT });
};
const numberPrefix = printAsFloat ? '' : '0x:';
const printActual = actual.subarray(printElementsStart, printElementsEnd);
const printExpected: Array<Iterable<string | number | bigint>> = [];
if (predicatePrinter) {
for (const { leftHeader, getValueForCell: cell } of predicatePrinter) {
printExpected.push(
(function* () {
yield* [leftHeader, ''];
yield* iterRange(printElementsCount, i => cell(printElementsStart + i));
})()
);
}
}
const printFailedValueMarkers = (function* () {
yield* ['failed ->', ''];
yield* range(printElementsCount, i => (failedElements[printElementsStart + i] ? 'xx' : ''));
})();
const opts = {
fillToWidth: 120,
numericToString,
};
const msg = `Array had unexpected contents at indices ${failedElementsFirst} through ${failedElementsLast}.
Starting at index ${printElementsStart}:
${generatePrettyTable(opts, [
['actual ==', numberPrefix, ...printActual],
printFailedValueMarkers,
...printExpected,
])}`;
return new ErrorWithExtra(msg, () => ({
actual: actual.slice(),
}));
}
// Helper helpers
/** Convert an integral `number` into a hex string, padded to the specified `byteLength`. */
function intToPaddedHex(val: number | bigint, { byteLength }: { byteLength: number }) {
assert(Number.isInteger(val), 'number must be integer');
const is_negative = typeof val === 'number' ? val < 0 : val < 0n;
let str = (is_negative ? -val : val).toString(16);
if (byteLength) str = str.padStart(byteLength * 2, '0');
if (is_negative) str = '-' + str;
return str;
}