tor-browser

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

support-promises.js (14317B)


      1 'use strict';
      2 
      3 // Returns an IndexedDB database name that is unique to the test case.
      4 function databaseName(testCase) {
      5  return 'db' + self.location.pathname + '-' + testCase.name;
      6 }
      7 
      8 // EventWatcher covering all the events defined on IndexedDB requests.
      9 //
     10 // The events cover IDBRequest and IDBOpenDBRequest.
     11 function requestWatcher(testCase, request) {
     12  return new EventWatcher(testCase, request,
     13                          ['blocked', 'error', 'success', 'upgradeneeded']);
     14 }
     15 
     16 // EventWatcher covering all the events defined on IndexedDB transactions.
     17 //
     18 // The events cover IDBTransaction.
     19 function transactionWatcher(testCase, request) {
     20  return new EventWatcher(testCase, request, ['abort', 'complete', 'error']);
     21 }
     22 
     23 // Promise that resolves with an IDBRequest's result.
     24 //
     25 // The promise only resolves if IDBRequest receives the "success" event. Any
     26 // other event causes the promise to reject with an error. This is correct in
     27 // most cases, but insufficient for indexedDB.open(), which issues
     28 // "upgradeneded" events under normal operation.
     29 function promiseForRequest(testCase, request) {
     30  const eventWatcher = requestWatcher(testCase, request);
     31  return eventWatcher.wait_for('success').then(event => event.target.result);
     32 }
     33 
     34 // Promise that resolves when an IDBTransaction completes.
     35 //
     36 // The promise resolves with undefined if IDBTransaction receives the "complete"
     37 // event, and rejects with an error for any other event.
     38 function promiseForTransaction(testCase, request) {
     39  const eventWatcher = transactionWatcher(testCase, request);
     40  return eventWatcher.wait_for('complete').then(() => {});
     41 }
     42 
     43 // Migrates an IndexedDB database whose name is unique for the test case.
     44 //
     45 // newVersion must be greater than the database's current version.
     46 //
     47 // migrationCallback will be called during a versionchange transaction and will
     48 // given the created database, the versionchange transaction, and the database
     49 // open request.
     50 //
     51 // Returns a promise. If the versionchange transaction goes through, the promise
     52 // resolves to an IndexedDB database that should be closed by the caller. If the
     53 // versionchange transaction is aborted, the promise resolves to an error.
     54 function migrateDatabase(testCase, newVersion, migrationCallback) {
     55  return migrateNamedDatabase(
     56      testCase, databaseName(testCase), newVersion, migrationCallback);
     57 }
     58 
     59 // Migrates an IndexedDB database.
     60 //
     61 // newVersion must be greater than the database's current version.
     62 //
     63 // migrationCallback will be called during a versionchange transaction and will
     64 // given the created database, the versionchange transaction, and the database
     65 // open request.
     66 //
     67 // Returns a promise. If the versionchange transaction goes through, the promise
     68 // resolves to an IndexedDB database that should be closed by the caller. If the
     69 // versionchange transaction is aborted, the promise resolves to an error.
     70 function migrateNamedDatabase(
     71    testCase, databaseName, newVersion, migrationCallback) {
     72  // We cannot use eventWatcher.wait_for('upgradeneeded') here, because
     73  // the versionchange transaction auto-commits before the Promise's then
     74  // callback gets called.
     75  return new Promise((resolve, reject) => {
     76    const request = indexedDB.open(databaseName, newVersion);
     77    request.onupgradeneeded = testCase.step_func(event => {
     78      const database = event.target.result;
     79      const transaction = event.target.transaction;
     80      let shouldBeAborted = false;
     81      let requestEventPromise = null;
     82 
     83      // We wrap IDBTransaction.abort so we can set up the correct event
     84      // listeners and expectations if the test chooses to abort the
     85      // versionchange transaction.
     86      const transactionAbort = transaction.abort.bind(transaction);
     87      transaction.abort = () => {
     88        transaction._willBeAborted();
     89        transactionAbort();
     90      }
     91      transaction._willBeAborted = () => {
     92        requestEventPromise = new Promise((resolve, reject) => {
     93          request.onerror = event => {
     94            event.preventDefault();
     95            resolve(event.target.error);
     96          };
     97          request.onsuccess = () => reject(new Error(
     98              'indexedDB.open should not succeed for an aborted ' +
     99              'versionchange transaction'));
    100        });
    101        shouldBeAborted = true;
    102      }
    103 
    104      // If migration callback returns a promise, we'll wait for it to resolve.
    105      // This simplifies some tests.
    106      const callbackResult = migrationCallback(database, transaction, request);
    107      if (!shouldBeAborted) {
    108        request.onerror = null;
    109        request.onsuccess = null;
    110        requestEventPromise = promiseForRequest(testCase, request);
    111      }
    112 
    113      // requestEventPromise needs to be the last promise in the chain, because
    114      // we want the event that it resolves to.
    115      resolve(Promise.resolve(callbackResult).then(() => requestEventPromise));
    116    });
    117    request.onerror = event => reject(event.target.error);
    118    request.onsuccess = () => {
    119      const database = request.result;
    120      testCase.add_cleanup(() => { database.close(); });
    121      reject(new Error(
    122          'indexedDB.open should not succeed without creating a ' +
    123          'versionchange transaction'));
    124    };
    125  }).then(databaseOrError => {
    126    if (databaseOrError instanceof IDBDatabase)
    127      testCase.add_cleanup(() => { databaseOrError.close(); });
    128    return databaseOrError;
    129  });
    130 }
    131 
    132 // Creates an IndexedDB database whose name is unique for the test case.
    133 //
    134 // setupCallback will be called during a versionchange transaction, and will be
    135 // given the created database, the versionchange transaction, and the database
    136 // open request.
    137 //
    138 // Returns a promise that resolves to an IndexedDB database. The caller should
    139 // close the database.
    140 function createDatabase(testCase, setupCallback) {
    141  return createNamedDatabase(testCase, databaseName(testCase), setupCallback);
    142 }
    143 
    144 // Creates an IndexedDB database.
    145 //
    146 // setupCallback will be called during a versionchange transaction, and will be
    147 // given the created database, the versionchange transaction, and the database
    148 // open request.
    149 //
    150 // Returns a promise that resolves to an IndexedDB database. The caller should
    151 // close the database.
    152 function createNamedDatabase(testCase, databaseName, setupCallback) {
    153  const request = indexedDB.deleteDatabase(databaseName);
    154  return promiseForRequest(testCase, request).then(() => {
    155    testCase.add_cleanup(() => { indexedDB.deleteDatabase(databaseName); });
    156    return migrateNamedDatabase(testCase, databaseName, 1, setupCallback)
    157  });
    158 }
    159 
    160 // Opens an IndexedDB database without performing schema changes.
    161 //
    162 // The given version number must match the database's current version.
    163 //
    164 // Returns a promise that resolves to an IndexedDB database. The caller should
    165 // close the database.
    166 function openDatabase(testCase, version) {
    167  return openNamedDatabase(testCase, databaseName(testCase), version);
    168 }
    169 
    170 // Opens an IndexedDB database without performing schema changes.
    171 //
    172 // The given version number must match the database's current version.
    173 //
    174 // Returns a promise that resolves to an IndexedDB database. The caller should
    175 // close the database.
    176 function openNamedDatabase(testCase, databaseName, version) {
    177  const request = indexedDB.open(databaseName, version);
    178  return promiseForRequest(testCase, request).then(database => {
    179    testCase.add_cleanup(() => { database.close(); });
    180    return database;
    181  });
    182 }
    183 
    184 // The data in the 'books' object store records in the first example of the
    185 // IndexedDB specification.
    186 const BOOKS_RECORD_DATA = [
    187  { title: 'Quarry Memories', author: 'Fred', isbn: 123456 },
    188  { title: 'Water Buffaloes', author: 'Fred', isbn: 234567 },
    189  { title: 'Bedrock Nights', author: 'Barney', isbn: 345678 },
    190 ];
    191 
    192 // Creates a 'books' object store whose contents closely resembles the first
    193 // example in the IndexedDB specification.
    194 const createBooksStore = (testCase, database) => {
    195  const store = database.createObjectStore('books',
    196      { keyPath: 'isbn', autoIncrement: true });
    197  store.createIndex('by_author', 'author');
    198  store.createIndex('by_title', 'title', { unique: true });
    199  for (const record of BOOKS_RECORD_DATA)
    200      store.put(record);
    201  return store;
    202 }
    203 
    204 // Creates a 'books' object store whose contents closely resembles the first
    205 // example in the IndexedDB specification, just without autoincrementing.
    206 const createBooksStoreWithoutAutoIncrement = (testCase, database) => {
    207  const store = database.createObjectStore('books',
    208      { keyPath: 'isbn' });
    209  store.createIndex('by_author', 'author');
    210  store.createIndex('by_title', 'title', { unique: true });
    211  for (const record of BOOKS_RECORD_DATA)
    212      store.put(record);
    213  return store;
    214 }
    215 
    216 // Creates a 'not_books' object store used to test renaming into existing or
    217 // deleted store names.
    218 function createNotBooksStore(testCase, database) {
    219  const store = database.createObjectStore('not_books');
    220  store.createIndex('not_by_author', 'author');
    221  store.createIndex('not_by_title', 'title', { unique: true });
    222  return store;
    223 }
    224 
    225 // Verifies that an object store's indexes match the indexes used to create the
    226 // books store in the test database's version 1.
    227 //
    228 // The errorMessage is used if the assertions fail. It can state that the
    229 // IndexedDB implementation being tested is incorrect, or that the testing code
    230 // is using it incorrectly.
    231 function checkStoreIndexes (testCase, store, errorMessage) {
    232  assert_array_equals(
    233      store.indexNames, ['by_author', 'by_title'], errorMessage);
    234  const authorIndex = store.index('by_author');
    235  const titleIndex = store.index('by_title');
    236  return Promise.all([
    237      checkAuthorIndexContents(testCase, authorIndex, errorMessage),
    238      checkTitleIndexContents(testCase, titleIndex, errorMessage),
    239  ]);
    240 }
    241 
    242 // Verifies that an object store's key generator is in the same state as the
    243 // key generator created for the books store in the test database's version 1.
    244 //
    245 // The errorMessage is used if the assertions fail. It can state that the
    246 // IndexedDB implementation being tested is incorrect, or that the testing code
    247 // is using it incorrectly.
    248 function checkStoreGenerator(testCase, store, expectedKey, errorMessage) {
    249  const request = store.put(
    250      { title: 'Bedrock Nights ' + expectedKey, author: 'Barney' });
    251  return promiseForRequest(testCase, request).then(result => {
    252    assert_equals(result, expectedKey, errorMessage);
    253  });
    254 }
    255 
    256 // Verifies that an object store's contents matches the contents used to create
    257 // the books store in the test database's version 1.
    258 //
    259 // The errorMessage is used if the assertions fail. It can state that the
    260 // IndexedDB implementation being tested is incorrect, or that the testing code
    261 // is using it incorrectly.
    262 function checkStoreContents(testCase, store, errorMessage) {
    263  const request = store.get(123456);
    264  return promiseForRequest(testCase, request).then(result => {
    265    assert_equals(result.isbn, BOOKS_RECORD_DATA[0].isbn, errorMessage);
    266    assert_equals(result.author, BOOKS_RECORD_DATA[0].author, errorMessage);
    267    assert_equals(result.title, BOOKS_RECORD_DATA[0].title, errorMessage);
    268  });
    269 }
    270 
    271 // Verifies that index matches the 'by_author' index used to create the
    272 // by_author books store in the test database's version 1.
    273 //
    274 // The errorMessage is used if the assertions fail. It can state that the
    275 // IndexedDB implementation being tested is incorrect, or that the testing code
    276 // is using it incorrectly.
    277 function checkAuthorIndexContents(testCase, index, errorMessage) {
    278  const request = index.get(BOOKS_RECORD_DATA[2].author);
    279  return promiseForRequest(testCase, request).then(result => {
    280    assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
    281    assert_equals(result.title, BOOKS_RECORD_DATA[2].title, errorMessage);
    282  });
    283 }
    284 
    285 // Verifies that an index matches the 'by_title' index used to create the books
    286 // store in the test database's version 1.
    287 //
    288 // The errorMessage is used if the assertions fail. It can state that the
    289 // IndexedDB implementation being tested is incorrect, or that the testing code
    290 // is using it incorrectly.
    291 function checkTitleIndexContents(testCase, index, errorMessage) {
    292  const request = index.get(BOOKS_RECORD_DATA[2].title);
    293  return promiseForRequest(testCase, request).then(result => {
    294    assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
    295    assert_equals(result.author, BOOKS_RECORD_DATA[2].author, errorMessage);
    296  });
    297 }
    298 
    299 // Returns an Uint8Array.
    300 // When `seed` is non-zero, the data is pseudo-random, otherwise it is repetitive.
    301 // The PRNG should be sufficient to defeat compression schemes, but it is not
    302 // cryptographically strong.
    303 function largeValue(size, seed) {
    304  const buffer = new Uint8Array(size);
    305  // Fill with a lot of the same byte.
    306  if (seed == 0) {
    307    buffer.fill(0x11, 0, size - 1);
    308    return buffer;
    309  }
    310 
    311  // 32-bit xorshift - the seed can't be zero
    312  let state = 1000 + seed;
    313 
    314  for (let i = 0; i < size; ++i) {
    315    state ^= state << 13;
    316    state ^= state >> 17;
    317    state ^= state << 5;
    318    buffer[i] = state & 0xff;
    319  }
    320 
    321  return buffer;
    322 }
    323 
    324 async function deleteAllDatabases(testCase) {
    325  const dbs_to_delete = await indexedDB.databases();
    326  for( const db_info of dbs_to_delete) {
    327    let request = indexedDB.deleteDatabase(db_info.name);
    328    let eventWatcher = requestWatcher(testCase, request);
    329    await eventWatcher.wait_for('success');
    330  }
    331 }
    332 
    333 // Keeps the passed transaction alive indefinitely (by making requests
    334 // against the named store). Returns a function that asserts that the
    335 // transaction has not already completed and then ends the request loop so that
    336 // the transaction may autocommit and complete.
    337 function keepAlive(testCase, transaction, storeName) {
    338  let completed = false;
    339  transaction.addEventListener('complete', () => { completed = true; });
    340 
    341  let keepSpinning = true;
    342 
    343  function spin() {
    344    if (!keepSpinning)
    345      return;
    346    transaction.objectStore(storeName).get(0).onsuccess = spin;
    347  }
    348  spin();
    349 
    350  return testCase.step_func(() => {
    351    assert_false(completed, 'Transaction completed while kept alive');
    352    keepSpinning = false;
    353  });
    354 }
    355 
    356 // Return a promise that resolves after a setTimeout finishes to break up the
    357 // scope of a function's execution.
    358 function timeoutPromise(ms) {
    359  return new Promise(resolve => { setTimeout(resolve, ms); });
    360 }