import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { stub } from "@std/testing/mock"; import { doNothing, expectClaimRefused, holdClaimUntilReleased, SETTINGS, withClaimDir, withTestClaim, writeClaim, } from "#test/scripts/stale-claim/helpers.ts"; describe("releasing a claim", () => { test("removes the claim so the next taker finds it free", async () => { await withClaimDir(async (path) => { await withTestClaim(path, doNothing); await expect(Deno.stat(path)).rejects.toThrow(); await withTestClaim(path, doNothing); }); }); test("reports a claim that has since become somebody else's, leaving it", async () => { await withClaimDir(async (path) => { await expect( withTestClaim(path, () => writeClaim(path, `new-owner\n${Date.now()}`)), ).rejects.toThrow("was lost while the work ran"); expect((await Deno.readTextFile(path)).startsWith("new-owner")).toBe( true, ); }); }); test("frees the claim even when the work itself fails", async () => { await withClaimDir(async (path) => { await expect( withTestClaim(path, () => Promise.reject(new Error("the work broke"))), ).rejects.toThrow("the work broke"); await expect(Deno.stat(path)).rejects.toThrow(); }); }); test("notes a lost claim alongside the work's own failure", async () => { await withClaimDir(async (path) => { await expect( withTestClaim(path, async () => { await writeClaim(path, `new-owner\n${Date.now()}`); throw new Error("the work broke"); }), ).rejects.toThrow(/the work broke.*was lost while the work ran/s); // The new owner's record is not the failed work's to remove. expect((await Deno.readTextFile(path)).startsWith("new-owner")).toBe( true, ); }); }); test("reports a claim that is already gone as lost", async () => { await withClaimDir(async (path) => { await expect( withTestClaim(path, () => Deno.remove(path)), ).rejects.toThrow("was lost while the work ran"); await expect(Deno.stat(path)).rejects.toThrow(); }); }); }); describe("waiting for a claim", () => { test("does the work once the claim's holder lets go, then frees it", async () => { await withClaimDir(async (path) => { const held = await holdClaimUntilReleased(path); setTimeout(held.release, 20); const answer = await withTestClaim( path, () => Promise.resolve("worked"), { ...SETTINGS, timeoutMs: 5_000 }, ); await held.holding; expect(answer).toBe("worked"); await expect(Deno.stat(path)).rejects.toThrow(); }); }); test("pauses between tries instead of asking as fast as it can", async () => { await withClaimDir(async (path) => { const held = await holdClaimUntilReleased(path); let tries = 0; const open = Deno.open; using _open = stub(Deno, "open", (( target: string | URL, options?: Deno.OpenOptions, ) => { if (`${target}` === path && options?.createNew) tries += 1; return open(target, options); }) as typeof Deno.open); await expectClaimRefused(path, { ...SETTINGS, retryMs: 5, timeoutMs: 60, }); // Sixty milliseconds of waiting is a handful of tries when they pause // five apart. With no pause between them it would be many thousands. expect(tries).toBeLessThan(50); held.release(); await held.holding; }); }); test("gives up by name when the holder never lets go", async () => { await withClaimDir(async (path) => { const held = await holdClaimUntilReleased(path); await expectClaimRefused(path, { ...SETTINGS, timeoutMs: 20 }); held.release(); await held.holding; }); }); });