syncova-backup/packages/benchmark/measurement_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

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)
}
}