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.
This commit is contained in:
361
internal/engine/engine_test.go
Normal file
361
internal/engine/engine_test.go
Normal file
@@ -0,0 +1,361 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user