lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests tools pruefsummendatum.test.ts
| 1 | import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
| 2 | import { spawnSync } from 'node:child_process'; |
| 3 | import os from 'node:os'; |
| 4 | import path from 'node:path'; |
| 5 | import { afterAll, beforeAll, describe, expect, it } from 'vitest'; |
| 6 | |
| 7 | /* |
| 8 | * Befund 67 zur Fassung 5.9.0. |
| 9 | * |
| 10 | * `scripts/pruefsummen.mjs` schrieb `new Date().toISOString().slice(0, 10)` als |
| 11 | * "Erstellt am" in die Pruefsummendatei. `toISOString` liefert Weltzeit, und |
| 12 | * `.slice(0, 10)` schneidet das 'Z' ab, das den Wert als Weltzeit auswiese. |
| 13 | * Uebrig bleibt ein nackter Kalendertag unter einer deutschen Beschriftung - |
| 14 | * nach ISO 8601 als Ortszeit zu lesen. Deutschland liegt oestlich von |
| 15 | * Greenwich; zwischen 00:00 und 02:00 Ortszeit (Sommerzeit) beziehungsweise |
| 16 | * 00:00 und 01:00 (Winterzeit) traegt die Datei damit den Vortag, waehrend die |
| 17 | * Pakete daneben den laufenden Tag nennen. Die Datei ist ausdruecklich als Nachweis gedacht: |
| 18 | * "Ein Pruefer muss sagen koennen, mit welchem Programmstand eine Unterlage |
| 19 | * erstellt wurde." |
| 20 | * |
| 21 | * WARUM DER LAUF IN EINEM EIGENEN VERZEICHNIS STATTFINDET: Das Skript ist ein |
| 22 | * ausfuehrbares Modul ohne Ausfuhren - es liest `package.json` und `release/` |
| 23 | * ueber seinen eigenen Ort und schreibt die Datei dorthin. Geprueft wird |
| 24 | * deshalb das Skript selbst, mit einer Nachbildung des Projektordners im |
| 25 | * Kratzbereich; der Projektbaum wird nicht angefasst. |
| 26 | * |
| 27 | * WARUM ZWEI ZEITZONEN: Zu jedem Zeitpunkt weicht mindestens eine der beiden |
| 28 | * vom Weltzeit-Tag ab - UTC+14 ab 10:00 UTC, UTC-12 bis 12:00 UTC. Damit faellt |
| 29 | * der Fall gegen den Altstand unabhaengig davon, wann er laeuft und in welcher |
| 30 | * Zeitzone der Rechner steht. |
| 31 | */ |
| 32 | |
| 33 | const VERSION = '9.9.9'; |
| 34 | const PAKETNAME = `LSA-Planer-Setup-${VERSION}.exe`; |
| 35 | /** Zeitzonen, deren Kalendertag zu keiner Stunde beide mit dem der Weltzeit uebereinstimmt. */ |
| 36 | const ZONEN = ['Etc/GMT-14', 'Etc/GMT+12'] as const; |
| 37 | |
| 38 | let wurzel = ''; |
| 39 | |
| 40 | /** Kalendertag, den eine Uhr in dieser Zeitzone gerade zeigt. */ |
| 41 | function tagIn(zone: string): string { |
| 42 | return new Intl.DateTimeFormat('en-CA', { |
| 43 | timeZone: zone, |
| 44 | year: 'numeric', |
| 45 | month: '2-digit', |
| 46 | day: '2-digit', |
| 47 | }).format(new Date()); |
| 48 | } |
| 49 | |
| 50 | beforeAll(() => { |
| 51 | wurzel = mkdtempSync(path.join(os.tmpdir(), 'lsa-pruefsummen-')); |
| 52 | mkdirSync(path.join(wurzel, 'scripts')); |
| 53 | mkdirSync(path.join(wurzel, 'release')); |
| 54 | writeFileSync(path.join(wurzel, 'package.json'), JSON.stringify({ version: VERSION }), 'utf8'); |
| 55 | writeFileSync(path.join(wurzel, 'release', PAKETNAME), 'Platzhalter statt eines Pakets', 'utf8'); |
| 56 | copyFileSync('scripts/pruefsummen.mjs', path.join(wurzel, 'scripts', 'pruefsummen.mjs')); |
| 57 | }); |
| 58 | |
| 59 | afterAll(() => { |
| 60 | if (wurzel !== '') rmSync(wurzel, { recursive: true, force: true }); |
| 61 | }); |
| 62 | |
| 63 | /** Laesst das Skript in der angegebenen Zeitzone laufen und gibt die Datei zurueck. */ |
| 64 | function laufIn(zone: string): string { |
| 65 | const lauf = spawnSync(process.execPath, [path.join(wurzel, 'scripts', 'pruefsummen.mjs')], { |
| 66 | env: { ...process.env, TZ: zone }, |
| 67 | encoding: 'utf8', |
| 68 | }); |
| 69 | expect(lauf.status, `Lauf in ${zone}: ${lauf.stderr}`).toBe(0); |
| 70 | return readFileSync(path.join(wurzel, 'release', `PRUEFSUMMEN-${VERSION}.txt`), 'utf8'); |
| 71 | } |
| 72 | |
| 73 | describe('Pruefsummendatei - das Erstellungsdatum', () => { |
| 74 | it.each(ZONEN)('nennt in %s den oertlichen Kalendertag, nicht den der Weltzeit', (zone) => { |
| 75 | const vorher = tagIn(zone); |
| 76 | const datei = laufIn(zone); |
| 77 | const nachher = tagIn(zone); |
| 78 | |
| 79 | const treffer = /Erstellt am (\d{4}-\d{2}-\d{2})/.exec(datei); |
| 80 | expect(treffer, 'Zeile "Erstellt am" nicht gefunden').not.toBeNull(); |
| 81 | // Zwei Werte, falls der Lauf ueber Mitternacht dieser Zone hinweg faellt. |
| 82 | expect([vorher, nachher], `Erstellungsdatum in ${zone}`).toContain(treffer![1]); |
| 83 | }); |
| 84 | |
| 85 | it('schreibt die uebrigen Angaben der Nachweisdatei unveraendert', () => { |
| 86 | // Gegenprobe: Die Aenderung betrifft nur das Datum. |
| 87 | const datei = laufIn('Etc/GMT-14'); |
| 88 | expect(datei, 'Kopfzeile').toContain(`LSA-Planer Professional ${VERSION} – Prüfsummen`); |
| 89 | expect(datei, 'Name des Pakets').toContain(PAKETNAME); |
| 90 | // Als Zeichennummer geschrieben, nicht als Zeichen: Die Bytefolgemarke |
| 91 | // selbst im Quelltext stehen zu lassen, beanstandet der Linter zu Recht. |
| 92 | expect(datei.charCodeAt(0), 'Bytefolgemarke').toBe(0xfeff); |
| 93 | expect(datei, 'Hinweis zur Aussagekraft').toContain('Was eine Prüfsumme leistet'); |
| 94 | }); |
| 95 | }); |