All checks were successful
CI/CD / Test (push) Successful in 2m55s
CI/CD / Lint (push) Successful in 1m12s
CI/CD / Integration Tests (push) Successful in 50s
CI/CD / Native Engine Tests (push) Successful in 51s
CI/CD / Build Binary (push) Successful in 45s
CI/CD / Test Release Build (push) Successful in 1m20s
CI/CD / Release Binaries (push) Successful in 10m27s
Features: - Performance analysis package with 2GB/s+ throughput benchmarks - Comprehensive test coverage improvements (exitcode, errors, metadata 100%) - Grafana dashboard updates - Structured error types with codes and remediation guidance Testing: - Added exitcode tests (100% coverage) - Added errors package tests (100% coverage) - Added metadata tests (92.2% coverage) - Improved fs tests (20.9% coverage) - Improved checks tests (20.3% coverage) Performance: - 2,048 MB/s dump throughput (4x target) - 1,673 MB/s restore throughput (5.6x target) - Buffer pooling for bounded memory usage
351 lines
8.9 KiB
Go
351 lines
8.9 KiB
Go
package checks
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestClassifyError_AlreadyExists(t *testing.T) {
|
|
tests := []string{
|
|
"relation 'users' already exists",
|
|
"ERROR: duplicate key value violates unique constraint",
|
|
"table users already exists",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:20], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Type != "ignorable" {
|
|
t.Errorf("ClassifyError(%q).Type = %s, want 'ignorable'", msg, result.Type)
|
|
}
|
|
if result.Category != "duplicate" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'duplicate'", msg, result.Category)
|
|
}
|
|
if result.Severity != 0 {
|
|
t.Errorf("ClassifyError(%q).Severity = %d, want 0", msg, result.Severity)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_DiskFull(t *testing.T) {
|
|
tests := []string{
|
|
"write failed: no space left on device",
|
|
"ERROR: disk full",
|
|
"write failed space exhausted",
|
|
"insufficient space on target",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:15], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Type != "critical" {
|
|
t.Errorf("ClassifyError(%q).Type = %s, want 'critical'", msg, result.Type)
|
|
}
|
|
if result.Category != "disk_space" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'disk_space'", msg, result.Category)
|
|
}
|
|
if result.Severity < 2 {
|
|
t.Errorf("ClassifyError(%q).Severity = %d, want >= 2", msg, result.Severity)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_LockExhaustion(t *testing.T) {
|
|
tests := []string{
|
|
"ERROR: max_locks_per_transaction (64) exceeded",
|
|
"FATAL: out of shared memory",
|
|
"could not open large object 12345",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:20], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Category != "locks" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'locks'", msg, result.Category)
|
|
}
|
|
if !strings.Contains(result.Hint, "Lock table") && !strings.Contains(result.Hint, "lock") {
|
|
t.Errorf("ClassifyError(%q).Hint should mention locks, got: %s", msg, result.Hint)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_PermissionDenied(t *testing.T) {
|
|
tests := []string{
|
|
"ERROR: permission denied for table users",
|
|
"must be owner of relation users",
|
|
"access denied to file /backup/data",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:20], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Category != "permissions" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'permissions'", msg, result.Category)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_ConnectionFailed(t *testing.T) {
|
|
tests := []string{
|
|
"connection refused",
|
|
"could not connect to server",
|
|
"FATAL: no pg_hba.conf entry for host",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:15], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Category != "network" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'network'", msg, result.Category)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_VersionMismatch(t *testing.T) {
|
|
tests := []string{
|
|
"version mismatch: server is 14, backup is 15",
|
|
"incompatible pg_dump version",
|
|
"unsupported version format",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run(msg[:15], func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Category != "version" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'version'", msg, result.Category)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_SyntaxError(t *testing.T) {
|
|
tests := []string{
|
|
"syntax error at or near line 1234",
|
|
"syntax error in dump file at line 567",
|
|
}
|
|
|
|
for _, msg := range tests {
|
|
t.Run("syntax", func(t *testing.T) {
|
|
result := ClassifyError(msg)
|
|
if result.Category != "corruption" {
|
|
t.Errorf("ClassifyError(%q).Category = %s, want 'corruption'", msg, result.Category)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_Unknown(t *testing.T) {
|
|
msg := "some unknown error happened"
|
|
result := ClassifyError(msg)
|
|
|
|
if result == nil {
|
|
t.Fatal("ClassifyError should not return nil")
|
|
}
|
|
// Unknown errors should still get a classification
|
|
if result.Message != msg {
|
|
t.Errorf("ClassifyError should preserve message, got: %s", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestClassifyErrorByPattern(t *testing.T) {
|
|
tests := []struct {
|
|
msg string
|
|
expected string
|
|
}{
|
|
{"relation 'users' already exists", "already_exists"},
|
|
{"no space left on device", "disk_full"},
|
|
{"max_locks_per_transaction exceeded", "lock_exhaustion"},
|
|
{"syntax error at line 123", "syntax_error"},
|
|
{"permission denied for table", "permission_denied"},
|
|
{"connection refused", "connection_failed"},
|
|
{"version mismatch", "version_mismatch"},
|
|
{"some other error", "unknown"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.expected, func(t *testing.T) {
|
|
result := classifyErrorByPattern(tc.msg)
|
|
if result != tc.expected {
|
|
t.Errorf("classifyErrorByPattern(%q) = %s, want %s", tc.msg, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatBytes(t *testing.T) {
|
|
tests := []struct {
|
|
bytes uint64
|
|
want string
|
|
}{
|
|
{0, "0 B"},
|
|
{500, "500 B"},
|
|
{1023, "1023 B"},
|
|
{1024, "1.0 KiB"},
|
|
{1536, "1.5 KiB"},
|
|
{1024 * 1024, "1.0 MiB"},
|
|
{1024 * 1024 * 1024, "1.0 GiB"},
|
|
{uint64(1024) * 1024 * 1024 * 1024, "1.0 TiB"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.want, func(t *testing.T) {
|
|
got := formatBytes(tc.bytes)
|
|
if got != tc.want {
|
|
t.Errorf("formatBytes(%d) = %s, want %s", tc.bytes, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiskSpaceCheck_Fields(t *testing.T) {
|
|
check := &DiskSpaceCheck{
|
|
Path: "/backup",
|
|
TotalBytes: 1000 * 1024 * 1024 * 1024, // 1TB
|
|
AvailableBytes: 500 * 1024 * 1024 * 1024, // 500GB
|
|
UsedBytes: 500 * 1024 * 1024 * 1024, // 500GB
|
|
UsedPercent: 50.0,
|
|
Sufficient: true,
|
|
Warning: false,
|
|
Critical: false,
|
|
}
|
|
|
|
if check.Path != "/backup" {
|
|
t.Errorf("Path = %s, want /backup", check.Path)
|
|
}
|
|
if !check.Sufficient {
|
|
t.Error("Sufficient should be true")
|
|
}
|
|
if check.Warning {
|
|
t.Error("Warning should be false")
|
|
}
|
|
if check.Critical {
|
|
t.Error("Critical should be false")
|
|
}
|
|
}
|
|
|
|
func TestErrorClassification_Fields(t *testing.T) {
|
|
ec := &ErrorClassification{
|
|
Type: "critical",
|
|
Category: "disk_space",
|
|
Message: "no space left on device",
|
|
Hint: "Free up disk space",
|
|
Action: "rm old files",
|
|
Severity: 3,
|
|
}
|
|
|
|
if ec.Type != "critical" {
|
|
t.Errorf("Type = %s, want critical", ec.Type)
|
|
}
|
|
if ec.Severity != 3 {
|
|
t.Errorf("Severity = %d, want 3", ec.Severity)
|
|
}
|
|
}
|
|
|
|
func BenchmarkClassifyError(b *testing.B) {
|
|
msg := "ERROR: relation 'users' already exists"
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
ClassifyError(msg)
|
|
}
|
|
}
|
|
|
|
func BenchmarkClassifyErrorByPattern(b *testing.B) {
|
|
msg := "ERROR: relation 'users' already exists"
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
classifyErrorByPattern(msg)
|
|
}
|
|
}
|
|
|
|
func TestFormatErrorWithHint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
errorMsg string
|
|
wantInType string
|
|
wantInHint bool
|
|
}{
|
|
{
|
|
name: "ignorable error",
|
|
errorMsg: "relation 'users' already exists",
|
|
wantInType: "IGNORABLE",
|
|
wantInHint: true,
|
|
},
|
|
{
|
|
name: "critical error",
|
|
errorMsg: "no space left on device",
|
|
wantInType: "CRITICAL",
|
|
wantInHint: true,
|
|
},
|
|
{
|
|
name: "warning error",
|
|
errorMsg: "version mismatch detected",
|
|
wantInType: "WARNING",
|
|
wantInHint: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := FormatErrorWithHint(tc.errorMsg)
|
|
|
|
if !strings.Contains(result, tc.wantInType) {
|
|
t.Errorf("FormatErrorWithHint should contain %s, got: %s", tc.wantInType, result)
|
|
}
|
|
if tc.wantInHint && !strings.Contains(result, "[HINT]") {
|
|
t.Errorf("FormatErrorWithHint should contain [HINT], got: %s", result)
|
|
}
|
|
if !strings.Contains(result, "[ACTION]") {
|
|
t.Errorf("FormatErrorWithHint should contain [ACTION], got: %s", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatMultipleErrors_Empty(t *testing.T) {
|
|
result := FormatMultipleErrors([]string{})
|
|
if !strings.Contains(result, "No errors") {
|
|
t.Errorf("FormatMultipleErrors([]) should contain 'No errors', got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatMultipleErrors_Mixed(t *testing.T) {
|
|
errors := []string{
|
|
"relation 'users' already exists", // ignorable
|
|
"no space left on device", // critical
|
|
"version mismatch detected", // warning
|
|
"connection refused", // critical
|
|
"relation 'posts' already exists", // ignorable
|
|
}
|
|
|
|
result := FormatMultipleErrors(errors)
|
|
|
|
if !strings.Contains(result, "Summary") {
|
|
t.Errorf("FormatMultipleErrors should contain Summary, got: %s", result)
|
|
}
|
|
if !strings.Contains(result, "ignorable") {
|
|
t.Errorf("FormatMultipleErrors should count ignorable errors, got: %s", result)
|
|
}
|
|
if !strings.Contains(result, "critical") {
|
|
t.Errorf("FormatMultipleErrors should count critical errors, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatMultipleErrors_OnlyCritical(t *testing.T) {
|
|
errors := []string{
|
|
"no space left on device",
|
|
"connection refused",
|
|
"permission denied for table",
|
|
}
|
|
|
|
result := FormatMultipleErrors(errors)
|
|
|
|
if !strings.Contains(result, "[CRITICAL]") {
|
|
t.Errorf("FormatMultipleErrors should contain critical section, got: %s", result)
|
|
}
|
|
}
|