diff --git a/test/cli/backup-concurrency.test.ts b/test/cli/backup-concurrency.test.ts index cd411d8..be17b94 100644 --- a/test/cli/backup-concurrency.test.ts +++ b/test/cli/backup-concurrency.test.ts @@ -459,6 +459,71 @@ describe("lib.backup()", () => { } }); + // Once the backup's lock is lost, another backup may be writing to the + // directory, so the backup starts no further original. It finishes the + // ones it is working on, and `lib.backup()` rejects once they are all in + // place. Here `backup.lock` is removed while the first five downloads + // wait, and proper-lockfile's timer finds it gone. As in + // `backup-end-to-end.test.ts`, only `setTimeout` and `clearTimeout` are + // faked, so the test can fire that timer without waiting 5 seconds. Only + // the first five downloads are held: a backup that went on would store + // the other three photos. + it("when its lock is lost, starts no further original, and rejects once the originals it was working on are in place", async () => { + const dir = mkdtempSync(join(root, "backup-")); + const lockPath = join(dir, "backup.lock"); + const downloads: number[] = []; + const held: (() => void)[] = []; + const fetch: typeof globalThis.fetch = async (input, init) => { + const id = downloadOf(input); + if (id !== undefined) { + downloads.push(id); + if (downloads.length <= 5) { + await new Promise((release) => held.push(release)); + } + } + return server(input, init); + }; + const lib = await openLibrary(dir, fetch); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + try { + const backup = lib.backup(); + // The photos whose originals are in place at the moment + // `lib.backup()` rejects. + let storedWhenRejected: number[] = []; + const rejection = backup.catch((err: unknown) => { + storedWhenRejected = PHOTO_IDS.filter((id) => + existsSync(savedAt(dir, id)), + ); + return err; + }); + + await vi.waitFor(() => expect(held).toHaveLength(5), { + interval: 5, + timeout: 5000, + }); + rmSync(lockPath, { recursive: true }); + // Fire proper-lockfile's timer, and wait for the look at the lock + // on disk that it starts to finish: Node lists a file system call + // in progress as an `FSReqCallback`. + vi.advanceTimersByTime(5_000); + while (process.getActiveResourcesInfo().includes("FSReqCallback")) { + await new Promise((resolve) => setImmediate(resolve)); + } + for (const release of held) release(); + + expect(await rejection).toEqual( + new Error( + `lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'`, + ), + ); + expect(downloads.sort()).toEqual([101, 102, 103, 104, 105]); + expect(storedWhenRejected).toEqual([101, 102, 103, 104, 105]); + } finally { + vi.useRealTimers(); + await lib.close(); + } + }); + // Working on several originals at once changes only how long a backup // takes. The same two runs, with a content pool of one, which backs up // one original at a time, and with the default pools, end the same way: