import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; // Regression coverage for the "database is locked" fix (#228 follow-up). // // tauri-plugin-sql loads SQLite through a default multi-connection sqlx pool // and exposes no transaction primitive, so a BEGIN and its COMMIT issued as // separate db.execute calls could land on different pooled connections and // strand an open write transaction (= permanent lock). db.ts funnels every // operation through one FIFO lock, and withTransaction holds it for the whole // BEGIN..COMMIT. These tests assert that serialization, without a real SQLite. // Ordered log of every fake-db operation's start/end, shared with the fake db. const events: string[] = []; // Hoisted handle so the (hoisted) vi.mock factory can reach the per-test db. const h = vi.hoisted(() => ({ fakeDb: null as unknown as FakeDb, // What `prepare_profile_db` answers (#390); a test may make it throw. prepare: async (): Promise => null, })); interface FakeDb { execute: (q: string, p?: unknown[]) => Promise<{ rowsAffected: number }>; select: (q: string, p?: unknown[]) => Promise; close: () => Promise; } vi.mock("@tauri-apps/plugin-sql", () => ({ default: { load: vi.fn(async () => h.fakeDb) }, })); vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async (command: string) => (command === "prepare_profile_db" ? h.prepare() : false)), })); vi.mock("./logService", () => ({ logInfo: vi.fn(), logWarn: vi.fn(), logError: vi.fn() })); import Database from "@tauri-apps/plugin-sql"; import { invoke } from "@tauri-apps/api/core"; import { logError, logInfo, logWarn } from "./logService"; import { BLOCKED_ALERT_MS, closeDb, connectToProfile, DbConnectionClosedError, describePreparation, getDb, initializeNewProfileDb, TransactionHandleCloseError, TransactionHandleRevokedError, withoutFolders, withTransaction, type ProfilePreparation, } from "./db"; // Each op records start, yields a real macrotask (a window for interleaving if // the lock were broken), then records end. The lock must keep them ordered. function makeFakeDb(name = "db"): FakeDb { return { execute: vi.fn(async (q: string) => { events.push(`start:${q}`); await new Promise((r) => setTimeout(r, 2)); events.push(`end:${q}`); return { rowsAffected: 0 }; }), select: vi.fn(async (q: string) => { events.push(`start:${q}`); await new Promise((r) => setTimeout(r, 2)); events.push(`end:${q}`); return []; }), close: vi.fn(async () => { events.push(`close:${name}`); }), }; } beforeEach(async () => { h.fakeDb = makeFakeDb(); h.prepare = async () => null; await connectToProfile("test.db"); // Cleared after connecting: the connection opens by closing the previous // test's fake, whose close is logged too. events.length = 0; }); describe("db serialization (database-locked fix)", () => { it("serializes concurrent execute calls — no interleaving", async () => { const db = await getDb(); // Fire both without awaiting between them: they race for the lock. await Promise.all([db.execute("A"), db.execute("B")]); // A must fully complete before B starts (FIFO, no overlap). expect(events).toEqual(["start:A", "end:A", "start:B", "end:B"]); }); it("holds the lock across a whole withTransaction — an outside read cannot interleave", async () => { const db = await getDb(); // Queue the transaction first, then an outside read. const txn = withTransaction(async (tx) => { await tx.execute("BEGIN"); await tx.execute("WRITE"); await tx.execute("COMMIT"); }); const outside = db.select("READ"); await Promise.all([txn, outside]); // The outside read runs entirely AFTER the transaction's COMMIT — the // pre-fix bug was exactly an outside read interleaving mid-transaction. expect(events).toEqual([ "start:BEGIN", "end:BEGIN", "start:WRITE", "end:WRITE", "start:COMMIT", "end:COMMIT", "start:READ", "end:READ", ]); }); it("makes a getDb() call from inside a transaction wait behind it — strict lock (#408)", async () => { // The lock is not reentrant: only the callback's handle runs directly. A // nested service reaching for getDb() queues behind the transaction (this // test releases the callback by hand; a callback awaiting it deadlocks, // which the blocking alert reports). let release: () => void = () => {}; const gate = new Promise((resolve) => { release = resolve; }); let nested: Promise = Promise.resolve(); const txn = withTransaction(async (tx) => { await tx.execute("BEGIN"); nested = (await getDb()).execute("NESTED"); await new Promise((r) => setTimeout(r, 10)); await tx.execute("COMMIT"); await gate; }); try { await vi.waitFor(() => expect(events).toContain("end:COMMIT")); expect(events).not.toContain("start:NESTED"); } finally { // Released even on failure, so a red assertion leaves no held lock. release(); await txn; await nested; } expect(events).toEqual(["start:BEGIN", "end:BEGIN", "start:COMMIT", "end:COMMIT", "start:NESTED", "end:NESTED"]); }); it("runs a read launched from elsewhere during the callback after the COMMIT (#408)", async () => { // The pre-#408 global flag let ANY query bypass the lock while a callback // ran: a read started after BEGIN ran in parallel with the transaction. const db = await getDb(); let outside: Promise = Promise.resolve(); const txn = withTransaction(async (tx) => { await tx.execute("BEGIN"); outside = db.select("READ"); await tx.execute("WRITE"); await tx.execute("COMMIT"); }); await txn; await outside; expect(events).toEqual([ "start:BEGIN", "end:BEGIN", "start:WRITE", "end:WRITE", "start:COMMIT", "end:COMMIT", "start:READ", "end:READ", ]); }); it("lets the callback's own handle run its statements without waiting", async () => { const rows = await withTransaction(async (tx) => { await tx.execute("BEGIN"); const result = await tx.select("SELECT"); await tx.execute("COMMIT"); return result; }); expect(rows).toEqual([]); expect(events).toEqual(["start:BEGIN", "end:BEGIN", "start:SELECT", "end:SELECT", "start:COMMIT", "end:COMMIT"]); }); it("revokes the transaction handle once the callback settled", async () => { let kept: Awaited> | null = null; await withTransaction(async (tx) => { kept = tx; }); await expect(kept!.execute("LATE")).rejects.toBeInstanceOf(TransactionHandleRevokedError); await expect(kept!.select("LATE")).rejects.toBeInstanceOf(TransactionHandleRevokedError); expect(events).not.toContain("start:LATE"); }); it("revokes the handle of a failed transaction too", async () => { let kept: Awaited> | null = null; await expect( withTransaction(async (tx) => { kept = tx; throw new Error("boom"); }) ).rejects.toThrow("boom"); await expect(kept!.execute("LATE")).rejects.toBeInstanceOf(TransactionHandleRevokedError); }); it("refuses close() on the transaction handle", async () => { await withTransaction(async (tx) => { await expect(tx.close()).rejects.toBeInstanceOf(TransactionHandleCloseError); }); expect(h.fakeDb.close).not.toHaveBeenCalled(); }); it("releases the lock after a failed transaction so later ops still run", async () => { const db = await getDb(); await expect( withTransaction(async (tx) => { await tx.execute("BEGIN"); throw new Error("boom"); }) ).rejects.toThrow("boom"); // The lock must be free again: a subsequent op completes (no stuck queue). await db.execute("AFTER"); expect(events).toContain("start:AFTER"); expect(events).toContain("end:AFTER"); }); it("withTransaction rejects when no profile is connected", async () => { const { closeDb } = await import("./db"); await closeDb(); await expect(withTransaction(async () => undefined)).rejects.toThrow( /No database connection/ ); }); }); // --------------------------------------------------------------------------- // Profile switch through the lock, connection generation, blocking alert (#408) // --------------------------------------------------------------------------- describe("profile switch goes through the lock (#408)", () => { afterEach(() => { vi.mocked(Database.load).mockImplementation(async () => h.fakeDb as never); }); it("closes the connection only after a running transaction's COMMIT", async () => { const old = h.fakeDb; let switching: Promise = Promise.resolve(); const txn = withTransaction(async (tx) => { await tx.execute("BEGIN"); h.fakeDb = makeFakeDb("next"); switching = connectToProfile("next.db"); await new Promise((r) => setTimeout(r, 10)); await tx.execute("COMMIT"); }); await txn; await switching; expect(events).toEqual(["start:BEGIN", "end:BEGIN", "start:COMMIT", "end:COMMIT", "close:db"]); expect(old.close).toHaveBeenCalledTimes(1); }); it("two concurrent switches leave one pool open, on the last file asked for", async () => { const pools = new Map(); vi.mocked(Database.load).mockImplementation(async (url: string) => { const fake = makeFakeDb(url); pools.set(url, fake); return fake as never; }); await Promise.all([connectToProfile("a.db"), connectToProfile("b.db")]); expect([...pools.keys()]).toEqual(["sqlite:a.db", "sqlite:b.db"]); expect(pools.get("sqlite:a.db")!.close).toHaveBeenCalledTimes(1); expect(pools.get("sqlite:b.db")!.close).not.toHaveBeenCalled(); await (await getDb()).execute("Q"); expect(pools.get("sqlite:b.db")!.execute).toHaveBeenCalledWith("Q", undefined); }); it("invokes prepare_profile_db only once the old connection's close() resolved", async () => { const old = h.fakeDb; vi.mocked(old.close).mockImplementation(async () => { events.push("close-start"); await new Promise((r) => setTimeout(r, 10)); events.push("close-end"); }); h.prepare = async () => { events.push("prepare"); return null; }; h.fakeDb = makeFakeDb("next"); await connectToProfile("next.db"); expect(events).toEqual(["close-start", "close-end", "prepare"]); }); it("fails a transaction queued behind a switch instead of running it on the new database", async () => { const old = h.fakeDb; const next = makeFakeDb("next"); h.fakeDb = next; const switching = connectToProfile("next.db"); const callback = vi.fn(async () => "ran"); const txn = withTransaction(callback); await switching; await expect(txn).rejects.toBeInstanceOf(DbConnectionClosedError); await expect(txn).rejects.toThrow("connection closed by profile switch"); expect(callback).not.toHaveBeenCalled(); expect(next.execute).not.toHaveBeenCalled(); expect(old.execute).not.toHaveBeenCalled(); }); it("fails a query queued on the old handle behind a switch", async () => { const before = await getDb(); const next = makeFakeDb("next"); h.fakeDb = next; const switching = connectToProfile("next.db"); const late = before.select("OLD"); await switching; await expect(late).rejects.toBeInstanceOf(DbConnectionClosedError); expect(next.select).not.toHaveBeenCalled(); }); it("closeDb waits for the queue, then the old handle refuses to run", async () => { const db = await getDb(); const first = db.execute("FIRST"); const closing = closeDb(); await first; await closing; expect(events).toEqual(["start:FIRST", "end:FIRST", "close:db"]); await expect(db.execute("AFTER")).rejects.toBeInstanceOf(DbConnectionClosedError); }); it("close() on a getDb() handle kept from the previous profile rejects and keeps the current one open", async () => { const before = await getDb(); const next = makeFakeDb("next"); h.fakeDb = next; await connectToProfile("next.db"); events.length = 0; await expect(before.close()).rejects.toBeInstanceOf(DbConnectionClosedError); expect(next.close).not.toHaveBeenCalled(); await (await getDb()).execute("STILL OPEN"); expect(next.execute).toHaveBeenCalledWith("STILL OPEN", undefined); }); it("close() on the current getDb() handle closes the connection", async () => { const current = h.fakeDb; await (await getDb()).close(); expect(current.close).toHaveBeenCalledTimes(1); await expect(getDb()).rejects.toThrow(/No database connection/); }); it("a failing close of the old pool does not prevent the switch, and goes to the Journal", async () => { vi.mocked(logWarn).mockClear(); const old = h.fakeDb; const before = await getDb(); vi.mocked(old.close).mockImplementation(async () => { throw new Error("close refused for /home/someone/.config/x.db"); }); const next = makeFakeDb("next"); h.fakeDb = next; await connectToProfile("next.db"); expect(old.close).toHaveBeenCalledTimes(1); expect(logWarn).toHaveBeenCalledWith( "DB: closing the previous connection failed, continuing without it: close refused for x.db" ); await (await getDb()).execute("ON NEXT"); expect(next.execute).toHaveBeenCalledWith("ON NEXT", undefined); await expect(before.execute("ON OLD")).rejects.toBeInstanceOf(DbConnectionClosedError); expect(old.execute).not.toHaveBeenCalled(); }); it("closeDb with a failing close resolves, warns, and leaves a coherent state without connection", async () => { vi.mocked(logWarn).mockClear(); const old = h.fakeDb; const before = await getDb(); vi.mocked(old.close).mockImplementation(async () => { throw new Error("close refused"); }); await expect(closeDb()).resolves.toBeUndefined(); expect(logWarn).toHaveBeenCalledWith("DB: closing the previous connection failed, continuing without it: close refused"); // No connection, and the generation moved: the old handle refuses to run. await expect(getDb()).rejects.toThrow(/No database connection/); await expect(withTransaction(async () => undefined)).rejects.toThrow(/No database connection/); await expect(before.execute("ON OLD")).rejects.toBeInstanceOf(DbConnectionClosedError); expect(old.execute).not.toHaveBeenCalled(); // The old pool is no longer referenced: the next open does not close it again. h.fakeDb = makeFakeDb("next"); await connectToProfile("next.db"); expect(old.close).toHaveBeenCalledTimes(1); }); it("initializeNewProfileDb runs its SQL inside the section, before a queued query", async () => { const next = makeFakeDb("new"); h.fakeDb = next; const init = initializeNewProfileDb("new.db", ["CREATE A", "CREATE B"]); const after = (async () => { await init; return (await getDb()).select("READ"); })(); await after; expect(events).toEqual([ "close:db", "start:CREATE A", "end:CREATE A", "start:CREATE B", "end:CREATE B", "start:READ", "end:READ", ]); }); }); describe("blocking alert (#408)", () => { beforeEach(() => { vi.mocked(logWarn).mockClear(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("warns once, naming the transaction that holds the lock, when getDb() is called from a callback", async () => { let release: () => void = () => {}; const gate = new Promise((resolve) => { release = resolve; }); let nested: Promise = Promise.resolve(); const startedAt = new Date(Date.now()).toISOString(); const txn = withTransaction(async () => { nested = (await getDb()).execute("NESTED"); await gate; }); try { await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS - 1); expect(logWarn).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(1); expect(logWarn).toHaveBeenCalledTimes(1); const message = String(vi.mocked(logWarn).mock.calls[0][0]); expect(message).toContain("a query has waited 15 s"); expect(message).toContain(`held by a transaction since ${startedAt}`); // A cause the reader may check, not a diagnosis: a long restore also waits 15 s. expect(message).toContain("(possible cause: a getDb() call inside a withTransaction callback"); // Once per operation: still one warning much later. await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS * 3); expect(logWarn).toHaveBeenCalledTimes(1); } finally { release(); await vi.advanceTimersByTimeAsync(10); await txn; await nested; } }); it("says the lock is free, not who held it last, when the check lands between two operations", async () => { // In the app only microtasks separate the end of one operation from the // start of the next, so a timer cannot fire there; the test puts the check // there to pin that the holder is cleared when an operation ends. const db = await getDb(); let release: () => void = () => {}; const gate = new Promise((resolve) => { release = resolve; }); vi.mocked(h.fakeDb.execute).mockImplementationOnce(async () => { await gate; return { rowsAffected: 0 }; }); const first = db.execute("LONG"); const waiting = db.select("WAITING"); // Registered after the queue's own reaction: runs once the first operation // ended (holder cleared), before the waiting one starts. const checked = first.then(() => vi.advanceTimersByTime(BLOCKED_ALERT_MS)); release(); await checked; await vi.advanceTimersByTimeAsync(10); await waiting; expect(logWarn).toHaveBeenCalledTimes(1); expect(String(vi.mocked(logWarn).mock.calls[0][0])).toBe("DB lock: a query has waited 15 s behind the queue"); }); it("says nothing for an operation that got the lock in time", async () => { const db = await getDb(); const run = db.execute("QUICK"); await vi.advanceTimersByTimeAsync(10); await run; await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS * 2); expect(logWarn).not.toHaveBeenCalled(); }); it("names a profile opening that holds the lock (long catch-up)", async () => { let finish: () => void = () => {}; h.prepare = () => new Promise((resolve) => { finish = () => resolve(null); }); h.fakeDb = makeFakeDb("next"); const switching = connectToProfile("next.db"); // Settled outcome caught at once: the rejection lands while timers advance. const waiting = withTransaction(async () => "late").catch((e: unknown) => e); try { await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS); expect(logWarn).toHaveBeenCalledTimes(1); expect(String(vi.mocked(logWarn).mock.calls[0][0])).toContain( "a transaction has waited 15 s, the lock is held by a profile opening" ); } finally { finish(); await vi.advanceTimersByTimeAsync(10); await switching; } expect(await waiting).toBeInstanceOf(DbConnectionClosedError); }); }); // --------------------------------------------------------------------------- // Profile preparation (#390) // --------------------------------------------------------------------------- function report(overrides: Partial = {}): ProfilePreparation { return { catch_up: null, catch_up_error: null, catch_up_backup: null, repaired_flags: 0, elapsed_ms: 12, ...overrides, }; } function catchUp(overrides: Partial> = {}) { return { floor: 6, recorded: [], applied: [], fk_repaired: [], fk_violations: [], backup_file: null, ...overrides, }; } describe("a profile is prepared before the plugin opens it (#390)", () => { beforeEach(() => { vi.mocked(invoke).mockClear(); vi.mocked(Database.load).mockClear(); vi.mocked(logInfo).mockClear(); vi.mocked(logError).mockClear(); }); it("prepares after the checksum repair and before Database.load", async () => { await connectToProfile("profile_0a1b2c3d.db"); const invoked = vi.mocked(invoke).mock; expect(invoked.calls.map((call) => call[0])).toEqual(["repair_migrations", "prepare_profile_db"]); expect(invoked.calls[1][1]).toEqual({ dbFilename: "profile_0a1b2c3d.db" }); expect(invoked.invocationCallOrder[1]).toBeLessThan( vi.mocked(Database.load).mock.invocationCallOrder[0] ); }); it("opens the database only once the preparation has finished", async () => { // In production the command runs on the main thread while Database.load // is asynchronous: started early, the two would overlap on the same file. let finish: (report: ProfilePreparation | null) => void = () => {}; h.prepare = () => new Promise((resolve) => { finish = resolve; }); const connecting = connectToProfile("profile_0a1b2c3d.db"); await vi.waitFor(() => expect(invoke).toHaveBeenCalledWith("prepare_profile_db", { dbFilename: "profile_0a1b2c3d.db" }) ); // Give a load started too early every chance to show up. await new Promise((resolve) => setTimeout(resolve, 10)); expect(Database.load).not.toHaveBeenCalled(); finish(null); await connecting; expect(Database.load).toHaveBeenCalledWith("sqlite:profile_0a1b2c3d.db"); }); it("still opens the profile when the preparation fails, and says so in the Journal", async () => { h.prepare = async () => { throw "invalid_profile_db_filename"; }; await connectToProfile("profile_0a1b2c3d.db"); expect(Database.load).toHaveBeenCalledWith("sqlite:profile_0a1b2c3d.db"); expect(logError).toHaveBeenCalledWith( "Profile profile_0a1b2c3d.db: database preparation failed: invalid_profile_db_filename" ); }); it("writes no folder when the preparation fails with a path (#395)", async () => { h.prepare = async () => { throw new Error( "Cannot read /home/someone/.config/com.simpl.resultat/profile_0a1b2c3d.db nor C:\\Users\\L'Heureux\\AppData\\x.db" ); }; await connectToProfile("profile_0a1b2c3d.db"); expect(logError).toHaveBeenCalledWith( "Profile profile_0a1b2c3d.db: database preparation failed: Cannot read profile_0a1b2c3d.db nor x.db" ); }); it("names the copy in the Journal when the catch-up stopped after taking it (#395)", async () => { h.prepare = async () => report({ catch_up_error: "history_write_failed: history refused", catch_up_backup: "profile_0a1b2c3d-before-v17.db", }); await connectToProfile("profile_0a1b2c3d.db"); expect(vi.mocked(logError).mock.calls.map((call) => call[0])).toEqual([ "Profile profile_0a1b2c3d.db: database update stopped: history_write_failed: history refused; copy of the previous state kept as profile_0a1b2c3d-before-v17.db", ]); }); it("writes what changed to the Journal", async () => { h.prepare = async () => report({ catch_up: catchUp({ applied: [7, 8, 17], backup_file: "profile_0a1b2c3d-before-v7.db" }), repaired_flags: 2, }); await connectToProfile("profile_0a1b2c3d.db"); expect(vi.mocked(logInfo).mock.calls.map((call) => call[0])).toEqual([ "Profile profile_0a1b2c3d.db: database updated with versions 7 to 17 in 12 ms, copy of the previous state kept as profile_0a1b2c3d-before-v7.db", "Profile profile_0a1b2c3d.db: 2 manual categorization flag(s) repaired", ]); expect(logError).not.toHaveBeenCalled(); }); }); describe("describePreparation (#390)", () => { it("says nothing when nothing changed", () => { expect(describePreparation("x.db", report({ catch_up: catchUp() }))).toEqual({ info: [], errors: [] }); }); it("tolerates a missing report", () => { expect(describePreparation("x.db", null)).toEqual({ info: [], errors: [] }); expect(describePreparation("x.db", false as never)).toEqual({ info: [], errors: [] }); }); it("notes a history written without any update", () => { const lines = describePreparation("x.db", report({ catch_up: catchUp({ recorded: [1, 2, 17] }) })); expect(lines.info).toEqual(["Profile x.db: migration history recorded up to version 17"]); }); it("names the rows a version detached from a missing parent", () => { const lines = describePreparation( "x.db", report({ catch_up: catchUp({ applied: [7], fk_repaired: [["categories", 3]] }) }) ); expect(lines.info).toContain("Profile x.db: 3 row(s) of categories detached from a missing parent"); }); it("reports a stopped catch-up as an error", () => { const lines = describePreparation("x.db", report({ catch_up_error: "migration_failed:v12: boom" })); expect(lines.errors).toEqual(["Profile x.db: database update stopped: migration_failed:v12: boom"]); }); it("names the rows that already pointed to a missing row (RT2, #395)", () => { const lines = describePreparation( "x.db", report({ catch_up: catchUp({ applied: [7], fk_violations: [["transactions", 1]] }) }) ); expect(lines.info).toContain("Profile x.db: 1 row(s) of transactions point to a missing row"); }); // The three ways a catch-up stops after its copy (profile_db.rs), each with // the message shape Rust produces. it.each([ ["history_write_failed: history refused"], ["pragma_failed: database is locked"], ["migration_failed:v18: no such table: no_such_table (applied before stopping: [17])"], ])("names the copy taken before a catch-up that stopped on %s (#395)", (error) => { const lines = describePreparation( "x.db", report({ catch_up_error: error, catch_up_backup: "x-before-v17.db" }) ); expect(lines.errors).toEqual([ `Profile x.db: database update stopped: ${error}; copy of the previous state kept as x-before-v17.db`, ]); }); it("writes no folder in an error line (#395)", () => { const lines = describePreparation( "x.db", report({ // What scrub leaves of a real VACUUM INTO failure, plus a copy name a // regression would turn into a path, on both separators. catch_up_error: "backup_failed: unable to open database: /migration-backups/x-before-v17.db.tmp", catch_up_backup: "C:\\Users\\someone\\migration-backups\\x-before-v17.db", }) ); expect(lines.errors).toEqual([ "Profile x.db: database update stopped: backup_failed: unable to open database: x-before-v17.db.tmp; copy of the previous state kept as x-before-v17.db", ]); for (const line of lines.errors) { expect(line).not.toMatch(/[\\/]/); } }); it("withoutFolders keeps text without a path as is", () => { expect(withoutFolders("migration_failed:v12: near \"x\": syntax error")).toBe( 'migration_failed:v12: near "x": syntax error' ); expect(withoutFolders("a /home/u/f.db, b")).toBe("a f.db, b"); // A separator that is no path: kept readable, still no slash. expect(withoutFolders("pragma_failed: disk I/O error")).toBe("pragma_failed: disk I-O error"); }); // A folder holding a blank must not leave its first word behind (#436 review, S1). it.each([ ["C:\\Users\\Jean Dupont\\AppData\\Roaming\\com.simpl.resultat\\profile_x.db", "profile_x.db"], ["C:/Users/Jean Dupont/x.db", "x.db"], ["/home/marie claire/.config/x.db", "x.db"], ["~/Jean Dupont/x.db", "x.db"], ["\\\\server\\share\\Jean Dupont\\x.db", "x.db"], ["C:\\Program Files (x86)\\Simpl\\x.db", "x.db"], ['"C:\\Users\\Jean Dupont\\x.db" failed', '"x.db" failed'], ["(see /home/marie claire/x.db) and I/O", "(see x.db) and I-O"], ["path=/home/marie claire/x.db;next", "path=x.db;next"], ])("withoutFolders reduces %s to its last segment", (input, expected) => { const cleaned = withoutFolders(input); expect(cleaned).toBe(expected); expect(cleaned).not.toMatch(/Jean|Dupont|marie|claire|Users|home|Program|server/); }); it("withoutFolders ends a path at sentence punctuation and at the next path", () => { expect(withoutFolders("unable to open /home/marie claire/x.db: disk I/O error")).toBe( "unable to open x.db: disk I-O error" ); expect(withoutFolders("read /home/marie claire/a.db, then C:\\Users\\Jean Dupont\\b.db")).toBe( "read a.db, then b.db" ); expect(withoutFolders("'C:\\a\\b.db' and 'C:\\Users\\Jean Dupont\\d.db'")).toBe("'b.db' and 'd.db'"); }); it.each([["\""], [";"], ["|"], ["`"], ["\n"], ["\r"]])("withoutFolders ends the folders at %j", (stop) => { expect(withoutFolders(`/home/marie claire/x.db${stop} I/O`)).toBe(`x.db${stop} I-O`); }); it("withoutFolders keeps a single quote around the file name, both sides", () => { expect(withoutFolders("'C:\\Users\\Bob\\x.db'")).toBe("'x.db'"); expect(withoutFolders("cannot open '/home/marie claire/x.db' now")).toBe("cannot open 'x.db' now"); }); it("withoutFolders drops more text rather than a folder when nothing ends the path", () => { // Conservative: everything up to the last separator of the line goes. expect(withoutFolders("/home/marie claire/x.db then I/O")).toBe("O"); }); });