// A special path component meaning "this directory." const kCurrentDirectory = '.'; // A special path component meaning "the parent directory." const kParentDirectory = '..'; // The lock modes of a writable file stream. const WFS_MODES = ['siloed', 'exclusive']; // The lock modes of an access handle. const SAH_MODES = ['readwrite', 'read-only', 'readwrite-unsafe']; // Possible return values of testLockAccess. const LOCK_ACCESS = { SHARED: 'shared', EXCLUSIVE: 'exclusive', }; function primitiveModesAreContentious(exclusiveMode, mode1, mode2) { return mode1 != mode2 || mode1 === exclusiveMode; } function sahModesAreContentious(mode1, mode2) { return primitiveModesAreContentious('readwrite', mode1, mode2); } function wfsModesAreContentious(mode1, mode2) { return primitiveModesAreContentious('exclusive', mode1, mode2); } // Array of separators used to separate components in hierarchical paths. // Consider both '/' and '\' as path separators to ensure file names are // platform-agnostic. let kPathSeparators = ['/', '\\']; async function getFileSize(handle) { const file = await handle.getFile(); return file.size; } async function getFileContents(handle) { const file = await handle.getFile(); return new Response(file).text(); } async function getDirectoryEntryCount(handle) { let result = 0; for await (let entry of handle) { result++; } return result; } async function getSortedDirectoryEntries(handle) { let result = []; for await (let entry of handle.values()) { if (entry.kind === 'directory') { result.push(entry.name + '/'); } else { result.push(entry.name); } } result.sort(); return result; } async function createDirectory(test, name, parent) { const new_dir_handle = await parent.getDirectoryHandle(name, {create: true}); cleanup(test, new_dir_handle, async () => { try { await parent.removeEntry(name, {recursive: true}); } catch (e) { // Ignore any errors when removing directories, as tests might // have already removed the directory. } }); return new_dir_handle; } async function createEmptyFile(test, name, parent) { const handle = await parent.getFileHandle(name, {create: true}); cleanup(test, handle, async () => { try { await parent.removeEntry(name); } catch (e) { // Ignore any errors when removing files, as tests might already remove // the file. } }); // Make sure the file is empty. assert_equals(await getFileSize(handle), 0); return handle; } async function createFileWithContents(test, name, contents, parent) { const handle = await createEmptyFile(test, name, parent); const writer = await handle.createWritable(); await writer.write(new Blob([contents])); await writer.close(); return handle; } var fs_cleanups = []; async function cleanup(test, value, cleanup_func) { if (fs_cleanups.length === 0) { // register to get called back once from cleanup test.add_cleanup(async () => { // Cleanup in LIFO order to ensure locks are released correctly relative // to thinks like removeEntry(). Do so in a serialized form, not in parallel! fs_cleanups.reverse(); for (let cleanup of fs_cleanups) { try { await cleanup(); } catch (e) { // Ignore any errors when removing files, as tests might already remove // the file. } } fs_cleanups.length = 0; }); } fs_cleanups.push(cleanup_func); return value; } async function cleanup_writable(test, value) { return cleanup(test, value, async () => { try { return (await value).close(); } catch (e) { // Ignore any errors when closing writables, since attempting to close // aborted or closed writables will error. } }); } function createFileHandles(dir, ...fileNames) { return Promise.all( fileNames.map(fileName => dir.getFileHandle(fileName, {create: true}))); } // Releases a lock created by one of the create*WithCleanup functions below. async function releaseLock(lockPromise) { const result = await lockPromise; if (result?.close) { await result.close(); } } function cleanupLockPromise(t, lockPromise) { return cleanup(t, lockPromise, () => releaseLock(lockPromise)); } function createWFSWithCleanup(t, fileHandle, wfsOptions) { return cleanupLockPromise(t, fileHandle.createWritable(wfsOptions)); } // Returns createWFSWithCleanup bound with wfsOptions. function createWFSWithCleanupFactory(wfsOptions) { return (t, fileHandle) => createWFSWithCleanup(t, fileHandle, wfsOptions); } function createSAHWithCleanup(t, fileHandle, sahOptions) { return cleanupLockPromise(t, fileHandle.createSyncAccessHandle(sahOptions)); } // Returns createSAHWithCleanup bound with sahOptions. function createSAHWithCleanupFactory(sahOptions) { return (t, fileHandle) => createSAHWithCleanup(t, fileHandle, sahOptions); } function createMoveWithCleanup( t, fileHandle, fileName = 'unique-file-name.test') { return cleanupLockPromise(t, fileHandle.move(fileName)); } function createRemoveWithCleanup(t, fileHandle) { return cleanupLockPromise(t, fileHandle.remove({recursive: true})); } // For each key in `testFuncs` if there is a matching key in `testDescs`, // creates a directory_test passing the respective key's value for the func and // description arguments. If there is not a matching key in `testDescs`, the // test is not created. This will throw if `testDescs` contains a key that is // not in `testFuncs`. function selectDirectoryTests(testDescs, testFuncs) { for (const testDesc in testDescs) { if (!testFuncs.hasOwnProperty(testDesc)) { throw new Error( 'Passed a test description in testDescs that wasn\'t in testFuncs.'); } directory_test(testFuncs[testDesc], testDescs[testDesc]); } } // Adds tests to test the interaction between a lock created by `createLock1` // and a lock created by `createLock2`. // // The description of each test is passed in through `testDescs`. If a test // description is omitted, it is not run. // // For all tests, `createLock1` is called first. function generateCrossLockTests(createLock1, createLock2, testDescs) { if (testDescs === undefined) { throw new Error('Must pass testDescs.'); } selectDirectoryTests(testDescs, { // This tests that a lock can't be acquired on a file that already has a // lock of another type. sameFile: async (t, rootDir) => { const [fileHandle] = await createFileHandles(rootDir, 'BFS.test'); createLock1(t, fileHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, fileHandle)); }, // This tests that a lock on one file does not interfere with the creation // of a lock on another file. diffFile: async (t, rootDir) => { const [fooFileHandle, barFileHandle] = await createFileHandles(rootDir, 'foo.test', 'bar.test'); createLock1(t, fooFileHandle); await createLock2(t, barFileHandle); }, // This tests that after a lock has been acquired on a file and then // released, another lock of another type can be acquired. This will fail if // `createLock1` and `createLock2` create the same shared lock. acquireAfterRelease: async (t, rootDir) => { let [fileHandle] = await createFileHandles(rootDir, 'BFS.test'); const lockPromise = createLock1(t, fileHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, fileHandle)); await releaseLock(lockPromise); // Recreate the file in case releasing the lock moves/removes it. [fileHandle] = await createFileHandles(rootDir, 'BFS.test'); await createLock2(t, fileHandle); }, // This tests that after multiple locks of some shared lock type have been // acquired on a file and then all released, another lock of another lock // type can be acquired. multiAcquireAfterRelease: async (t, rootDir) => { const [fileHandle] = await createFileHandles(rootDir, 'BFS.test'); const lock1 = await createLock1(t, fileHandle); const lock2 = await createLock1(t, fileHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, fileHandle)); await lock1.close(); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, fileHandle)); await lock2.close(); await createLock2(t, fileHandle); }, // This tests that a lock taken on a directory prevents a lock being // acquired on a file contained within that directory. takeDirThenFile: async (t, rootDir) => { const dirHandle = await rootDir.getDirectoryHandle('foo', {create: true}); const [fileHandle] = await createFileHandles(dirHandle, 'BFS.test'); createLock1(t, dirHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, fileHandle)); }, // This tests that a lock acquired on a file prevents a lock being acquired // on an ancestor of that file. takeFileThenDir: async (t, rootDir) => { const grandparentHandle = await rootDir.getDirectoryHandle('foo', {create: true}); const parentHandle = await grandparentHandle.getDirectoryHandle('bar', {create: true}); let [fileHandle] = await createFileHandles(parentHandle, 'BFS.test'); // Test parent handle. const lock1 = createLock1(t, fileHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, parentHandle)); // Release the lock so we can recreate it. await releaseLock(lock1); // Recreate the file in case releasing the lock moves/removes it. [fileHandle] = await createFileHandles(parentHandle, 'BFS.test'); // Test grandparent handle. createLock1(t, fileHandle); await promise_rejects_dom( t, 'NoModificationAllowedError', createLock2(t, grandparentHandle)); }, }); } // Tests whether the multiple locks can be created by createLock on a file // handle or if only one can. Returns LOCK_ACCESS.SHARED for the former and // LOCK_ACCESS.EXCLUSIVE for the latter. async function testLockAccess(t, fileHandle, createLock) { createLock(t, fileHandle); let access; try { await createLock(t, fileHandle); access = LOCK_ACCESS.SHARED; } catch (e) { access = LOCK_ACCESS.EXCLUSIVE; assert_throws_dom('NoModificationAllowedError', () => { throw e; }); } return access; } // Creates a test with description `testDesc` to test behavior of the BFCache // with `testFunc`. function createBFCacheTest(testFunc, testDesc) { // In the remote context `rc`, calls the `funcName` export of // `bfcache-test-page.js` with `args`. // // Will import `bfcache-test-page.js` if it hasn't been imported already. function executeFunc(rc, funcName, args) { return rc.executeScript(async (funcName, args) => { if (self.testPageFuncs === undefined) { self.testPageFuncs = (await import('/fs/resources/bfcache-test-page.js')); } return await self.testPageFuncs[funcName](...args); }, [funcName, args]); } promise_test(async t => { const rcHelper = new RemoteContextHelper(); // Open a window with noopener so that BFCache will work. const backRc = await rcHelper.addWindow(null, {features: 'noopener'}); let curRc = backRc; // Functions given to the test to control the BFCache test. const testControls = { // Returns an array of functions that bind `executeFunc` with curRc and // their respective function name from `funcName`. getRemoteFuncs: (...funcNames) => { return funcNames.map( funcName => (...args) => executeFunc(curRc, funcName, args)); }, forward: async () => { if (curRc !== backRc) { throw new Error('Can only navigate forward once.'); } prepareForBFCache(curRc); curRc = await curRc.navigateToNew(); }, back: async (shouldRestoreFromBFCache) => { if (curRc === backRc) { throw new Error( 'Can\'t navigate back if you haven\'t navigated forward.'); } await curRc.historyBack(); curRc = backRc; if (shouldRestoreFromBFCache) { await assertImplementsBFCacheOptional(curRc); } else { await assertNotRestoredFromBFCache(curRc); } }, assertBFCacheEligibility(shouldRestoreFromBFCache) { return assertBFCacheEligibility(curRc, shouldRestoreFromBFCache); } }; await testFunc(t, testControls); }, testDesc); }