test: pin the surviving marked-t keys and document the known test limits (#437) #473
12 changed files with 141 additions and 24 deletions
|
|
@ -58,7 +58,7 @@ Comme sqlx pour le profil par défaut. Une version qui échoue sur une clé étr
|
|||
|
||||
### 7. L'échec reste ouvert
|
||||
|
||||
Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` réduit chaque chemin d'une ligne d'erreur, message de repli compris, à son dernier segment (le nom de fichier). Un chemin part d'un début reconnu (lecteur `C:\`, `/`, `~/`, `./`, partage `\\`, ou le `<profile folder>` de `scrub`) et court jusqu'à son dernier séparateur, espaces compris, pour qu'un dossier comme `C:\Users\Jean Dupont\` ou `/home/marie claire/` ne laisse aucun mot derrière lui ; il s'arrête à un saut de ligne, un guillemet double, une apostrophe inverse, `;`, `|`, une virgule ou un deux-points suivis d'un blanc, une parenthèse fermante qui n'est pas suivie d'un séparateur (`Program Files (x86)\` continue), ou un blanc suivi d'un autre chemin. Une apostrophe placée juste avant le chemin reste autour du nom de fichier (`'C:\Users\Bob\x.db'` donne `'x.db'`). Quand rien ne l'arrête, la règle est prudente : elle retire le texte qui suit jusqu'au dernier séparateur de la ligne plutôt que de laisser passer un dossier. Limite : un chemin relatif sans début reconnu (`dossier/sous dossier/x.db`) n'a que ses séparateurs remplacés par `-` ; il ne part pas du dossier personnel, donc n'en contient pas le nom. Tout autre séparateur devient `-` : aucune ligne d'erreur ne contient `/` ni `\`.
|
||||
Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` réduit chaque chemin d'une ligne d'erreur, message de repli compris, à son dernier segment (le nom de fichier). Un chemin part d'un début reconnu (lecteur `C:\`, `/`, `~/`, `./`, partage `\\`, ou le `<profile folder>` de `scrub`) et court jusqu'à son dernier séparateur, espaces compris, pour qu'un dossier comme `C:\Users\Jean Dupont\` ou `/home/marie claire/` ne laisse aucun mot derrière lui ; il s'arrête à un saut de ligne, un guillemet double, une apostrophe inverse, `;`, `|`, une virgule ou un deux-points suivis d'un blanc, une parenthèse fermante qui n'est pas suivie d'un séparateur (`Program Files (x86)\` continue), ou un blanc suivi d'un autre chemin. Une apostrophe placée juste avant le chemin reste autour du nom de fichier (`'C:\Users\Bob\x.db'` donne `'x.db'`). Quand rien ne l'arrête, la règle est prudente : elle retire le texte qui suit jusqu'au dernier séparateur de la ligne plutôt que de laisser passer un dossier. Deux limites (précisées par #437). Un chemin relatif sans début reconnu garde l'ancien repli, d'un blanc à l'autre : un morceau qui porte deux séparateurs ou plus est réduit à son dernier segment (`dossier/sous/x.db` donne `x.db`), un morceau qui n'en porte qu'un voit son séparateur remplacé par `-`, si bien que les mots d'un dossier qui contient un blanc restent (`dossier/sous dossier/x.db` donne `dossier-sous dossier-x.db`) ; un tel chemin ne part pas du dossier personnel, donc n'en contient pas le nom. Et un nom de dossier qui contient l'un des arrêts (une apostrophe inverse, `;`, `|`, une parenthèse fermante suivie d'autre chose qu'un séparateur…) arrête le chemin là : ce nom, ou son début, reste dans la ligne, et ce peut être un nom d'utilisateur (`C:\Users\Jean (Work) Smith\AppData\x.db` donne `Jean (Work) x.db`). Tout autre séparateur devient `-` : aucune ligne d'erreur ne contient `/` ni `\`.
|
||||
|
||||
### 8. Un seul dossier des bases
|
||||
|
||||
|
|
|
|||
|
|
@ -659,6 +659,12 @@ Garde : `src/__integration__/i18n-mock-guard.test.ts` lit tous les `*.test.ts(x)
|
|||
|
||||
`CategoryCombobox` rend sa liste ouverte dans un portail sur `document.body`, en position fixe au-dessus de toutes les fenêtres (`z-[300]`) : aucun conteneur défilant ne la coupe. Fermé, il ne rend que `<div class="relative"><input …/></div>`, sans portail, et son rendu statique n'en demande rien. La règle de placement (sous le champ, au-dessus quand la place manque en dessous et suffit au-dessus) et les écouteurs qui gardent la liste sur son champ pendant qu'elle est ouverte (`scroll` en capture, `resize`, retirés avec elle) sont des fonctions pures de `src/components/shared/placeListbox.ts`, testées sans DOM. Le composant ouvert, lui, ne se rend pas sans DOM : `renderToStaticMarkup` le rend fermé et le harnais refuse ses `useId` et `useLayoutEffect` ; le reste (portail, clic dans la liste, clavier) se vérifie dans l'application. La liste n'étant plus dans le DOM de son conteneur, un écouteur natif de clic extérieur posé par un conteneur la verrait dehors : celui du combobox la compte comme intérieure.
|
||||
|
||||
### Limites connues (#437)
|
||||
|
||||
- **Emplacements préréglés du harnais.** Le harnais donne un emplacement à chaque appel de hook, tous types confondus (`useState`, `useReducer`, `useRef`, `useMemo`, `useCallback`, `useEffect`), dans l'ordre des appels : c'est le helper `slot` de `fakeReact`. Un test qui prérègle un emplacement par sa position (`harness.slots[0] = …`) dépend donc de l'ordre des hooks du composant : un hook ajouté avant, de n'importe quel type, décale les emplacements sans rien dire, et la valeur préréglée tombe sur un autre hook. Préréglages actuels, chacun commenté dans son fichier : `PriceFetchConsentToggle.test.tsx` (emplacements 0 à 2), `PriceFetchControl.test.tsx` (la fenêtre de consentement ouverte), `useOnboardingChecklist.wiring.test.ts` (`mountWith` et deux écritures directes) et `SystemsSettingsPage.test.tsx` ; `OnboardingStepCard.test.tsx` lit le `useRef` de l'emplacement 0. Qui ajoute un hook avant ces emplacements met le test à jour. Un test qui ouvre un état par un clic dans l'arbre rendu (`TransactionTable.wiring.test.ts`) n'a pas cette dépendance.
|
||||
- **`withoutFolders`.** Ses arrêts et ses deux limites (chemin relatif sans début reconnu ; nom de dossier qui contient l'un des arrêts) sont dans l'[ADR 0021](adr/0021-migrations-profils-secondaires.md), § 7.
|
||||
- **Mesure du rattrapage.** Le test `#[ignore]` `a_large_profile_catch_up_duration` (`profile_db.rs`) ne tourne pas en CI et n'asserte rien : il imprime une durée. Sa mesure, prise sur un SSD NVMe, et le risque qu'elle ne couvre pas (disque lent, antivirus qui inspecte la copie) sont dans l'[ADR 0021](adr/0021-migrations-profils-secondaires.md), « Négatives et points de vigilance », point « Le délai de démarrage couvre le rattrapage du profil actif ».
|
||||
|
||||
## CI/CD
|
||||
|
||||
Quatre workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** : les jobs se suivent, ils ne tournent pas en parallèle.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// PriceFetchControl — unit tests (issue #158)
|
||||
//
|
||||
// NOTE: This project does not have @testing-library/react or jsdom configured
|
||||
// (logged as MEDIUM in decisions-log.md). Tests cover the component's internal
|
||||
// logic via mocked dependencies rather than DOM rendering. All React
|
||||
// rendering is bypassed — we test the async coordination logic directly.
|
||||
// (logged as MEDIUM in decisions-log.md). Most tests cover the component's
|
||||
// internal logic via mocked dependencies rather than DOM rendering; the
|
||||
// "displayed texts" tests render it with renderToStaticMarkup, its hooks
|
||||
// coming from the shared harness (src/test/hookHarness.ts).
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
|
|
@ -11,6 +12,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||
// Mocks — declared before imports to satisfy vi.mock hoisting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// React's hooks from the shared harness (#409): the consent dialog opens on a
|
||||
// click (internal state), so the render test below presets its slot (#437).
|
||||
vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig()));
|
||||
|
||||
vi.mock("../../hooks/useIsPremium", () => ({
|
||||
useIsPremium: vi.fn(),
|
||||
}));
|
||||
|
|
@ -40,6 +45,7 @@ vi.mock("lucide-react", () => ({
|
|||
// Imports (after mock declarations)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useIsPremium } from "../../hooks/useIsPremium";
|
||||
import { prices } from "../../services/balance.service";
|
||||
import type { PriceResult } from "../../services/balance.service";
|
||||
|
|
@ -48,6 +54,7 @@ import {
|
|||
setPreference,
|
||||
} from "../../services/userPreferenceService";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { hookHarness } from "../../test/hookHarness";
|
||||
import PriceFetchControl, {
|
||||
__resetBestEffortDismissForTests,
|
||||
} from "./PriceFetchControl";
|
||||
|
|
@ -56,6 +63,7 @@ import PriceFetchControl, {
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const harness = hookHarness();
|
||||
const mockUseIsPremium = vi.mocked(useIsPremium);
|
||||
const mockFetchPrice = vi.mocked(prices.fetchPrice);
|
||||
const mockGetPreference = vi.mocked(getPreference);
|
||||
|
|
@ -363,18 +371,35 @@ describe("PriceFetchControl — displayed texts", () => {
|
|||
__resetBestEffortDismissForTests();
|
||||
vi.resetAllMocks();
|
||||
setPremium(true);
|
||||
harness.clear();
|
||||
});
|
||||
|
||||
it("names the fetch button and the stock best-effort notice through t", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<PriceFetchControl
|
||||
symbol="AAPL"
|
||||
date="2026-04-25"
|
||||
categoryKind="priced"
|
||||
assetType="stock"
|
||||
onPriceFetched={vi.fn()}
|
||||
/>,
|
||||
/**
|
||||
* One render of a stock's control, its hooks from the harness. `consentOpen`
|
||||
* presets slot 0, the control's first hook (`showConsentModal`). The slots
|
||||
* follow the order of the control's hook calls: a hook of any kind added
|
||||
* before it shifts them without a word, and the preset lands on another hook
|
||||
* (one slot per hook call: the `slot` helper of `fakeReact`,
|
||||
* src/test/hookHarness.ts).
|
||||
*/
|
||||
function markup(consentOpen = false): string {
|
||||
if (consentOpen) harness.slots[0] = true;
|
||||
return renderToStaticMarkup(
|
||||
harness.render(
|
||||
() =>
|
||||
PriceFetchControl({
|
||||
symbol: "AAPL",
|
||||
date: "2026-04-25",
|
||||
categoryKind: "priced",
|
||||
assetType: "stock",
|
||||
onPriceFetched: vi.fn(),
|
||||
}) as ReactNode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
it("names the fetch button and the stock best-effort notice through t", () => {
|
||||
const html = markup();
|
||||
expect(html).toContain('aria-label="t(balance.priceFetching.button)"');
|
||||
expect(html).toContain(">t(balance.priceFetching.button)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.bestEffortNotice)<");
|
||||
|
|
@ -382,4 +407,15 @@ describe("PriceFetchControl — displayed texts", () => {
|
|||
expect(html).not.toContain(">best-effort<");
|
||||
expect(html).toContain('aria-label="t(common.close)"');
|
||||
});
|
||||
|
||||
it("names the consent dialog through t once it is open (#437)", () => {
|
||||
expect(markup()).not.toContain('role="dialog"');
|
||||
harness.clear();
|
||||
const html = markup(true);
|
||||
expect(html).toContain('role="dialog"');
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.title)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.body)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.decline)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.accept)<");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Render tests for the key entry form of the License card (#347). No jsdom needed:
|
||||
// react-dom/server renders in vitest's node environment, and `t` echoes its key.
|
||||
// react-dom/server renders in vitest's node environment, and the marked `t` of
|
||||
// src/test/i18nMock.ts renders `t(key)`.
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
|
|
@ -44,4 +45,10 @@ describe("LicenseKeyForm", () => {
|
|||
expect(render(null, true)).toMatch(/<button type="submit" disabled=""/);
|
||||
expect(render(null, false)).not.toMatch(/<button type="submit" disabled=""/);
|
||||
});
|
||||
|
||||
it("names the key field and the submit button through t (#437)", () => {
|
||||
const html = render(null);
|
||||
expect(html).toContain('placeholder="t(license.keyPlaceholder)"');
|
||||
expect(html).toMatch(/<button type="submit"[^>]*>t\(license\.activate\)<\/button>/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -226,7 +226,13 @@ describe("PriceFetchConsentToggle — displayed texts", () => {
|
|||
harness.clear();
|
||||
});
|
||||
|
||||
/** One render once loaded: slots by call order, hasConsent, showConfirm, loading. */
|
||||
/**
|
||||
* One render once loaded: slots by call order, hasConsent, showConfirm, loading.
|
||||
* The presets depend on the order of the toggle's hook calls: slots 0 to 2 are
|
||||
* its first three hooks, and a hook of any kind added before them shifts the
|
||||
* slots without a word, each preset then landing on another hook (one slot per
|
||||
* hook call, every kind: the `slot` helper of `fakeReact`, src/test/hookHarness.ts).
|
||||
*/
|
||||
function markup(premium: boolean, consent: boolean, confirming: boolean): string {
|
||||
setPremium(premium);
|
||||
harness.slots[0] = consent;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nM
|
|||
import TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable";
|
||||
import type { Category, TransactionRow } from "../../shared/types";
|
||||
import type { SplitAccess } from "../../shared/splitGate";
|
||||
import type { LinkedTransferTooltipRow } from "../../services/balance.service";
|
||||
|
||||
const HIGHLIGHT = 'data-onboarding-highlight="true"';
|
||||
const count = (html: string, needle: string) => html.split(needle).length - 1;
|
||||
|
|
@ -46,7 +47,12 @@ const row = (
|
|||
/** The guided entry: uncategorized rows only. */
|
||||
const UNCATEGORIZED = [row(1, "IGA 123"), row(2, "METRO 456"), row(3, "SAQ 789")];
|
||||
|
||||
const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess: SplitAccess = "open") =>
|
||||
const markup = (
|
||||
rows: TransactionRow[],
|
||||
onboardingTarget?: boolean,
|
||||
splitAccess: SplitAccess = "open",
|
||||
linkedTransfersByTxId?: Map<number, LinkedTransferTooltipRow[]>,
|
||||
) =>
|
||||
renderToStaticMarkup(
|
||||
<TransactionTable
|
||||
rows={rows}
|
||||
|
|
@ -60,6 +66,7 @@ const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess:
|
|||
onSaveSplit={vi.fn(async () => undefined)}
|
||||
onDeleteSplit={vi.fn(async () => undefined)}
|
||||
splitAccess={splitAccess}
|
||||
linkedTransfersByTxId={linkedTransfersByTxId}
|
||||
onboardingTarget={onboardingTarget}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -130,3 +137,22 @@ describe("TransactionTable — the Répartition button (#369)", () => {
|
|||
expect(buttons[0]).toContain("text-orange-500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TransactionTable — texts that go through t (#437)", () => {
|
||||
it("names the transfer icon through t, on the linked row only", () => {
|
||||
const links = new Map<number, LinkedTransferTooltipRow[]>([
|
||||
[2, [{ transaction_id: 2, account_id: 5, account_name: "CELI", direction: "in" }]],
|
||||
]);
|
||||
const html = markup(UNCATEGORIZED, undefined, "open", links);
|
||||
const icon = 'aria-label="t(transactions.transferIcon.ariaLabel)"';
|
||||
expect(count(html, icon)).toBe(1);
|
||||
expect(bodyRows(html)[1]).toContain(icon);
|
||||
});
|
||||
|
||||
it("names every row's notes button through t", () => {
|
||||
// The textarea that shows the same key renders for an expanded row only
|
||||
// (internal state): TransactionTable.wiring.test.ts opens one.
|
||||
const html = markup(UNCATEGORIZED);
|
||||
expect(count(html, 'title="t(transactions.notes.placeholder)"')).toBe(UNCATEGORIZED.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// TransactionTable — what the Répartition button opens (#369).
|
||||
// TransactionTable — what the Répartition button opens (#369), and the notes
|
||||
// row its notes button opens (#437).
|
||||
//
|
||||
// A static render never shows a window: they open on a click, through the
|
||||
// table's own state. React's hooks come from the shared harness
|
||||
// (src/test/hookHarness.ts, #409): the table runs as a plain function, its
|
||||
// state survives from one call to the next as between two renders of a mounted
|
||||
// component, and the test clicks through the props the table hands to each
|
||||
// SplitButton, then renders again to read what opened.
|
||||
// SplitButton (or to a row's notes button), then renders again to read what
|
||||
// opened.
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
|
||||
|
|
@ -140,3 +142,18 @@ describe("TransactionTable — the Répartition button's wiring (#369)", () => {
|
|||
expect(splitWindows(upgraded)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TransactionTable — the notes row (#437)", () => {
|
||||
/** The rows' notes buttons, named by their title (pinned in TransactionTable.render.test.tsx). */
|
||||
const notesButtons = (tree: ReactNode) =>
|
||||
find(tree, "button").filter((b) => b.props.title === "t(transactions.notes.placeholder)");
|
||||
|
||||
it("a row's notes button opens one textarea, named through t", () => {
|
||||
expect(find(render("open"), "textarea")).toHaveLength(0);
|
||||
(notesButtons(render("open"))[1].props.onClick as () => void)();
|
||||
const opened = render("open");
|
||||
const areas = find(opened, "textarea");
|
||||
expect(areas).toHaveLength(1);
|
||||
expect(areas[0].props.placeholder).toBe("t(transactions.notes.placeholder)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,7 +70,15 @@ const ready: OnboardingChecklistState = [
|
|||
/** One render of the mounted provider. */
|
||||
const render = () => harness.render(useOnboardingChecklist);
|
||||
|
||||
/** The state the provider mounts with: its `useReducer` is its first React hook. */
|
||||
/**
|
||||
* The state the provider mounts with: its `useReducer` is its first React hook
|
||||
* (`useEntitlement` and `useLocation` are mocked, so they take no slot). This
|
||||
* preset, like the writes to `harness.slots[0]` further down, depends on the
|
||||
* order of the hook's calls to React: a hook of any kind added before the
|
||||
* reducer shifts the slots without a word, and the state lands on another hook
|
||||
* (one slot per hook call, every kind: the `slot` helper of `fakeReact`,
|
||||
* src/test/hookHarness.ts).
|
||||
*/
|
||||
const mountWith = (state: OnboardingChecklistState) => {
|
||||
harness.slots[0] = state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -163,3 +163,9 @@ describe("DashboardPage on the shared path (#352, #396, #397)", () => {
|
|||
expect(html).toContain('data-marker="filters"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("DashboardPage — texts that go through t (#437)", () => {
|
||||
it("names the KPI section through t", () => {
|
||||
expect(render()).toContain(">t(dashboard.kpiSectionTitle)<");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ beforeEach(() => {
|
|||
describe("SystemsSettingsPage — order of the sections (#372)", () => {
|
||||
it("renders Version, Update, the Journal, then the version history and the data notice", () => {
|
||||
vi.mocked(getVersion).mockResolvedValue("0.17.2");
|
||||
// Slot 0 is the page's first hook, its `version` state. This preset, and the
|
||||
// next test's, depend on the order of the page's hook calls: a hook of any
|
||||
// kind added before it shifts the slots without a word (one slot per hook
|
||||
// call, every kind: the `slot` helper of `fakeReact`, src/test/hookHarness.ts).
|
||||
harness.slots[0] = "0.17.2"; // the version, already read
|
||||
const html = renderToStaticMarkup(render());
|
||||
const at = (marker: string) => {
|
||||
|
|
|
|||
|
|
@ -736,7 +736,7 @@ describe("describePreparation (#390)", () => {
|
|||
expect(withoutFolders("'C:\\a\\b.db' and 'C:\\Users\\Jean Dupont\\d.db'")).toBe("'b.db' and 'd.db'");
|
||||
});
|
||||
|
||||
it.each([["\""], [";"], ["|"], ["`"], ["\n"]])("withoutFolders ends the folders at %j", (stop) => {
|
||||
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`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -380,10 +380,11 @@ function lastSeparator(text: string, from: number): number {
|
|||
* (Loi 25). A folder may contain blanks (`C:\Users\Jean Dupont\…`,
|
||||
* `/home/marie claire/…`): from its start, a path runs to its LAST separator
|
||||
* before a line break, a double quote, a backtick, `;`, `|`, a `,` or `:`
|
||||
* followed by a blank, a closing parenthesis, or a blank followed by another
|
||||
* path. When nothing ends it, the rule is conservative: text after a path is
|
||||
* dropped up to the last separator of the line (`/a/x.db then I/O` gives
|
||||
* `O`), rather than letting a folder through. A quote right before the path is
|
||||
* followed by a blank, a closing parenthesis not followed by a separator, or a
|
||||
* blank followed by another path. When nothing ends it, the rule is
|
||||
* conservative: text after a path is dropped up to the last separator of the
|
||||
* line (`/a/x.db then I/O` gives `O`), rather than letting a folder through.
|
||||
* Limits in ADR 0021, section 7. A quote right before the path is
|
||||
* not part of it, so both quotes stay around the file name
|
||||
* (`'C:\Users\Bob\x.db'` gives `'x.db'`). Any other separator becomes `-` (SQLite's "disk I/O error" reads
|
||||
* "disk I-O error"), so an error line never contains a `/` nor a `\` (#395).
|
||||
|
|
|
|||
Loading…
Reference in a new issue