lsa-planer

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

/ tests domain notiztabellen.test.ts

5,7 KB Rohdatei
tests/domain/notiztabellen.test.ts — 152 Zeilen
1 import { readFileSync, readdirSync } from 'node:fs';
2 import { join } from 'node:path';
3 import { describe, expect, it } from 'vitest';
4 import { validateProject } from '@/domain/validation/engine';
5 import { buildSignalPlan, type PlanNote, type SignalPlan } from '@/domain/plan/signalPlan';
6 import { createStandardIntersectionProject } from '@/domain/model/factory';
7
8 /*
9 * Aufraeumen in den Notiztabellen des Pruefberichts (Fassung 5.9.0).
10 *
11 * Zwei Befunde, die sich gegenseitig bedingen: Ein Titel ohne Erzeuger bleibt
12 * unbemerkt stehen, weil ihn nie jemand sieht; ein Sachgebiet, das niemand
13 * prueft, wandert beim naechsten Praefixtest zurueck.
14 */
15
16 const DATUM = new Date('2026-01-01T00:00:00Z');
17 const ENGINE = 'src/domain/validation/engine.ts';
18
19 /** Alle Quelldateien des Fachkerns - dort und nur dort entstehen Notizcodes. */
20 function fachkernDateien(verzeichnis = 'src/domain'): string[] {
21 const gefunden: string[] = [];
22 for (const eintrag of readdirSync(verzeichnis, { withFileTypes: true })) {
23 const pfad = join(verzeichnis, eintrag.name);
24 if (eintrag.isDirectory()) gefunden.push(...fachkernDateien(pfad));
25 else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad);
26 }
27 return gefunden;
28 }
29
30 /** Jeder Code, den irgendeine Rechnung des Fachkerns erzeugt. */
31 function erzeugteCodes(): ReadonlySet<string> {
32 const codes = new Set<string>();
33 for (const datei of fachkernDateien()) {
34 if (datei.replace(/\\/g, '/') === ENGINE) continue;
35 for (const [, code] of readFileSync(datei, 'utf8').matchAll(/\bcode: '([^']+)'/g)) {
36 if (code !== undefined) codes.add(code);
37 }
38 }
39 return codes;
40 }
41
42 /**
43 * Schluessel einer der beiden Tabellen aus engine.ts.
44 *
45 * Gelesen wird der Quelltext: Die Tabellen stehen in Funktionen und sind
46 * bewusst nicht ausgefuehrt. Ein Aufruf je Code wuerde nur zeigen, dass ein
47 * Titel herauskommt - nicht, dass ihn noch jemand braucht.
48 */
49 function tabellenschluessel(einleitung: string): readonly string[] {
50 const text = readFileSync(ENGINE, 'utf8');
51 const beginn = text.indexOf(einleitung);
52 expect(beginn).toBeGreaterThan(-1);
53 const ende = text.indexOf('\n };', beginn);
54 expect(ende).toBeGreaterThan(beginn);
55 const koerper = text.slice(beginn + einleitung.length, ende);
56 const schluessel: string[] = [];
57 for (const [, mitAnfuehrung, ohne] of koerper.matchAll(
58 /^ {4}(?:'([^']+)'|([A-Za-z][A-Za-z0-9]*)):/gm,
59 )) {
60 const name = mitAnfuehrung ?? ohne;
61 if (name !== undefined) schluessel.push(name);
62 }
63 return schluessel;
64 }
65
66 describe('Kein Titel und kein Rat ohne Erzeuger', () => {
67 const codes = erzeugteCodes();
68
69 it('findet die Notizcodes des Fachkerns', () => {
70 // Faellt die Suche leer aus, bewacht der Waechter nichts mehr.
71 expect(codes.size).toBeGreaterThan(40);
72 expect(codes.has('freigabezeit-rest')).toBe(true);
73 });
74
75 it('findet die Schluessel beider Tabellen', () => {
76 expect(tabellenschluessel('const titles: Record<string, string> = {').length).toBeGreaterThan(
77 30,
78 );
79 expect(
80 tabellenschluessel('const suggestions: Record<string, string> = {').length,
81 ).toBeGreaterThan(30);
82 });
83
84 it('fuehrt in der Ueberschriftentabelle nur Codes, die auch entstehen', () => {
85 // 'umlaufzeit-zu-kurz-fuer-freigabezeiten' stand hier, obwohl ihn seit
86 // Langem niemand mehr erzeugt: eine Ueberschrift zu einer Meldung, die es
87 // nicht gibt, gepflegt bei jeder Aenderung mit.
88 const ohneErzeuger = tabellenschluessel('const titles: Record<string, string> = {').filter(
89 (code) => !codes.has(code),
90 );
91 expect(ohneErzeuger).toEqual([]);
92 });
93
94 it('fuehrt in der Rattabelle nur Codes, die auch entstehen', () => {
95 // Dieser Fall war vor der Behebung bereits gruen: Zu
96 // 'umlaufzeit-zu-kurz-fuer-freigabezeiten' stand nur eine Ueberschrift,
97 // nie ein Rat (nachgemessen am Stand vor der Behebung: 21 Schluessel,
98 // keiner ohne Erzeuger). Er belegt die Behebung also nicht - er faengt
99 // kuenftige verwaiste Raete.
100 const ohneErzeuger = tabellenschluessel('const suggestions: Record<string, string> = {').filter(
101 (code) => !codes.has(code),
102 );
103 expect(ohneErzeuger).toEqual([]);
104 });
105 });
106
107 describe('Der ungueltige Saettigungsgrad steht bei der Leistungsfaehigkeit', () => {
108 const project = createStandardIntersectionProject('Sachgebiet', DATUM);
109 const plan = buildSignalPlan(project);
110
111 function befundZu(note: PlanNote) {
112 const mitNotiz: SignalPlan = { ...plan, notes: [note] };
113 return validateProject(project, mitNotiz, DATUM).findings.find(
114 (f) => f.rule === `signalplan.${note.code}`,
115 );
116 }
117
118 it('fuehrt saettigungsgrad-ungueltig unter Leistungsfaehigkeit', () => {
119 // Der Praefixtest lautete 'saettigungsgrade' (Mehrzahl) und traf die
120 // Einzahl nicht: Die Meldung stand in der Karte "Signalzeitenplan",
121 // waehrend die Meldung zum FEHLENDEN Saettigungsgrad daneben unter
122 // "Leistungsfähigkeit" stand.
123 const befund = befundZu({
124 severity: 'fehler',
125 code: 'saettigungsgrad-ungueltig',
126 message: 'Prüftext',
127 });
128
129 expect(befund?.category).toBe('leistungsfaehigkeit');
130 });
131
132 it('laesst die Meldung zu fehlenden Saettigungsgraden, wo sie war', () => {
133 const befund = befundZu({
134 severity: 'fehler',
135 code: 'saettigungsgrade-fehlen',
136 message: 'Prüftext',
137 });
138
139 expect(befund?.category).toBe('leistungsfaehigkeit');
140 });
141
142 it('fuehrt die unvollstaendig erfassten Verkehrsstaerken ebenfalls dort', () => {
143 // Derselbe Sachverhalt in schwaecherer Form (Befund 32).
144 const befund = befundZu({
145 severity: 'warnung',
146 code: 'saettigungsgrade-unvollstaendig',
147 message: 'Prüftext',
148 });
149
150 expect(befund?.category).toBe('leistungsfaehigkeit');
151 });
152 });