lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain paarschluessel.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { validateProject } from '@/domain/validation/engine'; |
| 3 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 4 | import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import type { Project } from '@/domain/model/project'; |
| 6 | |
| 7 | /* |
| 8 | * Befund 16 (Fassung 5.11.0): Der Pruefbericht verschwieg ein zweites |
| 9 | * feindliches Paar mit gleichzeitiger Freigabe. |
| 10 | * |
| 11 | * `checkSimultaneousGreen` und `pruefeFeindlichInPhase` teilen sich eine Menge |
| 12 | * bereits gemeldeter Paare. Der Schluessel dieser Menge entstand aus |
| 13 | * `[a, b].sort().join('|')` - genau der zusammengeklebte Schluessel, den |
| 14 | * Befund 61 in `intergreenKey` und in `parseConflicts` beseitigt hat. |
| 15 | * |
| 16 | * Eine Signalgruppenkennung kommt ungeprueft aus der Projektdatei: Das |
| 17 | * Einlesen beanstandet nur die leere und die doppelt vergebene Kennung, kein |
| 18 | * Zeichenrepertoire. Die Paare ("a|b", "c") und ("a", "b|c") ergaben damit |
| 19 | * beide den Schluessel "a|b|c"; das zuerst gemeldete Paar sperrte das zweite |
| 20 | * ueber `if (reported.has(key)) continue;`. Weil beide Teilpruefungen dieselbe |
| 21 | * Menge benutzen, stand das zweite Paar danach ueberhaupt nicht mehr im |
| 22 | * Bericht - weder als 'phasen.feindlich-in-phase' noch als |
| 23 | * 'signalplan.feindliche-freigabe' -, und die Fehlerzahl im Kopf des Berichts |
| 24 | * nannte eine Ursache zu wenig. |
| 25 | * |
| 26 | * Das ist die gefaehrliche Richtung: Die unterdrueckte Meldung ist die, deren |
| 27 | * Wortlaut lautet "Dieser Plan darf nicht geschaltet werden". |
| 28 | * |
| 29 | * GEGEN DEN ALTSTAND schlagen die beiden Faelle mit "|" in der Kennung fehl |
| 30 | * (zwei statt drei Paare, eine Fehlerzahl zu wenig); die Gegenprobe mit |
| 31 | * gewoehnlichen Kennungen besteht dort ebenso. |
| 32 | */ |
| 33 | |
| 34 | const STICHTAG = new Date('2026-01-01T00:00:00Z'); |
| 35 | |
| 36 | /** |
| 37 | * Vier Signalgruppen des Regelknotenpunkts in EINER Phase, die Kennungen frei |
| 38 | * gewaehlt. Erhalten bleiben die drei Konfliktpaare K1/K3, K1/F2 und K3/F1 - |
| 39 | * jedes in beiden Richtungen erfasst. |
| 40 | */ |
| 41 | function vierGruppenEinePhase(kennungen: Readonly<Record<string, string>>): Project { |
| 42 | const roh = createStandardIntersectionProject('Paarschluessel', STICHTAG); |
| 43 | const behalten = ['K1', 'K3', 'F1', 'F2']; |
| 44 | const gruppen = roh.signalGroups.filter((g) => behalten.includes(g.name)); |
| 45 | const neueId = new Map(gruppen.map((g) => [g.id, kennungen[g.name] ?? g.id])); |
| 46 | |
| 47 | const signalGroups = gruppen.map((g) => ({ ...g, id: neueId.get(g.id)! })); |
| 48 | const conflicts = roh.conflicts |
| 49 | .filter((c) => neueId.has(c.fromId) && neueId.has(c.toId)) |
| 50 | .map((c) => ({ |
| 51 | ...c, |
| 52 | fromId: neueId.get(c.fromId)!, |
| 53 | toId: neueId.get(c.toId)!, |
| 54 | })); |
| 55 | const phase = createPhase( |
| 56 | 'Phase 1', |
| 57 | signalGroups.map((g) => g.id), |
| 58 | ); |
| 59 | |
| 60 | return { |
| 61 | ...roh, |
| 62 | signalGroups, |
| 63 | conflicts, |
| 64 | phases: [phase], |
| 65 | program: { ...roh.program, phaseOrder: [phase.id] }, |
| 66 | demands: [], |
| 67 | }; |
| 68 | } |
| 69 | |
| 70 | /** Titel aller Meldungen, die ein feindliches Paar mit Freigabe beanstanden. */ |
| 71 | function feindlicheMeldungen(projekt: Project): string[] { |
| 72 | const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); |
| 73 | return bericht.findings |
| 74 | .filter( |
| 75 | (f) => f.rule === 'phasen.feindlich-in-phase' || f.rule === 'signalplan.feindliche-freigabe', |
| 76 | ) |
| 77 | .map((f) => f.title); |
| 78 | } |
| 79 | |
| 80 | /* |
| 81 | * "a|b" (Kfz), "c" (Furt), "a" (Kfz), "b|c" (Furt): Die Paare ("a|b", "c") und |
| 82 | * ("a", "b|c") fielen auf denselben zusammengeklebten Schluessel "a|b|c". |
| 83 | */ |
| 84 | const MIT_STRICH = { K1: 'a|b', F2: 'c', K3: 'a', F1: 'b|c' } as const; |
| 85 | |
| 86 | describe('Paarschluessel des Pruefberichts bei einer Kennung mit "|"', () => { |
| 87 | it('meldet jedes feindliche Paar einer gemeinsamen Phase, auch bei Kennungen mit "|"', () => { |
| 88 | const gewoehnlich = feindlicheMeldungen(vierGruppenEinePhase({})); |
| 89 | // Voraussetzung des Aufbaus: drei feindliche Paare in einer Phase. |
| 90 | expect(gewoehnlich).toHaveLength(3); |
| 91 | |
| 92 | const mitStrich = feindlicheMeldungen(vierGruppenEinePhase(MIT_STRICH)); |
| 93 | expect( |
| 94 | mitStrich, |
| 95 | 'Das Paar ("a", "b|c") fiel auf denselben Schluessel wie ("a|b", "c") und fehlte ganz', |
| 96 | ).toHaveLength(3); |
| 97 | }); |
| 98 | |
| 99 | it('zaehlt die verschwiegene Ursache auch im Kopf des Berichts mit', () => { |
| 100 | const projekt = vierGruppenEinePhase(MIT_STRICH); |
| 101 | const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); |
| 102 | const gewoehnlichesProjekt = vierGruppenEinePhase({}); |
| 103 | const vergleich = validateProject( |
| 104 | gewoehnlichesProjekt, |
| 105 | buildSignalPlan(gewoehnlichesProjekt), |
| 106 | STICHTAG, |
| 107 | ); |
| 108 | |
| 109 | // Dieselbe Anlage, nur andere Kennungen: Die Fehlerzahl darf nicht davon |
| 110 | // abhaengen, welche Zeichen der Bearbeiter in einer Kennung verwendet. |
| 111 | expect(bericht.errorCount).toBe(vergleich.errorCount); |
| 112 | }); |
| 113 | }); |