931 lines
26 KiB
Go
931 lines
26 KiB
Go
// Copyright 2025 PingCAP, Inc. Licensed under Apache-2.0.
|
|
|
|
package operator
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/pingcap/errors"
|
|
"github.com/pingcap/tidb/pkg/objstore"
|
|
"github.com/pingcap/tidb/pkg/objstore/storeapi"
|
|
"github.com/pingcap/tidb/pkg/util/logutil"
|
|
"github.com/spf13/pflag"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
testFileName1 = "br-test-file-1.tmp"
|
|
testFileName2 = "br-test-file-2.tmp"
|
|
testFileNameRenamed = "br-test-file-renamed.tmp"
|
|
testDirName = "br-test-dir"
|
|
defaultTestDataSize = 1024 * 1024 // 1MB test data
|
|
)
|
|
|
|
// TestResult represents the result of a single test operation.
|
|
type TestResult struct {
|
|
Name string // Name of the test
|
|
Passed bool // Whether the test passed
|
|
Duration time.Duration // Time taken to execute the test
|
|
Details string // Additional details about the test result
|
|
Error error // Error if the test failed
|
|
}
|
|
|
|
// TestReport contains all test results and summary information.
|
|
type TestReport struct {
|
|
StorageURI string // The storage URI being tested
|
|
StartTime time.Time // When the test started
|
|
EndTime time.Time // When the test ended
|
|
TotalTests int // Total number of tests
|
|
PassedTests int // Number of tests that passed
|
|
FailedTests int // Number of tests that failed
|
|
TestResults []TestResult // Individual test results
|
|
TotalBytes int64 // Total bytes read/written during tests
|
|
}
|
|
|
|
// AddResult adds a test result to the report.
|
|
func (r *TestReport) AddResult(result TestResult) {
|
|
r.TestResults = append(r.TestResults, result)
|
|
r.TotalTests++
|
|
if result.Passed {
|
|
r.PassedTests++
|
|
} else {
|
|
r.FailedTests++
|
|
}
|
|
}
|
|
|
|
// Print outputs a formatted report to stdout.
|
|
func (r *TestReport) Print() {
|
|
duration := r.EndTime.Sub(r.StartTime)
|
|
|
|
fmt.Println()
|
|
printStep("=" + repeatString("=", 70))
|
|
printStep("STORAGE TEST REPORT")
|
|
printStep("=" + repeatString("=", 70))
|
|
fmt.Printf("Storage URI: %s\n", r.StorageURI)
|
|
fmt.Printf("Start Time: %s\n", r.StartTime.Format("2006-01-02 15:04:05"))
|
|
fmt.Printf("End Time: %s\n", r.EndTime.Format("2006-01-02 15:04:05"))
|
|
fmt.Printf("Total Duration: %s\n", duration.Round(time.Millisecond))
|
|
fmt.Printf("Total Tests: %d\n", r.TotalTests)
|
|
fmt.Printf("Passed: %s\n", color.GreenString("%d", r.PassedTests))
|
|
fmt.Printf("Failed: %s\n", color.RedString("%d", r.FailedTests))
|
|
fmt.Printf("Total Data: %s\n", formatBytes(r.TotalBytes))
|
|
|
|
printStep("-" + repeatString("-", 70))
|
|
printStep("TEST DETAILS")
|
|
printStep("-" + repeatString("-", 70))
|
|
|
|
for i, result := range r.TestResults {
|
|
status := "✓"
|
|
statusColor := color.GreenString
|
|
if !result.Passed {
|
|
status = "✗"
|
|
statusColor = color.RedString
|
|
}
|
|
|
|
fmt.Printf("%2d. %s %s (%s)\n",
|
|
i+1,
|
|
statusColor(status),
|
|
result.Name,
|
|
result.Duration.Round(time.Millisecond))
|
|
|
|
if result.Details != "" {
|
|
fmt.Printf(" %s\n", color.CyanString(result.Details))
|
|
}
|
|
|
|
if result.Error != nil {
|
|
fmt.Printf(" %s\n", color.RedString("Error: %v", result.Error))
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
printStep("=" + repeatString("=", 70))
|
|
if r.FailedTests == 0 {
|
|
printSuccess("✅ ALL TESTS PASSED!")
|
|
} else {
|
|
printError("❌ %d TEST(S) FAILED", r.FailedTests)
|
|
}
|
|
printStep("=" + repeatString("=", 70))
|
|
}
|
|
|
|
func repeatString(s string, count int) string {
|
|
result := ""
|
|
for range count {
|
|
result += s
|
|
}
|
|
return result
|
|
}
|
|
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// TestStorageConfig is the configuration for testing external storage.
|
|
type TestStorageConfig struct {
|
|
objstore.BackendOptions
|
|
StorageURI string
|
|
// CleanupOnSuccess indicates whether to cleanup test files on success
|
|
CleanupOnSuccess bool
|
|
// PauseWhenFail pauses the test when a case fails for debugging
|
|
PauseWhenFail bool
|
|
// TestDataSize is the size of test data in bytes
|
|
TestDataSize int64
|
|
}
|
|
|
|
// TestContext bundles context, report, and storage for test execution.
|
|
type TestContext struct {
|
|
Ctx context.Context
|
|
Report *TestReport
|
|
Store storeapi.Storage
|
|
PauseWhenFail bool
|
|
}
|
|
|
|
// AddResult adds a test result and handles pause-on-fail logic.
|
|
func (tc *TestContext) AddResult(result TestResult) {
|
|
tc.Report.AddResult(result)
|
|
|
|
// If test failed and pause-on-fail is enabled, wait for user input
|
|
if !result.Passed && tc.PauseWhenFail {
|
|
fmt.Println()
|
|
printError("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
printError("TEST FAILED: %s", result.Name)
|
|
printError("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
if result.Error != nil {
|
|
printError("Error: %v", result.Error)
|
|
}
|
|
fmt.Println()
|
|
fmt.Println(color.YellowString("⏸ Test paused. You can now inspect the storage content."))
|
|
fmt.Println(color.YellowString(" Storage URI: %s", tc.Report.StorageURI))
|
|
fmt.Println()
|
|
fmt.Print(color.CyanString("Press Enter to continue with remaining tests, or Ctrl+C to abort: "))
|
|
|
|
// Wait for user input
|
|
_, _ = fmt.Scanln()
|
|
fmt.Println()
|
|
printStep("Resuming tests...")
|
|
}
|
|
}
|
|
|
|
// DefineFlagsForTestStorageConfig defines flags for test-storage command.
|
|
func DefineFlagsForTestStorageConfig(flags *pflag.FlagSet) {
|
|
objstore.DefineFlags(flags)
|
|
flags.StringP(flagStorage, "s", "", "The external storage URI to test.")
|
|
flags.Bool("cleanup", true, "Whether to cleanup test files on success.")
|
|
flags.Bool("pause-when-fail", false, "Pause the test when a case fails for debugging.")
|
|
flags.Int64("test-data-size", defaultTestDataSize, "The size of test data in bytes (default 1MB).")
|
|
}
|
|
|
|
// ParseFromFlags parses the config from flags.
|
|
func (cfg *TestStorageConfig) ParseFromFlags(flags *pflag.FlagSet) error {
|
|
var err error
|
|
err = cfg.BackendOptions.ParseFromFlags(flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.StorageURI, err = flags.GetString(flagStorage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.CleanupOnSuccess, err = flags.GetBool("cleanup")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.PauseWhenFail, err = flags.GetBool("pause-when-fail")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.TestDataSize, err = flags.GetInt64("test-data-size")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cfg.StorageURI == "" {
|
|
return errors.New("storage URI cannot be empty, please specify with --storage")
|
|
}
|
|
if cfg.TestDataSize <= 0 {
|
|
return errors.New("test data size must be greater than 0")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunTestStorage tests all operations of an external storage.
|
|
func RunTestStorage(ctx context.Context, cfg TestStorageConfig) error {
|
|
logger := logutil.Logger(ctx)
|
|
logger.Info("Starting external storage test", zap.String("storage", cfg.StorageURI), zap.Any("options", cfg.BackendOptions))
|
|
|
|
// Initialize test report
|
|
report := &TestReport{
|
|
StorageURI: cfg.StorageURI,
|
|
StartTime: time.Now(),
|
|
TestResults: make([]TestResult, 0),
|
|
}
|
|
|
|
// Parse and create storage
|
|
backend, err := objstore.ParseBackend(cfg.StorageURI, &cfg.BackendOptions)
|
|
if err != nil {
|
|
return errors.Annotate(err, "failed to parse storage backend")
|
|
}
|
|
|
|
store, err := objstore.New(ctx, backend, &storeapi.Options{
|
|
SendCredentials: true,
|
|
})
|
|
if err != nil {
|
|
return errors.Annotate(err, "failed to create external storage")
|
|
}
|
|
defer store.Close()
|
|
|
|
printStep("Testing external storage: %s", store.URI())
|
|
report.StorageURI = store.URI()
|
|
|
|
// Create test context
|
|
tc := &TestContext{
|
|
Ctx: ctx,
|
|
Report: report,
|
|
Store: store,
|
|
PauseWhenFail: cfg.PauseWhenFail,
|
|
}
|
|
|
|
// Generate test data
|
|
testData := make([]byte, cfg.TestDataSize)
|
|
if _, err := rand.Read(testData); err != nil {
|
|
return errors.Annotate(err, "failed to generate test data")
|
|
}
|
|
printStep("Generated test data: %s", formatBytes(cfg.TestDataSize))
|
|
|
|
cleanup := func() {
|
|
if !cfg.CleanupOnSuccess {
|
|
return
|
|
}
|
|
printStep("Cleaning up test files...")
|
|
_ = store.DeleteFile(ctx, testFileName1)
|
|
_ = store.DeleteFile(ctx, testFileName2)
|
|
_ = store.DeleteFile(ctx, testFileNameRenamed)
|
|
_ = store.DeleteFile(ctx, filepath.Join(testDirName, testFileName1))
|
|
_ = store.DeleteFile(ctx, filepath.Join(testDirName, testFileName2))
|
|
}
|
|
defer cleanup()
|
|
|
|
// Run all tests
|
|
testWriteFile(tc, testFileName1, testData)
|
|
testFileExists(tc, testFileName1, true)
|
|
testReadFile(tc, testFileName1, testData)
|
|
testOpen(tc, testFileName1, testData)
|
|
testOpenWithRange(tc, testFileName1, testData)
|
|
testCreate(tc, testFileName2, testData)
|
|
testRename(tc, testFileName2, testFileNameRenamed)
|
|
testWalkDir(tc)
|
|
testWalkDirWithSubDir(tc, testFileName1, testData)
|
|
testWalkDirWithPagination(tc, testData)
|
|
testDeleteFile(tc, testFileName1)
|
|
testDeleteFiles(tc, testFileNameRenamed)
|
|
testFileExists(tc, testFileName1, false)
|
|
|
|
// Finalize report
|
|
report.EndTime = time.Now()
|
|
report.TotalBytes = int64(len(testData) * 4) // Approximate: write + read + create + range read
|
|
|
|
// Print report
|
|
report.Print()
|
|
|
|
// Return error if any tests failed
|
|
if report.FailedTests > 0 {
|
|
return errors.New("storage test failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func testWriteFile(tc *TestContext, name string, data []byte) {
|
|
testName := fmt.Sprintf("WriteFile(%s)", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
err := tc.Store.WriteFile(tc.Ctx, name, data)
|
|
duration := time.Since(start)
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
Duration: duration,
|
|
Details: fmt.Sprintf("Wrote %d bytes", len(data)),
|
|
}
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WriteFile failed")
|
|
} else {
|
|
printSuccess(" ✓ Passed")
|
|
result.Passed = true
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testFileExists(tc *TestContext, name string, expected bool) {
|
|
testName := fmt.Sprintf("FileExists(%s) - expecting %v", name, expected)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
exists, err := tc.Store.FileExists(tc.Ctx, name)
|
|
duration := time.Since(start)
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
Duration: duration,
|
|
}
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "FileExists failed")
|
|
} else if exists != expected {
|
|
printError(" ❌ Failed: expected %v, got %v", expected, exists)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("FileExists returned unexpected result: expected %v, got %v", expected, exists)
|
|
} else {
|
|
printSuccess(" ✓ Passed")
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("File exists: %v", exists)
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testReadFile(tc *TestContext, name string, expectedData []byte) {
|
|
testName := fmt.Sprintf("ReadFile(%s)", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
data, err := tc.Store.ReadFile(tc.Ctx, name)
|
|
duration := time.Since(start)
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
Duration: duration,
|
|
}
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "ReadFile failed")
|
|
} else if len(data) != len(expectedData) {
|
|
printError(" ❌ Failed: expected size %d, got %d", len(expectedData), len(data))
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("ReadFile returned wrong size: expected %d, got %d", len(expectedData), len(data))
|
|
} else {
|
|
// Verify data integrity
|
|
dataValid := true
|
|
for i := range data {
|
|
if data[i] != expectedData[i] {
|
|
printError(" ❌ Failed: data mismatch at offset %d", i)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("ReadFile returned corrupted data at offset %d", i)
|
|
dataValid = false
|
|
break
|
|
}
|
|
}
|
|
if dataValid {
|
|
printSuccess(" ✓ Passed (verified %d bytes)", len(data))
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Verified %d bytes", len(data))
|
|
}
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testOpen(tc *TestContext, name string, expectedData []byte) {
|
|
testName := fmt.Sprintf("Open(%s) - streaming read", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
reader, err := tc.Store.Open(tc.Ctx, name, nil)
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Open failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Check file size
|
|
size, err := reader.GetFileSize()
|
|
if err != nil {
|
|
printError(" ❌ Failed to get file size: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "GetFileSize failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
if size != int64(len(expectedData)) {
|
|
printError(" ❌ Failed: expected size %d, got %d", len(expectedData), size)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("GetFileSize returned wrong size: expected %d, got %d", len(expectedData), size)
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Read all data
|
|
data, err := io.ReadAll(reader)
|
|
duration := time.Since(start)
|
|
result.Duration = duration
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed to read: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "ReadAll failed")
|
|
} else if len(data) != len(expectedData) {
|
|
printError(" ❌ Failed: expected size %d, got %d", len(expectedData), len(data))
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("Read returned wrong size: expected %d, got %d", len(expectedData), len(data))
|
|
} else {
|
|
printSuccess(" ✓ Passed (read %d bytes)", len(data))
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Read %d bytes", len(data))
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testOpenWithRange(tc *TestContext, name string, expectedData []byte) {
|
|
testName := fmt.Sprintf("Open(%s) - with range [100, 200)", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
startOffset := int64(100)
|
|
endOffset := int64(200)
|
|
reader, err := tc.Store.Open(tc.Ctx, name, &storeapi.ReaderOption{
|
|
StartOffset: &startOffset,
|
|
EndOffset: &endOffset,
|
|
})
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Open with range failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
data, err := io.ReadAll(reader)
|
|
duration := time.Since(start)
|
|
result.Duration = duration
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed to read: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "ReadAll failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
expectedSize := endOffset - startOffset
|
|
if int64(len(data)) != expectedSize {
|
|
printError(" ❌ Failed: expected size %d, got %d", expectedSize, len(data))
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("Range read returned wrong size: expected %d, got %d", expectedSize, len(data))
|
|
} else {
|
|
// Verify data matches the expected range
|
|
dataValid := true
|
|
for i := range data {
|
|
if data[i] != expectedData[startOffset+int64(i)] {
|
|
printError(" ❌ Failed: data mismatch at offset %d", i)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("Range read returned corrupted data at offset %d", i)
|
|
dataValid = false
|
|
break
|
|
}
|
|
}
|
|
if dataValid {
|
|
printSuccess(" ✓ Passed (read %d bytes)", len(data))
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Read %d bytes from range [%d, %d)", len(data), startOffset, endOffset)
|
|
}
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testCreate(tc *TestContext, name string, data []byte) {
|
|
testName := fmt.Sprintf("Create(%s) - streaming write", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
writer, err := tc.Store.Create(tc.Ctx, name, nil)
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Create failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Write in chunks
|
|
chunkSize := 64 * 1024 // 64KB chunks
|
|
offset := 0
|
|
for offset < len(data) {
|
|
end := offset + chunkSize
|
|
if end > len(data) {
|
|
end = len(data)
|
|
}
|
|
n, err := writer.Write(tc.Ctx, data[offset:end])
|
|
if err != nil {
|
|
printError(" ❌ Failed to write: %v", err)
|
|
_ = writer.Close(tc.Ctx)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Write failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
offset += n
|
|
}
|
|
|
|
if err := writer.Close(tc.Ctx); err != nil {
|
|
printError(" ❌ Failed to close: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Close writer failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
result.Duration = duration
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Wrote %d bytes in chunks", len(data))
|
|
printSuccess(" ✓ Passed (wrote %d bytes)", len(data))
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testRename(tc *TestContext, oldName, newName string) {
|
|
testName := fmt.Sprintf("Rename(%s -> %s)", oldName, newName)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
if err := tc.Store.Rename(tc.Ctx, oldName, newName); err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "Rename failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Verify old file doesn't exist
|
|
exists, err := tc.Store.FileExists(tc.Ctx, oldName)
|
|
if err != nil {
|
|
printError(" ❌ Failed to check old file: (%T) %v", err, err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "FileExists check failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
if exists {
|
|
printError(" ❌ Failed: old file still exists after rename")
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.New("old file still exists after rename")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Verify new file exists
|
|
exists, err = tc.Store.FileExists(tc.Ctx, newName)
|
|
if err != nil {
|
|
printError(" ❌ Failed to check new file: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "FileExists check failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
if !exists {
|
|
printError(" ❌ Failed: new file doesn't exist after rename")
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.New("new file doesn't exist after rename")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
result.Duration = time.Since(start)
|
|
result.Passed = true
|
|
result.Details = "File renamed successfully"
|
|
printSuccess(" ✓ Passed")
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testWalkDir(tc *TestContext) {
|
|
testName := "WalkDir() - list all files"
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
var fileCount int
|
|
err := tc.Store.WalkDir(tc.Ctx, &storeapi.WalkOption{}, func(path string, size int64) error {
|
|
fileCount++
|
|
return nil
|
|
})
|
|
|
|
result.Duration = time.Since(start)
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WalkDir failed")
|
|
} else {
|
|
printSuccess(" ✓ Passed (found %d files)", fileCount)
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Found %d files", fileCount)
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testWalkDirWithSubDir(tc *TestContext, testFile string, testData []byte) {
|
|
testName := "WalkDir() - with subdirectory"
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
// Create test files in subdirectory
|
|
subFile1 := filepath.Join(testDirName, testFile)
|
|
subFile2 := filepath.Join(testDirName, "file2.tmp")
|
|
|
|
if err := tc.Store.WriteFile(tc.Ctx, subFile1, testData[:512]); err != nil {
|
|
printError(" ❌ Failed to create test file in subdir: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WriteFile in subdir failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
if err := tc.Store.WriteFile(tc.Ctx, subFile2, testData[:256]); err != nil {
|
|
printError(" ❌ Failed to create test file in subdir: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WriteFile in subdir failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Walk the subdirectory
|
|
var fileCount int
|
|
err := tc.Store.WalkDir(tc.Ctx, &storeapi.WalkOption{
|
|
SubDir: testDirName,
|
|
}, func(path string, size int64) error {
|
|
fileCount++
|
|
return nil
|
|
})
|
|
|
|
result.Duration = time.Since(start)
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WalkDir with subdir failed")
|
|
} else if fileCount < 2 {
|
|
printError(" ❌ Failed: expected at least 2 files in subdir, got %d", fileCount)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("WalkDir found insufficient files: expected at least 2, got %d", fileCount)
|
|
} else {
|
|
printSuccess(" ✓ Passed (found %d files in subdirectory)", fileCount)
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Found %d files in subdirectory '%s'", fileCount, testDirName)
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testWalkDirWithPagination(tc *TestContext, testData []byte) {
|
|
testName := "WalkDir() - with pagination (small ListCount)"
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
// Create multiple test files to test pagination
|
|
paginationTestDir := "br-test-pagination"
|
|
numFiles := 10
|
|
fileNames := make([]string, numFiles)
|
|
|
|
for i := range numFiles {
|
|
fileName := filepath.Join(paginationTestDir, fmt.Sprintf("pagefile-%03d.tmp", i))
|
|
fileNames[i] = fileName
|
|
if err := tc.Store.WriteFile(tc.Ctx, fileName, testData[:1024]); err != nil {
|
|
printError(" ❌ Failed to create test file %s: %v", fileName, err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotatef(err, "WriteFile %s failed", fileName)
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Cleanup pagination test files
|
|
defer func() {
|
|
for _, name := range fileNames {
|
|
_ = tc.Store.DeleteFile(tc.Ctx, name)
|
|
}
|
|
}()
|
|
|
|
// Test pagination with small page size
|
|
// This forces WalkDir to make multiple internal requests
|
|
pageSize := int64(3)
|
|
|
|
opt := &storeapi.WalkOption{
|
|
SubDir: paginationTestDir,
|
|
ListCount: pageSize, // Small page size to force multiple internal pages
|
|
}
|
|
|
|
var collectedFiles []string
|
|
err := tc.Store.WalkDir(tc.Ctx, opt, func(path string, size int64) error {
|
|
collectedFiles = append(collectedFiles, path)
|
|
return nil
|
|
})
|
|
|
|
result.Duration = time.Since(start)
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "WalkDir with pagination failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
if len(collectedFiles) != numFiles {
|
|
printError(" ❌ Failed: expected %d files total, got %d", numFiles, len(collectedFiles))
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("WalkDir pagination returned wrong file count: expected %d, got %d", numFiles, len(collectedFiles))
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Verify all files are unique
|
|
fileSet := make(map[string]bool)
|
|
for _, file := range collectedFiles {
|
|
if fileSet[file] {
|
|
printError(" ❌ Failed: duplicate file %s found", file)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("WalkDir pagination returned duplicate file: %s", file)
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
fileSet[file] = true
|
|
}
|
|
|
|
printSuccess(" ✓ Passed (retrieved %d unique files with ListCount=%d)", len(collectedFiles), pageSize)
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Retrieved %d unique files with ListCount=%d", len(collectedFiles), pageSize)
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testDeleteFile(tc *TestContext, name string) {
|
|
testName := fmt.Sprintf("DeleteFile(%s)", name)
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
if err := tc.Store.DeleteFile(tc.Ctx, name); err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "DeleteFile failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Verify file is deleted
|
|
exists, err := tc.Store.FileExists(tc.Ctx, name)
|
|
result.Duration = time.Since(start)
|
|
|
|
if err != nil {
|
|
printError(" ❌ Failed to verify deletion: %v", err)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "FileExists check failed")
|
|
} else if exists {
|
|
printError(" ❌ Failed: file still exists after deletion")
|
|
result.Passed = false
|
|
result.Error = errors.New("file still exists after deletion")
|
|
} else {
|
|
printSuccess(" ✓ Passed")
|
|
result.Passed = true
|
|
result.Details = "File deleted successfully"
|
|
}
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func testDeleteFiles(tc *TestContext, names ...string) {
|
|
testName := fmt.Sprintf("DeleteFiles() - batch delete %d files", len(names))
|
|
printStep("Test: %s", testName)
|
|
start := time.Now()
|
|
|
|
result := TestResult{
|
|
Name: testName,
|
|
}
|
|
|
|
if err := tc.Store.DeleteFiles(tc.Ctx, names); err != nil {
|
|
printError(" ❌ Failed: %v", err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotate(err, "DeleteFiles failed")
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
|
|
// Verify files are deleted
|
|
for _, name := range names {
|
|
exists, err := tc.Store.FileExists(tc.Ctx, name)
|
|
if err != nil {
|
|
printError(" ❌ Failed to verify deletion of %s: (%T) %v", name, err, err)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Annotatef(err, "FileExists check failed for %s", name)
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
if exists {
|
|
printError(" ❌ Failed: file %s still exists after deletion", name)
|
|
result.Duration = time.Since(start)
|
|
result.Passed = false
|
|
result.Error = errors.Errorf("file %s still exists after deletion", name)
|
|
tc.AddResult(result)
|
|
return
|
|
}
|
|
}
|
|
|
|
result.Duration = time.Since(start)
|
|
result.Passed = true
|
|
result.Details = fmt.Sprintf("Deleted %d files", len(names))
|
|
printSuccess(" ✓ Passed")
|
|
|
|
tc.AddResult(result)
|
|
}
|
|
|
|
func printStep(format string, args ...any) {
|
|
fmt.Fprintf(os.Stdout, color.CyanString("► "+format+"\n"), args...)
|
|
}
|
|
|
|
func printSuccess(format string, args ...any) {
|
|
fmt.Fprintf(os.Stdout, color.GreenString(format+"\n"), args...)
|
|
}
|
|
|
|
func printError(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, color.RedString(format+"\n"), args...)
|
|
}
|