Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
5.5 KiB
Go
169 lines
5.5 KiB
Go
package benchmark
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestValidateRejectsCompressibleThroughput ist der wichtigste Test des Pakets.
|
|
//
|
|
// In Phase 4 zeigte ein Messlauf eine 1021-fache Kompression — die Testdaten
|
|
// waren periodisch, und die Zahl damit wertlos. Ein Durchsatz aus
|
|
// komprimierbaren Daten hat dasselbe Problem: Er misst, wie schnell zstd
|
|
// Wiederholungen erkennt, nicht wie schnell die Anlage arbeitet.
|
|
//
|
|
// Die Prüfung sitzt im Modell, nicht in der Anleitung: Wer eine Messreihe
|
|
// ergänzt, stolpert darüber, statt sie zu übersehen.
|
|
func TestValidateRejectsCompressibleThroughput(testInstance *testing.T) {
|
|
compressibleResult := &Result{
|
|
ScenarioName: "komprimierbar",
|
|
Data: DataShape{Compressible: true, TotalBytes: 1 << 30},
|
|
BytesProcessed: 1 << 30,
|
|
Usage: ResourceUsage{WallClockSeconds: 5},
|
|
}
|
|
|
|
if validationError := compressibleResult.Validate(); validationError == nil {
|
|
testInstance.Error("ein Durchsatz aus komprimierbaren Daten wurde angenommen")
|
|
}
|
|
}
|
|
|
|
// TestValidateRejectsTooShortRuns prueft die Mindestlaufzeit.
|
|
//
|
|
// Unter einer halben Sekunde schlagen der Start der Arbeiter, das Öffnen des
|
|
// Repositorys und die erste Speicherbereinigung stärker durch als die
|
|
// eigentliche Arbeit.
|
|
func TestValidateRejectsTooShortRuns(testInstance *testing.T) {
|
|
shortResult := &Result{
|
|
ScenarioName: "zu kurz",
|
|
Data: DataShape{Compressible: false, TotalBytes: 1024},
|
|
BytesProcessed: 1024,
|
|
Usage: ResourceUsage{WallClockSeconds: 0.05},
|
|
}
|
|
|
|
if validationError := shortResult.Validate(); validationError == nil {
|
|
testInstance.Error("ein Lauf von 50 ms wurde als aussagekräftig angenommen")
|
|
}
|
|
|
|
longEnoughResult := &Result{
|
|
ScenarioName: "lang genug",
|
|
Data: DataShape{Compressible: false, TotalBytes: 1 << 30},
|
|
BytesProcessed: 1 << 30,
|
|
Usage: ResourceUsage{WallClockSeconds: 3},
|
|
}
|
|
|
|
if validationError := longEnoughResult.Validate(); validationError != nil {
|
|
testInstance.Errorf("ein Lauf von drei Sekunden wurde abgelehnt: %v", validationError)
|
|
}
|
|
}
|
|
|
|
// TestRecorderMeasuresRealWork prueft, dass ueberhaupt gemessen wird.
|
|
//
|
|
// Ein Messwerk, das immer null liefert, ist von einer unendlich schnellen
|
|
// Anlage nicht zu unterscheiden.
|
|
func TestRecorderMeasuresRealWork(testInstance *testing.T) {
|
|
recorder := StartRecording()
|
|
|
|
// Etwas Arbeit, die Rechenzeit **und** Speicher kostet.
|
|
accumulatedSum := 0
|
|
|
|
for iteration := 0; iteration < 40; iteration++ {
|
|
workBuffer := make([]byte, 4*1024*1024)
|
|
for bufferIndex := range workBuffer {
|
|
workBuffer[bufferIndex] = byte(bufferIndex)
|
|
}
|
|
|
|
accumulatedSum += len(workBuffer)
|
|
}
|
|
|
|
measuredUsage := recorder.Stop()
|
|
|
|
if accumulatedSum == 0 {
|
|
testInstance.Fatal("die Arbeit wurde wegoptimiert")
|
|
}
|
|
|
|
if measuredUsage.WallClockSeconds <= 0 {
|
|
testInstance.Error("es wurde keine Laufzeit gemessen")
|
|
}
|
|
|
|
if measuredUsage.UserCPUSeconds <= 0 {
|
|
testInstance.Error("es wurde keine Rechenzeit gemessen")
|
|
}
|
|
|
|
if measuredUsage.AllocatedBytes <= 0 {
|
|
testInstance.Error("es wurde keine Speicheranforderung gemessen")
|
|
}
|
|
}
|
|
|
|
// TestReportNamesWhatItDidNotMeasure prueft die Vollstaendigkeit des Berichts.
|
|
//
|
|
// Ein Messbericht, der verschweigt, was er nicht gemessen hat, liest sich
|
|
// vollständig und ist es nicht — dieselbe Regel wie bei den ungeprüften
|
|
// Bereichen im Security Center (Phase 15).
|
|
func TestReportNamesWhatItDidNotMeasure(testInstance *testing.T) {
|
|
measurementReport := &Report{
|
|
Title: "Messreihe",
|
|
Machine: CurrentMachine(),
|
|
GeneratedAt: time.Now().UTC(),
|
|
}
|
|
|
|
measurementReport.AddSkipped("Eine große VM",
|
|
"Ohne Proxmox-Verbund nicht messbar; der Provider ist nicht abgeschlossen.")
|
|
|
|
var outputBuffer bytes.Buffer
|
|
measurementReport.WriteText(&outputBuffer)
|
|
|
|
reportText := outputBuffer.String()
|
|
|
|
if !strings.Contains(reportText, "Nicht gemessen") {
|
|
testInstance.Error("der Bericht nennt den übersprungenen Abschnitt nicht")
|
|
}
|
|
|
|
if !strings.Contains(reportText, "Proxmox") {
|
|
testInstance.Error("der Bericht nennt den Grund nicht")
|
|
}
|
|
}
|
|
|
|
// TestCompressibleThroughputIsNotPrinted prueft die Ausgabe.
|
|
//
|
|
// Auch wenn ein Lauf komprimierbare Daten misst — etwa um die Kompression zu
|
|
// zeigen —, darf im Bericht keine Durchsatzzahl stehen, die jemand
|
|
// herausschneiden und zitieren kann.
|
|
func TestCompressibleThroughputIsNotPrinted(testInstance *testing.T) {
|
|
measurementReport := &Report{
|
|
Title: "Messreihe",
|
|
Machine: CurrentMachine(),
|
|
GeneratedAt: time.Now().UTC(),
|
|
Results: []*Result{{
|
|
ScenarioName: "Kompression",
|
|
Description: "Gut komprimierbare Textdaten.",
|
|
Data: DataShape{Description: "Text", TotalBytes: 1 << 30, Compressible: true},
|
|
BytesProcessed: 1 << 30,
|
|
Usage: ResourceUsage{WallClockSeconds: 2},
|
|
}},
|
|
}
|
|
|
|
var outputBuffer bytes.Buffer
|
|
measurementReport.WriteText(&outputBuffer)
|
|
|
|
if !strings.Contains(outputBuffer.String(), "nicht ausgewiesen") {
|
|
testInstance.Error("bei komprimierbaren Daten wurde ein Durchsatz ausgewiesen")
|
|
}
|
|
}
|
|
|
|
// TestCPUEfficiencyReflectsParallelism prueft die Auslastungskennzahl.
|
|
func TestCPUEfficiencyReflectsParallelism(testInstance *testing.T) {
|
|
singleCoreUsage := ResourceUsage{WallClockSeconds: 10, UserCPUSeconds: 9, SystemCPUSeconds: 1}
|
|
|
|
if efficiency := singleCoreUsage.CPUEfficiency(); efficiency < 0.99 || efficiency > 1.01 {
|
|
testInstance.Errorf("ein ausgelasteter Kern ergab %.2f statt 1,0", efficiency)
|
|
}
|
|
|
|
idleUsage := ResourceUsage{WallClockSeconds: 10, UserCPUSeconds: 0.5}
|
|
|
|
if efficiency := idleUsage.CPUEfficiency(); efficiency > 0.1 {
|
|
testInstance.Errorf("ein wartender Lauf ergab %.2f", efficiency)
|
|
}
|
|
}
|