lsa-planer

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

/ tests tools werkbank.test.ts

41,7 KB Rohdatei
tests/tools/werkbank.test.ts — 932 Zeilen
1 import { spawnSync } from 'node:child_process';
2 import {
3 existsSync,
4 mkdirSync,
5 mkdtempSync,
6 readFileSync,
7 readdirSync,
8 rmSync,
9 writeFileSync,
10 } from 'node:fs';
11 import os from 'node:os';
12 import path from 'node:path';
13 import { fileURLToPath } from 'node:url';
14 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
15
16 /*
17 * Werkbank. Zusagen, die nichts rechnen.
18 *
19 * F.1 `.githooks/pre-commit` faehrt die Schnellstufe, greift aber nur ueber
20 * `core.hooksPath` in `.git/config`. Das steht ausserhalb des Baumes und
21 * wird von `git clone` nicht mitgefuehrt; wer das Einrichten von Hand
22 * vergisst, committet ungeprueft. `package.json` richtet den Haken jetzt
23 * mit einem `prepare`-Skript ein - npm faehrt es bei jedem `npm install`.
24 * Bewacht wird nicht der Wortlaut des Skripts, sondern seine Wirkung: Es
25 * laeuft hier in einer Wegwerf-Arbeitskopie und muss dort den Eintrag
26 * setzen; ohne `.git` muss es folgenlos mit 0 enden, weil ein
27 * fehlschlagendes `prepare` das ganze `npm install` abbricht.
28 *
29 * F.2 `jspdf`, `jspdf-autotable`, Electron und Chromium gehen mit der Fassung
30 * an den Anwender, waren aber nirgends als Fremdbestandteil genannt.
31 * `docs/fremdbestandteile.md` fuehrt sie jetzt auf. Ein solches
32 * Verzeichnis ist nur so viel wert wie seine Aktualitaet: Diese Wache
33 * haelt jeden Eintrag gegen das installierte Paket, die Menge der
34 * Eintraege gegen das Bauergebnis und die Zahl der erhaltenen
35 * Urhebervermerke gegen die Ausgabedateien. Ein `npm install`, das eine
36 * Fassung anhebt, macht sie rot.
37 *
38 * SEIT 5.12.0 KOMMEN DIE MITGELIEFERTEN LIZENZTEXTE DAZU. Das Verzeichnis
39 * fuehrte drei Punkte als ausstehend: keine Lizenzdatei im Paket, im
40 * uebersetzten Kode nur vier von dreizehn Urhebervermerken, und das
41 * Verzeichnis selbst nur im Quellbaum. Punkt 1 und 3 sind behoben -
42 * `scripts/lizenztexte.mjs` sammelt die Texte aus `node_modules` ein, und
43 * `build.extraFiles` legt sie samt dem Verzeichnis neben die
44 * Programmdatei, dorthin, wo electron-builder auch seine eigenen beiden
45 * Lizenzdateien ablegt. Bewacht wird beides: dass package.json es mitgibt
46 * UND dass die Sammeldatei zu JEDER gebuendelten Bibliothek einen Text
47 * fuehrt. Eine Sammeldatei, in der eine fehlt, sieht nach Erfuellung aus
48 * und ist keine - deshalb prueft der Fall je Bibliothek den Namen ihres
49 * Rechteinhabers nach, und ein weiterer prueft, dass das Skript lieber
50 * abbricht als eine unvollstaendige Datei zu schreiben.
51 *
52 * SEIT 5.14.0 IST AUCH PUNKT 2 BEHOBEN. Der Vermerk wird in der Ausgabe
53 * nicht mehr gesucht, sondern hineingeschrieben: `lizenzbanner` in
54 * `vite.config.ts` setzt je Ausgabebrocken einen legalen Kommentar
55 * (`/*!`) mit den Lizenztexten der Bibliotheken, deren Kode wirklich
56 * darin steht. Vier Faelle halten das fest, und zwar am gebauten Stand:
57 * dass jede der dreizehn ihren Urhebervermerk in ihrer Ausgabedatei
58 * traegt; dass der Banner GENAU die Bibliotheken nennt, die die
59 * Quellkarte dieser Datei zuweist - ein Vermerk am falschen Ort ist eine
60 * falsche Angabe, keine Erfuellung; dass der Text woertlich der des
61 * installierten Pakets ist; und dass eine Ausgabedatei ohne
62 * Fremdbestandteil gar keinen Banner bekommt.
63 *
64 * DIE GEGENPROBE IST DER EIGENTLICHE WAECHTER. Der Banner entsteht zur
65 * Bauzeit aus `chunk.moduleIds`, die Zuordnung hier aus den Quellkarten -
66 * zwei Ableitungen desselben Sachverhalts. Laufen sie auseinander, faellt
67 * einer der beiden Faelle. Ohne die Gegenprobe genuegte es, jeder Datei
68 * alle dreizehn Texte mitzugeben; das saehe im Bericht genauso gruen aus.
69 *
70 * DREI WEITERE FAELLE HALTEN DIE ZAHLEN IM TEXT FEST und brauchen dafuer
71 * kein Bauergebnis: dass der Kopfkommentar hier so viele uebersprungene
72 * Faelle nennt, wie die Datei fuehrt; dass Verzeichnis und
73 * `vite.config.ts` so viele Wortlaute des Genehmigungsvermerks nennen,
74 * wie die Pakete fuehren; und dass die Groesse der DOMPurify-Lizenzdatei
75 * in Byte dasteht, nicht in Zeichen. Alle drei Zahlen standen einmal
76 * falsch da - eine Zahl im Text, die niemand nachrechnet, wandert.
77 *
78 * SECHS DIESER FAELLE SETZEN EIN BAUERGEBNIS VORAUS. `build/` steht in
79 * `.gitignore`, und kein Skript baut vor den Tests: `gate:schnell` baut
80 * nicht, `gate` baut erst nach dem Testlauf. Eine frische Arbeitskopie
81 * hat also nichts zu messen. Die sechs Faelle werden dann
82 * UEBERSPRUNGEN und nicht etwa schwaecher geprueft - ein stiller
83 * Rueckfall auf eine Ersatzzusicherung sieht im Bericht aus wie eine
84 * bestandene Messung und ist keine.
85 *
86 * NACHGETRAGEN ZU 5.43.0: DIE LAUFZEIT JENSEITS VON ELECTRON. Gehalten
87 * war bis dahin nur die Zeile "Electron" der Tabelle "Die Laufzeit". Mit
88 * 5.26.0 kam Electron 44.3.0; die Zeilen Electron und Chromium wurden von
89 * Hand nachgezogen, die Zeile Node.js nicht - sie nannte bis 5.42.1
90 * weiter 20.18.3, waehrend das ausgelieferte Programm Node.js 24.20.0
91 * mitbringt. Kein Fall merkte es. Ebenso stand bis 5.42.1 eine Zaehlung
92 * reziproker Lizenzen als Messung am ausgelieferten Stand da, deren neun
93 * Namen aus der Uebersicht von Electron 33.4.11 stammten; einer davon
94 * steht in der heutigen nicht mehr. Zwei Faelle halten das jetzt: Die
95 * Fassungen von Chromium und Node.js fragt der eine bei der
96 * Programmdatei selbst ab (`process.versions` unter
97 * `ELECTRON_RUN_AS_NODE`), nicht an einer Zeichenkette im Text; der andere
98 * prueft je genanntem Namen an `LICENSES.chromium.html`, ob er dort als
99 * Eintrag steht, und verlangt, dass das Verzeichnis genau die fehlenden
100 * als fehlend nennt. Beide brauchen das heruntergeladene Electron unter
101 * `node_modules/electron/dist` und ueberspringen sich ohne es sichtbar.
102 *
103 * WARUM HIER `git` UND `node` ANGEWORFEN WERDEN: Die Punkte sind Zusagen
104 * ueber das Verhalten von Werkzeugen, nicht ueber Text. Ein Textabgleich haette
105 * bei F.1 nur belegt, dass eine Zeichenkette dasteht - nicht, dass npm sie in
106 * der Kommandozeile von Windows wie in der von POSIX ohne Abbruch faehrt. Die
107 * Laeufe kosten zusammen unter einer Sekunde.
108 */
109
110 const WURZEL = process.cwd();
111
112 interface Paketdatei {
113 readonly version: string;
114 readonly scripts: Record<string, string>;
115 readonly dependencies?: Record<string, string>;
116 readonly devDependencies: Record<string, string>;
117 readonly build: {
118 readonly files: readonly string[];
119 readonly extraFiles?: readonly { readonly from?: string; readonly to?: string }[];
120 };
121 }
122
123 const PAKET = JSON.parse(readFileSync('package.json', 'utf8')) as Paketdatei;
124
125 /* -------------------------------------------------------------------------
126 * F.1 - das prepare-Skript
127 * ---------------------------------------------------------------------- */
128
129 describe('F.1: package.json richtet den pre-commit-Haken selbst ein', () => {
130 const PREPARE = PAKET.scripts['prepare'];
131 let ablage = '';
132
133 beforeAll(() => {
134 ablage = mkdtempSync(path.join(os.tmpdir(), 'lsa-prepare-'));
135 });
136
137 afterAll(() => {
138 rmSync(ablage, { recursive: true, force: true });
139 });
140
141 /** Faehrt das prepare-Skript so, wie npm es faehrt: durch die Kommandozeile des Systems. */
142 function faehrtIn(wo: string): { status: number | null; ausgabe: string } {
143 const lauf = spawnSync(PREPARE ?? '', { cwd: wo, shell: true, encoding: 'utf8' });
144 return { status: lauf.status, ausgabe: `${lauf.stdout}${lauf.stderr}` };
145 }
146
147 /** Der oertliche Wert von core.hooksPath in der angegebenen Arbeitskopie, oder ''. */
148 function hakenpfad(wo: string): string {
149 const lauf = spawnSync('git', ['config', '--local', '--get', 'core.hooksPath'], {
150 cwd: wo,
151 encoding: 'utf8',
152 });
153 return lauf.stdout.trim();
154 }
155
156 it('fuehrt package.json ueberhaupt ein prepare-Skript', () => {
157 expect(PREPARE, 'kein prepare-Skript in package.json').toBeDefined();
158 expect(PREPARE).toContain('core.hooksPath');
159 expect(PREPARE).toContain('.githooks');
160 });
161
162 it('setzt es in einer frischen Arbeitskopie core.hooksPath auf .githooks', () => {
163 const wo = path.join(ablage, 'mitgit');
164 const anlegen = spawnSync('git', ['init', '-q', wo], { encoding: 'utf8' });
165 expect(anlegen.status, `git init: ${anlegen.stderr}`).toBe(0);
166 expect(hakenpfad(wo), 'die frische Arbeitskopie hat den Eintrag schon').toBe('');
167
168 const lauf = faehrtIn(wo);
169 expect(lauf.status, `prepare brach ab: ${lauf.ausgabe}`).toBe(0);
170 expect(hakenpfad(wo), 'prepare hat den Eintrag nicht gesetzt').toBe('.githooks');
171 });
172
173 it('endet es ohne .git-Verzeichnis folgenlos mit 0', () => {
174 // Ein fehlschlagendes prepare bricht npm install ab. Auslieferung als
175 // Tarball und Bauumgebungen ohne Arbeitskopie duerfen daran nicht scheitern.
176 const wo = path.join(ablage, 'ohnegit');
177 const lauf = spawnSync('git', ['init', '-q', wo], { encoding: 'utf8' });
178 expect(lauf.status, `git init: ${lauf.stderr}`).toBe(0);
179 rmSync(path.join(wo, '.git'), { recursive: true, force: true });
180
181 const ergebnis = faehrtIn(wo);
182 expect(ergebnis.status, `prepare brach ab: ${ergebnis.ausgabe}`).toBe(0);
183 expect(existsSync(path.join(wo, '.git')), 'prepare hat ein .git angelegt').toBe(false);
184 });
185 });
186
187 /* -------------------------------------------------------------------------
188 * F.2 - das Verzeichnis der Fremdbestandteile
189 * ---------------------------------------------------------------------- */
190
191 const DOK_PFAD = 'docs/fremdbestandteile.md';
192 const DOK = readFileSync(DOK_PFAD, 'utf8');
193
194 interface Eintrag {
195 readonly name: string;
196 readonly fassung: string;
197 readonly lizenz: string;
198 }
199
200 /** Die Tabellenzeilen eines Abschnitts, zerlegt in Name, Fassung und Lizenz. */
201 function eintraege(ueberschrift: string): Eintrag[] {
202 const anfang = DOK.indexOf(`## ${ueberschrift}`);
203 expect(anfang, `Abschnitt "${ueberschrift}" nicht gefunden`).toBeGreaterThanOrEqual(0);
204 const naechste = DOK.indexOf('\n## ', anfang + 1);
205 const abschnitt = DOK.slice(anfang, naechste < 0 ? DOK.length : naechste);
206 const gefunden: Eintrag[] = [];
207 for (const zeile of abschnitt.split('\n')) {
208 if (!zeile.startsWith('|')) continue;
209 const spalten = zeile.split('|').map((s) => s.trim());
210 // spalten[0] ist leer (die Zeile beginnt mit |), dann Name, Fassung, Lizenz.
211 const name = (spalten[1] ?? '').replace(/`/g, '');
212 if (name === '' || name === 'Bestandteil' || name.startsWith('---')) continue;
213 gefunden.push({
214 name,
215 fassung: spalten[2] ?? '',
216 lizenz: (spalten[3] ?? '').replace(/`/g, ''),
217 });
218 }
219 return gefunden;
220 }
221
222 /** Fassung und Lizenzangabe, wie sie im installierten Paket stehen. */
223 function installiert(name: string): { fassung: string; lizenz: string } {
224 const roh = JSON.parse(
225 readFileSync(path.join('node_modules', name, 'package.json'), 'utf8'),
226 ) as Record<string, unknown>;
227 return { fassung: String(roh['version']), lizenz: String(roh['license']) };
228 }
229
230 const BAUABLAGE = 'build/renderer/assets';
231
232 /**
233 * Welches Paket in welchen Ausgabedateien des gebauten Renderers steckt - aus
234 * den Quellkarten gelesen. Ist nichts gebaut, gibt es nichts zu vergleichen;
235 * dann steht hier null, und die davon abhaengigen Faelle werden uebersprungen.
236 *
237 * Die Zuordnung Paket -> Datei ist nicht bloss Beiwerk: Der Fall zu den
238 * Urhebervermerken sucht je Paket NUR in der Datei seines eigenen Buendels.
239 * Sucht man ueber alle Ausgabedateien, faengt man jsPDFs mitgefuehrte
240 * Fremdteile ein und zaehlt sie fremden Paketen zu.
241 */
242 function buendelzuordnung(): Map<string, string[]> | null {
243 if (!existsSync(BAUABLAGE)) return null;
244 const karten = readdirSync(BAUABLAGE).filter((d) => d.endsWith('.js.map'));
245 if (karten.length === 0) return null;
246 const gefunden = new Map<string, Set<string>>();
247 for (const karte of karten) {
248 const inhalt = JSON.parse(readFileSync(path.join(BAUABLAGE, karte), 'utf8')) as {
249 sources?: string[];
250 };
251 for (const quelle of inhalt.sources ?? []) {
252 const stelle = quelle.lastIndexOf('node_modules/');
253 if (stelle < 0) continue;
254 const teile = quelle.slice(stelle + 'node_modules/'.length).split('/');
255 const name = teile[0]?.startsWith('@') ? `${teile[0]}/${teile[1]}` : String(teile[0]);
256 const eintrag = gefunden.get(name) ?? new Set<string>();
257 eintrag.add(karte.slice(0, -'.map'.length));
258 gefunden.set(name, eintrag);
259 }
260 }
261 return new Map([...gefunden].map(([name, dateien]) => [name, [...dateien].sort()]));
262 }
263
264 const BUENDEL = buendelzuordnung();
265 /** Ohne Bauergebnis ist nichts zu messen - die betroffenen Faelle entfallen sichtbar. */
266 const OHNE_BAU = BUENDEL === null;
267
268 /**
269 * Der Rechteinhaber je Paket, wie ihn die Lizenzdatei des installierten Pakets
270 * nennt. Er dient als Fuehler: Steht der Name in der Ausgabedatei, steht dort
271 * der Urhebervermerk. Ein Fall unten prueft jeden Fuehler zuerst am
272 * installierten Paket nach - ein Schreibfehler ginge sonst stumm als
273 * "Vermerk fehlt" durch und machte die Zaehlung wertlos.
274 */
275 const RECHTEINHABER: Readonly<Record<string, string>> = {
276 '@babel/runtime': 'Sebastian McKenzie',
277 canvg: 'Gabe Lerner',
278 'core-js': 'Denis Pushkarev',
279 dompurify: 'Cure53',
280 fflate: 'Arjun Barrett',
281 html2canvas: 'Niklas von Hertzen',
282 jspdf: 'James Hall',
283 'jspdf-autotable': 'Simon Bengtsson',
284 'performance-now': 'Braveg1rl',
285 raf: 'Chris Dickinson',
286 rgbcolor: 'Stoyan Stefanov',
287 'stackblur-canvas': 'Mario Klingemann',
288 'svg-pathdata': 'Nicolas Froidure',
289 };
290
291 /** Zahlwoerter, so weit das Verzeichnis sie braucht - dreizehn Pakete, nicht mehr. */
292 const ZAHLWORT: Readonly<Record<number, string>> = {
293 0: 'keines',
294 1: 'eines',
295 2: 'zwei',
296 3: 'drei',
297 4: 'vier',
298 5: 'fünf',
299 6: 'sechs',
300 7: 'sieben',
301 8: 'acht',
302 9: 'neun',
303 10: 'zehn',
304 11: 'elf',
305 12: 'zwölf',
306 13: 'dreizehn',
307 };
308
309 /**
310 * Woran der Banner aus `vite.config.ts` zu erkennen ist.
311 *
312 * `/*!` und nicht `/*`: esbuild gibt nur legale Kommentare wieder. Steht diese
313 * Zeichenfolge nicht am Anfang, hat der Banner das Verkuerzen nicht ueberlebt -
314 * und dann faellt der Fall, der ihn sucht.
315 */
316 const BANNERBEGINN = '/*!\nLSA-Planer Professional ';
317
318 /**
319 * Die Bibliotheken, die der Banner einer Ausgabedatei nennt - gelesen an den
320 * Herkunftszeilen `(node_modules/<paket>/<lizenzdatei>)`, die er je Text
321 * fuehrt, nicht an einer Aufzaehlung im Kopf.
322 */
323 function bannergenannt(datei: string): string[] {
324 const code = readFileSync(path.join(BAUABLAGE, datei), 'utf8');
325 const anfang = code.indexOf(BANNERBEGINN);
326 if (anfang < 0) return [];
327 const banner = code.slice(anfang, code.indexOf('*/', anfang));
328 const namen = new Set<string>();
329 for (const treffer of banner.matchAll(/^\(node_modules\/(.+)\/[^/]+\)$/gm)) {
330 namen.add(String(treffer[1]));
331 }
332 return [...namen].sort();
333 }
334
335 /** Die Lizenzdateien, die neben `package.json` eines installierten Pakets liegen. */
336 function lizenzdateien(name: string): string[] {
337 const ablage = path.join('node_modules', name);
338 return readdirSync(ablage)
339 .filter((d) => /^(licen[sc]e|copying)/i.test(d))
340 .map((d) => path.join(ablage, d));
341 }
342
343 /** `a`, `b` und `c` - die Aufzaehlung, wie das Verzeichnis sie schreibt. */
344 function aufzaehlung(namen: readonly string[]): string {
345 const gesetzt = namen.map((n) => `\`${n}\``);
346 if (gesetzt.length <= 1) return gesetzt.join('');
347 return `${gesetzt.slice(0, -1).join(', ')} und ${gesetzt[gesetzt.length - 1]}`;
348 }
349
350 /** Tausenderabstand, wie ihn die Unterlagen setzen: 27 729, nicht 27729. */
351 function mitAbstand(zahl: number): string {
352 return String(zahl).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
353 }
354
355 /** Grosser Anfangsbuchstabe - das Verzeichnis beginnt Saetze mit dem Zahlwort. */
356 function gross(wort: string): string {
357 return wort.slice(0, 1).toUpperCase() + wort.slice(1);
358 }
359
360 /** Der Satz, mit dem der MIT-Text seinen Genehmigungsvermerk beginnt. */
361 const GENEHMIGUNG = 'Permission is hereby granted';
362
363 /** Diese Datei selbst - der Fall zum Kopfkommentar liest sie. */
364 const EIGENE_QUELLE = fileURLToPath(import.meta.url);
365
366 /** Wo `npm install` die Laufzeit von Electron ablegt. */
367 const ELECTRON_ABLAGE = path.join('node_modules', 'electron', 'dist');
368
369 /** Die Bestandteiluebersicht von Chromium, die Electron mitliefert. */
370 const CHROMIUM_UEBERSICHT = path.join(ELECTRON_ABLAGE, 'LICENSES.chromium.html');
371
372 /**
373 * Die Programmdatei des installierten Electron, oder null.
374 *
375 * Ihren Namen je Plattform schreibt das Paket beim Herunterladen nach
376 * `node_modules/electron/path.txt`; unter Windows ist es `electron.exe`.
377 */
378 const ELECTRON_PROGRAMM = ((): string | null => {
379 const namensdatei = path.join('node_modules', 'electron', 'path.txt');
380 if (!existsSync(namensdatei)) return null;
381 const programm = path.join(ELECTRON_ABLAGE, readFileSync(namensdatei, 'utf8').trim());
382 return existsSync(programm) ? programm : null;
383 })();
384
385 /** Ohne heruntergeladenes Electron gibt es nichts abzufragen - die Faelle entfallen sichtbar. */
386 const OHNE_ELECTRON = ELECTRON_PROGRAMM === null || !existsSync(CHROMIUM_UEBERSICHT);
387
388 /** Ein Text auf eine Zeile gebracht - das Verzeichnis bricht Saetze beliebig um. */
389 function einzeilig(text: string): string {
390 return text.replace(/\s+/g, ' ');
391 }
392
393 /** Die Namen der Eintraege in `LICENSES.chromium.html`, Zeichenreferenzen aufgeloest. */
394 function chromiumEintraege(): Set<string> {
395 const html = readFileSync(CHROMIUM_UEBERSICHT, 'utf8');
396 const namen = new Set<string>();
397 for (const treffer of html.matchAll(/<span class="title">([^<]*)<\/span>/g)) {
398 namen.add(
399 String(treffer[1])
400 .replace(/&lt;/g, '<')
401 .replace(/&gt;/g, '>')
402 .replace(/&quot;/g, '"')
403 .replace(/&#39;/g, "'")
404 .replace(/&amp;/g, '&')
405 .trim(),
406 );
407 }
408 return namen;
409 }
410
411 describe('F.2: docs/fremdbestandteile.md haelt gegen den Baum', () => {
412 const GEBUENDELT = eintraege('In den Renderer gebündelt');
413
414 /** Wegwerfablage fuer die Laeufe von scripts/lizenztexte.mjs. */
415 let ablage = '';
416
417 beforeAll(() => {
418 ablage = mkdtempSync(path.join(os.tmpdir(), 'lsa-lizenztexte-'));
419 });
420
421 afterAll(() => {
422 rmSync(ablage, { recursive: true, force: true });
423 });
424
425 it('nennt zu jedem Eintrag die Fassung und die Lizenz des installierten Pakets', () => {
426 expect(GEBUENDELT.length, 'keine Tabellenzeilen gefunden').toBeGreaterThan(0);
427 for (const eintrag of GEBUENDELT) {
428 const ist = installiert(eintrag.name);
429 expect(eintrag.fassung, `Fassung von ${eintrag.name}`).toBe(ist.fassung);
430 expect(eintrag.lizenz, `Lizenz von ${eintrag.name}`).toBe(ist.lizenz);
431 }
432 });
433
434 it.skipIf(OHNE_BAU)(
435 'deckt sich die Aufstellung mit dem, was im gebauten Renderer steckt (nur mit Bauergebnis messbar)',
436 () => {
437 // Ohne gebauten Stand entfaellt dieser Fall. Kein Rueckfall auf eine
438 // schwaechere Zusicherung: Was hier gemessen wird, ist der Zuwachs, und
439 // den sieht man nur am Bauergebnis. Es bleibt der Fall oben, der jeden
440 // GENANNTEN Eintrag am installierten Paket prueft.
441 const ist = [...(BUENDEL as Map<string, string[]>).keys()].sort();
442 expect(GEBUENDELT.map((e) => e.name).sort()).toEqual(ist);
443 },
444 );
445
446 it('sind die Fuehler fuer die Urhebervermerke am installierten Paket belegt', () => {
447 // Ohne diesen Fall bewacht der naechste nichts: Ein Name, der im Paket
448 // selbst nirgends steht, steht auch in der Ausgabedatei nicht - und die
449 // Zaehlung meldete brav "Vermerk fehlt".
450 expect(Object.keys(RECHTEINHABER).sort()).toEqual(GEBUENDELT.map((e) => e.name).sort());
451 for (const [name, inhaber] of Object.entries(RECHTEINHABER)) {
452 const dateien = lizenzdateien(name);
453 expect(dateien.length, `keine Lizenzdatei in node_modules/${name}`).toBeGreaterThan(0);
454 const steht = dateien.some((d) => readFileSync(d, 'utf8').includes(inhaber));
455 expect(steht, `"${inhaber}" steht in keiner Lizenzdatei von ${name}`).toBe(true);
456 }
457 });
458
459 it.skipIf(OHNE_BAU)(
460 'traegt jede gebuendelte Bibliothek ihren Urhebervermerk in ihrer Ausgabedatei (nur mit Bauergebnis messbar)',
461 () => {
462 const zuordnung = BUENDEL as Map<string, string[]>;
463 const ohne: string[] = [];
464 for (const [name, inhaber] of Object.entries(RECHTEINHABER)) {
465 // Nur die Ausgabedateien DIESES Pakets. Ueber alle Dateien gesucht
466 // faende man Stoyan Stefanov in pdf-*.js und schriebe ihn rgbcolor zu,
467 // obwohl das dort jsPDFs eigene mitgefuehrte Farbwertzerlegung ist.
468 const dateien = zuordnung.get(name) ?? [];
469 expect(dateien.length, `${name} steckt in keiner Ausgabedatei`).toBeGreaterThan(0);
470 const steht = dateien.some((d) =>
471 readFileSync(path.join(BAUABLAGE, d), 'utf8').includes(inhaber),
472 );
473 if (!steht) ohne.push(name);
474 }
475 expect(ohne.sort(), 'diese Bibliotheken tragen ihren Urhebervermerk nicht').toEqual([]);
476
477 // Und das Verzeichnis gibt die Messung wortwoertlich wieder - mit der
478 // Zahl, die hier herauskommt, nicht mit einer gepflegten.
479 const anzahl = Object.keys(RECHTEINHABER).length;
480 const flach = DOK.replace(/\s+/g, ' ');
481 expect(flach, `alle ${anzahl} Bibliotheken tragen ihren Vermerk`).toContain(
482 `tragen alle **${ZAHLWORT[anzahl]}** gebündelten Bibliotheken ihren Urheber- und Genehmigungsvermerk.`,
483 );
484 },
485 );
486
487 it.skipIf(OHNE_BAU)(
488 'nennt der Banner je Ausgabedatei genau die Bibliotheken, deren Kode darin steckt (nur mit Bauergebnis messbar)',
489 () => {
490 /*
491 * DIE GEGENPROBE ZUM FALL DARUEBER. Dort wird gesucht, ob der Vermerk
492 * DA ist; hier, ob er am RICHTIGEN Ort steht. Ohne diesen Fall waere die
493 * billigste Loesung, jeder Ausgabedatei alle dreizehn Texte mitzugeben -
494 * das saehe genauso gruen aus und waere eine falsche Angabe: In
495 * index-*.js steckt kein fremder Kode, und ein Vermerk dort behauptete
496 * das Gegenteil.
497 *
498 * Gemessen werden zwei Ableitungen desselben Sachverhalts gegeneinander:
499 * Der Banner entsteht zur Bauzeit aus `chunk.moduleIds`, die Zuordnung
500 * hier nach dem Bau aus den Quellkarten. Laufen sie auseinander, faellt
501 * dieser Fall.
502 */
503 const zuordnung = BUENDEL as Map<string, string[]>;
504 for (const datei of readdirSync(BAUABLAGE).filter((d) => d.endsWith('.js'))) {
505 const erwartet = [...zuordnung]
506 .filter(([, dateien]) => dateien.includes(datei))
507 .map(([name]) => name)
508 .sort();
509 expect(bannergenannt(datei), `Banner von ${datei}`).toEqual(erwartet);
510 }
511 },
512 );
513
514 it.skipIf(OHNE_BAU)(
515 'traegt der Banner den Lizenztext woertlich aus dem installierten Paket (nur mit Bauergebnis messbar)',
516 () => {
517 // Der Name des Rechteinhabers allein belegt nur, dass IRGENDETWAS
518 // dasteht. Der MIT-Text verlangt Urheber- UND Genehmigungsvermerk;
519 // geprueft wird deshalb der ganze Text, Zeichen fuer Zeichen.
520 const zuordnung = BUENDEL as Map<string, string[]>;
521 for (const [name, dateien] of zuordnung) {
522 for (const quelle of lizenzdateien(name)) {
523 const text = readFileSync(quelle, 'utf8').replace(/\r\n/g, '\n').trim();
524 for (const datei of dateien) {
525 const code = readFileSync(path.join(BAUABLAGE, datei), 'utf8');
526 expect(code, `${quelle} steht nicht in ${datei}`).toContain(text);
527 }
528 }
529 }
530 },
531 );
532
533 it.skipIf(OHNE_BAU)(
534 'bekommt eine Ausgabedatei ohne Fremdbestandteil keinen Banner (nur mit Bauergebnis messbar)',
535 () => {
536 // Der Einstiegsbrocken enthaelt nur eigenen Kode. Ein Banner dort waere
537 // nicht bloss ueberfluessig, sondern unrichtig - und er kostete in der
538 // groessten Ausgabedatei am meisten.
539 const zuordnung = BUENDEL as Map<string, string[]>;
540 const belegt = new Set([...zuordnung.values()].flat());
541 const leere = readdirSync(BAUABLAGE)
542 .filter((d) => d.endsWith('.js'))
543 .filter((d) => !belegt.has(d));
544 expect(leere.length, 'keine Ausgabedatei ohne Fremdbestandteil gefunden').toBeGreaterThan(0);
545 for (const datei of leere) {
546 expect(
547 readFileSync(path.join(BAUABLAGE, datei), 'utf8'),
548 `${datei} traegt einen Banner, obwohl kein fremder Kode darin steckt`,
549 ).not.toContain(BANNERBEGINN);
550 }
551 },
552 );
553
554 it('nennt der Kopfkommentar so viele uebersprungene Faelle, wie die Datei fuehrt', () => {
555 /*
556 * EINE ZAHL IM TEXT, DIE NIEMAND NACHRECHNET, WANDERT. Genau das ist hier
557 * passiert: Der Kopfkommentar sprach von drei Faellen, waehrend die Datei
558 * sechs fuehrte, und das Verzeichnis sagte schon sechs. Gezaehlt wird
559 * deshalb am Kode - an `it.skipIf(OHNE_BAU)` -, und beide Texte muessen
560 * dieselbe Zahl nennen.
561 */
562 const quelle = readFileSync(EIGENE_QUELLE, 'utf8');
563 const anzahl = (quelle.match(/it\.skipIf\(OHNE_BAU\)\(/g) ?? []).length;
564 expect(anzahl, 'kein uebersprungener Fall gefunden').toBeGreaterThan(0);
565 const wort = String(ZAHLWORT[anzahl]);
566
567 expect(quelle, `${anzahl} Faelle setzen ein Bauergebnis voraus`).toContain(
568 `${wort.toUpperCase()} DIESER FAELLE SETZEN EIN BAUERGEBNIS VORAUS`,
569 );
570 expect(quelle, 'der Kopfkommentar nennt eine andere Zahl').toContain(
571 `Die ${wort} Faelle werden dann`,
572 );
573
574 const flach = DOK.replace(/\s+/g, ' ');
575 expect(flach, 'das Verzeichnis nennt eine andere Zahl').toContain(
576 `**${gross(wort)} dieser Zusagen setzen ein Bauergebnis voraus**`,
577 );
578 expect(flach, 'das Verzeichnis nennt eine andere Zahl').toContain(`Alle ${wort} messen an`);
579 expect(flach, 'das Verzeichnis nennt eine andere Zahl').toContain(
580 `diese ${wort} Fälle sichtbar`,
581 );
582 });
583
584 it('nennt der Text so viele Wortlaute des Genehmigungsvermerks, wie die Pakete fuehren', () => {
585 /*
586 * "Drei verschiedene Wortlaute" und "drei Pakete weichen ab" ist nicht
587 * dasselbe - gemessen sind es drei Wortlaute bei zwei abweichenden
588 * Paketen. Die Zaehlung steht hier, damit der Satz nicht wieder das eine
589 * sagt und das andere meint. Verglichen wird der Absatz ab
590 * "Permission is hereby granted", Leerraum vereinheitlicht: Zeilenumbrueche
591 * unterscheiden keinen Wortlaut.
592 */
593 const gruppen = new Map<string, string[]>();
594 for (const name of Object.keys(RECHTEINHABER)) {
595 const text = lizenzdateien(name)
596 .map((d) => readFileSync(d, 'utf8').replace(/\r\n/g, '\n'))
597 .join('\n');
598 const stelle = text.indexOf(GENEHMIGUNG);
599 if (stelle < 0) continue;
600 const absatz = text.slice(stelle).replace(/\s+/g, ' ').trim();
601 gruppen.set(absatz, [...(gruppen.get(absatz) ?? []), name]);
602 }
603 const mitVermerk = [...gruppen.values()].flat();
604 const groesste = [...gruppen.values()].sort((a, b) => b.length - a.length)[0] ?? [];
605 const abweichend = mitVermerk.filter((n) => !groesste.includes(n)).sort();
606
607 const flach = DOK.replace(/\s+/g, ' ');
608 expect(flach, `${mitVermerk.length} Pakete fuehren einen Genehmigungsvermerk`).toContain(
609 `die ${ZAHLWORT[mitVermerk.length]} MIT-Pakete führen den Genehmigungsvermerk`,
610 );
611 expect(flach, `${gruppen.size} verschiedene Wortlaute`).toContain(
612 `in **${ZAHLWORT[gruppen.size]}** verschiedenen Wortlauten`,
613 );
614 expect(
615 flach,
616 `${groesste.length} gleichlautend, abweichend ${abweichend.join(', ')}`,
617 ).toContain(
618 `${ZAHLWORT[groesste.length]} sind gleichlautend, ${aufzaehlung(abweichend)} weichen je für sich ab`,
619 );
620
621 // Und vite.config.ts sagt dasselbe. Der Kommentar dort begruendet, warum
622 // der ganze Text in den Banner geht; er zaehlt deshalb mit und ist beim
623 // letzten Mal gerade nicht mitgezogen worden.
624 const konfiguration = readFileSync('vite.config.ts', 'utf8');
625 expect(konfiguration, 'vite.config.ts nennt eine andere Zahl').toContain(
626 `${ZAHLWORT[gruppen.size]} verschiedenen Wortlauten`,
627 );
628 for (const name of abweichend) {
629 expect(konfiguration, `vite.config.ts nennt ${name} nicht`).toContain(name);
630 }
631 });
632
633 it('nennt das Verzeichnis die Groesse der DOMPurify-Lizenzdatei in Byte, nicht in Zeichen', () => {
634 /*
635 * Der Unterschied ist keine Spitzfindigkeit: Die Datei fuehrt 92
636 * Mehrbyte-Zeichen, Zeichen- und Bytezahl gehen um eben diese 92
637 * auseinander, und jede andere Zahl des Abschnitts ist in Byte. Sie
638 * begruendet den groessten Einzelposten der Groessentafel.
639 */
640 const dateien = lizenzdateien('dompurify');
641 expect(dateien.length, 'keine Lizenzdatei in node_modules/dompurify').toBe(1);
642 const roh = readFileSync(String(dateien[0]));
643 const imBanner = roh.toString('utf8').replace(/\r\n/g, '\n').trim();
644
645 const flach = DOK.replace(/\s+/g, ' ');
646 expect(flach, 'das Verzeichnis nennt eine andere Groesse').toContain(
647 `in einer Datei von ${mitAbstand(roh.length)} Byte mit, von denen ${mitAbstand(
648 Buffer.byteLength(imBanner, 'utf8'),
649 )} in den Banner gehen`,
650 );
651 expect(flach, 'die Zeichenzahl steht als Bytezahl da').not.toContain(
652 `${mitAbstand(imBanner.length)} Byte`,
653 );
654 });
655
656 it('nennt es die Laufzeit mit der Fassung des installierten Electron', () => {
657 const laufzeit = eintraege('Die Laufzeit');
658 const electron = laufzeit.find((e) => e.name === 'Electron');
659 expect(electron, 'kein Eintrag "Electron"').toBeDefined();
660 expect(electron?.fassung, 'Fassung von Electron').toBe(installiert('electron').fassung);
661 });
662
663 it.skipIf(OHNE_ELECTRON)(
664 'nennt es Chromium und Node.js mit den Fassungen, die das installierte Electron meldet (nur mit heruntergeladenem Electron messbar)',
665 () => {
666 /*
667 * Gefragt wird die Programmdatei selbst und nicht eine Liste hier: Die
668 * Zeile Node.js stand von 5.26.0 bis 5.42.1 auf 20.18.3, waehrend
669 * Electron 44.3.0 Node.js 24.20.0 mitbringt - eine Fassung, die ein
670 * Mensch nachzieht, bleibt beim naechsten Sprung wieder stehen.
671 */
672 const lauf = spawnSync(
673 String(ELECTRON_PROGRAMM),
674 ['-p', 'JSON.stringify(process.versions)'],
675 { encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } },
676 );
677 expect(lauf.status, `Electron meldete keine Fassungen: ${lauf.stderr}`).toBe(0);
678 const gemeldet = JSON.parse(lauf.stdout) as Readonly<Record<string, string>>;
679 // Die Gegenprobe, dass hier die Laufzeit des installierten Pakets antwortet.
680 expect(gemeldet['electron'], 'die Programmdatei gehoert zu einem anderen Electron').toBe(
681 installiert('electron').fassung,
682 );
683
684 const laufzeit = eintraege('Die Laufzeit');
685 for (const [name, schluessel] of [
686 ['Chromium', 'chrome'],
687 ['Node.js', 'node'],
688 ] as const) {
689 const eintrag = laufzeit.find((e) => e.name === name);
690 expect(eintrag, `kein Eintrag "${name}"`).toBeDefined();
691 expect(gemeldet[schluessel], `Electron meldet keine Fassung fuer ${name}`).toBeDefined();
692 expect(eintrag?.fassung, `Fassung von ${name}`).toBe(gemeldet[schluessel]);
693 }
694 },
695 );
696
697 it.skipIf(OHNE_ELECTRON)(
698 'nennt es die Namen aus der alten Lizenzzaehlung so, wie die heutige Chromium-Uebersicht sie fuehrt (nur mit heruntergeladenem Electron messbar)',
699 () => {
700 /*
701 * Die Zaehlung reziproker Lizenzen stammt aus der Uebersicht von
702 * Electron 33.4.11 und ist an der heutigen nicht wiederholt. Was das
703 * Verzeichnis ueber die heutige sagt, ist deshalb nur dies: welche der
704 * aufgezaehlten Namen dort noch als Eintrag stehen. Genau das wird hier
705 * nachgesehen - mit der Zahl und mit den Namen, die fehlen.
706 */
707 const absatz = einzeilig(
708 DOK.slice(
709 DOK.indexOf('**Die Einträge beschreiben den Quellbaum'),
710 DOK.indexOf('\n\n', DOK.indexOf('**Die Einträge beschreiben den Quellbaum')),
711 ),
712 );
713 const aufgezaehlt = /darunter ((?:`[^`]+`(?:, | und ))+`[^`]+`), also/.exec(absatz);
714 expect(aufgezaehlt, 'der Absatz zaehlt keine Namen mehr auf').not.toBeNull();
715 const namen = [...String(aufgezaehlt?.[1]).matchAll(/`([^`]+)`/g)].map((t) => String(t[1]));
716
717 const heute = chromiumEintraege();
718 const vorhanden = namen.filter((n) => heute.has(n));
719 const fehlend = namen.filter((n) => !heute.has(n));
720
721 expect(absatz, `${namen.length} Namen sind aufgezaehlt`).toContain(
722 `welche der ${String(ZAHLWORT[namen.length])} Namen`,
723 );
724 const aussage =
725 /noch als Eintrag führt: (\S+); ((?:`[^`]+`(?:, | und )?)+) steh(?:t|en) nicht mehr darin/.exec(
726 absatz,
727 );
728 expect(aussage, 'der Absatz sagt nicht, welche Namen heute fehlen').not.toBeNull();
729 expect(aussage?.[1], 'so viele der Namen stehen heute darin').toBe(
730 ZAHLWORT[vorhanden.length],
731 );
732 expect(
733 [...String(aussage?.[2]).matchAll(/`([^`]+)`/g)].map((t) => String(t[1])).sort(),
734 'diese Namen fehlen in der heutigen Uebersicht',
735 ).toEqual([...fehlend].sort());
736
737 // Und der Satz zu Node.js nennt Eintraege, die es gibt.
738 const nodeSatz =
739 /Es steht als Eintrag „([^"]+)" in `LICENSES\.chromium\.html`, ebenso wie „([^"]+)"/.exec(
740 einzeilig(DOK),
741 );
742 expect(nodeSatz, 'der Satz zu Node.js in der Uebersicht fehlt').not.toBeNull();
743 for (const name of [nodeSatz?.[1], nodeSatz?.[2]]) {
744 expect(heute.has(String(name)), `„${String(name)}" ist kein Eintrag der Uebersicht`).toBe(
745 true,
746 );
747 }
748 },
749 );
750
751 it('stimmt die Zahl der Eintraege in der Chromium-Uebersicht', () => {
752 const uebersicht = 'node_modules/electron/dist/LICENSES.chromium.html';
753 if (!existsSync(uebersicht)) return;
754 const anzahl = (readFileSync(uebersicht, 'utf8').match(/<div class="product">/g) ?? []).length;
755 expect(DOK, `LICENSES.chromium.html fuehrt ${anzahl} Eintraege`).toContain(
756 `**${anzahl} Einträge**`,
757 );
758 });
759
760 it('fuehrt es jeden kuenftigen Eintrag aus dependencies mit', () => {
761 const laufzeitpakete = Object.keys(PAKET.dependencies ?? {});
762 if (laufzeitpakete.length === 0) {
763 expect(DOK, 'der Text sagt weiterhin, dependencies sei leer').toContain(
764 'Der Abschnitt `dependencies` ist leer',
765 );
766 return;
767 }
768 for (const name of laufzeitpakete) {
769 expect(DOK, `${name} steht in dependencies, aber nicht im Verzeichnis`).toContain(
770 `\`${name}\``,
771 );
772 }
773 });
774
775 it('gibt package.json die Lizenztexte und das Verzeichnis mit dem Paket mit', () => {
776 // extraFiles und nicht extraResources: electron-builder legt seine eigenen
777 // beiden Lizenzdateien NEBEN die Programmdatei, nicht nach resources/.
778 const mitgegeben = PAKET.build.extraFiles ?? [];
779 expect(mitgegeben.map((e) => e.from)).toContain('build/DRITTANBIETER-LIZENZEN.txt');
780 expect(mitgegeben.map((e) => e.from)).toContain('docs/fremdbestandteile.md');
781
782 // Ohne diesen Aufruf legte electron-builder eine Sammeldatei von gestern
783 // bei - `npm run dist` baut, packt und rechnet die Pruefsummen, aber
784 // sammelt nicht von sich aus ein.
785 expect(PAKET.scripts['lizenztexte'], 'kein Skript lizenztexte').toContain(
786 'scripts/lizenztexte.mjs',
787 );
788 expect(PAKET.scripts['build'], 'npm run build ruft das Sammelskript nicht').toContain(
789 'lizenztexte',
790 );
791
792 // Und das Verzeichnis darf den behobenen Punkt nicht weiter als offen
793 // fuehren.
794 expect(DOK).not.toContain(
795 'Die Lizenztexte der gebündelten Bibliotheken liegen dem Paket nicht',
796 );
797 expect(DOK).toContain('## Was dem Paket beiliegt');
798 });
799
800 it('datiert es seine Messungen auf die Fassung aus package.json', () => {
801 /*
802 * Die Fassungsnummer steht einmal - in package.json. Zwei Stellen des
803 * Verzeichnisses datieren eine Messung und muessen sie deshalb nennen: der
804 * Kopf und der Spiegelstrich "Was im Paket ankommt". Letzterer zeigte
805 * zunaechst noch auf einen gebauten Stand 5.11.0 und belegte damit gerade
806 * NICHT, was der Abschnitt "Was dem Paket beiliegt" zusagt: Ein Stand vor
807 * 5.12.0 kann DRITTANBIETER-LIZENZEN.txt und FREMDBESTANDTEILE.md nicht
808 * enthalten haben, weil es beide erst seit 5.12.0 gibt.
809 *
810 * Der Rueckblick weiter unten ("Bis 5.11.0 stand hier ...") ist keine
811 * Messung, sondern eine Angabe darueber, was frueher dastand; er liegt in
812 * keinem der beiden hier geprueften Stuecke und darf eine aeltere Fassung
813 * nennen.
814 */
815 const kopf = DOK.split('\n').find((z) => z.startsWith('Stand: Fassung '));
816 expect(kopf, 'Zeile "Stand: Fassung ..." nicht gefunden').toBeDefined();
817 expect(kopf, 'der Kopf datiert auf eine andere Fassung').toContain(
818 `Stand: Fassung ${PAKET.version},`,
819 );
820
821 const zeilen = DOK.split('\n');
822 const anfang = zeilen.findIndex((z) => z.startsWith('- **Was im Paket ankommt**'));
823 expect(anfang, 'Spiegelstrich "Was im Paket ankommt" nicht gefunden').toBeGreaterThanOrEqual(0);
824 let ende = anfang + 1;
825 while ((zeilen[ende] ?? '').startsWith(' ')) ende += 1;
826 const spiegelstrich = zeilen.slice(anfang, ende).join(' ').replace(/\s+/g, ' ');
827 const stand = /Standes (\d+\.\d+\.\d+)/.exec(spiegelstrich);
828 expect(stand, 'der Spiegelstrich nennt keinen gebauten Stand').not.toBeNull();
829 expect(stand?.[1], 'der Beleg zeigt nicht auf die ausgelieferte Fassung').toBe(PAKET.version);
830 });
831
832 it('zaehlt der Abschnitt "Was dem Paket beiliegt" nur die Dateien mit Lizenztext als solche', () => {
833 /*
834 * Der Einleitungssatz geht ueber `build.extraFiles` woertlich an den
835 * Anwender. Er darf deshalb nicht mehr zusagen als die Tabelle darunter:
836 * FREMDBESTANDTEILE.md fuehrt keinen Lizenztext, sondern sagt, was das ist,
837 * woher es kommt und wozu es dient. "Vier Dateien mit Lizenztexten" waere
838 * eine Zusage, die das Verzeichnis mit seinem "Das ist keine
839 * Rechtsauskunft" ausdruecklich nicht geben will.
840 *
841 * Gemessen wird an der Tabelle, nicht an einer zweiten Zahl im Text: Wer
842 * eine Zeile hinzunimmt, muss beide Zahlwoerter nachziehen.
843 */
844 const anfang = DOK.indexOf('## Was dem Paket beiliegt');
845 expect(anfang, 'Abschnitt "Was dem Paket beiliegt" nicht gefunden').toBeGreaterThanOrEqual(0);
846 const naechste = DOK.indexOf('\n## ', anfang + 1);
847 const abschnitt = DOK.slice(anfang, naechste < 0 ? DOK.length : naechste);
848 const reihen = abschnitt
849 .split('\n')
850 .filter((z) => z.startsWith('|'))
851 .map((z) => z.split('|').map((s) => s.trim()))
852 .filter((s) => (s[1] ?? '') !== 'Datei' && !(s[1] ?? '').startsWith('---'));
853 expect(reihen.length, 'keine Tabellenzeilen gefunden').toBeGreaterThan(0);
854
855 // Die Zeile, die dieses Verzeichnis selbst mitgibt, ist keine mit
856 // Lizenztext - sie steht in der Spalte "Kommt von" als solche da.
857 const selbst = reihen.filter((s) => (s[2] ?? '').includes('dieses Verzeichnis')).length;
858 expect(selbst, 'keine Zeile fuer das Verzeichnis selbst').toBe(1);
859
860 const flach = DOK.replace(/\s+/g, ' ');
861 expect(flach, `die Tabelle fuehrt ${reihen.length} Dateien`).toContain(
862 `Neben der Programmdatei liegen ${ZAHLWORT[reihen.length]} Dateien`,
863 );
864 expect(flach, `${selbst} davon fuehrt keinen Lizenztext`).toContain(
865 `${ZAHLWORT[reihen.length - selbst]} mit Lizenztexten`,
866 );
867 });
868
869 it.skipIf(OHNE_BAU)(
870 'schreibt scripts/lizenztexte.mjs zu jeder gebuendelten Bibliothek einen Text (nur mit Bauergebnis messbar)',
871 () => {
872 const ziel = path.join(ablage, 'DRITTANBIETER-LIZENZEN.txt');
873 const lauf = spawnSync(process.execPath, ['scripts/lizenztexte.mjs', '--ziel', ziel], {
874 cwd: WURZEL,
875 encoding: 'utf8',
876 });
877 expect(lauf.status, `das Skript brach ab: ${lauf.stdout}${lauf.stderr}`).toBe(0);
878
879 const sammlung = readFileSync(ziel, 'utf8');
880 const gebuendelt = [...(BUENDEL as Map<string, string[]>).keys()].sort();
881 // Nicht "irgendwelche Texte", sondern zu JEDER Bibliothek einer: Der
882 // Name des Rechteinhabers ist derselbe Fuehler, den der Fall oben am
883 // installierten Paket belegt hat.
884 for (const name of gebuendelt) {
885 const inhaber = RECHTEINHABER[name];
886 expect(inhaber, `kein Fuehler fuer ${name}`).toBeDefined();
887 expect(sammlung, `${name} steht mit keinem Abschnitt in der Sammeldatei`).toContain(
888 `${name} ${installiert(name).fassung}`,
889 );
890 expect(sammlung, `der Text von ${name} fehlt in der Sammeldatei`).toContain(
891 String(inhaber),
892 );
893 }
894 expect(sammlung, 'die Sammeldatei nennt eine andere Zahl').toContain(
895 `Es folgen ${gebuendelt.length} Texte.`,
896 );
897 // Mit Bytefolgemarke - sonst zeigt ein Windows-Editor aus den Umlauten
898 // Buchstabensalat, und die Datei liegt beim Anwender, nicht hier.
899 expect(sammlung.charCodeAt(0), 'keine Bytefolgemarke').toBe(0xfeff);
900 },
901 );
902
903 it('schreibt es lieber nichts, als eine Bibliothek zu unterschlagen', () => {
904 // Der gefaehrliche Ausgang waere nicht der Abbruch, sondern eine Datei
905 // ohne den fehlenden Text: Sie sieht nach Erfuellung aus. Gemessen an
906 // einer erfundenen Quellkarte, die ein nicht installiertes Paket nennt -
907 // das braucht kein Bauergebnis und greift auch in einer frischen
908 // Arbeitskopie.
909 const bau = path.join(ablage, 'erfundener-bau');
910 mkdirSync(bau, { recursive: true });
911 writeFileSync(
912 path.join(bau, 'index-AAAAAAAA.js.map'),
913 JSON.stringify({ sources: ['../../node_modules/gibt-es-nicht/index.js'] }),
914 'utf8',
915 );
916 const ziel = path.join(bau, 'soll-nicht-entstehen.txt');
917
918 const lauf = spawnSync(
919 process.execPath,
920 ['scripts/lizenztexte.mjs', '--bau', bau, '--ziel', ziel],
921 { cwd: WURZEL, encoding: 'utf8' },
922 );
923 expect(lauf.status, 'das Skript endete mit 0').not.toBe(0);
924 expect(`${lauf.stdout}${lauf.stderr}`).toContain('gibt-es-nicht');
925 expect(existsSync(ziel), 'es hat trotzdem eine Sammeldatei geschrieben').toBe(false);
926 });
927
928 it('gibt es sich nicht als Rechtsauskunft aus', () => {
929 expect(DOK).toContain('**Das ist keine Rechtsauskunft.**');
930 expect(DOK).toContain('## Was aussteht');
931 });
932 });