package agate import ( "bytes" "context" "log" "os" "path/filepath" "strings" "testing" "time" "gitea.unprism.ru/KRBL/Agate/remote" "gitea.unprism.ru/KRBL/Agate/store" ) // TestGRPCServerClient tests the interaction between a gRPC server and client. // It creates multiple snapshots with different content on the server, // connects a client to the server, downloads the latest snapshot, // and verifies the contents of the files. func TestGRPCServerClient(t *testing.T) { // Skip this test in short mode if testing.Short() { t.Skip("Skipping gRPC server-client test in short mode") } // Create a temporary directory for the server serverDir, err := os.MkdirTemp("", "agate-server-*") if err != nil { t.Fatalf("Failed to create server temp directory: %v", err) } defer os.RemoveAll(serverDir) // Create a temporary directory for the client clientDir, err := os.MkdirTemp("", "agate-client-*") if err != nil { t.Fatalf("Failed to create client temp directory: %v", err) } defer os.RemoveAll(clientDir) // Create Agate options for the server serverOptions := AgateOptions{ WorkDir: serverDir, } // Create Agate instance for the server serverAgate, err := New(serverOptions) if err != nil { t.Fatalf("Failed to create server Agate instance: %v", err) } defer serverAgate.Close() // Create a data directory dataDir := serverAgate.options.BlobStore.GetActiveDir() if err := os.MkdirAll(dataDir, 0755); err != nil { t.Fatalf("Failed to create data directory: %v", err) } // Create initial test files for the first snapshot 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) } } // Create the first snapshot ctx := context.Background() snapshot1ID, err := serverAgate.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) // Modify some files and add a new file for the second snapshot 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) } } // Create the second snapshot snapshot2ID, err := serverAgate.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) // Delete a file and modify another for the third snapshot 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) } // Create the third snapshot snapshot3ID, err := serverAgate.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) // Start the gRPC server serverAddress := "localhost:50051" server, err := remote.RunServer(ctx, serverAgate.manager, serverAddress) if err != nil { t.Fatalf("Failed to start gRPC server: %v", err) } defer server.Stop(ctx) // Give the server a moment to start time.Sleep(100 * time.Millisecond) // Connect a client to the server client, err := remote.NewClient(serverAddress) if err != nil { t.Fatalf("Failed to connect client to server: %v", err) } defer client.Close() // List snapshots from the client snapshots, err := client.ListSnapshots(ctx) if err != nil { t.Fatalf("Failed to list snapshots from client: %v", err) } // Verify we have 3 snapshots if len(snapshots) != 3 { t.Errorf("Expected 3 snapshots, got %d", len(snapshots)) } // Find the latest snapshot (should be snapshot3) var latestSnapshot store.SnapshotInfo for _, snapshot := range snapshots { if latestSnapshot.CreationTime.Before(snapshot.CreationTime) { latestSnapshot = snapshot } } // Verify the latest snapshot is snapshot3 if latestSnapshot.ID != snapshot3ID { t.Errorf("Latest snapshot ID is %s, expected %s", latestSnapshot.ID, snapshot3ID) } // Get detailed information about the latest snapshot snapshotDetails, err := client.FetchSnapshotDetails(ctx, latestSnapshot.ID) if err != nil { t.Fatalf("Failed to fetch snapshot details: %v", err) } // Verify the snapshot details if snapshotDetails.ID != snapshot3ID { t.Errorf("Snapshot details ID is %s, expected %s", snapshotDetails.ID, snapshot3ID) } // Create a directory to download the snapshot to downloadDir := filepath.Join(clientDir, "download") if err := os.MkdirAll(downloadDir, 0755); err != nil { t.Fatalf("Failed to create download directory: %v", err) } // Download the snapshot err = client.DownloadSnapshot(ctx, latestSnapshot.ID, downloadDir, "") if err != nil { t.Fatalf("Failed to download snapshot: %v", err) } // Verify the downloaded files match the expected content expectedFiles := map[string]string{ filepath.Join(downloadDir, "file1.txt"): "Modified content of file 1", filepath.Join(downloadDir, "file4.txt"): "Content of new file 4", filepath.Join(downloadDir, "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 downloaded file %s: %v", path, err) } if string(content) != expectedContent { t.Errorf("Downloaded file %s has wrong content: got %s, want %s", path, string(content), expectedContent) } } // Verify that file2.txt doesn't exist in the downloaded snapshot if _, err := os.Stat(filepath.Join(downloadDir, "file2.txt")); !os.IsNotExist(err) { t.Errorf("file2.txt should not exist in the downloaded snapshot") } } // TestGRPC_GetRemoteSnapshot_Incremental tests the incremental download functionality // of GetRemoteSnapshot, verifying that it reuses files from a local parent snapshot // instead of downloading them again. func TestGRPC_GetRemoteSnapshot_Incremental(t *testing.T) { // Skip this test in short mode if testing.Short() { t.Skip("Skipping incremental GetRemoteSnapshot test in short mode") } // Create a temporary directory for the server serverDir, err := os.MkdirTemp("", "agate-server-*") if err != nil { t.Fatalf("Failed to create server temp directory: %v", err) } defer os.RemoveAll(serverDir) // Create a temporary directory for the client clientDir, err := os.MkdirTemp("", "agate-client-*") if err != nil { t.Fatalf("Failed to create client temp directory: %v", err) } defer os.RemoveAll(clientDir) // Create a buffer to capture client logs var clientLogBuffer bytes.Buffer clientLogger := log.New(&clientLogBuffer, "", 0) // Create Agate options for the server serverOptions := AgateOptions{ WorkDir: serverDir, } // Create Agate options for the client with logger clientOptions := AgateOptions{ WorkDir: clientDir, Logger: clientLogger, } // Create Agate instances for server and client serverAgate, err := New(serverOptions) if err != nil { t.Fatalf("Failed to create server Agate instance: %v", err) } defer serverAgate.Close() clientAgate, err := New(clientOptions) if err != nil { t.Fatalf("Failed to create client Agate instance: %v", err) } defer clientAgate.Close() // Create a data directory on the server serverDataDir := serverAgate.options.BlobStore.GetActiveDir() if err := os.MkdirAll(serverDataDir, 0755); err != nil { t.Fatalf("Failed to create server data directory: %v", err) } // Create test files for snapshot A on the server if err := os.MkdirAll(filepath.Join(serverDataDir, "subdir"), 0755); err != nil { t.Fatalf("Failed to create subdirectory: %v", err) } snapshotAFiles := map[string]string{ filepath.Join(serverDataDir, "file1.txt"): "Content of file 1", filepath.Join(serverDataDir, "file2.txt"): "Content of file 2", filepath.Join(serverDataDir, "subdir/file3.txt"): "Content of file 3", } for path, content := range snapshotAFiles { if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file %s: %v", path, err) } } // Create snapshot A on the server ctx := context.Background() snapshotAID, err := serverAgate.SaveSnapshot(ctx, "Snapshot A", "") if err != nil { t.Fatalf("Failed to create snapshot A: %v", err) } t.Logf("Created snapshot A with ID: %s", snapshotAID) // Modify some files and add a new file for snapshot B snapshotBChanges := map[string]string{ filepath.Join(serverDataDir, "file1.txt"): "Modified content of file 1", // Modified file filepath.Join(serverDataDir, "file4.txt"): "Content of new file 4", // New file filepath.Join(serverDataDir, "subdir/file5.txt"): "Content of new file 5", // New file in subdir } for path, content := range snapshotBChanges { // Ensure parent directory exists if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatalf("Failed to create directory for %s: %v", path, err) } if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("Failed to create/modify test file %s: %v", path, err) } } // Create snapshot B on the server (with A as parent) snapshotBID, err := serverAgate.SaveSnapshot(ctx, "Snapshot B", snapshotAID) if err != nil { t.Fatalf("Failed to create snapshot B: %v", err) } t.Logf("Created snapshot B with ID: %s", snapshotBID) // Start the gRPC server serverAddress := "localhost:50052" // Use a different port than the other test server, err := remote.RunServer(ctx, serverAgate.manager, serverAddress) if err != nil { t.Fatalf("Failed to start gRPC server: %v", err) } defer server.Stop(ctx) // Give the server a moment to start time.Sleep(100 * time.Millisecond) // Step 1: Client downloads snapshot A err = clientAgate.GetRemoteSnapshot(ctx, serverAddress, snapshotAID, "") if err != nil { t.Fatalf("Failed to download snapshot A: %v", err) } t.Log("Client successfully downloaded snapshot A") // Clear the log buffer to capture only logs from the incremental download clientLogBuffer.Reset() // Step 2: Client downloads snapshot B, specifying A as the local parent err = clientAgate.GetRemoteSnapshot(ctx, serverAddress, snapshotBID, snapshotAID) if err != nil { t.Fatalf("Failed to download snapshot B: %v", err) } t.Log("Client successfully downloaded snapshot B") // Step 3: Verify that snapshot B was correctly imported // Restore snapshot B to a directory restoreDir := filepath.Join(clientDir, "restore") if err := os.MkdirAll(restoreDir, 0755); err != nil { t.Fatalf("Failed to create restore directory: %v", err) } err = clientAgate.RestoreSnapshotToDir(ctx, snapshotBID, restoreDir) if err != nil { t.Fatalf("Failed to restore snapshot B: %v", err) } // Verify the restored files match the expected content expectedFiles := map[string]string{ filepath.Join(restoreDir, "file1.txt"): "Modified content of file 1", // Modified file filepath.Join(restoreDir, "file2.txt"): "Content of file 2", // Unchanged file filepath.Join(restoreDir, "file4.txt"): "Content of new file 4", // New file filepath.Join(restoreDir, "subdir/file3.txt"): "Content of file 3", // Unchanged file filepath.Join(restoreDir, "subdir/file5.txt"): "Content of new file 5", // New file } 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) } } // Step 4: Analyze logs to verify incremental download behavior logs := clientLogBuffer.String() // Check for evidence of file reuse if !strings.Contains(logs, "Reusing file") { t.Errorf("No evidence of file reuse in logs") } // Check for evidence of downloading only new/changed files if !strings.Contains(logs, "Downloading file") { t.Errorf("No evidence of downloading new files in logs") } // Log the relevant parts for debugging t.Logf("Log evidence of incremental download:\n%s", logs) }