import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { spawnSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; /* * Befund 13 zur Fassung 5.10.0. * * `npm run clean` entfernte neben dem jederzeit neu baubaren `build/` auch * `release/` - rekursiv, mit force, ohne Rueckfrage und ohne Ausnahme fuer die * Dateien PRUEFSUMMEN-.txt. * * `release/` steht in .gitignore und ist damit die einzige Kopie. Die * Pruefsummendateien sind ausdruecklich als Nachweis gedacht: "Ein Pruefer muss * sagen koennen, mit welchem Programmstand eine Unterlage erstellt wurde" * (scripts/pruefsummen.mjs). Wiederherstellbar sind sie nicht: Ein Neubau aus * derselben Marke ergibt zwar denselben Quelltext, aber eine andere Pruefsumme. * Und nach dem Loeschen kann `npm run pruefsummen` gar nichts mehr schreiben: * Es braucht das Paket, an das es die Summe haengt. * * Die beiden Skripte widersprachen sich damit: pruefsummen.mjs behandelt * `release/` als Archiv ("Im Ordner liegen erfahrungsgemaess auch aeltere * Staende"), clean.mjs behandelte denselben Ordner als Zwischenstand. * * WARUM DER LAUF IN EINEM EIGENEN VERZEICHNIS STATTFINDET: Das Skript ist ein * ausfuehrbares Modul ohne Ausfuhren und bildet seine Ziele aus dem eigenen Ort * (`new URL('../build', import.meta.url)`). Geprueft wird deshalb das Skript * selbst, mit einer Nachbildung des Projektordners im Kratzbereich - der * Projektbaum und sein echtes `release/` werden nicht angefasst. * * WARUM DER AUFRUF MIT DOPPELTEM STRICH GENANNT SEIN MUSS: Der dokumentierte * Einstiegspunkt ist `npm run clean` (package.json). npm nimmt ein * unbekanntes Argument als eigene Einstellung und reicht es nicht weiter: * `npm run clean --auch-release` erreicht das Skript nicht und laesst * `release/` vollstaendig liegen, erst `npm run clean -- --auch-release` wirkt. * Ausgabe und Kopfkommentar muessen deshalb den Aufruf nennen, der wirkt - * sonst tippt den Befehl zweimal vergeblich, wer der eigenen Ausgabe folgt. */ /** Der wirksame Aufruf ueber den dokumentierten Einstiegspunkt. */ const AUFRUF = 'npm run clean -- --auch-release'; const PRUEFSUMMEN = 'PRUEFSUMMEN-9.9.9.txt'; const PAKET = 'LSA-Planer-Setup-9.9.9.exe'; let wurzel = ''; beforeEach(() => { wurzel = mkdtempSync(path.join(os.tmpdir(), 'lsa-clean-')); mkdirSync(path.join(wurzel, 'scripts')); mkdirSync(path.join(wurzel, 'build', 'assets'), { recursive: true }); mkdirSync(path.join(wurzel, 'release')); writeFileSync(path.join(wurzel, 'build', 'index.html'), 'Platzhalter statt eines Baus', 'utf8'); writeFileSync(path.join(wurzel, 'build', 'assets', 'haupt.js'), 'Platzhalter', 'utf8'); writeFileSync(path.join(wurzel, 'release', PAKET), 'Platzhalter statt eines Pakets', 'utf8'); writeFileSync(path.join(wurzel, 'release', PRUEFSUMMEN), 'Platzhalter statt Summen', 'utf8'); copyFileSync('scripts/clean.mjs', path.join(wurzel, 'scripts', 'clean.mjs')); }); afterEach(() => { if (wurzel !== '') rmSync(wurzel, { recursive: true, force: true }); wurzel = ''; }); /** Laesst das Skript in der Nachbildung laufen und gibt seine Ausgabe zurueck. */ function lauf(...argumente: string[]): string { const ergebnis = spawnSync( process.execPath, [path.join(wurzel, 'scripts', 'clean.mjs'), ...argumente], { encoding: 'utf8' }, ); expect(ergebnis.status, `Lauf mit [${argumente.join(' ')}]: ${ergebnis.stderr}`).toBe(0); return ergebnis.stdout; } /** Liegt die Datei noch im nachgebildeten release-Ordner? */ function imArchiv(name: string): boolean { return existsSync(path.join(wurzel, 'release', name)); } describe('npm run clean - was es entfernen darf', () => { it('entfernt den Bauordner', () => { lauf(); expect(existsSync(path.join(wurzel, 'build')), 'build/ nach dem Lauf').toBe(false); }); it('laesst den Auslieferungsordner ohne ausdrueckliche Ansage unberuehrt', () => { lauf(); expect(imArchiv(PAKET), 'Paket nach dem Lauf').toBe(true); expect(imArchiv(PRUEFSUMMEN), 'Pruefsummendatei nach dem Lauf').toBe(true); }); it('sagt in der Ausgabe, dass der Auslieferungsordner liegen bleibt', () => { // Wer aufraeumen wollte, soll nicht raten muessen, was liegen blieb - und // erfahren, wie er ihn doch loswird. Genannt sein muss der Aufruf, der // wirklich wirkt (siehe Kopf dieser Datei), nicht nur der blosse Schalter. const ausgabe = lauf(); expect(ausgabe, 'Ausgabe des Laufs').toContain('behalten: release'); expect(ausgabe, 'Hinweis auf den wirksamen Aufruf').toContain(AUFRUF); }); it('nennt im Kopfkommentar denselben Aufruf wie in der Ausgabe', () => { // Zusagen in Kommentaren sind Teil des Programms: Der Kopf darf nicht den // Aufruf nennen, der ueber `npm run` verpufft. const kopf = readFileSync('scripts/clean.mjs', 'utf8').replace(/\s+/g, ' '); expect(kopf, 'Kopfkommentar von scripts/clean.mjs').toContain(AUFRUF); }); it('entfernt mit --auch-release die Pakete, aber nie die Pruefsummen', () => { lauf('--auch-release'); expect(imArchiv(PAKET), 'Paket nach dem Lauf mit --auch-release').toBe(false); expect(imArchiv(PRUEFSUMMEN), 'Pruefsummendatei nach dem Lauf mit --auch-release').toBe(true); }); it('kommt ohne vorhandenen Auslieferungsordner zurecht', () => { // Ein frischer Klon hat kein release/ - der Lauf darf daran nicht brechen. rmSync(path.join(wurzel, 'release'), { recursive: true, force: true }); lauf('--auch-release'); expect(existsSync(path.join(wurzel, 'build')), 'build/ nach dem Lauf').toBe(false); }); });