tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

util.ts (16067B)


      1 import { Float16Array } from '../../external/petamoriken/float16/float16.js';
      2 import { SkipTestCase } from '../framework/fixture.js';
      3 import { globalTestConfig } from '../framework/test_config.js';
      4 
      5 import { keysOf } from './data_tables.js';
      6 import { timeout } from './timeout.js';
      7 
      8 /**
      9 * Error with arbitrary `extra` data attached, for debugging.
     10 * The extra data is omitted if not running the test in debug mode (`?debug=1`).
     11 */
     12 export class ErrorWithExtra extends Error {
     13  readonly extra: { [k: string]: unknown };
     14 
     15  /**
     16   * `extra` function is only called if in debug mode.
     17   * If an `ErrorWithExtra` is passed, its message is used and its extras are passed through.
     18   */
     19  constructor(message: string, extra: () => {});
     20  constructor(base: ErrorWithExtra, newExtra: () => {});
     21  constructor(baseOrMessage: string | ErrorWithExtra, newExtra: () => {}) {
     22    const message = typeof baseOrMessage === 'string' ? baseOrMessage : baseOrMessage.message;
     23    super(message);
     24 
     25    const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {};
     26    this.extra = globalTestConfig.enableDebugLogs
     27      ? { ...oldExtras, ...newExtra() }
     28      : { omitted: 'pass ?debug=1' };
     29  }
     30 }
     31 
     32 /**
     33 * Asserts `condition` is true. Otherwise, throws an `Error` with the provided message.
     34 */
     35 export function assert(condition: boolean, msg?: string | (() => string)): asserts condition {
     36  if (!condition) {
     37    throw new Error(msg && (typeof msg === 'string' ? msg : msg()));
     38  }
     39 }
     40 
     41 /** If the argument is an Error, throw it. Otherwise, pass it back. */
     42 export function assertOK<T>(value: Error | T): T {
     43  if (value instanceof Error) {
     44    throw value;
     45  }
     46  return value;
     47 }
     48 
     49 /** Options for assertReject, shouldReject, and friends. */
     50 export type ExceptionCheckOptions = { allowMissingStack?: boolean; message?: string };
     51 
     52 /**
     53 * Resolves if the provided promise rejects; rejects if it does not.
     54 */
     55 export async function assertReject(
     56  expectedName: string,
     57  p: Promise<unknown>,
     58  { allowMissingStack = false, message }: ExceptionCheckOptions = {}
     59 ): Promise<void> {
     60  await p.then(
     61    () => {
     62      unreachable(message);
     63    },
     64    ex => {
     65      assert(ex instanceof Error, 'rejected with a non-Error object');
     66      assert(ex.name === expectedName, `rejected with name ${ex.name} instead of ${expectedName}`);
     67      // Asserted as expected
     68      if (!allowMissingStack) {
     69        const m = message ? ` (${message})` : '';
     70        assert(typeof ex.stack === 'string', 'threw as expected, but missing stack' + m);
     71      }
     72    }
     73  );
     74 }
     75 
     76 /**
     77 * Assert this code is unreachable. Unconditionally throws an `Error`.
     78 */
     79 export function unreachable(msg?: string): never {
     80  throw new Error(msg);
     81 }
     82 
     83 /**
     84 * Throw a `SkipTestCase` exception, which skips the test case.
     85 */
     86 export function skipTestCase(msg: string): never {
     87  throw new SkipTestCase(msg);
     88 }
     89 
     90 /**
     91 * The `performance` interface.
     92 * It is available in all browsers, but it is not in scope by default in Node.
     93 */
     94 /* eslint-disable-next-line n/no-restricted-require */
     95 const perf = typeof performance !== 'undefined' ? performance : require('perf_hooks').performance;
     96 
     97 /**
     98 * Calls the appropriate `performance.now()` depending on whether running in a browser or Node.
     99 */
    100 export function now(): number {
    101  return perf.now();
    102 }
    103 
    104 /**
    105 * Returns a promise which resolves after the specified time.
    106 */
    107 export function resolveOnTimeout(ms: number): Promise<void> {
    108  return new Promise(resolve => {
    109    timeout(() => {
    110      resolve();
    111    }, ms);
    112  });
    113 }
    114 
    115 export class PromiseTimeoutError extends Error {}
    116 
    117 /**
    118 * Returns a promise which rejects after the specified time.
    119 */
    120 export function rejectOnTimeout(ms: number, msg: string): Promise<never> {
    121  return new Promise((_resolve, reject) => {
    122    timeout(() => {
    123      reject(new PromiseTimeoutError(msg));
    124    }, ms);
    125  });
    126 }
    127 
    128 /**
    129 * Takes a promise `p`, and returns a new one which rejects if `p` takes too long,
    130 * and otherwise passes the result through.
    131 */
    132 export function raceWithRejectOnTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
    133  if (globalTestConfig.noRaceWithRejectOnTimeout) {
    134    return p;
    135  }
    136  // Setup a promise that will reject after `ms` milliseconds. We cancel this timeout when
    137  // `p` is finalized, so the JavaScript VM doesn't hang around waiting for the timer to
    138  // complete, once the test runner has finished executing the tests.
    139  const timeoutPromise = new Promise((_resolve, reject) => {
    140    const handle = timeout(() => {
    141      reject(new PromiseTimeoutError(msg));
    142    }, ms);
    143    p = p.finally(() => clearTimeout(handle));
    144  });
    145  return Promise.race([p, timeoutPromise]) as Promise<T>;
    146 }
    147 
    148 /**
    149 * Takes a promise `p` and returns a new one which rejects if `p` resolves or rejects,
    150 * and otherwise resolves after the specified time.
    151 */
    152 export function assertNotSettledWithinTime(
    153  p: Promise<unknown>,
    154  ms: number,
    155  msg: string
    156 ): Promise<undefined> {
    157  // Rejects regardless of whether p resolves or rejects.
    158  const rejectWhenSettled = p.then(() => Promise.reject(new Error(msg)));
    159  // Resolves after `ms` milliseconds.
    160  const timeoutPromise = new Promise<undefined>(resolve => {
    161    const handle = timeout(() => {
    162      resolve(undefined);
    163    }, ms);
    164    void p.finally(() => clearTimeout(handle));
    165  });
    166  return Promise.race([rejectWhenSettled, timeoutPromise]);
    167 }
    168 
    169 /**
    170 * Returns a `Promise.reject()`, but also registers a dummy `.catch()` handler so it doesn't count
    171 * as an uncaught promise rejection in the runtime.
    172 */
    173 export function rejectWithoutUncaught<T>(err: unknown): Promise<T> {
    174  const p = Promise.reject(err);
    175  // Suppress uncaught promise rejection.
    176  p.catch(() => {});
    177  return p;
    178 }
    179 
    180 /**
    181 * Returns true if v is a plain JavaScript object.
    182 */
    183 export function isPlainObject(v: unknown) {
    184  return !!v && Object.getPrototypeOf(v).constructor === Object.prototype.constructor;
    185 }
    186 
    187 /**
    188 * Makes a copy of a JS `object`, with the keys reordered into sorted order.
    189 */
    190 export function sortObjectByKey(v: { [k: string]: unknown }): { [k: string]: unknown } {
    191  const sortedObject: { [k: string]: unknown } = {};
    192  for (const k of Object.keys(v).sort()) {
    193    sortedObject[k] = v[k];
    194  }
    195  return sortedObject;
    196 }
    197 
    198 /**
    199 * Determines whether two JS values are equal, recursing into objects and arrays.
    200 * NaN is treated specially, such that `objectEquals(NaN, NaN)`. +/-0.0 are treated as equal
    201 * by default, but can be opted to be distinguished.
    202 * @param x the first JS values that get compared
    203 * @param y the second JS values that get compared
    204 * @param distinguishSignedZero if set to true, treat 0.0 and -0.0 as unequal. Default to false.
    205 */
    206 export function objectEquals(
    207  x: unknown,
    208  y: unknown,
    209  distinguishSignedZero: boolean = false
    210 ): boolean {
    211  if (typeof x !== 'object' || typeof y !== 'object') {
    212    if (typeof x === 'number' && typeof y === 'number' && Number.isNaN(x) && Number.isNaN(y)) {
    213      return true;
    214    }
    215    // Object.is(0.0, -0.0) is false while (0.0 === -0.0) is true. Other than +/-0.0 and NaN cases,
    216    // Object.is works in the same way as ===.
    217    return distinguishSignedZero ? Object.is(x, y) : x === y;
    218  }
    219  if (x === null || y === null) return x === y;
    220  if (x.constructor !== y.constructor) return false;
    221  if (x instanceof Function) return x === y;
    222  if (x instanceof RegExp) return x === y;
    223  if (x === y || x.valueOf() === y.valueOf()) return true;
    224  if (Array.isArray(x) && Array.isArray(y) && x.length !== y.length) return false;
    225  if (x instanceof Date) return false;
    226  if (!(x instanceof Object)) return false;
    227  if (!(y instanceof Object)) return false;
    228 
    229  const x1 = x as { [k: string]: unknown };
    230  const y1 = y as { [k: string]: unknown };
    231  const p = Object.keys(x);
    232  return Object.keys(y).every(i => p.indexOf(i) !== -1) && p.every(i => objectEquals(x1[i], y1[i]));
    233 }
    234 
    235 /**
    236 * Generates a range of values `fn(0)..fn(n-1)`.
    237 */
    238 export function range<T>(n: number, fn: (i: number) => T): T[] {
    239  return [...new Array(n)].map((_, i) => fn(i));
    240 }
    241 
    242 /**
    243 * Generates a range of values `fn(0)..fn(n-1)`.
    244 */
    245 export function* iterRange<T>(n: number, fn: (i: number) => T): Iterable<T> {
    246  for (let i = 0; i < n; ++i) {
    247    yield fn(i);
    248  }
    249 }
    250 
    251 /** Creates a (reusable) iterable object that maps `f` over `xs`, lazily. */
    252 export function mapLazy<T, R>(xs: Iterable<T>, f: (x: T) => R): Iterable<R> {
    253  return {
    254    *[Symbol.iterator]() {
    255      for (const x of xs) {
    256        yield f(x);
    257      }
    258    },
    259  };
    260 }
    261 
    262 /** Count the number of elements `x` for which `predicate(x)` is true. */
    263 export function count<T>(xs: Iterable<T>, predicate: (x: T) => boolean): number {
    264  let count = 0;
    265  for (const x of xs) {
    266    if (predicate(x)) count++;
    267  }
    268  return count;
    269 }
    270 
    271 const ReorderOrders = {
    272  forward: true,
    273  backward: true,
    274  shiftByHalf: true,
    275 };
    276 export type ReorderOrder = keyof typeof ReorderOrders;
    277 export const kReorderOrderKeys = keysOf(ReorderOrders);
    278 
    279 /**
    280 * Creates a new array from the given array with the first half
    281 * swapped with the last half.
    282 */
    283 export function shiftByHalf<R>(arr: R[]): R[] {
    284  const len = arr.length;
    285  const half = (len / 2) | 0;
    286  const firstHalf = arr.splice(0, half);
    287  return [...arr, ...firstHalf];
    288 }
    289 
    290 /**
    291 * Creates a reordered array from the input array based on the Order
    292 */
    293 export function reorder<R>(order: ReorderOrder, arr: R[]): R[] {
    294  switch (order) {
    295    case 'forward':
    296      return arr.slice();
    297    case 'backward':
    298      return arr.slice().reverse();
    299    case 'shiftByHalf': {
    300      // should this be pseudo random?
    301      return shiftByHalf(arr);
    302    }
    303  }
    304 }
    305 
    306 /**
    307 * A typed version of Object.entries
    308 */
    309 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    310 export function typedEntries<T extends Record<string, any>>(obj: T): Array<[keyof T, T[keyof T]]> {
    311  // The cast is done once, inside the helper function,
    312  // keeping the call site clean and type-safe.
    313  return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
    314 }
    315 
    316 const TypedArrayBufferViewInstances = [
    317  new Uint8Array(),
    318  new Uint8ClampedArray(),
    319  new Uint16Array(),
    320  new Uint32Array(),
    321  new Int8Array(),
    322  new Int16Array(),
    323  new Int32Array(),
    324  new Float16Array(),
    325  new Float32Array(),
    326  new Float64Array(),
    327  new BigInt64Array(),
    328  new BigUint64Array(),
    329 ] as const;
    330 
    331 export type TypedArrayBufferView = (typeof TypedArrayBufferViewInstances)[number];
    332 
    333 export type TypedArrayBufferViewConstructor<A extends TypedArrayBufferView = TypedArrayBufferView> =
    334  {
    335    // Interface copied from Uint8Array, and made generic.
    336    readonly prototype: A;
    337    readonly BYTES_PER_ELEMENT: number;
    338 
    339    new (): A;
    340    new (elements: Iterable<number>): A;
    341    new (array: ArrayLike<number> | ArrayBufferLike): A;
    342    new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): A;
    343    new (length: number): A;
    344 
    345    from(arrayLike: ArrayLike<number>): A;
    346    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    347    from(arrayLike: Iterable<number>, mapfn?: (v: number, k: number) => number, thisArg?: any): A;
    348    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    349    from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): A;
    350    of(...items: number[]): A;
    351  };
    352 
    353 export const kTypedArrayBufferViews: {
    354  readonly [k: string]: TypedArrayBufferViewConstructor;
    355 } = {
    356  ...(() => {
    357    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    358    const result: { [k: string]: any } = {};
    359    for (const v of TypedArrayBufferViewInstances) {
    360      result[v.constructor.name] = v.constructor;
    361    }
    362    return result;
    363  })(),
    364 };
    365 export const kTypedArrayBufferViewKeys = keysOf(kTypedArrayBufferViews);
    366 export const kTypedArrayBufferViewConstructors = Object.values(kTypedArrayBufferViews);
    367 
    368 interface TypedArrayMap {
    369  Int8Array: Int8Array;
    370  Uint8Array: Uint8Array;
    371  Int16Array: Int16Array;
    372  Uint16Array: Uint16Array;
    373  Uint8ClampedArray: Uint8ClampedArray;
    374  Int32Array: Int32Array;
    375  Uint32Array: Uint32Array;
    376  Float32Array: Float32Array;
    377  Float64Array: Float64Array;
    378  BigInt64Array: BigInt64Array;
    379  BigUint64Array: BigUint64Array;
    380 }
    381 
    382 type TypedArrayParam<K extends keyof TypedArrayMap> = {
    383  type: K;
    384  data: readonly number[];
    385 };
    386 
    387 /**
    388 * Creates a case parameter for a typedarray.
    389 *
    390 * You can't put typedarrays in case parameters directly so instead of
    391 *
    392 * ```
    393 * u.combine('data', [
    394 *   new Uint8Array([1, 2, 3]),
    395 *   new Float32Array([4, 5, 6]),
    396 * ])
    397 * ```
    398 *
    399 * You can use
    400 *
    401 * ```
    402 * u.combine('data', [
    403 *   typedArrayParam('Uint8Array' [1, 2, 3]),
    404 *   typedArrayParam('Float32Array' [4, 5, 6]),
    405 * ])
    406 * ```
    407 *
    408 * and then convert the params to typedarrays eg.
    409 *
    410 * ```
    411 *  .fn(t => {
    412 *    const data = t.params.data.map(v => typedArrayFromParam(v));
    413 *  })
    414 * ```
    415 */
    416 export function typedArrayParam<K extends keyof TypedArrayMap>(
    417  type: K,
    418  data: number[]
    419 ): TypedArrayParam<K> {
    420  return { type, data };
    421 }
    422 
    423 export function createTypedArray<K extends keyof TypedArrayMap>(
    424  type: K,
    425  data: readonly number[]
    426 ): TypedArrayMap[K] {
    427  return new kTypedArrayBufferViews[type](data) as TypedArrayMap[K];
    428 }
    429 
    430 /**
    431 * Converts a TypedArrayParam to a typedarray. See typedArrayParam
    432 */
    433 export function typedArrayFromParam<K extends keyof TypedArrayMap>(
    434  param: TypedArrayParam<K>
    435 ): TypedArrayMap[K] {
    436  const { type, data } = param;
    437  return createTypedArray(type, data);
    438 }
    439 
    440 function subarrayAsU8(
    441  buf: ArrayBuffer | TypedArrayBufferView,
    442  { start = 0, length }: { start?: number; length?: number }
    443 ): Uint8Array | Uint8ClampedArray {
    444  if (buf instanceof ArrayBuffer) {
    445    return new Uint8Array(buf, start, length);
    446  } else if (buf instanceof Uint8Array || buf instanceof Uint8ClampedArray) {
    447    // Don't wrap in new views if we don't need to.
    448    if (start === 0 && (length === undefined || length === buf.byteLength)) {
    449      return buf;
    450    }
    451  }
    452  const byteOffset = buf.byteOffset + start * buf.BYTES_PER_ELEMENT;
    453  const byteLength =
    454    length !== undefined
    455      ? length * buf.BYTES_PER_ELEMENT
    456      : buf.byteLength - (byteOffset - buf.byteOffset);
    457  return new Uint8Array(buf.buffer, byteOffset, byteLength);
    458 }
    459 
    460 /**
    461 * Copy a range of bytes from one ArrayBuffer or TypedArray to another.
    462 *
    463 * `start`/`length` are in elements (or in bytes, if ArrayBuffer).
    464 */
    465 export function memcpy(
    466  src: { src: ArrayBuffer | TypedArrayBufferView; start?: number; length?: number },
    467  dst: { dst: ArrayBuffer | TypedArrayBufferView; start?: number }
    468 ): void {
    469  subarrayAsU8(dst.dst, dst).set(subarrayAsU8(src.src, src));
    470 }
    471 
    472 /**
    473 * Used to create a value that is specified by multiplying some runtime value
    474 * by a constant and then adding a constant to it.
    475 */
    476 export interface ValueTestVariant {
    477  mult: number;
    478  add: number;
    479 }
    480 
    481 /**
    482 * Filters out SpecValues that are the same.
    483 */
    484 export function filterUniqueValueTestVariants(valueTestVariants: ValueTestVariant[]) {
    485  return new Map<string, ValueTestVariant>(
    486    valueTestVariants.map(v => [`m:${v.mult},a:${v.add}`, v])
    487  ).values();
    488 }
    489 
    490 /**
    491 * Used to create a value that is specified by multiplied some runtime value
    492 * by a constant and then adding a constant to it. This happens often in test
    493 * with limits that can only be known at runtime and yet we need a way to
    494 * add parameters to a test and those parameters must be constants.
    495 */
    496 export function makeValueTestVariant(base: number, variant: ValueTestVariant) {
    497  return base * variant.mult + variant.add;
    498 }
    499 
    500 /**
    501 * Use instead of features.has because feature's has takes any string
    502 * and we want to prevent typos.
    503 */
    504 export function hasFeature(features: GPUSupportedFeatures, feature: GPUFeatureName) {
    505  // eslint-disable-next-line no-restricted-syntax
    506  return features.has(feature);
    507 }
    508 
    509 /** Convenience helper for combinations of 1-2 usage bits from a list of usage bits. */
    510 export function combinationsOfOneOrTwoUsages(usages: readonly number[]) {
    511  const combinations = [];
    512  for (const usage0 of usages) {
    513    for (const usage1 of usages) {
    514      if (usage0 <= usage1) {
    515        combinations.push(usage0 | usage1);
    516      }
    517    }
    518  }
    519  return combinations;
    520 }