diff --git a/tests/browser-match-patterns/check.mjs b/tests/browser-match-patterns/check.mjs index 9fe58d6..862d715 100644 --- a/tests/browser-match-patterns/check.mjs +++ b/tests/browser-match-patterns/check.mjs @@ -3,11 +3,12 @@ import assert from "node:assert/strict"; import {spawn} from "node:child_process"; import {once} from "node:events"; -import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises"; +import {mkdir, mkdtemp, writeFile} from "node:fs/promises"; import {createServer} from "node:http"; import {tmpdir} from "node:os"; import {join} from "node:path"; import {createBrowserHarness, createTabFixture} from "../../dist/testing/index.js"; +import {removeBrowserTemporaryDirectory} from "./cleanup.mjs"; import {browserSmokeError, inspectBrowser} from "./launcher.mjs"; // Reject unsupported binaries before opening a server, creating a profile or waiting for extension results. @@ -107,6 +108,7 @@ async function probe(config) { let browser; let browserExit; let timeout; +let assertions = 0; try { server.listen(0, "127.0.0.1"); @@ -193,7 +195,6 @@ try { }, 30000); await finished; - let assertions = 0; for (const profile of profiles) { const result = results.get(profile.name); @@ -226,10 +227,6 @@ try { assertions++; } } - - console.log( - `Real Chromium smoke: ${assertions} harness/browser comparisons passed across ${profiles.length} permission profiles.` - ); } finally { clearTimeout(timeout); @@ -242,5 +239,9 @@ try { server.closeAllConnections(); await new Promise(resolveClose => server.close(resolveClose)); - await rm(temporary, {recursive: true, force: true}); + await removeBrowserTemporaryDirectory(temporary); } + +console.log( + `Real Chromium smoke: ${assertions} harness/browser comparisons passed across ${profiles.length} permission profiles; temporary profile removed.` +); diff --git a/tests/browser-match-patterns/cleanup.mjs b/tests/browser-match-patterns/cleanup.mjs new file mode 100644 index 0000000..5bb93c4 --- /dev/null +++ b/tests/browser-match-patterns/cleanup.mjs @@ -0,0 +1,11 @@ +import fs from "node:fs/promises"; + +export const removeBrowserTemporaryDirectory = async directory => { + try { + // Chromium can still flush profile files after its main process exits. + // Let Node retry transient filesystem errors, without hiding persistent failures. + await fs.rm(directory, {recursive: true, force: true, maxRetries: 10, retryDelay: 100}); + } catch (error) { + throw new Error(`Browser smoke cleanup failed after filesystem retries: ${directory}`, {cause: error}); + } +}; diff --git a/tests/browser-match-patterns/cleanup.test.mjs b/tests/browser-match-patterns/cleanup.test.mjs new file mode 100644 index 0000000..d618a70 --- /dev/null +++ b/tests/browser-match-patterns/cleanup.test.mjs @@ -0,0 +1,59 @@ +import fs from "node:fs/promises"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; +import {afterEach, describe, expect, jest, test} from "@jest/globals"; +import {removeBrowserTemporaryDirectory} from "./cleanup.mjs"; + +afterEach(() => jest.restoreAllMocks()); + +describe("browser smoke cleanup", () => { + test("removes an actual temporary profile with bounded native filesystem retries enabled", async () => { + const directory = await fs.mkdtemp(join(tmpdir(), "browser-smoke-cleanup-test-")); + + try { + const profile = join(directory, "profile", "Default"); + await fs.mkdir(profile, {recursive: true}); + await fs.writeFile(join(profile, "Preferences"), "{}"); + const remove = jest.spyOn(fs, "rm"); + await removeBrowserTemporaryDirectory(directory); + + // ENOTEMPTY retries are implemented by Node. Guard the options at that boundary: + // force alone ignores ENOENT, but leaves maxRetries at zero. + expect(remove).toHaveBeenCalledTimes(1); + + expect(remove).toHaveBeenCalledWith(directory, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + + await expect(fs.stat(directory)).rejects.toMatchObject({code: "ENOENT"}); + } finally { + jest.restoreAllMocks(); + await fs.rm(directory, {recursive: true, force: true}); + } + }); + + test("accepts an already removed temporary directory", async () => { + const directory = await fs.mkdtemp(join(tmpdir(), "browser-smoke-cleanup-test-")); + + try { + await removeBrowserTemporaryDirectory(directory); + await expect(removeBrowserTemporaryDirectory(directory)).resolves.toBeUndefined(); + } finally { + await fs.rm(directory, {recursive: true, force: true}); + } + }); + + test.each(["ENOTEMPTY", "EBUSY", "EACCES"])("propagates %s after native removal fails", async code => { + const directory = join(tmpdir(), "browser-smoke-cleanup-mocked"); + const failure = Object.assign(new Error(`Filesystem failure: ${code}`), {code}); + jest.spyOn(fs, "rm").mockRejectedValue(failure); + + await expect(removeBrowserTemporaryDirectory(directory)).rejects.toMatchObject({ + message: `Browser smoke cleanup failed after filesystem retries: ${directory}`, + cause: failure, + }); + }); +});