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 }