waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app tests reuse-abdeckung.test.ts
| 1 | // @vitest-environment node |
| 2 | /** |
| 3 | * Jede aufgenommene Inhaltsdatei braucht eine Lizenzangabe. |
| 4 | * |
| 5 | * Anlass ist ein selbst verursachter Rückschritt: Mit `content/glossar.json` |
| 6 | * kam eine neue Inhaltsdatei ins Projekt, ohne Eintrag in `REUSE.toml`. Damit |
| 7 | * war die Zusage aus einem früheren Stand – „reuse lint ist konform“ – nicht |
| 8 | * mehr wahr, und niemand hat es bemerkt. Aufgefallen ist es erst, als jemand |
| 9 | * für einen ganz anderen Zweck nachgesehen hat. |
| 10 | * |
| 11 | * `reuse lint` selbst läuft hier nicht: Es ist ein Python-Werkzeug und keine |
| 12 | * Abhängigkeit dieses Pakets; ein Test, der es aufruft, wäre auf einem |
| 13 | * Rechner ohne Python rot, ohne dass etwas falsch wäre. Geprüft wird deshalb |
| 14 | * genau die eine Regel, an der es gescheitert ist – dass keine aufgenommene |
| 15 | * Inhaltsdatei ohne Eintrag bleibt. Das ersetzt `reuse lint` nicht, es fängt |
| 16 | * den Fall ab, der wirklich passiert ist. |
| 17 | * |
| 18 | * Gefragt wird `git ls-files`, nicht das Dateisystem: `reuse lint` sieht nur |
| 19 | * aufgenommene Dateien, und unter `content/gesetze/` liegen heruntergeladene |
| 20 | * Gesetzesarchive, die niemand einchecken will. |
| 21 | */ |
| 22 | |
| 23 | import { execFileSync } from 'node:child_process'; |
| 24 | import { readFileSync } from 'node:fs'; |
| 25 | import { join } from 'node:path'; |
| 26 | import { fileURLToPath } from 'node:url'; |
| 27 | |
| 28 | import { describe, expect, it } from 'vitest'; |
| 29 | |
| 30 | const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..'); |
| 31 | |
| 32 | /** Aufgenommene Dateien unter `content/`, oder `null` ohne Git. */ |
| 33 | function aufgenommeneInhalte(): string[] | null { |
| 34 | try { |
| 35 | return execFileSync('git', ['ls-files', 'content'], { |
| 36 | cwd: wurzel, |
| 37 | encoding: 'utf8', |
| 38 | stdio: ['ignore', 'pipe', 'ignore'], |
| 39 | }) |
| 40 | .split('\n') |
| 41 | .map((zeile) => zeile.trim()) |
| 42 | .filter((zeile) => zeile.length > 0); |
| 43 | } catch { |
| 44 | return null; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Aufgenommene Dateien unmittelbar im Projektverzeichnis, oder `null` ohne Git. |
| 50 | * |
| 51 | * `git ls-files` liefert ohne Pfadangabe den ganzen Baum; hier interessieren |
| 52 | * nur die Dateien ohne Schrägstrich im Namen. Der Anlass ist derselbe wie |
| 53 | * oben, eine Ebene höher: `CHANGELOG.md` kam als neue Datei in die Wurzel, |
| 54 | * und dort greift kein `**`-Muster – sie hätte ohne Lizenzangabe bleiben |
| 55 | * können, ohne dass es auffällt. |
| 56 | */ |
| 57 | function aufgenommeneWurzeldateien(): string[] | null { |
| 58 | try { |
| 59 | return execFileSync('git', ['ls-files'], { |
| 60 | cwd: wurzel, |
| 61 | encoding: 'utf8', |
| 62 | stdio: ['ignore', 'pipe', 'ignore'], |
| 63 | }) |
| 64 | .split('\n') |
| 65 | .map((zeile) => zeile.trim()) |
| 66 | .filter((zeile) => zeile.length > 0 && !zeile.includes('/')); |
| 67 | } catch { |
| 68 | return null; |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Die Pfadmuster aus den `path = [...]`-Angaben von REUSE.toml. |
| 74 | * |
| 75 | * Gelesen werden gezielt diese Listen und nicht jede Zeichenkette der Datei. |
| 76 | * Der erste Versuch nahm alle Anführungszeichen – und verrutschte an |
| 77 | * `SPDX-PackageDownloadLocation = ""`: Die leere Zeichenkette fiel durch das |
| 78 | * Muster, danach war jedes Paar um eins verschoben und die Liste bestand aus |
| 79 | * Bruchstücken. Aufgefallen ist das nur, weil der Test unten die Ausbeute |
| 80 | * gegenprüft. |
| 81 | */ |
| 82 | function pfadmuster(): string[] { |
| 83 | const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8') |
| 84 | .split('\n') |
| 85 | .filter((zeile) => !zeile.trimStart().startsWith('#')) |
| 86 | .join('\n'); |
| 87 | |
| 88 | return [...toml.matchAll(/path\s*=\s*\[([^\]]*)\]/gu)].flatMap((liste) => |
| 89 | [...(liste[1] ?? '').matchAll(/"([^"]*)"/gu)] |
| 90 | .map((treffer) => treffer[1] ?? '') |
| 91 | .filter((pfad) => pfad.length > 0), |
| 92 | ); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Deckt ein Muster aus REUSE.toml diesen Pfad ab? |
| 97 | * |
| 98 | * Nachgebildet wird nur, was das Projekt tatsächlich verwendet: der genaue |
| 99 | * Pfad und ein abschließendes `**`. Ein vollständiger Glob-Vergleich wäre |
| 100 | * mehr Code als die Regel, die er prüfen soll. |
| 101 | */ |
| 102 | function abgedeckt(pfad: string, muster: readonly string[]): boolean { |
| 103 | return muster.some((m) => (m.endsWith('/**') ? pfad.startsWith(m.slice(0, -2)) : m === pfad)); |
| 104 | } |
| 105 | |
| 106 | describe('Lizenzauszeichnung der Inhalte', () => { |
| 107 | const muster = pfadmuster(); |
| 108 | |
| 109 | it('liest überhaupt Pfade aus REUSE.toml', () => { |
| 110 | /* Ohne diese Gegenprobe wäre der Test unten immer grün und damit wertlos |
| 111 | – dieselbe Falle wie eine leere Prüfliste. Genau hier ist der |
| 112 | Verschiebefehler des ersten Versuchs aufgefallen. */ |
| 113 | expect(muster).toContain('app/**'); |
| 114 | expect(muster).toContain('content/katalog/**'); |
| 115 | expect(muster.every((m) => !m.includes('\n'))).toBe(true); |
| 116 | }); |
| 117 | |
| 118 | it('zeichnet jede aufgenommene Datei unter content/ aus', () => { |
| 119 | const dateien = aufgenommeneInhalte(); |
| 120 | if (dateien === null) { |
| 121 | /* Kein Git – dann ist nicht feststellbar, was `reuse lint` sähe. */ |
| 122 | return; |
| 123 | } |
| 124 | |
| 125 | expect(dateien.length).toBeGreaterThan(3); |
| 126 | |
| 127 | const ohne = dateien.filter((datei) => !abgedeckt(datei, muster)); |
| 128 | |
| 129 | expect( |
| 130 | ohne, |
| 131 | 'Neue Inhaltsdateien gehören in REUSE.toml – sonst ist ihre Herkunft nicht ausgewiesen.', |
| 132 | ).toEqual([]); |
| 133 | }); |
| 134 | |
| 135 | it('zeichnet jede aufgenommene Datei im Projektverzeichnis aus', () => { |
| 136 | /* |
| 137 | Eine Ebene höher als der Test darüber, und aus demselben Anlass: In der |
| 138 | Wurzel greift kein `**`-Muster, jede Datei dort braucht einen eigenen |
| 139 | Eintrag. Als `CHANGELOG.md` dazukam, fiel das nur auf, weil jemand |
| 140 | danach gesucht hat. |
| 141 | */ |
| 142 | const dateien = aufgenommeneWurzeldateien(); |
| 143 | if (dateien === null) { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | expect(dateien).toContain('README.md'); |
| 148 | |
| 149 | const ohne = dateien.filter((datei) => !abgedeckt(datei, muster)); |
| 150 | |
| 151 | expect( |
| 152 | ohne, |
| 153 | 'Neue Dateien im Projektverzeichnis gehören einzeln in REUSE.toml – ' + |
| 154 | 'ein `**`-Muster deckt die Wurzel nicht ab.', |
| 155 | ).toEqual([]); |
| 156 | }); |
| 157 | |
| 158 | it('nennt für den amtlichen Katalog eine eigene Lizenzkennung', () => { |
| 159 | /* Der Katalog darf nie unter die eigene Lizenz des Projekts geraten: Auf |
| 160 | ein amtliches Werk kann niemand eine Lizenz erteilen. */ |
| 161 | const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); |
| 162 | const abKatalog = toml.slice(toml.indexOf('"content/katalog/**"')); |
| 163 | const bisKennung = abKatalog.slice(0, abKatalog.indexOf('SPDX-License-Identifier') + 60); |
| 164 | |
| 165 | expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA'); |
| 166 | expect(bisKennung).not.toContain('EUPL-1.2'); |
| 167 | }); |
| 168 | |
| 169 | /* |
| 170 | Die genauere Angabe muss hinter der allgemeinen stehen. |
| 171 | |
| 172 | Treffen mehrere Einträge auf dieselbe Datei, gilt der **letzte**. Bis |
| 173 | Fassung 0.24.1 stand `docs/**` hinter der Aufzählung der Bilder mit |
| 174 | amtlichem Wortlaut und überschrieb sie vollständig: Am `reuse lint --json` |
| 175 | nachgemessen trugen alle neun Dateien und der Prüfbericht `EUPL-1.2` – |
| 176 | also genau die Zuordnung, die die Aufzählung ausschließen sollte. Zwölf |
| 177 | Zeilen Begründung ohne jede Wirkung, und niemand hat es gesehen, weil |
| 178 | `reuse lint` in keinem Torwächter läuft. |
| 179 | |
| 180 | Geprüft wird die Reihenfolge und nicht das Ergebnis: `reuse` ist ein |
| 181 | Python-Werkzeug und keine Abhängigkeit dieses Pakets – dieselbe Überlegung |
| 182 | wie im Kopf dieser Datei. |
| 183 | */ |
| 184 | it('stellt die Ausnahmen hinter die pauschale docs-Regel', () => { |
| 185 | const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); |
| 186 | |
| 187 | const pauschal = toml.indexOf('path = ["docs/**"]'); |
| 188 | const ausnahme = toml.indexOf('"docs/katalog-pruefbericht.html"'); |
| 189 | |
| 190 | expect(pauschal, 'Die pauschale docs-Regel fehlt.').toBeGreaterThanOrEqual(0); |
| 191 | expect(ausnahme, 'Die Aufzählung der Bilder mit amtlichem Wortlaut fehlt.').toBeGreaterThan(0); |
| 192 | expect( |
| 193 | ausnahme, |
| 194 | 'Die Aufzählung steht vor `docs/**` und wird davon überschrieben – ' + |
| 195 | 'bei mehreren Treffern gilt der letzte Eintrag.', |
| 196 | ).toBeGreaterThan(pauschal); |
| 197 | }); |
| 198 | |
| 199 | it('führt jedes Bildschirmfoto mit amtlichem Wortlaut auf', () => { |
| 200 | /* Die Liste hier ist die Gegenprobe zur Liste dort: Wer ein Bild |
| 201 | hinzunimmt, auf dem eine Frage im Wortlaut steht, muss beide anfassen. */ |
| 202 | const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); |
| 203 | const block = toml.slice(toml.indexOf('"docs/katalog-pruefbericht.html"')); |
| 204 | const bisKennung = block.slice(0, block.indexOf('SPDX-License-Identifier') + 60); |
| 205 | |
| 206 | for (const datei of [ |
| 207 | 'docs/bildschirmfotos/04-lernsitzung.png', |
| 208 | 'docs/bildschirmfotos/05-lernsitzung-feedback.png', |
| 209 | 'docs/bildschirmfotos/08-pruefungslauf.png', |
| 210 | 'docs/bildschirmfotos/10-erklaerung.png', |
| 211 | 'docs/bildschirmfotos/13-suche.png', |
| 212 | 'docs/store-bilder/01-frage-mit-antwortmoeglichkeiten.png', |
| 213 | 'docs/store-bilder/02-rueckmeldung-mit-begruendung.png', |
| 214 | 'docs/store-bilder/05-fragen-durchsuchen.png', |
| 215 | 'app/v2-frage.png', |
| 216 | ]) { |
| 217 | expect(bisKennung, `${datei} fehlt in der Aufzählung.`).toContain(`"${datei}"`); |
| 218 | } |
| 219 | expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA'); |
| 220 | }); |
| 221 | }); |