Files
dbbackup/internal/engine/engine_test.go
Alexander Renz dbb0f6f942 feat(engine): physical backup revolution - XtraBackup capabilities in pure Go
Why wrap external tools when you can BE the tool?

New physical backup engines:
• MySQL Clone Plugin - native 8.0.17+ physical backup
• Filesystem Snapshots - LVM/ZFS/Btrfs orchestration
• Binlog Streaming - continuous backup with seconds RPO
• Parallel Cloud Upload - stream directly to S3, skip local disk

Smart engine selection automatically picks the optimal strategy based on:
- MySQL version and edition
- Available filesystem features
- Database size
- Cloud connectivity

Zero external dependencies. Single binary. Enterprise capabilities.

Commercial backup vendors: we need to talk.
2025-12-13 21:21:17 +01:00

362 lines
7.7 KiB
Go

package engine
import (
"context"
"io"
"testing"
"time"
)
// MockBackupEngine implements BackupEngine for testing
type MockBackupEngine struct {
name string
description string
available bool
availReason string
supportsRestore bool
supportsIncr bool
supportsStreaming bool
backupResult *BackupResult
backupError error
restoreError error
}
func (m *MockBackupEngine) Name() string { return m.name }
func (m *MockBackupEngine) Description() string { return m.description }
func (m *MockBackupEngine) CheckAvailability(ctx context.Context) (*AvailabilityResult, error) {
return &AvailabilityResult{
Available: m.available,
Reason: m.availReason,
}, nil
}
func (m *MockBackupEngine) Backup(ctx context.Context, opts *BackupOptions) (*BackupResult, error) {
if m.backupError != nil {
return nil, m.backupError
}
if m.backupResult != nil {
return m.backupResult, nil
}
return &BackupResult{
Engine: m.name,
StartTime: time.Now().Add(-time.Minute),
EndTime: time.Now(),
TotalSize: 1024 * 1024,
}, nil
}
func (m *MockBackupEngine) Restore(ctx context.Context, opts *RestoreOptions) error {
return m.restoreError
}
func (m *MockBackupEngine) SupportsRestore() bool { return m.supportsRestore }
func (m *MockBackupEngine) SupportsIncremental() bool { return m.supportsIncr }
func (m *MockBackupEngine) SupportsStreaming() bool { return m.supportsStreaming }
// MockStreamingEngine implements StreamingEngine
type MockStreamingEngine struct {
MockBackupEngine
backupToWriterResult *BackupResult
backupToWriterError error
}
func (m *MockStreamingEngine) BackupToWriter(ctx context.Context, w io.Writer, opts *BackupOptions) (*BackupResult, error) {
if m.backupToWriterError != nil {
return nil, m.backupToWriterError
}
if m.backupToWriterResult != nil {
return m.backupToWriterResult, nil
}
// Write some test data
w.Write([]byte("test backup data"))
return &BackupResult{
Engine: m.name,
StartTime: time.Now().Add(-time.Minute),
EndTime: time.Now(),
TotalSize: 16,
}, nil
}
func TestRegistryRegisterAndGet(t *testing.T) {
registry := NewRegistry()
engine := &MockBackupEngine{
name: "test-engine",
description: "Test backup engine",
available: true,
}
registry.Register(engine)
got, err := registry.Get("test-engine")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil {
t.Fatal("expected to get registered engine")
}
if got.Name() != "test-engine" {
t.Errorf("expected name 'test-engine', got %s", got.Name())
}
}
func TestRegistryGetNonExistent(t *testing.T) {
registry := NewRegistry()
_, err := registry.Get("nonexistent")
if err == nil {
t.Error("expected error for non-existent engine")
}
}
func TestRegistryList(t *testing.T) {
registry := NewRegistry()
engine1 := &MockBackupEngine{name: "engine1"}
engine2 := &MockBackupEngine{name: "engine2"}
registry.Register(engine1)
registry.Register(engine2)
list := registry.List()
if len(list) != 2 {
t.Errorf("expected 2 engines, got %d", len(list))
}
}
func TestRegistryRegisterDuplicate(t *testing.T) {
registry := NewRegistry()
engine1 := &MockBackupEngine{name: "test", description: "first"}
engine2 := &MockBackupEngine{name: "test", description: "second"}
registry.Register(engine1)
registry.Register(engine2) // Should replace
got, _ := registry.Get("test")
if got.Description() != "second" {
t.Error("duplicate registration should replace existing engine")
}
}
func TestBackupResult(t *testing.T) {
result := &BackupResult{
Engine: "test",
StartTime: time.Now().Add(-time.Minute),
EndTime: time.Now(),
TotalSize: 1024 * 1024 * 100, // 100 MB
BinlogFile: "mysql-bin.000001",
BinlogPos: 12345,
GTIDExecuted: "uuid:1-100",
Files: []BackupFile{
{
Path: "/backup/backup.tar.gz",
Size: 1024 * 1024 * 100,
Checksum: "sha256:abc123",
},
},
}
if result.Engine != "test" {
t.Errorf("expected engine 'test', got %s", result.Engine)
}
if len(result.Files) != 1 {
t.Errorf("expected 1 file, got %d", len(result.Files))
}
}
func TestProgress(t *testing.T) {
progress := Progress{
Stage: "copying",
Percent: 50.0,
BytesDone: 512 * 1024 * 1024,
BytesTotal: 1024 * 1024 * 1024,
}
if progress.Stage != "copying" {
t.Errorf("expected stage 'copying', got %s", progress.Stage)
}
if progress.Percent != 50.0 {
t.Errorf("expected percent 50.0, got %f", progress.Percent)
}
}
func TestAvailabilityResult(t *testing.T) {
tests := []struct {
name string
result AvailabilityResult
}{
{
name: "available",
result: AvailabilityResult{
Available: true,
Info: map[string]string{"version": "8.0.30"},
},
},
{
name: "not available",
result: AvailabilityResult{
Available: false,
Reason: "MySQL 8.0.17+ required for clone plugin",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.result.Available && tt.result.Reason == "" {
t.Error("unavailable result should have a reason")
}
})
}
}
func TestRecoveryTarget(t *testing.T) {
now := time.Now()
tests := []struct {
name string
target RecoveryTarget
}{
{
name: "time target",
target: RecoveryTarget{
Type: "time",
Time: now,
},
},
{
name: "gtid target",
target: RecoveryTarget{
Type: "gtid",
GTID: "uuid:1-100",
},
},
{
name: "position target",
target: RecoveryTarget{
Type: "position",
File: "mysql-bin.000001",
Pos: 12345,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.target.Type == "" {
t.Error("target type should be set")
}
})
}
}
func TestMockEngineBackup(t *testing.T) {
engine := &MockBackupEngine{
name: "mock",
available: true,
backupResult: &BackupResult{
Engine: "mock",
TotalSize: 1024,
BinlogFile: "test",
BinlogPos: 123,
},
}
ctx := context.Background()
opts := &BackupOptions{
OutputDir: "/test",
}
result, err := engine.Backup(ctx, opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Engine != "mock" {
t.Errorf("expected engine 'mock', got %s", result.Engine)
}
if result.BinlogFile != "test" {
t.Errorf("expected binlog file 'test', got %s", result.BinlogFile)
}
}
func TestMockStreamingEngine(t *testing.T) {
engine := &MockStreamingEngine{
MockBackupEngine: MockBackupEngine{
name: "mock-streaming",
supportsStreaming: true,
},
}
if !engine.SupportsStreaming() {
t.Error("expected streaming support")
}
ctx := context.Background()
var buf mockWriter
opts := &BackupOptions{}
result, err := engine.BackupToWriter(ctx, &buf, opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Engine != "mock-streaming" {
t.Errorf("expected engine 'mock-streaming', got %s", result.Engine)
}
if len(buf.data) == 0 {
t.Error("expected data to be written")
}
}
type mockWriter struct {
data []byte
}
func (m *mockWriter) Write(p []byte) (int, error) {
m.data = append(m.data, p...)
return len(p), nil
}
func TestDefaultRegistry(t *testing.T) {
// DefaultRegistry should be initialized
if DefaultRegistry == nil {
t.Error("DefaultRegistry should not be nil")
}
}
// Benchmark tests
func BenchmarkRegistryGet(b *testing.B) {
registry := NewRegistry()
for i := 0; i < 10; i++ {
registry.Register(&MockBackupEngine{
name: string(rune('a' + i)),
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.Get("e")
}
}
func BenchmarkRegistryList(b *testing.B) {
registry := NewRegistry()
for i := 0; i < 10; i++ {
registry.Register(&MockBackupEngine{
name: string(rune('a' + i)),
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.List()
}
}