lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests services wiederherstellungspunkteFehlschlag.test.ts

6,9 KB Rohdatei
tests/services/wiederherstellungspunkteFehlschlag.test.ts — 176 Zeilen
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 });