tor-browser

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

test-helpers.js (12468B)


      1 // A special path component meaning "this directory."
      2 const kCurrentDirectory = '.';
      3 
      4 // A special path component meaning "the parent directory."
      5 const kParentDirectory = '..';
      6 
      7 // The lock modes of a writable file stream.
      8 const WFS_MODES = ['siloed', 'exclusive'];
      9 
     10 // The lock modes of an access handle.
     11 const SAH_MODES = ['readwrite', 'read-only', 'readwrite-unsafe'];
     12 
     13 // Possible return values of testLockAccess.
     14 const LOCK_ACCESS = {
     15  SHARED: 'shared',
     16  EXCLUSIVE: 'exclusive',
     17 };
     18 
     19 function primitiveModesAreContentious(exclusiveMode, mode1, mode2) {
     20  return mode1 != mode2 || mode1 === exclusiveMode;
     21 }
     22 
     23 function sahModesAreContentious(mode1, mode2) {
     24  return primitiveModesAreContentious('readwrite', mode1, mode2);
     25 }
     26 
     27 function wfsModesAreContentious(mode1, mode2) {
     28  return primitiveModesAreContentious('exclusive', mode1, mode2);
     29 }
     30 
     31 // Array of separators used to separate components in hierarchical paths.
     32 // Consider both '/' and '\' as path separators to ensure file names are
     33 // platform-agnostic.
     34 let kPathSeparators = ['/', '\\'];
     35 
     36 async function getFileSize(handle) {
     37  const file = await handle.getFile();
     38  return file.size;
     39 }
     40 
     41 async function getFileContents(handle) {
     42  const file = await handle.getFile();
     43  return new Response(file).text();
     44 }
     45 
     46 async function getDirectoryEntryCount(handle) {
     47  let result = 0;
     48  for await (let entry of handle) {
     49    result++;
     50  }
     51  return result;
     52 }
     53 
     54 async function getSortedDirectoryEntries(handle) {
     55  let result = [];
     56  for await (let entry of handle.values()) {
     57    if (entry.kind === 'directory') {
     58      result.push(entry.name + '/');
     59    } else {
     60      result.push(entry.name);
     61    }
     62  }
     63  result.sort();
     64  return result;
     65 }
     66 
     67 async function createDirectory(name, parent) {
     68  return await parent.getDirectoryHandle(name, {create: true});
     69 }
     70 
     71 async function createEmptyFile(name, parent) {
     72  const handle = await parent.getFileHandle(name, {create: true});
     73  // Make sure the file is empty.
     74  assert_equals(await getFileSize(handle), 0);
     75  return handle;
     76 }
     77 
     78 async function createFileWithContents(name, contents, parent) {
     79  const handle = await createEmptyFile(name, parent);
     80  const writer = await handle.createWritable();
     81  await writer.write(new Blob([contents]));
     82  await writer.close();
     83  return handle;
     84 }
     85 
     86 var fs_cleanups = [];
     87 
     88 async function cleanup(test, value, cleanup_func) {
     89  if (fs_cleanups.length === 0) {
     90    // register to get called back once from cleanup
     91    test.add_cleanup(async () => {
     92      // Cleanup in LIFO order to ensure locks are released correctly relative
     93      // to thinks like removeEntry().  Do so in a serialized form, not in parallel!
     94      fs_cleanups.reverse();
     95      for (let cleanup of fs_cleanups) {
     96        try {
     97          await cleanup();
     98        } catch (e) {
     99          // Ignore any errors when removing files, as tests might already remove
    100          // the file.
    101        }
    102      }
    103      fs_cleanups.length = 0;
    104    });
    105  }
    106  fs_cleanups.push(cleanup_func);
    107  return value;
    108 }
    109 
    110 async function cleanup_writable(test, value) {
    111  return cleanup(test, value, async () => {
    112    try {
    113      return (await value).close();
    114    } catch (e) {
    115      // Ignore any errors when closing writables, since attempting to close
    116      // aborted or closed writables will error.
    117    }
    118  });
    119 }
    120 
    121 function getUniqueName(name) {
    122  return `unique${Date.now()}${Math.random().toString().slice(2)}`;
    123 }
    124 
    125 function createFileHandles(dir, ...fileNames) {
    126  return Promise.all(
    127      fileNames.map(fileName => dir.getFileHandle(fileName, {create: true})));
    128 }
    129 
    130 function createDirectoryHandles(dir, ...dirNames) {
    131  return Promise.all(
    132      dirNames.map(dirName => dir.getDirectoryHandle(dirName, {create: true})));
    133 }
    134 
    135 // Releases a lock created by one of the create*WithCleanup functions below.
    136 async function releaseLock(lockPromise) {
    137  const result = await lockPromise;
    138  if (result?.close) {
    139    await result.close();
    140  }
    141 }
    142 
    143 function cleanupLockPromise(t, lockPromise) {
    144  return cleanup(t, lockPromise, () => releaseLock(lockPromise));
    145 }
    146 
    147 function createWFSWithCleanup(t, fileHandle, wfsOptions) {
    148  return cleanupLockPromise(t, fileHandle.createWritable(wfsOptions));
    149 }
    150 
    151 // Returns createWFSWithCleanup bound with wfsOptions.
    152 function createWFSWithCleanupFactory(wfsOptions) {
    153  return (t, fileHandle) => createWFSWithCleanup(t, fileHandle, wfsOptions);
    154 }
    155 
    156 function createSAHWithCleanup(t, fileHandle, sahOptions) {
    157  return cleanupLockPromise(t, fileHandle.createSyncAccessHandle(sahOptions));
    158 }
    159 
    160 // Returns createSAHWithCleanup bound with sahOptions.
    161 function createSAHWithCleanupFactory(sahOptions) {
    162  return (t, fileHandle) => createSAHWithCleanup(t, fileHandle, sahOptions);
    163 }
    164 
    165 function createMoveWithCleanup(
    166    t, fileHandle, fileName = 'unique-file-name.test') {
    167  return cleanupLockPromise(t, fileHandle.move(fileName));
    168 }
    169 
    170 function createRemoveWithCleanup(t, fileHandle) {
    171  return cleanupLockPromise(t, fileHandle.remove({recursive: true}));
    172 }
    173 
    174 // For each key in `testFuncs` if there is a matching key in `testDescs`,
    175 // creates a directory_test passing the respective key's value for the func and
    176 // description arguments. If there is not a matching key in `testDescs`, the
    177 // test is not created. This will throw if `testDescs` contains a key that is
    178 // not in `testFuncs`.
    179 function selectDirectoryTests(testDescs, testFuncs) {
    180  for (const testDesc in testDescs) {
    181    if (!testFuncs.hasOwnProperty(testDesc)) {
    182      throw new Error(
    183          'Passed a test description in testDescs that wasn\'t in testFuncs.');
    184    }
    185    directory_test(testFuncs[testDesc], testDescs[testDesc]);
    186  }
    187 }
    188 
    189 // Adds tests to test the interaction between a lock created by `createLock1`
    190 // and a lock created by `createLock2`.
    191 //
    192 // The description of each test is passed in through `testDescs`. If a test
    193 // description is omitted, it is not run.
    194 //
    195 // For all tests, `createLock1` is called first.
    196 function generateCrossLockTests(createLock1, createLock2, testDescs) {
    197  if (testDescs === undefined) {
    198    throw new Error('Must pass testDescs.');
    199  }
    200  selectDirectoryTests(testDescs, {
    201 
    202    // This tests that a lock can't be acquired on a file that already has a
    203    // lock of another type.
    204    sameFile: async (t, rootDir) => {
    205      const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
    206 
    207      createLock1(t, fileHandle);
    208      await promise_rejects_dom(
    209          t, 'NoModificationAllowedError', createLock2(t, fileHandle));
    210    },
    211 
    212    // This tests that a lock on one file does not interfere with the creation
    213    // of a lock on another file.
    214    diffFile: async (t, rootDir) => {
    215      const [fooFileHandle, barFileHandle] =
    216          await createFileHandles(rootDir, 'foo.test', 'bar.test');
    217 
    218      createLock1(t, fooFileHandle);
    219      await createLock2(t, barFileHandle);
    220    },
    221 
    222    // This tests that after a lock has been acquired on a file and then
    223    // released, another lock of another type can be acquired. This will fail if
    224    // `createLock1` and `createLock2` create the same shared lock.
    225    acquireAfterRelease: async (t, rootDir) => {
    226      let [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
    227 
    228      const lockPromise = createLock1(t, fileHandle);
    229      await promise_rejects_dom(
    230          t, 'NoModificationAllowedError', createLock2(t, fileHandle));
    231 
    232      await releaseLock(lockPromise);
    233      // Recreate the file in case releasing the lock moves/removes it.
    234      [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
    235      await createLock2(t, fileHandle);
    236    },
    237 
    238    // This tests that after multiple locks of some shared lock type have been
    239    // acquired on a file and then all released, another lock of another lock
    240    // type can be acquired.
    241    multiAcquireAfterRelease: async (t, rootDir) => {
    242      const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
    243 
    244      const lock1 = await createLock1(t, fileHandle);
    245      const lock2 = await createLock1(t, fileHandle);
    246 
    247      await promise_rejects_dom(
    248          t, 'NoModificationAllowedError', createLock2(t, fileHandle));
    249      await lock1.close();
    250      await promise_rejects_dom(
    251          t, 'NoModificationAllowedError', createLock2(t, fileHandle));
    252      await lock2.close();
    253 
    254      await createLock2(t, fileHandle);
    255    },
    256 
    257    // This tests that a lock taken on a directory prevents a lock being
    258    // acquired on a file contained within that directory.
    259    takeDirThenFile: async (t, rootDir) => {
    260      const dirHandle = await rootDir.getDirectoryHandle('foo', {create: true});
    261      const [fileHandle] = await createFileHandles(dirHandle, 'BFS.test');
    262 
    263      createLock1(t, dirHandle);
    264      await promise_rejects_dom(
    265          t, 'NoModificationAllowedError', createLock2(t, fileHandle));
    266    },
    267 
    268    // This tests that a lock acquired on a file prevents a lock being acquired
    269    // on an ancestor of that file.
    270    takeFileThenDir: async (t, rootDir) => {
    271      const grandparentHandle =
    272          await rootDir.getDirectoryHandle('foo', {create: true});
    273      const parentHandle =
    274          await grandparentHandle.getDirectoryHandle('bar', {create: true});
    275      let [fileHandle] = await createFileHandles(parentHandle, 'BFS.test');
    276 
    277      // Test parent handle.
    278      const lock1 = createLock1(t, fileHandle);
    279      await promise_rejects_dom(
    280          t, 'NoModificationAllowedError', createLock2(t, parentHandle));
    281 
    282      // Release the lock so we can recreate it.
    283      await releaseLock(lock1);
    284      // Recreate the file in case releasing the lock moves/removes it.
    285      [fileHandle] = await createFileHandles(parentHandle, 'BFS.test');
    286 
    287      // Test grandparent handle.
    288      createLock1(t, fileHandle);
    289      await promise_rejects_dom(
    290          t, 'NoModificationAllowedError', createLock2(t, grandparentHandle));
    291    },
    292  });
    293 }
    294 
    295 // Tests whether the multiple locks can be created by createLock on a file
    296 // handle or if only one can. Returns LOCK_ACCESS.SHARED for the former and
    297 // LOCK_ACCESS.EXCLUSIVE for the latter.
    298 async function testLockAccess(t, fileHandle, createLock) {
    299  createLock(t, fileHandle);
    300 
    301  let access;
    302  try {
    303    await createLock(t, fileHandle);
    304    access = LOCK_ACCESS.SHARED;
    305  } catch (e) {
    306    access = LOCK_ACCESS.EXCLUSIVE;
    307    assert_throws_dom('NoModificationAllowedError', () => {
    308      throw e;
    309    });
    310  }
    311 
    312  return access;
    313 }
    314 
    315 // Creates a test with description `testDesc` to test behavior of the BFCache
    316 // with `testFunc`.
    317 function createBFCacheTest(testFunc, testDesc) {
    318  // In the remote context `rc`, calls the `funcName` export of
    319  // `bfcache-test-page.js` with `args`.
    320  //
    321  // Will import `bfcache-test-page.js` if it hasn't been imported already.
    322  function executeFunc(rc, funcName, args) {
    323    return rc.executeScript(async (funcName, args) => {
    324      if (self.testPageFuncs === undefined) {
    325        self.testPageFuncs =
    326            (await import('/fs/resources/bfcache-test-page.js'));
    327      }
    328      return await self.testPageFuncs[funcName](...args);
    329    }, [funcName, args]);
    330  }
    331 
    332  promise_test(async t => {
    333    const rcHelper = new RemoteContextHelper();
    334 
    335    // Open a window with noopener so that BFCache will work.
    336    const backRc = await rcHelper.addWindow(null, {features: 'noopener'});
    337    let curRc = backRc;
    338 
    339    // Functions given to the test to control the BFCache test.
    340    const testControls = {
    341      // Returns an array of functions that bind `executeFunc` with curRc and
    342      // their respective function name from `funcName`.
    343      getRemoteFuncs: (...funcNames) => {
    344        return funcNames.map(
    345            funcName => (...args) => executeFunc(curRc, funcName, args));
    346      },
    347      forward: async () => {
    348        if (curRc !== backRc) {
    349          throw new Error('Can only navigate forward once.');
    350        }
    351        prepareForBFCache(curRc);
    352        curRc = await curRc.navigateToNew();
    353      },
    354      back: async (shouldRestoreFromBFCache) => {
    355        if (curRc === backRc) {
    356          throw new Error(
    357              'Can\'t navigate back if you haven\'t navigated forward.');
    358        }
    359        await curRc.historyBack();
    360        curRc = backRc;
    361        if (shouldRestoreFromBFCache) {
    362          await assertImplementsBFCacheOptional(curRc);
    363        } else {
    364          await assertNotRestoredFromBFCache(curRc);
    365        }
    366      },
    367      assertBFCacheEligibility(shouldRestoreFromBFCache) {
    368        return assertBFCacheEligibility(curRc, shouldRestoreFromBFCache);
    369      }
    370    };
    371 
    372    await testFunc(t, testControls);
    373  }, testDesc);
    374 }