lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests services wiederherstellungspunkteFehlschlag.test.ts
| 1 | import { readFileSync, readdirSync } from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import { afterEach, describe, expect, it } from 'vitest'; |
| 4 | import { addRecoveryPoint, setzeSpeicherUmgebung } from '@/services/storage'; |
| 5 | import { createEmptyProject } from '@/domain/model/factory'; |
| 6 | |
| 7 | /** |
| 8 | * Befunde 31 und 32 zur Fassung 5.9.0. |
| 9 | * |
| 10 | * BEFUND 31: `src/services/autosave.ts` sagte im Klassenkommentar zu, dass sich |
| 11 | * auf einen Wiederherstellungspunkt "gezielt zurueckgehen laesst". |
| 12 | * `listRecoveryPoints` hat ausserhalb der Pruefungen keinen Aufrufer, und in der |
| 13 | * Oberflaeche gibt es kein Element, keinen Menuepunkt und kein Tastenkuerzel |
| 14 | * dafuer. Zurueckgehen laesst sich auf keinen. Zusagen in Kommentaren sind |
| 15 | * Teil des Programms; Zusage und Bedienweg gehoeren also zusammen. |
| 16 | * |
| 17 | * BEFUND 32: `addRecoveryPoint` fing seinen Fehler selbst ab und erfuellte das |
| 18 | * Versprechen auch dann, wenn nichts geschrieben wurde. `AutoSave` setzte |
| 19 | * daraufhin `letzterPunkt` und sperrte damit jeden weiteren Versuch fuer |
| 20 | * denselben Projektzustand - obwohl der Kommentar drei Zeilen darueber |
| 21 | * ausdruecklich festlegt: "Erst NACH dem geglueckten Schreiben gesetzt: Ein |
| 22 | * gescheiterter Punkt liegt nirgends und darf den naechsten Versuch nicht |
| 23 | * sperren." Wer nach der letzten Aenderung nicht weiterarbeitet - der Regelfall |
| 24 | * am Feierabend -, bekam fuer diesen Stand nie einen Punkt. |
| 25 | * |
| 26 | * Im Ersatzweg wiegt es schwerer: `fuegePunktErsatzweiseHinzu` LEERT bei einem |
| 27 | * Kontingentfehler die ganze Punkteliste. Danach loeste das Versprechen auf, |
| 28 | * `letzterPunkt` wurde gesetzt, und die eben geleerte Liste wurde fuer diesen |
| 29 | * Arbeitsstand nie wieder gefuellt. |
| 30 | */ |
| 31 | |
| 32 | // --- Befund 31: die Zusage im Kommentar ------------------------------------- |
| 33 | |
| 34 | const AUTOSAVE = readFileSync('src/services/autosave.ts', 'utf8'); |
| 35 | |
| 36 | /** Alle Quelldateien unter `src/`. */ |
| 37 | function quelldateien(verzeichnis: string): string[] { |
| 38 | const gefunden: string[] = []; |
| 39 | for (const eintrag of readdirSync(verzeichnis, { withFileTypes: true })) { |
| 40 | const pfad = path.join(verzeichnis, eintrag.name); |
| 41 | if (eintrag.isDirectory()) gefunden.push(...quelldateien(pfad)); |
| 42 | else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad); |
| 43 | } |
| 44 | return gefunden; |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Ruft die Datei die Leseseite wirklich auf? |
| 49 | * |
| 50 | * Kommentarzeilen bleiben aussen vor: Eine Erwaehnung im Fliesstext ist kein |
| 51 | * Bedienweg, wuerde die Wache aber sonst still ausser Kraft setzen. |
| 52 | */ |
| 53 | function ruftLeseseiteAuf(quelltext: string): boolean { |
| 54 | return quelltext.split('\n').some((zeile) => { |
| 55 | const rumpf = zeile.trim(); |
| 56 | if (rumpf.startsWith('*') || rumpf.startsWith('//') || rumpf.startsWith('/*')) return false; |
| 57 | return /listRecoveryPoints\s*\(/.test(rumpf); |
| 58 | }); |
| 59 | } |
| 60 | |
| 61 | describe('Wiederherstellungspunkte: Zusage und Bedienweg', () => { |
| 62 | it('verspricht nur dann ein gezieltes Zurueckgehen, wenn es einen Weg dorthin gibt', () => { |
| 63 | // Sachstand zuerst: Ruft ausser der Speicherschicht selbst irgendetwas in |
| 64 | // `src/` die Leseseite auf? Nur ueber sie kaeme ein Bedienweg an die Punkte. |
| 65 | const eigenerOrt = path.join('src', 'services', 'storage.ts'); |
| 66 | const aufrufer = quelldateien('src').filter( |
| 67 | (datei) => datei !== eigenerOrt && ruftLeseseiteAuf(readFileSync(datei, 'utf8')), |
| 68 | ); |
| 69 | |
| 70 | // Und dann: Der Kommentar sagt genau so viel zu, wie eingeloest ist. |
| 71 | expect( |
| 72 | /gezielt zurueckgehen/.test(AUTOSAVE), |
| 73 | aufrufer.length === 0 |
| 74 | ? 'Der Kommentar verspricht ein gezieltes Zurueckgehen, das es nicht gibt' |
| 75 | : 'Es gibt einen Bedienweg, aber der Kommentar nennt ihn nicht mehr', |
| 76 | ).toBe(aufrufer.length > 0); |
| 77 | }); |
| 78 | }); |
| 79 | |
| 80 | // --- Befund 32: ein gescheiterter Punkt darf nicht als geschrieben gelten ---- |
| 81 | |
| 82 | /** Eine Datenbank, deren Transaktionen sich nicht oeffnen lassen. */ |
| 83 | function fabrikOhneTransaktion(): IDBFactory { |
| 84 | return { |
| 85 | open: (): unknown => { |
| 86 | const anfrage: Record<string, unknown> = { |
| 87 | result: undefined, |
| 88 | onupgradeneeded: null, |
| 89 | onsuccess: null, |
| 90 | onerror: null, |
| 91 | onblocked: null, |
| 92 | }; |
| 93 | queueMicrotask(() => { |
| 94 | anfrage['result'] = { |
| 95 | objectStoreNames: { contains: (): boolean => true }, |
| 96 | createObjectStore: (): unknown => ({}), |
| 97 | transaction: (): never => { |
| 98 | throw new Error('Die Transaktion wurde abgebrochen.'); |
| 99 | }, |
| 100 | close: (): void => undefined, |
| 101 | onversionchange: null, |
| 102 | }; |
| 103 | (anfrage['onsuccess'] as (() => void) | null)?.(); |
| 104 | }); |
| 105 | return anfrage; |
| 106 | }, |
| 107 | } as unknown as IDBFactory; |
| 108 | } |
| 109 | |
| 110 | const KEY_RECOVERY = 'lsa-planer.v5.recovery'; |
| 111 | |
| 112 | /** |
| 113 | * Ein Ersatzspeicher, dem beim Ablegen eines Punktes der Platz ausgeht. |
| 114 | * |
| 115 | * Die Probe von `storage()` und das Leeren der Liste gelingen weiterhin - genau |
| 116 | * so verhaelt sich ein voller localStorage: Was kuerzer ist als das |
| 117 | * Vorhandene, passt noch hinein. |
| 118 | */ |
| 119 | function speicherMitVollerPunkteliste(): Storage { |
| 120 | const daten = new Map<string, string>(); |
| 121 | return { |
| 122 | get length(): number { |
| 123 | return daten.size; |
| 124 | }, |
| 125 | clear: (): void => { |
| 126 | daten.clear(); |
| 127 | }, |
| 128 | getItem: (schluessel: string): string | null => daten.get(schluessel) ?? null, |
| 129 | key: (index: number): string | null => [...daten.keys()][index] ?? null, |
| 130 | removeItem: (schluessel: string): void => { |
| 131 | daten.delete(schluessel); |
| 132 | }, |
| 133 | setItem: (schluessel: string, wert: string): void => { |
| 134 | if (schluessel === KEY_RECOVERY && wert !== '[]') { |
| 135 | const fehler = new Error('Der Speicher ist voll.'); |
| 136 | fehler.name = 'QuotaExceededError'; |
| 137 | throw fehler; |
| 138 | } |
| 139 | daten.set(schluessel, String(wert)); |
| 140 | }, |
| 141 | }; |
| 142 | } |
| 143 | |
| 144 | afterEach(() => { |
| 145 | setzeSpeicherUmgebung(null); |
| 146 | }); |
| 147 | |
| 148 | describe('Ein Wiederherstellungspunkt, der nicht geschrieben werden konnte', () => { |
| 149 | it('meldet den Fehlschlag, statt das Versprechen zu erfuellen', async () => { |
| 150 | setzeSpeicherUmgebung({ indexedDB: fabrikOhneTransaktion(), localStorage: null }); |
| 151 | |
| 152 | await expect( |
| 153 | addRecoveryPoint(createEmptyProject('Knotenpunkt')), |
| 154 | 'Ein gescheiterter Punkt gilt als geschrieben und sperrt den naechsten Versuch', |
| 155 | ).rejects.toThrow(); |
| 156 | }); |
| 157 | |
| 158 | it('meldet ihn auch im Ersatzweg, wo dabei die ganze Liste geleert wird', async () => { |
| 159 | const speicher = speicherMitVollerPunkteliste(); |
| 160 | setzeSpeicherUmgebung({ indexedDB: null, localStorage: speicher }); |
| 161 | |
| 162 | await expect(addRecoveryPoint(createEmptyProject('Knotenpunkt'))).rejects.toThrow(); |
| 163 | |
| 164 | // Der Verlust ist echt: Vorher konnten dort bis zu fuenf Staende liegen. |
| 165 | expect(speicher.getItem(KEY_RECOVERY)).toBe('[]'); |
| 166 | }); |
| 167 | |
| 168 | it('meldet keinen Fehlschlag, wenn es ueberhaupt keinen Speicher gibt', async () => { |
| 169 | // Ohne jeden Speicher gibt es nichts zu wiederholen - ein erneuter Versuch |
| 170 | // alle zwei Minuten schriebe wieder nichts. Das ist kein Fehlschlag, |
| 171 | // sondern eine Umgebung ohne Ablage. |
| 172 | setzeSpeicherUmgebung({ indexedDB: null, localStorage: null }); |
| 173 | |
| 174 | await expect(addRecoveryPoint(createEmptyProject('Knotenpunkt'))).resolves.toBeUndefined(); |
| 175 | }); |
| 176 | }); |