mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-13 13:10:01 +00:00
0ef3b5cd03
Missing-status checks previously only surfaced Hint to users, leaving them with a fix (Install postgresqlNN-client) but no explanation of the cause (pg_dump NN cannot dump PostgreSQL MM server). Now both warning and missing statuses append Detail before Hint. Follow-up to #830.
265 lines
7.4 KiB
Go
265 lines
7.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestFormatBytes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
b int64
|
|
want string
|
|
}{
|
|
{"0 bytes", 0, "0 B"},
|
|
{"500 bytes", 500, "500 B"},
|
|
{"1 KB", 1 << 10, "1.0 KB"},
|
|
{"512 KB", 512 * (1 << 10), "512.0 KB"},
|
|
{"1 MB", 1 << 20, "1.0 MB"},
|
|
{"340 MB", 340 * (1 << 20), "340.0 MB"},
|
|
{"1 GB", 1 << 30, "1.0 GB"},
|
|
{"15 GB", 15 * (1 << 30), "15.0 GB"},
|
|
{"1.5 MB (computed)", 1572864, "1.5 MB"}, // 1.5 * 1048576
|
|
{"2.3 GB (computed)", 2469493248, "2.3 GB"}, // 2.3 * 1073741824
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := FormatBytes(tc.b)
|
|
if got != tc.want {
|
|
t.Errorf("FormatBytes(%d) = %q, want %q", tc.b, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirSize_EmptyPath(t *testing.T) {
|
|
// Empty path should return 0
|
|
size := DirSize("")
|
|
if size != 0 {
|
|
t.Errorf("DirSize(\"\") = %d, want 0", size)
|
|
}
|
|
}
|
|
|
|
func TestDirSize_NonexistentDir(t *testing.T) {
|
|
// Nonexistent directory should return 0 (graceful error handling)
|
|
size := DirSize("/nonexistent/path/that/does/not/exist")
|
|
if size != 0 {
|
|
t.Errorf("DirSize(nonexistent) = %d, want 0", size)
|
|
}
|
|
}
|
|
|
|
func TestDirSize_SingleFile(t *testing.T) {
|
|
// Create a temporary directory with a single file
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
testContent := "hello world"
|
|
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
|
|
size := DirSize(tmpDir)
|
|
expectedSize := int64(len(testContent))
|
|
if size != expectedSize {
|
|
t.Errorf("DirSize(single file) = %d, want %d", size, expectedSize)
|
|
}
|
|
}
|
|
|
|
func TestDirSize_MultipleFiles(t *testing.T) {
|
|
// Create a temporary directory with multiple files
|
|
tmpDir := t.TempDir()
|
|
|
|
files := map[string]string{
|
|
"file1.txt": "hello",
|
|
"file2.txt": "world",
|
|
"file3.txt": "test",
|
|
}
|
|
|
|
totalSize := int64(0)
|
|
for name, content := range files {
|
|
filePath := filepath.Join(tmpDir, name)
|
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file %s: %v", name, err)
|
|
}
|
|
totalSize += int64(len(content))
|
|
}
|
|
|
|
size := DirSize(tmpDir)
|
|
if size != totalSize {
|
|
t.Errorf("DirSize(multiple files) = %d, want %d", size, totalSize)
|
|
}
|
|
}
|
|
|
|
func TestDirSize_NestedDirs(t *testing.T) {
|
|
// Create nested directories with files
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create nested directory
|
|
nestedDir := filepath.Join(tmpDir, "subdir", "nested")
|
|
if err := os.MkdirAll(nestedDir, 0755); err != nil {
|
|
t.Fatalf("failed to create nested dir: %v", err)
|
|
}
|
|
|
|
// Write files at different levels
|
|
f1 := filepath.Join(tmpDir, "top.txt")
|
|
f2 := filepath.Join(tmpDir, "subdir", "middle.txt")
|
|
f3 := filepath.Join(nestedDir, "deep.txt")
|
|
|
|
content1 := "top"
|
|
content2 := "middle"
|
|
content3 := "deep"
|
|
|
|
for path, content := range map[string]string{f1: content1, f2: content2, f3: content3} {
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
}
|
|
|
|
expectedSize := int64(len(content1) + len(content2) + len(content3))
|
|
size := DirSize(tmpDir)
|
|
if size != expectedSize {
|
|
t.Errorf("DirSize(nested dirs) = %d, want %d", size, expectedSize)
|
|
}
|
|
}
|
|
|
|
func TestCheckDiskSpace(t *testing.T) {
|
|
// Test current directory
|
|
check, freeDisk := checkDiskSpace(".")
|
|
if check.Name != "disk_space" {
|
|
t.Errorf("check.Name = %q, want disk_space", check.Name)
|
|
}
|
|
|
|
// Status should be either "ok" or "warning" (not "missing" for normal systems)
|
|
if check.Status != "ok" && check.Status != "warning" {
|
|
t.Errorf("check.Status = %q, want 'ok' or 'warning'", check.Status)
|
|
}
|
|
|
|
// Free disk should be > 0 on normal systems
|
|
if freeDisk <= 0 {
|
|
t.Errorf("freeDisk = %d, want > 0", freeDisk)
|
|
}
|
|
|
|
// Detail field should contain useful information
|
|
if check.Detail == "" {
|
|
t.Error("check.Detail should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestPreflightResult_FlatFields(t *testing.T) {
|
|
// Verify that the flat fields are populated correctly
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a test file to measure directory size
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
|
|
// We can't easily test with a real DSN, so test the structure with empty DSN
|
|
result := RunPreflight(context.Background(), "", tmpDir, "")
|
|
|
|
// Verify flat fields exist
|
|
if !result.DiskSpaceOK && result.DiskSpaceOK {
|
|
// This tautology is to just check the field exists
|
|
t.Error("result.DiskSpaceOK field not accessible")
|
|
}
|
|
|
|
if result.FreeDiskBytes <= 0 {
|
|
t.Errorf("result.FreeDiskBytes = %d, want > 0", result.FreeDiskBytes)
|
|
}
|
|
|
|
dataDirSize := result.DataDirSizeBytes
|
|
if dataDirSize <= 0 {
|
|
t.Errorf("result.DataDirSizeBytes = %d, want > 0 (has test file)", dataDirSize)
|
|
}
|
|
|
|
// Warnings should be a slice (may be nil if no warnings)
|
|
// Just verify it's accessible
|
|
_ = result.Warnings
|
|
|
|
// Checks should be populated with at least disk_space check
|
|
if len(result.Checks) == 0 {
|
|
t.Error("result.Checks should have at least one check")
|
|
}
|
|
|
|
// Find disk_space check
|
|
var diskCheck *PreflightCheck
|
|
for i := range result.Checks {
|
|
if result.Checks[i].Name == "disk_space" {
|
|
diskCheck = &result.Checks[i]
|
|
break
|
|
}
|
|
}
|
|
if diskCheck == nil {
|
|
t.Error("disk_space check not found")
|
|
} else if diskCheck.Status != "ok" && diskCheck.Status != "warning" {
|
|
t.Errorf("disk_space check status = %q, want 'ok' or 'warning'", diskCheck.Status)
|
|
}
|
|
}
|
|
|
|
// TestCollectWarnings_IncludesDetailAndHint verifies that both "warning"
|
|
// and "missing" checks surface their Detail (the problem) alongside their
|
|
// Hint (the fix). Regression guard: earlier versions only surfaced Detail
|
|
// for "warning" status, leaving "missing" checks to display the fix with
|
|
// no explanation of the cause.
|
|
func TestCollectWarnings_IncludesDetailAndHint(t *testing.T) {
|
|
checks := []PreflightCheck{
|
|
{Name: "ok_check", Status: "ok", Detail: "all good"},
|
|
{Name: "warn_check", Status: "warning", Detail: "degraded service", Hint: "do X to recover"},
|
|
{Name: "missing_check", Status: "missing", Detail: "pg_dump 17 cannot dump PostgreSQL 18 server", Hint: "Install postgresql18-client"},
|
|
{Name: "ok_with_hint", Status: "ok", Detail: "informational", Hint: "optional tune-up"},
|
|
}
|
|
|
|
warnings := make([]string, 0)
|
|
for _, c := range checks {
|
|
if (c.Status == "warning" || c.Status == "missing") && c.Detail != "" {
|
|
warnings = append(warnings, c.Detail)
|
|
}
|
|
if c.Hint != "" {
|
|
warnings = append(warnings, c.Hint)
|
|
}
|
|
}
|
|
|
|
want := []string{
|
|
"degraded service",
|
|
"do X to recover",
|
|
"pg_dump 17 cannot dump PostgreSQL 18 server",
|
|
"Install postgresql18-client",
|
|
"optional tune-up",
|
|
}
|
|
if len(warnings) != len(want) {
|
|
t.Fatalf("warnings len = %d, want %d\ngot: %v", len(warnings), len(want), warnings)
|
|
}
|
|
for i, w := range want {
|
|
if warnings[i] != w {
|
|
t.Errorf("warnings[%d] = %q, want %q", i, warnings[i], w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatBytes_EdgeCases(t *testing.T) {
|
|
// Test boundary values
|
|
tests := []struct {
|
|
b int64
|
|
// Just verify it doesn't panic and returns a string
|
|
}{
|
|
{1 << 30}, // 1 GB exact
|
|
{1<<30 - 1}, // Just below 1 GB
|
|
{1<<30 + 1}, // Just above 1 GB
|
|
{1 << 40}, // 1 TB (should still use GB)
|
|
{-1}, // Negative (edge case)
|
|
{1<<63 - 1}, // Max int64
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
result := FormatBytes(tc.b)
|
|
if result == "" {
|
|
t.Errorf("FormatBytes(%d) returned empty string", tc.b)
|
|
}
|
|
})
|
|
}
|
|
}
|