Files
rippy/rippy-windows/test/rettung.test.ts
T
HitonabiandClaude Fable 5.1 3dc6798c79 feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:51:15 +02:00

121 lines
5.7 KiB
TypeScript

// Tests des Rettungs-Abbilds (kern/rip/rettung.ts, 5.5.0) mit einem
// nachgebauten Gerät: heile Bereiche kommen 1:1, ein Sektor, der einmal
// zickt, wird beim zweiten Versuch gelesen, was dauerhaft unlesbar ist,
// wird mit Nullen gefüllt — und NUR das. Dazu Abbruch, unbekannte Größe,
// der Bericht.
import { describe, expect, it } from 'vitest'
import { BLOCK_BYTES, SEKTOR_BYTES, rettungsAbbild, rettungsBericht, stelleHinzu, type UnlesbareStelle } from '../src/kern/rip/rettung'
/** Ein Gerät mit `groesse` Bytes (Muster: Byte = Position mod 251), dauerhaft
* kaputten Sektoren und solchen, die genau `zickt`-mal scheitern. */
function geraet(groesse: number, kaputt: number[], zickt: Record<number, number> = {}) {
const daten = Buffer.alloc(groesse)
for (let i = 0; i < groesse; i++) daten[i] = i % 251
const versuche: Array<[number, number]> = []
const restZicken = { ...zickt }
const leser = async (puffer: Buffer, laenge: number, position: number): Promise<number> => {
versuche.push([position, laenge])
const ersterSektor = Math.floor(position / SEKTOR_BYTES)
const letzterSektor = Math.floor((position + laenge - 1) / SEKTOR_BYTES)
for (let s = ersterSektor; s <= letzterSektor; s++) {
if (kaputt.includes(s)) throw new Error(`I/O-Fehler Sektor ${s}`)
if ((restZicken[s] ?? 0) > 0) {
restZicken[s] -= 1
throw new Error(`zickt Sektor ${s}`)
}
}
daten.copy(puffer, 0, position, position + laenge)
return laenge
}
const teile: Buffer[] = []
const schreiber = async (puffer: Buffer, laenge: number): Promise<void> => {
teile.push(Buffer.from(puffer.subarray(0, laenge)))
}
return { daten, leser, schreiber, ausgabe: () => Buffer.concat(teile), versuche }
}
describe('rettungsAbbild', () => {
it('eine heile Disc kommt 1:1 — ohne feines Lesen, ohne Nullen', async () => {
const g = geraet(2 * BLOCK_BYTES + 5000, [])
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length })
expect(e.status).toBe('success')
expect(e.bytes).toBe(g.daten.length)
expect(e.unlesbareSektoren).toBe(0)
expect(g.ausgabe().equals(g.daten)).toBe(true)
// Drei Lesevorgänge: zwei volle Blöcke und der Rest.
expect(g.versuche).toHaveLength(3)
})
it('zwei dauerhaft kaputte Sektoren werden genullt — der Rest bleibt', async () => {
const kaputt = [600, 601] // im zweiten Block (Block = 512 Sektoren)
const g = geraet(3 * BLOCK_BYTES, kaputt)
const fortschritte: number[] = []
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length, aufFortschritt: (p) => fortschritte.push(p) })
expect(e.status).toBe('success')
expect(e.unlesbareSektoren).toBe(2)
expect(e.stellen).toEqual<UnlesbareStelle[]>([{ offset: 600 * SEKTOR_BYTES, bytes: 2 * SEKTOR_BYTES }])
const erwartet = Buffer.from(g.daten)
erwartet.fill(0, 600 * SEKTOR_BYTES, 602 * SEKTOR_BYTES)
expect(g.ausgabe().equals(erwartet)).toBe(true)
expect(fortschritte[fortschritte.length - 1]).toBe(100)
})
it('ein Sektor, der einmal zickt, wird beim zweiten Versuch gelesen — keine Null', async () => {
const g = geraet(BLOCK_BYTES, [], { 100: 1 })
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length })
expect(e.status).toBe('success')
expect(e.unlesbareSektoren).toBe(0)
expect(g.ausgabe().equals(g.daten)).toBe(true)
})
it('zickt er öfter als es Versuche gibt, wird er genullt', async () => {
const g = geraet(BLOCK_BYTES, [], { 100: 10 })
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: g.daten.length, versuche: 2 })
expect(e.unlesbareSektoren).toBe(1)
expect(e.stellen).toEqual([{ offset: 100 * SEKTOR_BYTES, bytes: SEKTOR_BYTES }])
})
it('Abbruch: cancelled mit dem Stand bis dahin', async () => {
const g = geraet(4 * BLOCK_BYTES, [])
const steuerung = new AbortController()
let geschrieben = 0
const schreiber = async (p: Buffer, n: number): Promise<void> => {
await g.schreiber(p, n)
geschrieben += 1
if (geschrieben === 2) steuerung.abort()
}
const e = await rettungsAbbild({ leser: g.leser, schreiber, erwarteteBytes: g.daten.length, abbruch: steuerung.signal })
expect(e.status).toBe('cancelled')
expect(e.bytes).toBe(2 * BLOCK_BYTES)
})
it('unbekannte Größe: Fehler, kein Abbild', async () => {
const g = geraet(10, [])
const e = await rettungsAbbild({ leser: g.leser, schreiber: g.schreiber, erwarteteBytes: 0 })
expect(e.status).toBe('error')
})
it('ein Schreibfehler bricht mit Stand ab', async () => {
const g = geraet(2 * BLOCK_BYTES, [])
const schreiber = async (): Promise<void> => {
throw new Error('ENOSPC')
}
const e = await rettungsAbbild({ leser: g.leser, schreiber, erwarteteBytes: g.daten.length })
expect(e.status).toBe('error')
if (e.status === 'error') expect(e.fehler).toContain('ENOSPC')
})
})
describe('stelleHinzu / rettungsBericht', () => {
it('fasst angrenzende Bereiche zusammen', () => {
const stellen: UnlesbareStelle[] = []
stelleHinzu(stellen, 0, 2048)
stelleHinzu(stellen, 2048, 2048)
stelleHinzu(stellen, 8192, 2048)
expect(stellen).toEqual([
{ offset: 0, bytes: 4096 },
{ offset: 8192, bytes: 2048 },
])
})
it('der Bericht nennt Stand, Sektoren, Anteil und die Stellen', () => {
const text = rettungsBericht('G:', { status: 'success', bytes: 1_000_000, unlesbareSektoren: 2, stellen: [{ offset: 4096, bytes: 4096 }] }, 1_000_000)
expect(text).toContain('Stand: vollständig')
expect(text).toContain('Unlesbare Sektoren: 2 (0.4096 %) an 1 Stellen')
expect(text).toContain('ab Byte 4096: 4096 Bytes genullt')
})
})