Add comprehensive test coverage for core functionalities
This commit introduces test cases for the API, archive, store, and filesystem functionalities, as well as a functional test for a full workflow. It ensures robust testing for snapshot operations, archiving, and blob management, significantly improving reliability.
This commit is contained in:
parent
65b1daa52c
commit
047e8d2df0
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@ grpc/google
|
|||||||
grpc/grafeas
|
grpc/grafeas
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
coverage.*
|
35
Makefile
35
Makefile
@ -14,3 +14,38 @@ gen-proto:
|
|||||||
--go-grpc_out=grpc --go-grpc_opt paths=source_relative \
|
--go-grpc_out=grpc --go-grpc_opt paths=source_relative \
|
||||||
--grpc-gateway_out=grpc --grpc-gateway_opt paths=source_relative \
|
--grpc-gateway_out=grpc --grpc-gateway_opt paths=source_relative \
|
||||||
./grpc/snapshot.proto
|
./grpc/snapshot.proto
|
||||||
|
|
||||||
|
# Запуск всех тестов
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Запуск модульных тестов
|
||||||
|
test-unit:
|
||||||
|
go test -v ./store/... ./hash/... ./archive/...
|
||||||
|
|
||||||
|
# Запуск интеграционных тестов
|
||||||
|
test-integration:
|
||||||
|
go test -v -tags=integration ./...
|
||||||
|
|
||||||
|
# Запуск функциональных тестов
|
||||||
|
test-functional:
|
||||||
|
go test -v -run TestFull ./...
|
||||||
|
|
||||||
|
# Запуск тестов производительности
|
||||||
|
test-performance:
|
||||||
|
go test -v -run TestPerformanceMetrics ./...
|
||||||
|
go test -v -bench=. ./...
|
||||||
|
|
||||||
|
# Запуск тестов с покрытием кода
|
||||||
|
test-coverage:
|
||||||
|
go test -v -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
|
||||||
|
# Запуск линтера
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# Запуск всех проверок (тесты + линтер)
|
||||||
|
check: test lint
|
||||||
|
|
||||||
|
.PHONY: download-third-party gen-proto test test-unit test-integration test-functional test-performance test-coverage lint check
|
||||||
|
46
api.go
46
api.go
@ -9,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.unprism.ru/KRBL/Agate/store"
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/stores"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgateOptions defines configuration options for the Agate library.
|
// AgateOptions defines configuration options for the Agate library.
|
||||||
@ -24,12 +25,14 @@ type AgateOptions struct {
|
|||||||
CloseFunc func() error
|
CloseFunc func() error
|
||||||
|
|
||||||
// MetadataStore is the implementation of the metadata store to use.
|
// MetadataStore is the implementation of the metadata store to use.
|
||||||
// Use the stores package to initialize the default implementation:
|
// If nil, a default SQLite-based metadata store will be created automatically.
|
||||||
|
// Use the stores package to initialize a custom implementation:
|
||||||
// metadataStore, err := stores.NewDefaultMetadataStore(metadataDir)
|
// metadataStore, err := stores.NewDefaultMetadataStore(metadataDir)
|
||||||
MetadataStore store.MetadataStore
|
MetadataStore store.MetadataStore
|
||||||
|
|
||||||
// BlobStore is the implementation of the blob store to use.
|
// BlobStore is the implementation of the blob store to use.
|
||||||
// Use the stores package to initialize the default implementation:
|
// If nil, a default filesystem-based blob store will be created automatically.
|
||||||
|
// Use the stores package to initialize a custom implementation:
|
||||||
// blobStore, err := stores.NewDefaultBlobStore(blobsDir)
|
// blobStore, err := stores.NewDefaultBlobStore(blobsDir)
|
||||||
BlobStore store.BlobStore
|
BlobStore store.BlobStore
|
||||||
}
|
}
|
||||||
@ -72,18 +75,37 @@ func New(options AgateOptions) (*Agate, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Use provided stores or initialize default ones
|
// Use provided stores or initialize default ones
|
||||||
if options.MetadataStore != nil {
|
if options.MetadataStore != nil && options.BlobStore != nil {
|
||||||
|
// Use the provided stores
|
||||||
metadataStore = options.MetadataStore
|
metadataStore = options.MetadataStore
|
||||||
} else {
|
|
||||||
// For default implementation, the user needs to initialize and provide the stores
|
|
||||||
return nil, errors.New("metadata store must be provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.BlobStore != nil {
|
|
||||||
blobStore = options.BlobStore
|
blobStore = options.BlobStore
|
||||||
|
} else if options.MetadataStore == nil && options.BlobStore == nil {
|
||||||
|
// Initialize both stores with default implementations
|
||||||
|
metadataStore, blobStore, err = stores.InitDefaultStores(options.WorkDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize default stores: %w", err)
|
||||||
|
}
|
||||||
|
// Update options with the created stores
|
||||||
|
options.MetadataStore = metadataStore
|
||||||
|
options.BlobStore = blobStore
|
||||||
|
} else if options.MetadataStore == nil {
|
||||||
|
// Initialize only the metadata store
|
||||||
|
metadataStore, err = stores.NewDefaultMetadataStore(metadataDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize default metadata store: %w", err)
|
||||||
|
}
|
||||||
|
blobStore = options.BlobStore
|
||||||
|
// Update options with the created metadata store
|
||||||
|
options.MetadataStore = metadataStore
|
||||||
} else {
|
} else {
|
||||||
// For default implementation, the user needs to initialize and provide the stores
|
// Initialize only the blob store
|
||||||
return nil, errors.New("blob store must be provided")
|
blobStore, err = stores.NewDefaultBlobStore(blobsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize default blob store: %w", err)
|
||||||
|
}
|
||||||
|
metadataStore = options.MetadataStore
|
||||||
|
// Update options with the created blob store
|
||||||
|
options.BlobStore = blobStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the snapshot manager
|
// Create the snapshot manager
|
||||||
@ -113,7 +135,7 @@ func New(options AgateOptions) (*Agate, error) {
|
|||||||
|
|
||||||
// Call OpenFunc if provided to initialize resources in the active directory
|
// Call OpenFunc if provided to initialize resources in the active directory
|
||||||
if options.OpenFunc != nil {
|
if options.OpenFunc != nil {
|
||||||
if err := options.OpenFunc(options.BlobStore.GetActiveDir()); err != nil {
|
if err := options.OpenFunc(blobStore.GetActiveDir()); err != nil {
|
||||||
return nil, fmt.Errorf("failed to open resources during initialization: %w", err)
|
return nil, fmt.Errorf("failed to open resources during initialization: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
286
api_test.go
Normal file
286
api_test.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package agate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestAPI creates a temporary directory and initializes an Agate instance
|
||||||
|
func setupTestAPI(t *testing.T) (*Agate, string, func()) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir)
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
createAPITestFiles(t, filepath.Join(dataDir, "blobs", "active"))
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a cleanup function
|
||||||
|
cleanup := func() {
|
||||||
|
ag.Close()
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ag, tempDir, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAPITestFiles creates test files in the specified directory
|
||||||
|
func createAPITestFiles(t *testing.T, dir string) {
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(dir, "subdir")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(dir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(dir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(subDir, "subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(subDir, "subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range testFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAgate(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir, "data")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Check that the Agate instance was created successfully
|
||||||
|
if ag == nil {
|
||||||
|
t.Fatalf("Agate instance is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveAndRestoreSnapshot(t *testing.T) {
|
||||||
|
ag, _, cleanup := setupTestAPI(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshot was created with the correct name
|
||||||
|
snapshot, err := ag.GetSnapshotDetails(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get snapshot details: %v", err)
|
||||||
|
}
|
||||||
|
if snapshot.Name != "Test Snapshot" {
|
||||||
|
t.Errorf("Snapshot has wrong name: got %s, want %s", snapshot.Name, "Test Snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify a file
|
||||||
|
dataDir := ag.options.BlobStore.GetActiveDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dataDir, "file1.txt"), []byte("Modified file 1"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the snapshot
|
||||||
|
err = ag.RestoreSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the file was restored
|
||||||
|
content, err := os.ReadFile(filepath.Join(dataDir, "file1.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read restored file: %v", err)
|
||||||
|
}
|
||||||
|
if string(content) != "This is file 1" {
|
||||||
|
t.Errorf("File content was not restored: got %s, want %s", string(content), "This is file 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRestoreSnapshotToDir(t *testing.T) {
|
||||||
|
ag, tempDir, cleanup := setupTestAPI(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a target directory
|
||||||
|
targetDir := filepath.Join(tempDir, "target")
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create target directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the snapshot to the target directory
|
||||||
|
err = ag.RestoreSnapshotToDir(ctx, snapshotID, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore snapshot to directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the files were restored
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(targetDir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(targetDir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(targetDir, "subdir/subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(targetDir, "subdir/subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, expectedContent := range testFiles {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read restored file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if string(content) != expectedContent {
|
||||||
|
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIListSnapshots(t *testing.T) {
|
||||||
|
ag, _, cleanup := setupTestAPI(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create multiple snapshots
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID1, err := ag.SaveSnapshot(ctx, "Snapshot 1", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify a file
|
||||||
|
dataDir := ag.options.WorkDir
|
||||||
|
if err := os.WriteFile(filepath.Join(dataDir, "file1.txt"), []byte("Modified file 1"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotID2, err := ag.SaveSnapshot(ctx, "Snapshot 2", snapshotID1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the snapshots
|
||||||
|
snapshots, err := ag.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that both snapshots are listed
|
||||||
|
if len(snapshots) != 2 {
|
||||||
|
t.Errorf("Wrong number of snapshots listed: got %d, want %d", len(snapshots), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshots have the correct information
|
||||||
|
for _, snap := range snapshots {
|
||||||
|
if snap.ID == snapshotID1 {
|
||||||
|
if snap.Name != "Snapshot 1" {
|
||||||
|
t.Errorf("Snapshot 1 has wrong name: got %s, want %s", snap.Name, "Snapshot 1")
|
||||||
|
}
|
||||||
|
if snap.ParentID != "" {
|
||||||
|
t.Errorf("Snapshot 1 has wrong parent ID: got %s, want %s", snap.ParentID, "")
|
||||||
|
}
|
||||||
|
} else if snap.ID == snapshotID2 {
|
||||||
|
if snap.Name != "Snapshot 2" {
|
||||||
|
t.Errorf("Snapshot 2 has wrong name: got %s, want %s", snap.Name, "Snapshot 2")
|
||||||
|
}
|
||||||
|
if snap.ParentID != snapshotID1 {
|
||||||
|
t.Errorf("Snapshot 2 has wrong parent ID: got %s, want %s", snap.ParentID, snapshotID1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Unexpected snapshot ID: %s", snap.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIDeleteSnapshot(t *testing.T) {
|
||||||
|
ag, _, cleanup := setupTestAPI(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the snapshot
|
||||||
|
err = ag.DeleteSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the deleted snapshot
|
||||||
|
_, err = ag.GetSnapshotDetails(ctx, snapshotID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when getting deleted snapshot, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List snapshots to confirm it's gone
|
||||||
|
snapshots, err := ag.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
if len(snapshots) != 0 {
|
||||||
|
t.Errorf("Expected 0 snapshots after deletion, got %d", len(snapshots))
|
||||||
|
}
|
||||||
|
}
|
236
archive/archive_test.go
Normal file
236
archive/archive_test.go
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateArchive(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a source directory with some files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(sourceDir, "subdir")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(sourceDir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(sourceDir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(subDir, "subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(subDir, "subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range testFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the archive
|
||||||
|
archivePath := filepath.Join(tempDir, "archive.zip")
|
||||||
|
err = CreateArchive(sourceDir, archivePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the archive file was created
|
||||||
|
if _, err := os.Stat(archivePath); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Archive file was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating archive with non-existent source directory
|
||||||
|
err = CreateArchive(filepath.Join(tempDir, "nonexistent"), archivePath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when creating archive from non-existent directory, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating archive with a file as source
|
||||||
|
fileSourcePath := filepath.Join(tempDir, "file_source.txt")
|
||||||
|
if err := os.WriteFile(fileSourcePath, []byte("This is a file"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
err = CreateArchive(fileSourcePath, archivePath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when creating archive from a file, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListArchiveContents(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a source directory with some files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(sourceDir, "subdir")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(sourceDir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(sourceDir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(subDir, "subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(subDir, "subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range testFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the archive
|
||||||
|
archivePath := filepath.Join(tempDir, "archive.zip")
|
||||||
|
err = CreateArchive(sourceDir, archivePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the archive contents
|
||||||
|
entries, err := ListArchiveContents(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list archive contents: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all files and directories are listed
|
||||||
|
expectedEntries := map[string]bool{
|
||||||
|
"file1.txt": false,
|
||||||
|
"file2.txt": false,
|
||||||
|
"subdir/": true,
|
||||||
|
"subdir/subfile1.txt": false,
|
||||||
|
"subdir/subfile2.txt": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) != len(expectedEntries) {
|
||||||
|
t.Errorf("Wrong number of entries: got %d, want %d", len(entries), len(expectedEntries))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
isDir, exists := expectedEntries[entry.Path]
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("Unexpected entry in archive: %s", entry.Path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.IsDir != isDir {
|
||||||
|
t.Errorf("Entry %s has wrong IsDir value: got %v, want %v", entry.Path, entry.IsDir, isDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test listing contents of non-existent archive
|
||||||
|
_, err = ListArchiveContents(filepath.Join(tempDir, "nonexistent.zip"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when listing contents of non-existent archive, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFileFromArchive(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a source directory with some files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(sourceDir, "subdir")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(sourceDir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(sourceDir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(subDir, "subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(subDir, "subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range testFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the archive
|
||||||
|
archivePath := filepath.Join(tempDir, "archive.zip")
|
||||||
|
err = CreateArchive(sourceDir, archivePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract a file from the archive
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = ExtractFileFromArchive(archivePath, "file1.txt", &buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to extract file from archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the extracted content matches the original
|
||||||
|
if buf.String() != "This is file 1" {
|
||||||
|
t.Errorf("Extracted content does not match: got %s, want %s", buf.String(), "This is file 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract a file from a subdirectory
|
||||||
|
buf.Reset()
|
||||||
|
err = ExtractFileFromArchive(archivePath, "subdir/subfile1.txt", &buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to extract file from archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the extracted content matches the original
|
||||||
|
if buf.String() != "This is subfile 1" {
|
||||||
|
t.Errorf("Extracted content does not match: got %s, want %s", buf.String(), "This is subfile 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract a non-existent file
|
||||||
|
buf.Reset()
|
||||||
|
err = ExtractFileFromArchive(archivePath, "nonexistent.txt", &buf)
|
||||||
|
if err != ErrFileNotFoundInArchive {
|
||||||
|
t.Fatalf("Expected ErrFileNotFoundInArchive when extracting non-existent file, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract a directory
|
||||||
|
buf.Reset()
|
||||||
|
err = ExtractFileFromArchive(archivePath, "subdir/", &buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when extracting a directory, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract from a non-existent archive
|
||||||
|
buf.Reset()
|
||||||
|
err = ExtractFileFromArchive(filepath.Join(tempDir, "nonexistent.zip"), "file1.txt", &buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when extracting from non-existent archive, got nil")
|
||||||
|
}
|
||||||
|
}
|
BIN
basic_usage
BIN
basic_usage
Binary file not shown.
284
functional_test.go
Normal file
284
functional_test.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package agate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFullWorkflow tests a complete workflow of creating snapshots, modifying files,
|
||||||
|
// creating more snapshots, and restoring snapshots.
|
||||||
|
func TestFullWorkflow(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: tempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := ag.options.BlobStore.GetActiveDir()
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initial test files
|
||||||
|
initialFiles := map[string]string{
|
||||||
|
filepath.Join(dataDir, "file1.txt"): "Initial content of file 1",
|
||||||
|
filepath.Join(dataDir, "file2.txt"): "Initial content of file 2",
|
||||||
|
filepath.Join(dataDir, "subdir", "file3.txt"): "Initial content of file 3",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subdirectory
|
||||||
|
if err := os.MkdirAll(filepath.Join(dataDir, "subdir"), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the files
|
||||||
|
for path, content := range initialFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Create the first snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot1ID, err := ag.SaveSnapshot(ctx, "Snapshot 1", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create first snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Created first snapshot with ID: %s", snapshot1ID)
|
||||||
|
|
||||||
|
// Step 2: Modify some files and add a new file
|
||||||
|
modifiedFiles := map[string]string{
|
||||||
|
filepath.Join(dataDir, "file1.txt"): "Modified content of file 1",
|
||||||
|
filepath.Join(dataDir, "file4.txt"): "Content of new file 4",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range modifiedFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify/create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Create the second snapshot
|
||||||
|
snapshot2ID, err := ag.SaveSnapshot(ctx, "Snapshot 2", snapshot1ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create second snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Created second snapshot with ID: %s", snapshot2ID)
|
||||||
|
|
||||||
|
// Step 4: Delete a file and modify another
|
||||||
|
if err := os.Remove(filepath.Join(dataDir, "file2.txt")); err != nil {
|
||||||
|
t.Fatalf("Failed to delete test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(dataDir, "subdir/file3.txt"), []byte("Modified content of file 3"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Create the third snapshot
|
||||||
|
snapshot3ID, err := ag.SaveSnapshot(ctx, "Snapshot 3", snapshot2ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create third snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Created third snapshot with ID: %s", snapshot3ID)
|
||||||
|
|
||||||
|
// Step 6: List all snapshots
|
||||||
|
snapshots, err := ag.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snapshots) != 3 {
|
||||||
|
t.Errorf("Expected 3 snapshots, got %d", len(snapshots))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Restore the first snapshot
|
||||||
|
err = ag.RestoreSnapshot(ctx, snapshot1ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore first snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Restored first snapshot")
|
||||||
|
|
||||||
|
// Step 8: Verify the restored files match the initial state
|
||||||
|
for path, expectedContent := range initialFiles {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read restored file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if string(content) != expectedContent {
|
||||||
|
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that file4.txt doesn't exist
|
||||||
|
if _, err := os.Stat(filepath.Join(dataDir, "file4.txt")); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("File4.txt should not exist after restoring first snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 9: Restore the third snapshot
|
||||||
|
err = ag.RestoreSnapshot(ctx, snapshot3ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore third snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Restored third snapshot")
|
||||||
|
|
||||||
|
// Step 10: Verify the restored files match the final state
|
||||||
|
expectedFiles := map[string]string{
|
||||||
|
filepath.Join(dataDir, "file1.txt"): "Modified content of file 1",
|
||||||
|
filepath.Join(dataDir, "file4.txt"): "Content of new file 4",
|
||||||
|
filepath.Join(dataDir, "subdir/file3.txt"): "Modified content of file 3",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, expectedContent := range expectedFiles {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read restored file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if string(content) != expectedContent {
|
||||||
|
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that file2.txt doesn't exist
|
||||||
|
if _, err := os.Stat(filepath.Join(dataDir, "file2.txt")); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("File2.txt should not exist after restoring third snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 11: Delete a snapshot
|
||||||
|
err = ag.DeleteSnapshot(ctx, snapshot2ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete snapshot: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Deleted second snapshot")
|
||||||
|
|
||||||
|
// Step 12: Verify the snapshot was deleted
|
||||||
|
snapshots, err = ag.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snapshots) != 2 {
|
||||||
|
t.Errorf("Expected 2 snapshots after deletion, got %d", len(snapshots))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, snap := range snapshots {
|
||||||
|
if snap.ID == snapshot2ID {
|
||||||
|
t.Errorf("Snapshot 2 should have been deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLargeFiles tests creating and restoring snapshots with large files
|
||||||
|
func TestLargeFiles(t *testing.T) {
|
||||||
|
// Skip this test in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping large file test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: tempDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := ag.options.BlobStore.GetActiveDir()
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a large file (10 MB)
|
||||||
|
largeFilePath := filepath.Join(dataDir, "large_file.bin")
|
||||||
|
largeFileSize := 10 * 1024 * 1024 // 10 MB
|
||||||
|
largeFile, err := os.Create(largeFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create large test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the file with a repeating pattern
|
||||||
|
pattern := []byte("0123456789ABCDEF")
|
||||||
|
buffer := make([]byte, 8192) // 8 KB buffer
|
||||||
|
for i := 0; i < len(buffer); i += len(pattern) {
|
||||||
|
copy(buffer[i:], pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the buffer multiple times to reach the desired size
|
||||||
|
bytesWritten := 0
|
||||||
|
for bytesWritten < largeFileSize {
|
||||||
|
n, err := largeFile.Write(buffer)
|
||||||
|
if err != nil {
|
||||||
|
largeFile.Close()
|
||||||
|
t.Fatalf("Failed to write to large test file: %v", err)
|
||||||
|
}
|
||||||
|
bytesWritten += n
|
||||||
|
}
|
||||||
|
largeFile.Close()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
startTime := time.Now()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Large File Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
t.Logf("Created snapshot with large file in %v", duration)
|
||||||
|
|
||||||
|
// Modify the large file
|
||||||
|
if err := os.WriteFile(largeFilePath, []byte("Modified content"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify large file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the snapshot
|
||||||
|
startTime = time.Now()
|
||||||
|
err = ag.RestoreSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore snapshot: %v", err)
|
||||||
|
}
|
||||||
|
duration = time.Since(startTime)
|
||||||
|
t.Logf("Restored snapshot with large file in %v", duration)
|
||||||
|
|
||||||
|
// Verify the file size is correct
|
||||||
|
fileInfo, err := os.Stat(largeFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to stat restored large file: %v", err)
|
||||||
|
}
|
||||||
|
if fileInfo.Size() != int64(largeFileSize) {
|
||||||
|
t.Errorf("Restored large file has wrong size: got %d, want %d", fileInfo.Size(), largeFileSize)
|
||||||
|
}
|
||||||
|
}
|
199
go.mod
199
go.mod
@ -1,6 +1,8 @@
|
|||||||
module gitea.unprism.ru/KRBL/Agate
|
module gitea.unprism.ru/KRBL/Agate
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.3
|
||||||
|
|
||||||
|
tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@ -12,8 +14,203 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||||
|
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||||
|
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||||
|
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
||||||
|
github.com/Antonboom/errname v1.1.0 // indirect
|
||||||
|
github.com/Antonboom/nilnil v1.1.0 // indirect
|
||||||
|
github.com/Antonboom/testifylint v1.6.1 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
|
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
|
||||||
|
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||||
|
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||||
|
github.com/alecthomas/chroma/v2 v2.17.2 // indirect
|
||||||
|
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||||
|
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||||
|
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||||
|
github.com/alingse/asasalint v0.0.11 // indirect
|
||||||
|
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||||
|
github.com/ashanbrown/forbidigo v1.6.0 // indirect
|
||||||
|
github.com/ashanbrown/makezero v1.2.0 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||||
|
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||||
|
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||||
|
github.com/breml/bidichk v0.3.3 // indirect
|
||||||
|
github.com/breml/errchkjson v0.4.1 // indirect
|
||||||
|
github.com/butuzov/ireturn v0.4.0 // indirect
|
||||||
|
github.com/butuzov/mirror v1.3.0 // indirect
|
||||||
|
github.com/catenacyber/perfsprint v0.9.1 // indirect
|
||||||
|
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/charithe/durationcheck v0.0.10 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/chavacava/garif v0.1.0 // indirect
|
||||||
|
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||||
|
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||||
|
github.com/daixiang0/gci v0.13.6 // indirect
|
||||||
|
github.com/dave/dst v0.27.3 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/ettle/strcase v0.2.0 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fatih/structtag v1.2.0 // indirect
|
||||||
|
github.com/firefart/nonamedreturns v1.0.6 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||||
|
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||||
|
github.com/ghostiam/protogetter v0.3.15 // indirect
|
||||||
|
github.com/go-critic/go-critic v0.13.0 // indirect
|
||||||
|
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||||
|
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||||
|
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||||
|
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||||
|
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||||
|
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||||
|
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||||
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||||
|
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||||
|
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
|
||||||
|
github.com/golangci/golangci-lint/v2 v2.1.6 // indirect
|
||||||
|
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
|
||||||
|
github.com/golangci/misspell v0.6.0 // indirect
|
||||||
|
github.com/golangci/plugin-module-register v0.1.1 // indirect
|
||||||
|
github.com/golangci/revgrep v0.8.0 // indirect
|
||||||
|
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/gordonklaus/ineffassign v0.1.0 // indirect
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||||
|
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||||
|
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||||
|
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jgautheron/goconst v1.8.1 // indirect
|
||||||
|
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||||
|
github.com/jjti/go-spancheck v0.6.4 // indirect
|
||||||
|
github.com/julz/importas v0.2.0 // indirect
|
||||||
|
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||||
|
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||||
|
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||||
|
github.com/kulti/thelper v0.6.3 // indirect
|
||||||
|
github.com/kunwardeep/paralleltest v1.0.14 // indirect
|
||||||
|
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
||||||
|
github.com/ldez/exptostd v0.4.3 // indirect
|
||||||
|
github.com/ldez/gomoddirectives v0.6.1 // indirect
|
||||||
|
github.com/ldez/grignotin v0.9.0 // indirect
|
||||||
|
github.com/ldez/tagliatelle v0.7.1 // indirect
|
||||||
|
github.com/ldez/usetesting v0.4.3 // indirect
|
||||||
|
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.6 // indirect
|
||||||
|
github.com/manuelarte/funcorder v0.2.1 // indirect
|
||||||
|
github.com/maratori/testableexamples v1.0.0 // indirect
|
||||||
|
github.com/maratori/testpackage v1.1.1 // indirect
|
||||||
|
github.com/matoous/godox v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mgechev/revive v1.9.0 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/moricho/tparallel v0.3.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||||
|
github.com/nishanths/exhaustive v0.12.0 // indirect
|
||||||
|
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||||
|
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
|
github.com/quasilyte/go-ruleguard v0.4.4 // indirect
|
||||||
|
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
|
||||||
|
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||||
|
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||||
|
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||||
|
github.com/raeperd/recvcheck v0.2.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||||
|
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
||||||
|
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||||
|
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||||
|
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
|
||||||
|
github.com/securego/gosec/v2 v2.22.3 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||||
|
github.com/sonatard/noctx v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.12.0 // indirect
|
||||||
|
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||||
|
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.4.1 // indirect
|
||||||
|
github.com/tdakkota/asciicheck v0.4.1 // indirect
|
||||||
|
github.com/tetafro/godot v1.5.1 // indirect
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
||||||
|
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||||
|
github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect
|
||||||
|
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||||
|
github.com/ultraware/funlen v0.2.0 // indirect
|
||||||
|
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||||
|
github.com/uudashr/gocognit v1.2.0 // indirect
|
||||||
|
github.com/uudashr/iface v1.3.1 // indirect
|
||||||
|
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||||
|
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||||
|
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||||
|
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||||
|
go-simpler.org/musttag v0.13.1 // indirect
|
||||||
|
go-simpler.org/sloglint v0.11.0 // indirect
|
||||||
|
go.augendre.info/fatcontext v0.8.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
|
go.uber.org/zap v1.24.0 // indirect
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
honnef.co/go/tools v0.6.1 // indirect
|
||||||
|
mvdan.cc/gofumpt v0.8.0 // indirect
|
||||||
|
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect
|
||||||
)
|
)
|
||||||
|
95
hash/hash_test.go
Normal file
95
hash/hash_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package hash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCalculateFileHash(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a test file with known content
|
||||||
|
testContent := "This is a test file for hashing"
|
||||||
|
testFilePath := filepath.Join(tempDir, "test_file.txt")
|
||||||
|
if err := os.WriteFile(testFilePath, []byte(testContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the expected hash manually
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(testContent))
|
||||||
|
expectedHash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
|
||||||
|
// Calculate the hash using the function
|
||||||
|
hash, err := CalculateFileHash(testFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to calculate file hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the hash matches the expected value
|
||||||
|
if hash != expectedHash {
|
||||||
|
t.Errorf("Hash does not match: got %s, want %s", hash, expectedHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a non-existent file
|
||||||
|
_, err = CalculateFileHash(filepath.Join(tempDir, "nonexistent.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when calculating hash of non-existent file, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a directory
|
||||||
|
dirPath := filepath.Join(tempDir, "test_dir")
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
_, err = CalculateFileHash(dirPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when calculating hash of a directory, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with an empty file
|
||||||
|
emptyFilePath := filepath.Join(tempDir, "empty_file.txt")
|
||||||
|
if err := os.WriteFile(emptyFilePath, []byte{}, 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create empty test file: %v", err)
|
||||||
|
}
|
||||||
|
emptyHash, err := CalculateFileHash(emptyFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to calculate hash of empty file: %v", err)
|
||||||
|
}
|
||||||
|
// The SHA-256 hash of an empty string is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||||
|
expectedEmptyHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
if emptyHash != expectedEmptyHash {
|
||||||
|
t.Errorf("Empty file hash does not match: got %s, want %s", emptyHash, expectedEmptyHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a large file
|
||||||
|
largeFilePath := filepath.Join(tempDir, "large_file.bin")
|
||||||
|
largeFileSize := 1024 * 1024 // 1 MB
|
||||||
|
largeFile, err := os.Create(largeFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create large test file: %v", err)
|
||||||
|
}
|
||||||
|
// Fill the file with a repeating pattern
|
||||||
|
pattern := []byte("0123456789")
|
||||||
|
for i := 0; i < largeFileSize/len(pattern); i++ {
|
||||||
|
if _, err := largeFile.Write(pattern); err != nil {
|
||||||
|
largeFile.Close()
|
||||||
|
t.Fatalf("Failed to write to large test file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
largeFile.Close()
|
||||||
|
|
||||||
|
// Calculate the hash of the large file
|
||||||
|
_, err = CalculateFileHash(largeFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to calculate hash of large file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
114
manager.go
114
manager.go
@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -59,19 +60,11 @@ func (data *SnapshotManagerData) CreateSnapshot(ctx context.Context, sourceDir s
|
|||||||
// Generate a unique ID for the snapshot
|
// Generate a unique ID for the snapshot
|
||||||
snapshotID := uuid.New().String()
|
snapshotID := uuid.New().String()
|
||||||
|
|
||||||
// Clean the active directory to avoid conflicts
|
// Create a temporary file for the archive in the working directory
|
||||||
if err := data.blobStore.CleanActiveDir(ctx); err != nil {
|
tempFilePath := filepath.Join(data.blobStore.GetBaseDir(), "temp-"+snapshotID+".zip")
|
||||||
return nil, fmt.Errorf("failed to clean active directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the active directory for operations
|
|
||||||
activeDir := data.blobStore.GetActiveDir()
|
|
||||||
|
|
||||||
// Create a temporary file for the archive in the active directory
|
|
||||||
tempFilePath := filepath.Join(activeDir, "temp-"+snapshotID+".zip")
|
|
||||||
tempFile, err := os.Create(tempFilePath)
|
tempFile, err := os.Create(tempFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create temporary file in active directory: %w", err)
|
return nil, fmt.Errorf("failed to create temporary file in working directory: %w", err)
|
||||||
}
|
}
|
||||||
tempFile.Close() // Close it as CreateArchive will reopen it
|
tempFile.Close() // Close it as CreateArchive will reopen it
|
||||||
defer os.Remove(tempFilePath) // Clean up temp file after we're done
|
defer os.Remove(tempFilePath) // Clean up temp file after we're done
|
||||||
@ -264,16 +257,11 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
|
|||||||
|
|
||||||
// If no specific path is provided, use the active directory
|
// If no specific path is provided, use the active directory
|
||||||
if path == "" {
|
if path == "" {
|
||||||
// Clean the active directory to avoid conflicts
|
path = data.blobStore.GetActiveDir()
|
||||||
if err := data.blobStore.CleanActiveDir(ctx); err != nil {
|
|
||||||
return fmt.Errorf("failed to clean active directory: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
path = filepath.Join(data.blobStore.GetActiveDir(), snapshotID)
|
// First check if the snapshot exists and get its metadata
|
||||||
}
|
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
||||||
|
|
||||||
// First check if the snapshot exists
|
|
||||||
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrNotFound) {
|
if errors.Is(err, ErrNotFound) {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
@ -297,6 +285,94 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
|
|||||||
return fmt.Errorf("failed to extract snapshot: %w", err)
|
return fmt.Errorf("failed to extract snapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create maps for files and directories in the snapshot for quick lookup
|
||||||
|
snapshotFiles := make(map[string]bool)
|
||||||
|
snapshotDirs := make(map[string]bool)
|
||||||
|
for _, file := range snapshot.Files {
|
||||||
|
if file.IsDir {
|
||||||
|
snapshotDirs[file.Path] = true
|
||||||
|
} else {
|
||||||
|
snapshotFiles[file.Path] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First pass: Collect all files and directories in the target
|
||||||
|
var allPaths []string
|
||||||
|
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the root directory itself
|
||||||
|
if filePath == path {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create relative path
|
||||||
|
relPath, err := filepath.Rel(path, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get relative path: %w", err)
|
||||||
|
}
|
||||||
|
relPath = filepath.ToSlash(relPath)
|
||||||
|
|
||||||
|
allPaths = append(allPaths, filePath)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan target directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort paths by length in descending order to process deepest paths first
|
||||||
|
// This ensures we process files before their parent directories
|
||||||
|
sort.Slice(allPaths, func(i, j int) bool {
|
||||||
|
return len(allPaths[i]) > len(allPaths[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second pass: Remove files and directories that aren't in the snapshot
|
||||||
|
for _, filePath := range allPaths {
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
// Skip if file no longer exists (might have been in a directory we already removed)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to stat file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create relative path
|
||||||
|
relPath, err := filepath.Rel(path, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get relative path: %w", err)
|
||||||
|
}
|
||||||
|
relPath = filepath.ToSlash(relPath)
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// For directories, check if it's in the snapshot or if it's empty
|
||||||
|
if !snapshotDirs[relPath] {
|
||||||
|
// Check if directory is empty
|
||||||
|
entries, err := os.ReadDir(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read directory %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If directory is empty, remove it
|
||||||
|
if len(entries) == 0 {
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove directory %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For files, remove if not in the snapshot
|
||||||
|
if !snapshotFiles[relPath] {
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
409
manager_test.go
Normal file
409
manager_test.go
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
package agate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store/filesystem"
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestEnvironment creates a temporary directory and initializes the stores
|
||||||
|
func setupTestEnvironment(t *testing.T) (string, store.MetadataStore, store.BlobStore, func()) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directories for metadata and blobs
|
||||||
|
metadataDir := filepath.Join(tempDir, "metadata")
|
||||||
|
blobsDir := filepath.Join(tempDir, "blobs")
|
||||||
|
if err := os.MkdirAll(metadataDir, 0755); err != nil {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create metadata directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(blobsDir, 0755); err != nil {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create blobs directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the stores
|
||||||
|
dbPath := filepath.Join(metadataDir, "snapshots.db")
|
||||||
|
metadataStore, err := sqlite.NewSQLiteStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create metadata store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blobStore, err := filesystem.NewFileSystemStore(blobsDir)
|
||||||
|
if err != nil {
|
||||||
|
metadataStore.Close()
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
t.Fatalf("Failed to create blob store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a cleanup function
|
||||||
|
cleanup := func() {
|
||||||
|
metadataStore.Close()
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempDir, metadataStore, blobStore, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestFiles creates test files in the specified directory
|
||||||
|
func createTestFiles(t *testing.T, dir string) {
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(dir, "subdir")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(dir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(dir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(subDir, "subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(subDir, "subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, content := range testFiles {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAndGetSnapshot(t *testing.T) {
|
||||||
|
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshot was created with the correct name
|
||||||
|
if snapshot.Name != "Test Snapshot" {
|
||||||
|
t.Errorf("Snapshot has wrong name: got %s, want %s", snapshot.Name, "Test Snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshot has the correct number of files
|
||||||
|
if len(snapshot.Files) != 5 { // 4 files + 1 directory
|
||||||
|
t.Errorf("Snapshot has wrong number of files: got %d, want %d", len(snapshot.Files), 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the snapshot details
|
||||||
|
retrievedSnapshot, err := manager.GetSnapshotDetails(ctx, snapshot.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get snapshot details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the retrieved snapshot matches the original
|
||||||
|
if retrievedSnapshot.ID != snapshot.ID {
|
||||||
|
t.Errorf("Retrieved snapshot ID does not match: got %s, want %s", retrievedSnapshot.ID, snapshot.ID)
|
||||||
|
}
|
||||||
|
if retrievedSnapshot.Name != snapshot.Name {
|
||||||
|
t.Errorf("Retrieved snapshot name does not match: got %s, want %s", retrievedSnapshot.Name, snapshot.Name)
|
||||||
|
}
|
||||||
|
if len(retrievedSnapshot.Files) != len(snapshot.Files) {
|
||||||
|
t.Errorf("Retrieved snapshot has wrong number of files: got %d, want %d", len(retrievedSnapshot.Files), len(snapshot.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSnapshots(t *testing.T) {
|
||||||
|
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multiple snapshots
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot1, err := manager.CreateSnapshot(ctx, sourceDir, "Snapshot 1", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify a file
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "file1.txt"), []byte("Modified file 1"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot2, err := manager.CreateSnapshot(ctx, sourceDir, "Snapshot 2", snapshot1.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the snapshots
|
||||||
|
snapshots, err := manager.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that both snapshots are listed
|
||||||
|
if len(snapshots) != 2 {
|
||||||
|
t.Errorf("Wrong number of snapshots listed: got %d, want %d", len(snapshots), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshots have the correct information
|
||||||
|
for _, snap := range snapshots {
|
||||||
|
if snap.ID == snapshot1.ID {
|
||||||
|
if snap.Name != "Snapshot 1" {
|
||||||
|
t.Errorf("Snapshot 1 has wrong name: got %s, want %s", snap.Name, "Snapshot 1")
|
||||||
|
}
|
||||||
|
if snap.ParentID != "" {
|
||||||
|
t.Errorf("Snapshot 1 has wrong parent ID: got %s, want %s", snap.ParentID, "")
|
||||||
|
}
|
||||||
|
} else if snap.ID == snapshot2.ID {
|
||||||
|
if snap.Name != "Snapshot 2" {
|
||||||
|
t.Errorf("Snapshot 2 has wrong name: got %s, want %s", snap.Name, "Snapshot 2")
|
||||||
|
}
|
||||||
|
if snap.ParentID != snapshot1.ID {
|
||||||
|
t.Errorf("Snapshot 2 has wrong parent ID: got %s, want %s", snap.ParentID, snapshot1.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Unexpected snapshot ID: %s", snap.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSnapshot(t *testing.T) {
|
||||||
|
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the snapshot
|
||||||
|
err = manager.DeleteSnapshot(ctx, snapshot.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the deleted snapshot
|
||||||
|
_, err = manager.GetSnapshotDetails(ctx, snapshot.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when getting deleted snapshot, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List snapshots to confirm it's gone
|
||||||
|
snapshots, err := manager.ListSnapshots(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
if len(snapshots) != 0 {
|
||||||
|
t.Errorf("Expected 0 snapshots after deletion, got %d", len(snapshots))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenFile(t *testing.T) {
|
||||||
|
_, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(blobStore.GetActiveDir(), "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a file from the snapshot
|
||||||
|
fileReader, err := manager.OpenFile(ctx, snapshot.ID, "file1.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open file from snapshot: %v", err)
|
||||||
|
}
|
||||||
|
defer fileReader.Close()
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
content, err := io.ReadAll(fileReader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read file content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the content matches the original
|
||||||
|
if string(content) != "This is file 1" {
|
||||||
|
t.Errorf("File content does not match: got %s, want %s", string(content), "This is file 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open a non-existent file
|
||||||
|
pipe, err := manager.OpenFile(ctx, snapshot.ID, "nonexistent.txt")
|
||||||
|
if err == nil {
|
||||||
|
tmp := make([]byte, 1)
|
||||||
|
_, err = pipe.Read(tmp)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when opening non-existent file, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSnapshot(t *testing.T) {
|
||||||
|
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a target directory for extraction
|
||||||
|
targetDir := filepath.Join(tempDir, "target")
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create target directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the snapshot
|
||||||
|
err = manager.ExtractSnapshot(ctx, snapshot.ID, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to extract snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the files were extracted correctly
|
||||||
|
testFiles := map[string]string{
|
||||||
|
filepath.Join(targetDir, "file1.txt"): "This is file 1",
|
||||||
|
filepath.Join(targetDir, "file2.txt"): "This is file 2",
|
||||||
|
filepath.Join(targetDir, "subdir/subfile1.txt"): "This is subfile 1",
|
||||||
|
filepath.Join(targetDir, "subdir/subfile2.txt"): "This is subfile 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path, expectedContent := range testFiles {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read extracted file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if string(content) != expectedContent {
|
||||||
|
t.Errorf("Extracted file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract a non-existent snapshot
|
||||||
|
err = manager.ExtractSnapshot(ctx, "nonexistent-id", targetDir)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when extracting non-existent snapshot, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSnapshotMetadata(t *testing.T) {
|
||||||
|
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create a source directory with test files
|
||||||
|
sourceDir := filepath.Join(tempDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
createTestFiles(t, sourceDir)
|
||||||
|
|
||||||
|
// Create a snapshot manager
|
||||||
|
manager, err := CreateSnapshotManager(metadataStore, blobStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the snapshot metadata
|
||||||
|
newName := "Updated Snapshot Name"
|
||||||
|
err = manager.UpdateSnapshotMetadata(ctx, snapshot.ID, newName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the updated snapshot
|
||||||
|
updatedSnapshot, err := manager.GetSnapshotDetails(ctx, snapshot.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get updated snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the name was updated
|
||||||
|
if updatedSnapshot.Name != newName {
|
||||||
|
t.Errorf("Snapshot name was not updated: got %s, want %s", updatedSnapshot.Name, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to update a non-existent snapshot
|
||||||
|
err = manager.UpdateSnapshotMetadata(ctx, "nonexistent-id", "New Name")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when updating non-existent snapshot, got nil")
|
||||||
|
}
|
||||||
|
}
|
353
performance_test.go
Normal file
353
performance_test.go
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
package agate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BenchmarkCreateSnapshot benchmarks the performance of creating snapshots with different numbers of files
|
||||||
|
func BenchmarkCreateSnapshot(b *testing.B) {
|
||||||
|
// Skip in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
b.Skip("Skipping benchmark in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with different numbers of files
|
||||||
|
fileCounts := []int{10, 100, 1000}
|
||||||
|
for _, fileCount := range fileCounts {
|
||||||
|
b.Run(fmt.Sprintf("Files-%d", fileCount), func(b *testing.B) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-bench-*")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir, "data")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
b.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
createBenchmarkFiles(b, dataDir, fileCount, 1024) // 1 KB per file
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Reset the timer before the benchmark loop
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
// Run the benchmark
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err := ag.SaveSnapshot(ctx, fmt.Sprintf("Benchmark Snapshot %d", i), "")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkRestoreSnapshot benchmarks the performance of restoring snapshots with different numbers of files
|
||||||
|
func BenchmarkRestoreSnapshot(b *testing.B) {
|
||||||
|
// Skip in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
b.Skip("Skipping benchmark in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with different numbers of files
|
||||||
|
fileCounts := []int{10, 100, 1000}
|
||||||
|
for _, fileCount := range fileCounts {
|
||||||
|
b.Run(fmt.Sprintf("Files-%d", fileCount), func(b *testing.B) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-bench-*")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir, "data")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
b.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
createBenchmarkFiles(b, dataDir, fileCount, 1024) // 1 KB per file
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Benchmark Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify some files
|
||||||
|
for i := 0; i < fileCount/2; i++ {
|
||||||
|
filePath := filepath.Join(dataDir, fmt.Sprintf("file_%d.txt", i))
|
||||||
|
if err := os.WriteFile(filePath, []byte(fmt.Sprintf("Modified content %d", i)), 0644); err != nil {
|
||||||
|
b.Fatalf("Failed to modify file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the timer before the benchmark loop
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
// Run the benchmark
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := ag.RestoreSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to restore snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkLargeFiles benchmarks the performance of creating and restoring snapshots with large files
|
||||||
|
func BenchmarkLargeFiles(b *testing.B) {
|
||||||
|
// Skip in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
b.Skip("Skipping benchmark in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with different file sizes
|
||||||
|
fileSizes := []int{1 * 1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024} // 1 MB, 10 MB, 100 MB
|
||||||
|
for _, fileSize := range fileSizes {
|
||||||
|
b.Run(fmt.Sprintf("Size-%dMB", fileSize/(1024*1024)), func(b *testing.B) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-bench-*")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir, "data")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
b.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a large file
|
||||||
|
largeFilePath := filepath.Join(dataDir, "large_file.bin")
|
||||||
|
createLargeFile(b, largeFilePath, fileSize)
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Create a snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Measure snapshot creation time
|
||||||
|
b.Run("Create", func(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := ag.SaveSnapshot(ctx, fmt.Sprintf("Large File Snapshot %d", i), "")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a snapshot for restoration benchmark
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Large File Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the large file
|
||||||
|
if err := os.WriteFile(largeFilePath, []byte("Modified content"), 0644); err != nil {
|
||||||
|
b.Fatalf("Failed to modify large file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure snapshot restoration time
|
||||||
|
b.Run("Restore", func(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := ag.RestoreSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to restore snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPerformanceMetrics runs performance tests and reports metrics
|
||||||
|
func TestPerformanceMetrics(t *testing.T) {
|
||||||
|
// Skip in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping performance metrics test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-perf-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a data directory
|
||||||
|
dataDir := filepath.Join(tempDir, "data")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with different numbers of files
|
||||||
|
fileCounts := []int{10, 100, 1000}
|
||||||
|
for _, fileCount := range fileCounts {
|
||||||
|
t.Run(fmt.Sprintf("Files-%d", fileCount), func(t *testing.T) {
|
||||||
|
// Create test files
|
||||||
|
createBenchmarkFiles(t, dataDir, fileCount, 1024) // 1 KB per file
|
||||||
|
|
||||||
|
// Create Agate options
|
||||||
|
options := AgateOptions{
|
||||||
|
WorkDir: dataDir,
|
||||||
|
OpenFunc: func(dir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
CloseFunc: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Agate instance
|
||||||
|
ag, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create Agate instance: %v", err)
|
||||||
|
}
|
||||||
|
defer ag.Close()
|
||||||
|
|
||||||
|
// Measure snapshot creation time
|
||||||
|
ctx := context.Background()
|
||||||
|
startTime := time.Now()
|
||||||
|
snapshotID, err := ag.SaveSnapshot(ctx, "Performance Test Snapshot", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot: %v", err)
|
||||||
|
}
|
||||||
|
createDuration := time.Since(startTime)
|
||||||
|
t.Logf("Created snapshot with %d files in %v (%.2f files/sec)", fileCount, createDuration, float64(fileCount)/createDuration.Seconds())
|
||||||
|
|
||||||
|
// Modify some files
|
||||||
|
for i := 0; i < fileCount/2; i++ {
|
||||||
|
filePath := filepath.Join(dataDir, fmt.Sprintf("file_%d.txt", i))
|
||||||
|
if err := os.WriteFile(filePath, []byte(fmt.Sprintf("Modified content %d", i)), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to modify file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure snapshot restoration time
|
||||||
|
startTime = time.Now()
|
||||||
|
err = ag.RestoreSnapshot(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to restore snapshot: %v", err)
|
||||||
|
}
|
||||||
|
restoreDuration := time.Since(startTime)
|
||||||
|
t.Logf("Restored snapshot with %d files in %v (%.2f files/sec)", fileCount, restoreDuration, float64(fileCount)/restoreDuration.Seconds())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create benchmark files
|
||||||
|
func createBenchmarkFiles(tb testing.TB, dir string, count, size int) {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
// Create files with sequential names
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
filePath := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))
|
||||||
|
|
||||||
|
// Create content of specified size
|
||||||
|
content := make([]byte, size)
|
||||||
|
for j := 0; j < size; j++ {
|
||||||
|
content[j] = byte(j % 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filePath, content, 0644); err != nil {
|
||||||
|
tb.Fatalf("Failed to create benchmark file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a large file
|
||||||
|
func createLargeFile(tb testing.TB, path string, size int) {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
// Create the file
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatalf("Failed to create large file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create a buffer with a pattern
|
||||||
|
bufferSize := 8192 // 8 KB buffer
|
||||||
|
buffer := make([]byte, bufferSize)
|
||||||
|
for i := 0; i < bufferSize; i++ {
|
||||||
|
buffer[i] = byte(i % 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the buffer multiple times to reach the desired size
|
||||||
|
bytesWritten := 0
|
||||||
|
for bytesWritten < size {
|
||||||
|
n, err := file.Write(buffer)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatalf("Failed to write to large file: %v", err)
|
||||||
|
}
|
||||||
|
bytesWritten += n
|
||||||
|
}
|
||||||
|
}
|
115
remote/remote_test.go
Normal file
115
remote/remote_test.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestClientConnect tests that the client can connect to a server
|
||||||
|
func TestClientConnect(t *testing.T) {
|
||||||
|
// Skip this test in short mode
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping remote test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test requires a running server
|
||||||
|
// For a real test, you would need to start a server
|
||||||
|
// Here we'll just test the client creation
|
||||||
|
_, err := NewClient("localhost:50051")
|
||||||
|
if err != nil {
|
||||||
|
// It's expected that this will fail if no server is running
|
||||||
|
t.Logf("Failed to connect to server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockClient tests the client functionality with a mock
|
||||||
|
func TestMockClient(t *testing.T) {
|
||||||
|
// Create a mock client
|
||||||
|
client := &MockClient{}
|
||||||
|
|
||||||
|
// Test ListSnapshots
|
||||||
|
snapshots, err := client.ListSnapshots(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MockClient.ListSnapshots failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(snapshots) != 1 {
|
||||||
|
t.Errorf("Expected 1 snapshot, got %d", len(snapshots))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test FetchSnapshotDetails
|
||||||
|
snapshot, err := client.FetchSnapshotDetails(context.Background(), "mock-snapshot-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MockClient.FetchSnapshotDetails failed: %v", err)
|
||||||
|
}
|
||||||
|
if snapshot.ID != "mock-snapshot-id" {
|
||||||
|
t.Errorf("Expected snapshot ID 'mock-snapshot-id', got '%s'", snapshot.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DownloadSnapshot
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-mock-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
err = client.DownloadSnapshot(context.Background(), "mock-snapshot-id", tempDir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MockClient.DownloadSnapshot failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the mock file was created
|
||||||
|
mockFilePath := filepath.Join(tempDir, "mock-file.txt")
|
||||||
|
if _, err := os.Stat(mockFilePath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Mock file was not created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient is a mock implementation of the Client for testing
|
||||||
|
type MockClient struct{}
|
||||||
|
|
||||||
|
// ListSnapshots returns a mock list of snapshots
|
||||||
|
func (m *MockClient) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
|
||||||
|
return []store.SnapshotInfo{
|
||||||
|
{
|
||||||
|
ID: "mock-snapshot-id",
|
||||||
|
Name: "Mock Snapshot",
|
||||||
|
ParentID: "",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchSnapshotDetails returns mock snapshot details
|
||||||
|
func (m *MockClient) FetchSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
|
||||||
|
return &store.Snapshot{
|
||||||
|
ID: snapshotID,
|
||||||
|
Name: "Mock Snapshot",
|
||||||
|
ParentID: "",
|
||||||
|
Files: []store.FileInfo{
|
||||||
|
{
|
||||||
|
Path: "mock-file.txt",
|
||||||
|
Size: 100,
|
||||||
|
IsDir: false,
|
||||||
|
SHA256: "mock-hash",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadSnapshot simulates downloading a snapshot
|
||||||
|
func (m *MockClient) DownloadSnapshot(ctx context.Context, snapshotID string, targetDir string, localParentID string) error {
|
||||||
|
// Create a mock file
|
||||||
|
mockFilePath := filepath.Join(targetDir, "mock-file.txt")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(mockFilePath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(mockFilePath, []byte("Mock file content"), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is a no-op for the mock client
|
||||||
|
func (m *MockClient) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
9
store/errors.go
Normal file
9
store/errors.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Common errors that can be used by store implementations
|
||||||
|
var (
|
||||||
|
// ErrNotFound means that a requested resource was not found
|
||||||
|
ErrNotFound = errors.New("resource not found")
|
||||||
|
)
|
@ -3,11 +3,11 @@ package filesystem
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.unprism.ru/KRBL/Agate"
|
|
||||||
"gitea.unprism.ru/KRBL/Agate/store"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const blobExtension = ".zip"
|
const blobExtension = ".zip"
|
||||||
@ -75,7 +75,7 @@ func (fs *fileSystemStore) RetrieveBlob(ctx context.Context, snapshotID string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Если файл не найден, возвращаем кастомную ошибку
|
// Если файл не найден, возвращаем кастомную ошибку
|
||||||
return nil, agate.ErrNotFound
|
return nil, store.ErrNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to open blob file %s: %w", blobPath, err)
|
return nil, fmt.Errorf("failed to open blob file %s: %w", blobPath, err)
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ func (fs *fileSystemStore) GetBlobPath(ctx context.Context, snapshotID string) (
|
|||||||
// Проверяем существование файла
|
// Проверяем существование файла
|
||||||
if _, err := os.Stat(blobPath); err != nil {
|
if _, err := os.Stat(blobPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return "", agate.ErrNotFound
|
return "", store.ErrNotFound
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("failed to stat blob file %s: %w", blobPath, err)
|
return "", fmt.Errorf("failed to stat blob file %s: %w", blobPath, err)
|
||||||
}
|
}
|
||||||
@ -118,6 +118,11 @@ func (fs *fileSystemStore) GetBlobPath(ctx context.Context, snapshotID string) (
|
|||||||
return blobPath, nil
|
return blobPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveDir возвращает путь к директории для активных операций.
|
||||||
|
func (fs *fileSystemStore) GetBaseDir() string {
|
||||||
|
return fs.baseDir
|
||||||
|
}
|
||||||
|
|
||||||
// GetActiveDir возвращает путь к директории для активных операций.
|
// GetActiveDir возвращает путь к директории для активных операций.
|
||||||
func (fs *fileSystemStore) GetActiveDir() string {
|
func (fs *fileSystemStore) GetActiveDir() string {
|
||||||
return fs.activeDir
|
return fs.activeDir
|
||||||
|
228
store/filesystem/filesystem_test.go
Normal file
228
store/filesystem/filesystem_test.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package filesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewFileSystemStore(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
store, err := NewFileSystemStore(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create filesystem store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that directories were created
|
||||||
|
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Base directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the store's base directory matches the expected path
|
||||||
|
if store.GetBaseDir() != tempDir {
|
||||||
|
t.Fatalf("Store base directory does not match: got %s, want %s", store.GetBaseDir(), tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeDir := filepath.Join(tempDir, "active")
|
||||||
|
if _, err := os.Stat(activeDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Active directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the store's active directory matches the expected path
|
||||||
|
if store.GetActiveDir() != activeDir {
|
||||||
|
t.Fatalf("Store active directory does not match: got %s, want %s", store.GetActiveDir(), activeDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreAndRetrieveBlob(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
store, err := NewFileSystemStore(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create filesystem store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
testData := []byte("test data for blob")
|
||||||
|
reader := bytes.NewReader(testData)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Store the blob
|
||||||
|
snapshotID := "test-snapshot-id"
|
||||||
|
path, err := store.StoreBlob(ctx, snapshotID, reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the file was created
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Blob file was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the blob
|
||||||
|
blobReader, err := store.RetrieveBlob(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to retrieve blob: %v", err)
|
||||||
|
}
|
||||||
|
defer blobReader.Close()
|
||||||
|
|
||||||
|
// Read the data
|
||||||
|
retrievedData, err := io.ReadAll(blobReader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read blob data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the data matches
|
||||||
|
if !bytes.Equal(testData, retrievedData) {
|
||||||
|
t.Fatalf("Retrieved data does not match original data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteBlob(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
store, err := NewFileSystemStore(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create filesystem store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
testData := []byte("test data for blob")
|
||||||
|
reader := bytes.NewReader(testData)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Store the blob
|
||||||
|
snapshotID := "test-snapshot-id"
|
||||||
|
path, err := store.StoreBlob(ctx, snapshotID, reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the blob
|
||||||
|
err = store.DeleteBlob(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the file was deleted
|
||||||
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Blob file was not deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting a non-existent blob should not return an error
|
||||||
|
err = store.DeleteBlob(ctx, "non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteBlob returned an error for non-existent blob: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBlobPath(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
store, err := NewFileSystemStore(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create filesystem store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
testData := []byte("test data for blob")
|
||||||
|
reader := bytes.NewReader(testData)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Store the blob
|
||||||
|
snapshotID := "test-snapshot-id"
|
||||||
|
expectedPath, err := store.StoreBlob(ctx, snapshotID, reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the blob path
|
||||||
|
path, err := store.GetBlobPath(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get blob path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the path matches
|
||||||
|
if path != expectedPath {
|
||||||
|
t.Fatalf("GetBlobPath returned incorrect path: got %s, want %s", path, expectedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting path for non-existent blob should return ErrNotFound
|
||||||
|
_, err = store.GetBlobPath(ctx, "non-existent-id")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetBlobPath did not return an error for non-existent blob")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanActiveDir(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
store, err := NewFileSystemStore(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create filesystem store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the active directory
|
||||||
|
activeDir := store.GetActiveDir()
|
||||||
|
|
||||||
|
// Create some test files in the active directory
|
||||||
|
testFile1 := filepath.Join(activeDir, "test1.txt")
|
||||||
|
testFile2 := filepath.Join(activeDir, "test2.txt")
|
||||||
|
|
||||||
|
if err := os.WriteFile(testFile1, []byte("test1"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(testFile2, []byte("test2"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the active directory
|
||||||
|
ctx := context.Background()
|
||||||
|
err = store.CleanActiveDir(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to clean active directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the files were deleted
|
||||||
|
entries, err := os.ReadDir(activeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read active directory: %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) > 0 {
|
||||||
|
t.Fatalf("Active directory was not cleaned, %d files remain", len(entries))
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.unprism.ru/KRBL/Agate"
|
|
||||||
"gitea.unprism.ru/KRBL/Agate/store"
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"os"
|
"os"
|
||||||
@ -131,7 +130,7 @@ func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
// Если запись не найдена, возвращаем кастомную ошибку
|
// Если запись не найдена, возвращаем кастомную ошибку
|
||||||
return nil, agate.ErrNotFound
|
return nil, store.ErrNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to query snapshot %s: %w", snapshotID, err)
|
return nil, fmt.Errorf("failed to query snapshot %s: %w", snapshotID, err)
|
||||||
}
|
}
|
||||||
|
241
store/sqlite/sqlite_test.go
Normal file
241
store/sqlite/sqlite_test.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.unprism.ru/KRBL/Agate/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSQLiteStore(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
dbPath := filepath.Join(tempDir, "test.db")
|
||||||
|
s, err := NewSQLiteStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite store: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Check that the database file was created
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Database file was not created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveAndGetSnapshotMetadata(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
dbPath := filepath.Join(tempDir, "test.db")
|
||||||
|
s, err := NewSQLiteStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite store: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create a test snapshot
|
||||||
|
now := time.Now().UTC().Truncate(time.Second) // SQLite doesn't store nanoseconds
|
||||||
|
testSnapshot := store.Snapshot{
|
||||||
|
ID: "test-snapshot-id",
|
||||||
|
Name: "Test Snapshot",
|
||||||
|
ParentID: "parent-snapshot-id",
|
||||||
|
CreationTime: now,
|
||||||
|
Files: []store.FileInfo{
|
||||||
|
{
|
||||||
|
Path: "/test/file1.txt",
|
||||||
|
Size: 100,
|
||||||
|
IsDir: false,
|
||||||
|
SHA256: "hash1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/test/dir1",
|
||||||
|
Size: 0,
|
||||||
|
IsDir: true,
|
||||||
|
SHA256: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
err = s.SaveSnapshotMetadata(ctx, testSnapshot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the snapshot
|
||||||
|
retrievedSnapshot, err := s.GetSnapshotMetadata(ctx, testSnapshot.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to retrieve snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the retrieved snapshot matches the original
|
||||||
|
if retrievedSnapshot.ID != testSnapshot.ID {
|
||||||
|
t.Errorf("Retrieved snapshot ID does not match: got %s, want %s", retrievedSnapshot.ID, testSnapshot.ID)
|
||||||
|
}
|
||||||
|
if retrievedSnapshot.Name != testSnapshot.Name {
|
||||||
|
t.Errorf("Retrieved snapshot name does not match: got %s, want %s", retrievedSnapshot.Name, testSnapshot.Name)
|
||||||
|
}
|
||||||
|
if retrievedSnapshot.ParentID != testSnapshot.ParentID {
|
||||||
|
t.Errorf("Retrieved snapshot parent ID does not match: got %s, want %s", retrievedSnapshot.ParentID, testSnapshot.ParentID)
|
||||||
|
}
|
||||||
|
if !retrievedSnapshot.CreationTime.Equal(testSnapshot.CreationTime) {
|
||||||
|
t.Errorf("Retrieved snapshot creation time does not match: got %v, want %v", retrievedSnapshot.CreationTime, testSnapshot.CreationTime)
|
||||||
|
}
|
||||||
|
if len(retrievedSnapshot.Files) != len(testSnapshot.Files) {
|
||||||
|
t.Errorf("Retrieved snapshot has wrong number of files: got %d, want %d", len(retrievedSnapshot.Files), len(testSnapshot.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSnapshotsMetadata(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
dbPath := filepath.Join(tempDir, "test.db")
|
||||||
|
s, err := NewSQLiteStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite store: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create test snapshots
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Now().UTC().Truncate(time.Second)
|
||||||
|
|
||||||
|
testSnapshots := []store.Snapshot{
|
||||||
|
{
|
||||||
|
ID: "snapshot-1",
|
||||||
|
Name: "Snapshot 1",
|
||||||
|
ParentID: "",
|
||||||
|
CreationTime: now.Add(-2 * time.Hour),
|
||||||
|
Files: []store.FileInfo{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "snapshot-2",
|
||||||
|
Name: "Snapshot 2",
|
||||||
|
ParentID: "snapshot-1",
|
||||||
|
CreationTime: now.Add(-1 * time.Hour),
|
||||||
|
Files: []store.FileInfo{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "snapshot-3",
|
||||||
|
Name: "Snapshot 3",
|
||||||
|
ParentID: "snapshot-2",
|
||||||
|
CreationTime: now,
|
||||||
|
Files: []store.FileInfo{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the snapshots
|
||||||
|
for _, snap := range testSnapshots {
|
||||||
|
err = s.SaveSnapshotMetadata(ctx, snap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the snapshots
|
||||||
|
snapshots, err := s.ListSnapshotsMetadata(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list snapshots: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all snapshots are listed
|
||||||
|
if len(snapshots) != len(testSnapshots) {
|
||||||
|
t.Errorf("Wrong number of snapshots listed: got %d, want %d", len(snapshots), len(testSnapshots))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the snapshots have the correct information
|
||||||
|
for i, snap := range testSnapshots {
|
||||||
|
found := false
|
||||||
|
for _, listedSnap := range snapshots {
|
||||||
|
if listedSnap.ID == snap.ID {
|
||||||
|
found = true
|
||||||
|
if listedSnap.Name != snap.Name {
|
||||||
|
t.Errorf("Snapshot %d has wrong name: got %s, want %s", i, listedSnap.Name, snap.Name)
|
||||||
|
}
|
||||||
|
if listedSnap.ParentID != snap.ParentID {
|
||||||
|
t.Errorf("Snapshot %d has wrong parent ID: got %s, want %s", i, listedSnap.ParentID, snap.ParentID)
|
||||||
|
}
|
||||||
|
if !listedSnap.CreationTime.Equal(snap.CreationTime) {
|
||||||
|
t.Errorf("Snapshot %d has wrong creation time: got %v, want %v", i, listedSnap.CreationTime, snap.CreationTime)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Snapshot %d (%s) not found in listed snapshots", i, snap.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSnapshotMetadata(t *testing.T) {
|
||||||
|
// Create a temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "agate-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after test
|
||||||
|
|
||||||
|
// Create a new store
|
||||||
|
dbPath := filepath.Join(tempDir, "test.db")
|
||||||
|
s, err := NewSQLiteStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite store: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create a test snapshot
|
||||||
|
ctx := context.Background()
|
||||||
|
testSnapshot := store.Snapshot{
|
||||||
|
ID: "test-snapshot-id",
|
||||||
|
Name: "Test Snapshot",
|
||||||
|
ParentID: "",
|
||||||
|
CreationTime: time.Now().UTC().Truncate(time.Second),
|
||||||
|
Files: []store.FileInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the snapshot
|
||||||
|
err = s.SaveSnapshotMetadata(ctx, testSnapshot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the snapshot
|
||||||
|
err = s.DeleteSnapshotMetadata(ctx, testSnapshot.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete snapshot metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to retrieve the deleted snapshot
|
||||||
|
_, err = s.GetSnapshotMetadata(ctx, testSnapshot.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error when retrieving deleted snapshot, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting a non-existent snapshot should not return an error
|
||||||
|
err = s.DeleteSnapshotMetadata(ctx, "non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteSnapshotMetadata returned an error for non-existent snapshot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
@ -72,7 +72,10 @@ type BlobStore interface {
|
|||||||
// Возвращает agate.ErrNotFound, если блоб не найден.
|
// Возвращает agate.ErrNotFound, если блоб не найден.
|
||||||
GetBlobPath(ctx context.Context, snapshotID string) (string, error)
|
GetBlobPath(ctx context.Context, snapshotID string) (string, error)
|
||||||
|
|
||||||
// GetActiveDir возвращает путь к директории для активных операций (создание и восстановление).
|
// GetBaseDir возвращает путь к основной директории
|
||||||
|
GetBaseDir() string
|
||||||
|
|
||||||
|
// GetActiveDir возвращает путь к директории для активных операций.
|
||||||
GetActiveDir() string
|
GetActiveDir() string
|
||||||
|
|
||||||
// CleanActiveDir очищает директорию для активных операций.
|
// CleanActiveDir очищает директорию для активных операций.
|
||||||
|
Loading…
Reference in New Issue
Block a user