lsa-planer

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

/ tests domain ueberlappungJePaar.test.ts

6,4 KB Rohdatei
tests/domain/ueberlappungJePaar.test.ts — 144 Zeilen
1 import { describe, expect, it, vi } from 'vitest';
2 import type { Conflict, Project } from '@/domain/model/project';
3 import type { PlannedSignalGroup } from '@/domain/plan/signalPlan';
4
5 /*
6 * Fassung 5.12.0: `ueberlappenImUmlauf` wurde je Paar mehrfach gerechnet.
7 *
8 * Zwei Regeln stellen dieselbe Frage. `checkSimultaneousGreen` fragt sie, weil
9 * gleichzeitige Freigabe feindlicher Signalgruppen ein Fehler ist;
10 * `checkIntergreenGaps` fragt sie, um bei Ueberlappung zu schweigen - dort ist
11 * kein Abstand messbar. Beide gehen ueber `project.conflicts`, und die stehen
12 * paarweise; beide merken sich ein Paar erst, wenn sie es MELDEN. Am haeufigen
13 * Regelfall - Paar erfasst, beide Richtungen, keine Ueberlappung - wurde
14 * derselbe Umlauf deshalb VIERMAL in Schritten von 0,5 s abgetastet.
15 *
16 * GEMESSEN (Knotenpunkt mit 12 Signalgruppen, 4 Phasen, 108 Beziehungen,
17 * Median aus 7 Runden zu je 200 Laeufen, derselbe Stand einmal mit und einmal
18 * ohne Gedaechtnis): 2,17 ms je Pruefung ohne, 0,74 ms mit. Die mehrfache
19 * Abtastung war dort rund zwei Drittel der Pruefungslaufzeit; die Befunde
20 * bleiben in Zahl und Wortlaut dieselben.
21 *
22 * DIE WACHE ZAEHLT STATT ZU MESSEN - dieselbe Entscheidung wie in
23 * pruefungLaufzeit.test.ts: Eine Zeitschranke haengt am Rechner, der sie
24 * ausfuehrt. Gezaehlt werden die Abtastungen selbst, also die Aufrufe von
25 * `isGreenAt` aus der Pruefung heraus. Sie sind vom Rechner unabhaengig.
26 *
27 * GEGEN DEN ALTSTAND schlagen die beiden ersten Faelle fehl (viermal statt
28 * einmal abgetastet); der dritte besteht dort ebenso - am Ergebnis der Pruefung
29 * darf sich nichts aendern, und das ist die Halbmiete dieses Punktes.
30 */
31
32 const DATUM = new Date('2026-01-01T00:00:00Z');
33
34 /** Jede Abtastung als "Gruppe@Zeitpunkt" - Doppelungen bleiben stehen. */
35 const abtastungen: string[] = [];
36
37 vi.mock('@/domain/plan/aspects', async (echt) => {
38 const modul = await echt<typeof import('@/domain/plan/aspects')>();
39 return {
40 ...modul,
41 isGreenAt: (
42 gruppe: Pick<PlannedSignalGroup, 'greens' | 'times'> & { readonly groupId?: string },
43 zeit: number,
44 umlauf: number,
45 ): boolean => {
46 abtastungen.push(`${gruppe.groupId ?? '?'}@${zeit}`);
47 return modul.isGreenAt(gruppe, zeit, umlauf);
48 },
49 };
50 });
51
52 const { createConflict, createEmptyProject, createPhase, createSignalGroup } =
53 await import('@/domain/model/factory');
54 const { buildSignalPlan } = await import('@/domain/plan/signalPlan');
55 const { validateProject } = await import('@/domain/validation/engine');
56
57 /**
58 * Zwei feindliche Signalgruppen in zwei Phasen, Beziehung in beiden Richtungen
59 * erfasst - der Regelfall, in dem beide Regeln bis zur Abtastung kommen und
60 * keine von beiden meldet.
61 */
62 function zweiGruppen(): Project {
63 const basis = createEmptyProject('Abtastung', DATUM, 'knotenpunkt');
64 const k1 = createSignalGroup({ name: 'K1', mode: 'kfz', armId: null, index: 0 });
65 const k2 = createSignalGroup({ name: 'K2', mode: 'kfz', armId: null, index: 1 });
66 const p1 = createPhase('Phase 1', [k1.id]);
67 const p2 = createPhase('Phase 2', [k2.id]);
68 const beziehung = (fromId: string, toId: string): Conflict => ({
69 ...createConflict(fromId, toId),
70 clearingDistance: 22,
71 enteringDistance: 8,
72 });
73 return {
74 ...basis,
75 signalGroups: [k1, k2],
76 phases: [p1, p2],
77 conflicts: [beziehung(k1.id, k2.id), beziehung(k2.id, k1.id)],
78 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
79 };
80 }
81
82 function abtastungenEinerPruefung(project: Project, plan = buildSignalPlan(project)): string[] {
83 abtastungen.length = 0;
84 validateProject(project, plan, DATUM);
85 return [...abtastungen];
86 }
87
88 describe('Abtastung des Umlaufs auf gleichzeitige Freigabe', () => {
89 it('tastet dasselbe Paar nur einmal ab', () => {
90 const gezaehlt = abtastungenEinerPruefung(zweiGruppen());
91
92 // Voraussetzung des Aufbaus: Es wird ueberhaupt abgetastet.
93 expect(gezaehlt.length).toBeGreaterThan(0);
94 // Innerhalb EINER Abtastung ist jede Kombination aus Signalgruppe und
95 // Zeitpunkt hoechstens einmal gefragt (die Schleife geht je Zeitpunkt
96 // einmal ueber a und hoechstens einmal ueber b). Bei nur zwei Gruppen im
97 // Projekt heisst eine Doppelung deshalb: derselbe Umlauf wurde fuer
98 // dasselbe Paar ein zweites Mal abgetastet.
99 expect(new Set(gezaehlt).size).toBe(gezaehlt.length);
100 });
101
102 it('tastet nicht oefter ab, wenn dasselbe Paar zweimal in der Liste steht', () => {
103 const beide = zweiGruppen();
104 // DERSELBE Plan fuer beide Faelle: Nimmt man der Anlage eine Richtung, wird
105 // der Plan ein anderer (Zwischenzeiten, Umlaufzeit) und mit ihm die Zahl
106 // der Zeitschritte. Verglichen werden soll aber allein, wie oft dieselbe
107 // Abtastung angefordert wird.
108 const plan = buildSignalPlan(beide);
109 const einseitig: Project = { ...beide, conflicts: beide.conflicts.slice(0, 1) };
110
111 const mitEiner = abtastungenEinerPruefung(einseitig, plan);
112 const mitBeiden = abtastungenEinerPruefung(beide, plan);
113
114 expect(mitEiner.length).toBeGreaterThan(0);
115 // Ohne Gedaechtnis stand hier das Doppelte: Jede Richtung brachte beide
116 // Regeln erneut zur Abtastung desselben Umlaufs.
117 expect(mitBeiden.length).toBe(mitEiner.length);
118 });
119
120 it('meldet dabei unveraendert dieselben Befunde', () => {
121 const project = zweiGruppen();
122 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
123 // Getrennte Phasen, kein Fehler aus diesen beiden Regeln.
124 expect(bericht.findings.some((f) => f.rule === 'signalplan.feindliche-freigabe')).toBe(false);
125 expect(bericht.findings.some((f) => f.rule === 'zwischenzeiten.abstand-zu-kurz')).toBe(false);
126
127 // Beide Gruppen in EINE Phase: Jetzt zeigen sie gleichzeitig Freigabe, und
128 // die Meldung darueber muss stehen - genau einmal fuer das Paar.
129 const zusammen: Project = {
130 ...project,
131 phases: [{ ...project.phases[0]!, signalGroupIds: project.signalGroups.map((g) => g.id) }],
132 program: { ...project.program, phaseOrder: [project.phases[0]!.id] },
133 };
134 const gemeinsam = validateProject(zusammen, buildSignalPlan(zusammen), DATUM);
135 expect(
136 gemeinsam.findings.filter(
137 (f) =>
138 f.rule === 'phasen.feindlich-in-phase' || f.rule === 'signalplan.feindliche-freigabe',
139 ),
140 ).toHaveLength(1);
141 // Und die Abstandsregel schweigt bei Ueberlappung weiterhin.
142 expect(gemeinsam.findings.some((f) => f.rule === 'zwischenzeiten.abstand-zu-kurz')).toBe(false);
143 });
144 });