feat(archive): BAK-06 Restore-Testverfahren
internal/restoretest wiederholt BAK-03s eigene Pruefdiskt als Produktcode: echter Restore der neuesten Sicherung, echter Kurzstart einer isolierten Postgres-Instanz, echte SELECT-1-Abfrage; Objekt-Seite echter restic-Restore + Inhaltspruefung. JSONL-Historie (append-only), sichtbare Warnung ueber denselben OPS-05-Pull-Weg wie BAK-08, eigenes /metrics-Modul (archive-restoretest). Zwei reale Defekte gefunden und behoben: Unix-Socket-Pfadlaenge unter tief verschachtelten Testpfaden, restic-stderr-Vermischung beim JSON-Parsen unter dem Dienstnutzer. Real auf 131 verdrahtet und ausgeloest (beide Testarten erfolgreich, End-zu-Ende ueber OPS-03 bestaetigt).
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package restoretest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// AppendHistory hängt result an die JSONL-Protokolldatei an
|
||||
// (Akzeptanzkriterium 2/Pflichtprüfung 3: Historie zeigt mehrere
|
||||
// zurückliegende Testläufe nachvollziehbar an — append-only, nichts wird
|
||||
// überschrieben).
|
||||
func AppendHistory(logPath string, result Result) error {
|
||||
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restoretest: protokolldatei öffnen: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
line, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restoretest: ergebnis kodieren: %w", err)
|
||||
}
|
||||
if _, err := f.Write(append(line, '\n')); err != nil {
|
||||
return fmt.Errorf("restoretest: ergebnis schreiben: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadHistory liest alle bisherigen Testlauf-Ergebnisse — Nachweis der
|
||||
// Nachvollziehbarkeit über mehrere Läufe hinweg.
|
||||
func ReadHistory(logPath string) ([]Result, error) {
|
||||
f, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("restoretest: protokolldatei lesen: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
var results []Result
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
var r Result
|
||||
if err := json.Unmarshal(line, &r); err != nil {
|
||||
return nil, fmt.Errorf("restoretest: protokollzeile dekodieren: %w", err)
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("restoretest: protokolldatei durchlaufen: %w", err)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package restoretest
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestHistory_ShowsMultiplePastRunsInOrder ist Pruefung 3: Protokoll-
|
||||
// historie zeigt mehrere zurueckliegende Testlaeufe nachvollziehbar an.
|
||||
func TestHistory_ShowsMultiplePastRunsInOrder(t *testing.T) {
|
||||
logPath := filepath.Join(t.TempDir(), "restoretest.log")
|
||||
|
||||
results := []Result{
|
||||
{Timestamp: time.Now().UTC(), Kind: KindDatabase, Source: "gen-1", Success: true, Detail: "ok"},
|
||||
{Timestamp: time.Now().UTC().Add(time.Hour), Kind: KindObjects, Source: "snap-1", Success: false, Detail: "kaputt"},
|
||||
{Timestamp: time.Now().UTC().Add(2 * time.Hour), Kind: KindDatabase, Source: "gen-2", Success: true, Detail: "ok"},
|
||||
}
|
||||
for _, r := range results {
|
||||
if err := AppendHistory(logPath, r); err != nil {
|
||||
t.Fatalf("appendHistory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := ReadHistory(logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("readHistory: %v", err)
|
||||
}
|
||||
if len(got) != len(results) {
|
||||
t.Fatalf("erwartet %d eintraege, habe %d", len(results), len(got))
|
||||
}
|
||||
for i, want := range results {
|
||||
if got[i].Source != want.Source || got[i].Success != want.Success || got[i].Kind != want.Kind {
|
||||
t.Fatalf("eintrag %d = %+v, want %+v", i, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadHistory_MissingFileReturnsEmpty - noch kein Testlauf ist kein
|
||||
// Fehlerzustand.
|
||||
func TestReadHistory_MissingFileReturnsEmpty(t *testing.T) {
|
||||
got, err := ReadHistory(filepath.Join(t.TempDir(), "nicht-vorhanden.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("erwartet keinen fehler, habe: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("erwartet leere historie, habe %d eintraege", len(got))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Package restoretest implementiert BAK-06: regelmäßiger, automatisierter
|
||||
// Testlauf des BAK-03-Restore-Verfahrens gegen eine isolierte
|
||||
// Testumgebung — "Wiederherstellung ist Routine, nicht Ausnahmefall"
|
||||
// (Produkt-DNA) heißt: nicht nur Dateien vorhanden pruefen, sondern
|
||||
// tatsächlich eine funktionsfähige Instanz aus der Sicherung starten und
|
||||
// befragen, exakt wie BAK-03s eigener Prüfungsnachweis — hier als
|
||||
// Produktcode statt Testcode, damit es regelmäßig UNBEAUFSICHTIGT laufen
|
||||
// kann.
|
||||
package restoretest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/backup"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/objectbackup"
|
||||
)
|
||||
|
||||
// Kind unterscheidet Datenbank- und Objekt-Storage-Testlauf im Protokoll.
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
KindDatabase Kind = "database"
|
||||
KindObjects Kind = "objects"
|
||||
)
|
||||
|
||||
// Result ist EIN Testlauf-Ergebnis (Akzeptanzkriterium 2: Ergebnis mit
|
||||
// Zeitstempel protokolliert).
|
||||
type Result struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Kind Kind `json:"kind"`
|
||||
Source string `json:"source"` // Generation-ID oder Snapshot-ID
|
||||
Success bool `json:"success"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
// PgConfig bündelt die zusätzlich zu backup.Config nötigen Angaben für
|
||||
// den Kurzstart der Test-Instanz (eigener Port/Socket, damit die
|
||||
// Testinstanz die echte Test-Datenbank auf demselben Host nicht stört).
|
||||
type PgConfig struct {
|
||||
PgCtlPath string // Default "pg_ctl"
|
||||
PsqlPath string // Default "psql"
|
||||
TestPort string // Default "55433"
|
||||
}
|
||||
|
||||
func (c PgConfig) ctlBinary() string {
|
||||
if c.PgCtlPath != "" {
|
||||
return c.PgCtlPath
|
||||
}
|
||||
return "pg_ctl"
|
||||
}
|
||||
|
||||
func (c PgConfig) psqlBinary() string {
|
||||
if c.PsqlPath != "" {
|
||||
return c.PsqlPath
|
||||
}
|
||||
return "psql"
|
||||
}
|
||||
|
||||
func (c PgConfig) port() string {
|
||||
if c.TestPort != "" {
|
||||
return c.TestPort
|
||||
}
|
||||
return "55433"
|
||||
}
|
||||
|
||||
// RunDatabaseTest führt einen vollständigen Restore der NEUESTEN
|
||||
// Generation in ein frisches, isoliertes Testverzeichnis durch und
|
||||
// beweist Wiederherstellbarkeit, indem daraus tatsächlich eine
|
||||
// eigenständige Postgres-Instanz gestartet und per echter Verbindung
|
||||
// abgefragt wird (Akzeptanzkriterium 1) — dieselbe Prüftiefe wie BAK-03s
|
||||
// eigener Nachweis, hier als wiederholbarer Produktcode.
|
||||
func RunDatabaseTest(ctx context.Context, cfg backup.Config, pgCfg PgConfig, testRoot string) Result {
|
||||
res := Result{Timestamp: time.Now().UTC(), Kind: KindDatabase}
|
||||
|
||||
generations, err := backup.ListGenerations(cfg.BackupDir)
|
||||
if err != nil || len(generations) == 0 {
|
||||
res.Detail = fmt.Sprintf("keine sicherungsgeneration gefunden: %v", err)
|
||||
return res
|
||||
}
|
||||
genID := generations[len(generations)-1]
|
||||
res.Source = genID
|
||||
|
||||
testDir, err := os.MkdirTemp(testRoot, "restoretest-db-*")
|
||||
if err != nil {
|
||||
res.Detail = fmt.Sprintf("testverzeichnis anlegen: %v", err)
|
||||
return res
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
|
||||
restoreOut := filepath.Join(testDir, "pgdata")
|
||||
if err := os.MkdirAll(restoreOut, 0o700); err != nil {
|
||||
res.Detail = fmt.Sprintf("pgdata-verzeichnis anlegen: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := backup.Restore(ctx, cfg, genID, restoreOut); err != nil {
|
||||
res.Detail = fmt.Sprintf("restore fehlgeschlagen: %v", err)
|
||||
return res
|
||||
}
|
||||
|
||||
// Minimalkonfiguration NUR für den isolierten Kurzstart (auf diesem
|
||||
// Debian-Postgres liegen postgresql.conf/pg_hba.conf ausserhalb von
|
||||
// PGDATA, siehe BAK-03-PRUEFPROTOKOLL.md - der reale Sicherungsinhalt
|
||||
// selbst enthält sie nicht).
|
||||
if err := os.WriteFile(filepath.Join(restoreOut, "postgresql.conf"), []byte("listen_addresses = ''\n"), 0o600); err != nil {
|
||||
res.Detail = fmt.Sprintf("testkonfiguration schreiben: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(restoreOut, "pg_hba.conf"), []byte("local all all trust\n"), 0o600); err != nil {
|
||||
res.Detail = fmt.Sprintf("testkonfiguration schreiben: %v", err)
|
||||
return res
|
||||
}
|
||||
|
||||
// EIGENES, kurzes Verzeichnis fuer den Unix-Socket - NICHT unter
|
||||
// testDir (Postgres begrenzt Socket-Pfade auf 107 Byte, ein tief
|
||||
// verschachtelter Testverzeichnis-Pfad reisst dieses Limit leicht).
|
||||
socketDir, err := os.MkdirTemp("", "nexarch-rt-sock-*")
|
||||
if err != nil {
|
||||
res.Detail = fmt.Sprintf("socket-verzeichnis anlegen: %v", err)
|
||||
return res
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(socketDir) }()
|
||||
logFile := filepath.Join(testDir, "postgres.log")
|
||||
|
||||
startCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
startCmd := exec.CommandContext(startCtx, pgCfg.ctlBinary(), "start", "-D", restoreOut, "-w", "-t", "30", "-l", logFile,
|
||||
"-o", fmt.Sprintf("-p %s -k %s -c listen_addresses=''", pgCfg.port(), socketDir))
|
||||
if err := startCmd.Run(); err != nil {
|
||||
logContent, _ := os.ReadFile(logFile)
|
||||
res.Detail = fmt.Sprintf("testinstanz starten fehlgeschlagen: %v (log: %s)", err, string(logContent))
|
||||
return res
|
||||
}
|
||||
defer func() {
|
||||
stopCmd := exec.Command(pgCfg.ctlBinary(), "stop", "-D", restoreOut, "-m", "fast")
|
||||
_ = stopCmd.Run()
|
||||
}()
|
||||
|
||||
psqlCtx, cancelPsql := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancelPsql()
|
||||
psqlOut, err := exec.CommandContext(psqlCtx, pgCfg.psqlBinary(),
|
||||
"-h", socketDir, "-p", pgCfg.port(), "-U", cfg.User, "-d", "postgres",
|
||||
"-tAc", "SELECT 1").CombinedOutput()
|
||||
if err != nil || strings.TrimSpace(string(psqlOut)) != "1" {
|
||||
res.Detail = fmt.Sprintf("verbindung zur testinstanz fehlgeschlagen: %v (ausgabe: %s)", err, string(psqlOut))
|
||||
return res
|
||||
}
|
||||
|
||||
res.Success = true
|
||||
res.Detail = "restore und verbindungspruefung erfolgreich"
|
||||
return res
|
||||
}
|
||||
|
||||
// RunObjectTest führt einen vollständigen Restore des NEUESTEN Snapshots
|
||||
// in ein frisches, isoliertes Testverzeichnis durch — restics eigene
|
||||
// Vollständigkeitsgarantie beim Restore (bricht bei fehlenden/beschädigten
|
||||
// Blöcken ab) ist der Wiederherstellbarkeitsnachweis, zusätzlich real
|
||||
// geprüft, dass das Zielverzeichnis tatsächlich Inhalt enthält.
|
||||
func RunObjectTest(ctx context.Context, cfg objectbackup.Config, testRoot string) Result {
|
||||
res := Result{Timestamp: time.Now().UTC(), Kind: KindObjects}
|
||||
|
||||
snapshotID, err := latestSnapshotID(ctx, cfg)
|
||||
if err != nil {
|
||||
res.Detail = fmt.Sprintf("neuesten snapshot ermitteln: %v", err)
|
||||
return res
|
||||
}
|
||||
res.Source = snapshotID
|
||||
|
||||
testDir, err := os.MkdirTemp(testRoot, "restoretest-objects-*")
|
||||
if err != nil {
|
||||
res.Detail = fmt.Sprintf("testverzeichnis anlegen: %v", err)
|
||||
return res
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
|
||||
if err := objectbackup.Restore(ctx, cfg, snapshotID, testDir); err != nil {
|
||||
res.Detail = fmt.Sprintf("restore fehlgeschlagen: %v", err)
|
||||
return res
|
||||
}
|
||||
|
||||
empty, err := dirIsEmpty(testDir)
|
||||
if err != nil {
|
||||
res.Detail = fmt.Sprintf("wiederhergestelltes verzeichnis pruefen: %v", err)
|
||||
return res
|
||||
}
|
||||
if empty {
|
||||
res.Detail = "restore lief ohne fehler, aber zielverzeichnis ist leer"
|
||||
return res
|
||||
}
|
||||
|
||||
res.Success = true
|
||||
res.Detail = "restore erfolgreich, inhalt vorhanden"
|
||||
return res
|
||||
}
|
||||
|
||||
func dirIsEmpty(dir string) (bool, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(entries) == 0, nil
|
||||
}
|
||||
|
||||
func latestSnapshotID(ctx context.Context, cfg objectbackup.Config) (string, error) {
|
||||
binary := cfg.ResticPath
|
||||
if binary == "" {
|
||||
binary = "restic"
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, binary, "-r", cfg.RepoDir, "snapshots", "--json")
|
||||
cmd.Env = append(os.Environ(), "RESTIC_PASSWORD="+cfg.Password)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
// NUR stdout, nicht CombinedOutput: restic schreibt bei fehlendem/
|
||||
// nicht beschreibbarem Cache-Verzeichnis eine Warnung nach stderr
|
||||
// (z.B. "unable to open cache: ..." - real beobachtet unter dem
|
||||
// systemd-Dienstnutzer "nexarch" ohne beschreibbares HOME), die vor
|
||||
// das JSON-Array gemischt worden waere und das Parsen bricht.
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("restic snapshots: %w (stderr: %s)", err, stderr.String())
|
||||
}
|
||||
var snapshots []struct {
|
||||
ShortID string `json:"short_id"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &snapshots); err != nil {
|
||||
return "", fmt.Errorf("snapshot-liste dekodieren: %w", err)
|
||||
}
|
||||
if len(snapshots) == 0 {
|
||||
return "", fmt.Errorf("keine snapshots vorhanden")
|
||||
}
|
||||
return snapshots[len(snapshots)-1].ShortID, nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package restoretest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/backup"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/objectbackup"
|
||||
)
|
||||
|
||||
func requireDBTestConfig(t *testing.T) backup.Config {
|
||||
t.Helper()
|
||||
user := os.Getenv("TEST_BACKUP_PG_USER")
|
||||
if user == "" {
|
||||
t.Skip("TEST_BACKUP_PG_USER nicht gesetzt, Integrationstest uebersprungen")
|
||||
}
|
||||
if _, err := exec.LookPath("pg_combinebackup"); err != nil {
|
||||
t.Skip("pg_combinebackup nicht installiert, Integrationstest uebersprungen")
|
||||
}
|
||||
if _, err := exec.LookPath("pg_ctl"); err != nil {
|
||||
t.Skip("pg_ctl nicht installiert, Integrationstest uebersprungen")
|
||||
}
|
||||
return backup.Config{
|
||||
Host: envOrT("TEST_BACKUP_PG_HOST", "localhost"),
|
||||
Port: envOrT("TEST_BACKUP_PG_PORT", "5432"),
|
||||
User: user,
|
||||
Password: os.Getenv("TEST_BACKUP_PG_PASSWORD"),
|
||||
BackupDir: t.TempDir(),
|
||||
}
|
||||
}
|
||||
|
||||
func envOrT(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// TestRunDatabaseTest_SucceedsAgainstRealBackup ist Pruefung 1: geplanter
|
||||
// Testlauf gegen aktuelle Sicherung erfolgreich durchgefuehrt.
|
||||
func TestRunDatabaseTest_SucceedsAgainstRealBackup(t *testing.T) {
|
||||
cfg := requireDBTestConfig(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
genID := backup.NewGenerationID(time.Now())
|
||||
if _, err := backup.FullBackup(ctx, cfg, genID); err != nil {
|
||||
t.Fatalf("fullbackup: %v", err)
|
||||
}
|
||||
|
||||
res := RunDatabaseTest(ctx, cfg, PgConfig{TestPort: "55434"}, t.TempDir())
|
||||
if !res.Success {
|
||||
t.Fatalf("erwartet erfolgreichen testlauf, habe: %+v", res)
|
||||
}
|
||||
if res.Source != genID {
|
||||
t.Fatalf("source = %q, want %q", res.Source, genID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunDatabaseTest_DetectsCorruptedBackup ist Pruefung 2: absichtlich
|
||||
// beschaedigte Sicherung laesst den Testlauf sichtbar fehlschlagen.
|
||||
func TestRunDatabaseTest_DetectsCorruptedBackup(t *testing.T) {
|
||||
cfg := requireDBTestConfig(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
genID := backup.NewGenerationID(time.Now())
|
||||
manifest, err := backup.FullBackup(ctx, cfg, genID)
|
||||
if err != nil {
|
||||
t.Fatalf("fullbackup: %v", err)
|
||||
}
|
||||
|
||||
// Absichtliche Beschaedigung: base.tar.gz durch Muell ersetzen.
|
||||
tarPath := filepath.Join(filepath.Dir(manifest), backup.BaseTarGzFile)
|
||||
if err := os.WriteFile(tarPath, []byte("das ist kein gueltiges tar.gz"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := RunDatabaseTest(ctx, cfg, PgConfig{TestPort: "55435"}, t.TempDir())
|
||||
if res.Success {
|
||||
t.Fatal("erwartet fehlschlag bei beschaedigter sicherung, testlauf meldete erfolg")
|
||||
}
|
||||
if res.Detail == "" {
|
||||
t.Fatal("erwartet aussagekraeftiges detail zum fehlschlag")
|
||||
}
|
||||
}
|
||||
|
||||
func requireObjTestConfig(t *testing.T) objectbackup.Config {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("restic"); err != nil {
|
||||
t.Skip("restic nicht installiert, Integrationstest uebersprungen")
|
||||
}
|
||||
cfg := objectbackup.Config{RepoDir: filepath.Join(t.TempDir(), "repo"), Password: "restoretest-passwort"}
|
||||
if err := objectbackup.InitRepo(context.Background(), cfg); err != nil {
|
||||
t.Fatalf("initrepo: %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// TestRunObjectTest_SucceedsAgainstRealSnapshot ist Pruefung 1 fuer den
|
||||
// Objekt-Storage-Teil.
|
||||
func TestRunObjectTest_SucceedsAgainstRealSnapshot(t *testing.T) {
|
||||
cfg := requireObjTestConfig(t)
|
||||
ctx := context.Background()
|
||||
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "datei.txt"), []byte("inhalt"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := objectbackup.Backup(ctx, cfg, sourceDir); err != nil {
|
||||
t.Fatalf("backup: %v", err)
|
||||
}
|
||||
|
||||
res := RunObjectTest(ctx, cfg, t.TempDir())
|
||||
if !res.Success {
|
||||
t.Fatalf("erwartet erfolgreichen testlauf, habe: %+v", res)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user