lsa-planer

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

/ tests domain pruefungLaufzeit.test.ts

5,5 KB Rohdatei
tests/domain/pruefungLaufzeit.test.ts — 131 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { validateProject } from '@/domain/validation/engine';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import {
5 createArm,
6 createConflict,
7 createEmptyProject,
8 createPhase,
9 createSignalGroup,
10 } from '@/domain/model/factory';
11 import type { Conflict, Project } from '@/domain/model/project';
12
13 /*
14 * Strittiger Befund S2 (Fassung 5.11.0): Die Pruefung tastete je
15 * Konfliktbeziehung die GANZE Konfliktliste erneut ab.
16 *
17 * engine.ts, checkConflicts: `if (!project.conflicts.some((c) =>
18 * c.fromId === conflict.toId && c.toId === conflict.fromId)) {`
19 *
20 * Die Zeile steht in der Schleife `for (const conflict of project.conflicts)`
21 * und sucht die Gegenrichtung jedes Mal von vorn. Der Aufwand waechst damit
22 * quadratisch in der Zahl der Beziehungen und, weil diese bei erfasster Matrix
23 * selbst quadratisch mit der Zahl der Signalgruppen waechst, in vierter Potenz
24 * der Gruppenzahl. `validateProject` laeuft ueber `ProjectStore.applyProject`
25 * bei JEDER Aenderung synchron mit; weder das Einlesen noch die Oberflaeche
26 * setzen eine Obergrenze fuer die Zahl der Signalgruppen.
27 *
28 * GEMESSEN am Altstand (dieselbe Anlage, nur groesser; Zugriffe auf die
29 * Konfliktliste je Beziehung): K = 50 -> 29,5 · K = 200 -> 104,5 ·
30 * K = 800 -> 404,5 · K = 3200 -> 1604,5. Der Anteil ist exakt K/2 + 4,5, also
31 * genau die halbe Liste je Beziehung. Laufzeit von `validateProject` allein:
32 * 200 Signalgruppen, 20 000 Beziehungen: 2,2 s je Aenderung.
33 *
34 * DIE WACHE ZAEHLT STATT ZU MESSEN: Eine Zeitschranke haengt am Rechner, der
35 * sie ausfuehrt. Gezaehlt werden deshalb die Elementzugriffe auf die
36 * Konfliktliste - dieselbe Groesse, aber vom Rechner unabhaengig. Die Zahl je
37 * Beziehung muss beschraenkt bleiben, statt mit der Liste zu wachsen.
38 *
39 * KEINE ZAHL AENDERT SICH: Die Pruefung liefert vorher wie nachher dieselben
40 * Befunde; der Nachweis steht als eigener Fall darunter.
41 *
42 * GEGEN DEN ALTSTAND schlagen die beiden ersten Faelle fehl (404,5 bzw.
43 * 1604,5 Zugriffe je Beziehung); der dritte besteht dort ebenso.
44 */
45
46 const DATUM = new Date('2026-01-01T00:00:00Z');
47
48 /**
49 * Knotenpunkt mit `n` Signalgruppen in zwei Phasen und vollstaendig erfasster
50 * Konfliktmatrix ueber die Phasengrenze - in beiden Richtungen, also der
51 * fehlerfreie Regelfall, in dem die Gegenrichtungsprobe die Liste bis zum Ende
52 * durchlaeuft.
53 */
54 function knotenpunkt(n: number): Project {
55 const base = createEmptyProject('Grosse Anlage', DATUM, 'knotenpunkt');
56 const arm = createArm('Nord', 'Nord', 2, 50);
57 const gruppen = Array.from({ length: n }, (_, i) =>
58 createSignalGroup({ name: `K${i + 1}`, mode: 'kfz', armId: arm.id, index: i }),
59 );
60 const erste = gruppen.filter((_, i) => i % 2 === 0);
61 const zweite = gruppen.filter((_, i) => i % 2 === 1);
62 const p1 = createPhase(
63 'Phase 1',
64 erste.map((g) => g.id),
65 );
66 const p2 = createPhase(
67 'Phase 2',
68 zweite.map((g) => g.id),
69 );
70 const conflicts: Conflict[] = [];
71 for (const a of erste) {
72 for (const b of zweite) {
73 conflicts.push({ ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8 });
74 conflicts.push({ ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 });
75 }
76 }
77 return {
78 ...base,
79 intersection: { ...base.intersection, arms: [arm] },
80 signalGroups: gruppen,
81 conflicts,
82 phases: [p1, p2],
83 program: { ...base.program, phaseOrder: [p1.id, p2.id] },
84 };
85 }
86
87 /** Elementzugriffe auf `project.conflicts` waehrend einer Pruefung, je Beziehung. */
88 function zugriffeJeBeziehung(projekt: Project): number {
89 const plan = buildSignalPlan(projekt);
90 let zugriffe = 0;
91 const beobachtet = new Proxy(projekt.conflicts as Conflict[], {
92 get(ziel, eigenschaft, empfaenger) {
93 if (typeof eigenschaft === 'string' && /^\d+$/.test(eigenschaft)) zugriffe += 1;
94 return Reflect.get(ziel, eigenschaft, empfaenger) as unknown;
95 },
96 });
97 validateProject({ ...projekt, conflicts: beobachtet }, plan, DATUM);
98 return zugriffe / projekt.conflicts.length;
99 }
100
101 describe('Aufwand der Pruefung bei vielen Signalgruppen', () => {
102 it('liest die Konfliktliste nicht je Beziehung einmal ganz durch', () => {
103 const projekt = knotenpunkt(40);
104 // Voraussetzung des Aufbaus: eine Liste, die gross genug ist, um den
105 // Unterschied zu zeigen.
106 expect(projekt.conflicts).toHaveLength(800);
107 expect(zugriffeJeBeziehung(projekt)).toBeLessThan(20);
108 });
109
110 it('haelt den Aufwand je Beziehung, wenn die Anlage waechst', () => {
111 const klein = zugriffeJeBeziehung(knotenpunkt(40));
112 const gross = zugriffeJeBeziehung(knotenpunkt(80));
113 // Viermal so viele Beziehungen duerfen den Aufwand JE BEZIEHUNG nicht
114 // vervierfachen; er darf ueberhaupt nicht mit der Listenlaenge wachsen.
115 expect(gross).toBeLessThanOrEqual(klein + 1);
116 });
117
118 it('meldet dabei unveraendert dieselben Befunde', () => {
119 const projekt = knotenpunkt(12);
120 const bericht = validateProject(projekt, buildSignalPlan(projekt), DATUM);
121 // Erfasste Gegenrichtungen: keine Meldung. Sobald eine fehlt, meldet die
122 // Regel sie - genau einmal, fuer die verbliebene Richtung.
123 expect(bericht.findings.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(false);
124
125 const halb: Project = { ...projekt, conflicts: projekt.conflicts.slice(0, -1) };
126 const fehlend = validateProject(halb, buildSignalPlan(halb), DATUM);
127 expect(fehlend.findings.filter((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toHaveLength(
128 1,
129 );
130 });
131 });