Files
wehub-resource-sync 75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

383 lines
14 KiB
Go

package runner
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"databasus-verification-agent/internal/config"
"databasus-verification-agent/internal/features/api"
"databasus-verification-agent/internal/features/dbconn"
"databasus-verification-agent/internal/features/restore"
"databasus-verification-agent/internal/features/verifier"
"databasus-verification-agent/internal/testutil"
)
func testCapacity() config.Capacity {
return config.Capacity{
MaxCPU: 4, MaxRAMMb: 4096, MaxDiskGb: 50, MaxConcurrentJobs: 2,
CPUPerJob: 2, RAMMbPerJob: 2048,
}
}
func postgresJob() *api.JobAssignment {
return &api.JobAssignment{
VerificationID: uuid.New(),
BackupID: uuid.New(),
BackupSizeMb: 50,
MaxContainerDiskMb: 200,
Database: api.AssignedDatabase{
Type: "POSTGRES_LOGICAL",
PostgresqlLogical: &api.AssignedPostgresql{Version: "16"},
},
}
}
func newTestRunner(
apiClient APIClient, spawner Spawner, restorer Restorer,
stats StatsCollector, reg Registrar,
) *Runner {
return NewRunner(
apiClient, testCapacity(), NewPool(2),
spawner, restorer, stats, reg, testutil.DiscardLogger(),
)
}
func testConn() dbconn.Conn {
return dbconn.Conn{
Host: "127.0.0.1", Port: 5432,
User: "postgres", Password: "pw", Database: "postgres",
}
}
func fakeContainerWith(conn dbconn.Conn) *fakeContainer {
return &fakeContainer{inContainerConn: conn, verifierConn: conn}
}
func okSpawner() *fakeSpawner {
return &fakeSpawner{container: fakeContainerWith(testConn())}
}
func Test_ExecuteJob_WhenPgMajorUnsupported_ReportsFailedWithoutExitCode(t *testing.T) {
apiClient := &fakeAPI{}
r := newTestRunner(apiClient, okSpawner(), &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
job := postgresJob()
job.Database.Type = "MYSQL"
r.executeJob(t.Context(), job)
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode)
}
func Test_ExecuteJob_WhenSpawnFails_ReportsFailedWithoutExitCode(t *testing.T) {
apiClient := &fakeAPI{}
spawner := &fakeSpawner{err: errors.New("image pull failed")}
r := newTestRunner(apiClient, spawner, &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode)
}
func Test_ExecuteJob_WhenStreamReturns410_DoesNotReport(t *testing.T) {
apiClient := &fakeAPI{downloadErr: &api.ResponseError{Op: "backup stream", StatusCode: 410}}
r := newTestRunner(apiClient, okSpawner(), &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
assert.Equal(t, 0, apiClient.reportCalled, "a 410 on the stream is an abort — no report")
}
func Test_ExecuteJob_WhenStreamExhaustsRetries_ReportsFailedWithoutExitCode(t *testing.T) {
original := backupDownloadBackoffFn
backupDownloadBackoffFn = func(int) time.Duration { return 0 }
t.Cleanup(func() { backupDownloadBackoffFn = original })
apiClient := &fakeAPI{downloadErr: errors.New("connection refused")}
r := newTestRunner(apiClient, okSpawner(), &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode)
}
func Test_ExecuteJob_WhenRestoreFailsAndDiskExhausted_ReportsFailedWithoutExitCode(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{
runErr: fmt.Errorf("restore: %w", restore.ErrRestoreFailed),
runResult: restore.Result{PgRestoreExitCode: 1, StderrTail: "could not write: No space left on device"},
}
r := newTestRunner(apiClient, okSpawner(), restorer, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode,
"hitting the estimate-derived disk ceiling is agent infra, not BackupRejected")
}
func Test_ExecuteJob_WhenDiskWatcherTrips_ReportsTerminalDiskLimitExceeded(t *testing.T) {
originalInterval := diskWatchInterval
diskWatchInterval = time.Millisecond
t.Cleanup(func() { diskWatchInterval = originalInterval })
originalBackoff := backupDownloadBackoffFn
backupDownloadBackoffFn = func(int) time.Duration { return 0 }
t.Cleanup(func() { backupDownloadBackoffFn = originalBackoff })
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
jobContainer := fakeContainerWith(testConn())
jobContainer.diskUsageBytes = 1 << 40
restorer := &fakeRestorer{runBlocks: true}
r := newTestRunner(
apiClient, &fakeSpawner{container: jobContainer}, restorer, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
require.NotNil(t, report.FailureKind)
assert.Equal(t, api.FailureKindDiskLimitExceeded, *report.FailureKind)
assert.Nil(t, report.PgRestoreExitCode, "a disk-limit verdict carries no pg_restore exit code")
assert.True(t, jobContainer.terminated, "the container must be torn down on disk-limit abort")
}
func Test_ExecuteJob_WhenServerSendsNoDiskBudget_DoesNotFalselyTrip(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
jobContainer := fakeContainerWith(testConn())
jobContainer.diskUsageBytes = 1 << 40
job := postgresJob()
job.MaxContainerDiskMb = 0
r := newTestRunner(
apiClient, &fakeSpawner{container: jobContainer}, &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), job)
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusCompleted, report.Status,
"a zero budget disables the watcher; the job must not be falsely failed")
assert.Nil(t, report.FailureKind)
}
func Test_ExecuteJob_WhenRestoreFailsNormally_ReportsFailedWithExitCode(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{
runErr: fmt.Errorf("restore: %w", restore.ErrRestoreFailed),
runResult: restore.Result{PgRestoreExitCode: 1, DurationMs: 42, StderrTail: "syntax error in dump"},
}
r := newTestRunner(apiClient, okSpawner(), restorer, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
require.NotNil(t, report.PgRestoreExitCode)
assert.Equal(t, 1, *report.PgRestoreExitCode)
}
func Test_ExecuteJob_WhenRestoreFailsWithOnlyMissingExtensions_ReportsCompleted(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
stderr := "pg_restore: error: could not execute query: ERROR: extension \"set_user\" is not available\n" +
"Command was: CREATE EXTENSION IF NOT EXISTS set_user WITH SCHEMA public;\n" +
"pg_restore: error: could not execute query: ERROR: extension \"set_user\" does not exist\n" +
"Command was: COMMENT ON EXTENSION set_user IS 'x';\n" +
"pg_restore: warning: errors ignored on restore: 2\n"
restorer := &fakeRestorer{
runErr: fmt.Errorf("restore: %w", restore.ErrRestoreFailed),
runResult: restore.Result{PgRestoreExitCode: 1, DurationMs: 10, StderrTail: stderr},
}
stats := &fakeStats{stats: verifier.Stats{DBSizeBytes: 9_000_000, TableCount: 3}}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return true }
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusCompleted, report.Status,
"a restore whose only failures are missing extensions is restorable")
require.NotNil(t, report.PgRestoreExitCode)
assert.Equal(t, 1, *report.PgRestoreExitCode,
"the real non-zero exit code is preserved on the success report")
require.NotNil(t, report.DBSizeBytesAfterRestore)
assert.Equal(t, int64(9_000_000), *report.DBSizeBytesAfterRestore)
}
func Test_ExecuteJob_WhenRestoreExecInfraFails_ReportsFailedWithoutExitCode(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runErr: errors.New("exec create failed")}
r := newTestRunner(apiClient, okSpawner(), restorer, &fakeStats{}, newFakeRegistrar())
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode)
}
func Test_ExecuteJob_WhenVerifyFailsAndConnDead_ReportsFailedWithoutExitCode(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runResult: restore.Result{PgRestoreExitCode: 0}}
stats := &fakeStats{err: errors.New("tier 1 failed")}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return false }
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
assert.Nil(t, report.PgRestoreExitCode, "a dead connection is agent infra (retryable)")
}
func Test_ExecuteJob_WhenVerifyFailsAndConnAlive_ReportsFailedWithExitCodeZero(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runResult: restore.Result{PgRestoreExitCode: 0}}
stats := &fakeStats{err: errors.New("tier 1 impossible result")}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return true }
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusFailed, report.Status)
require.NotNil(t, report.PgRestoreExitCode)
assert.Equal(t, 0, *report.PgRestoreExitCode,
"a live conn with a broken tier-1 result is BackupRejected with exit 0")
}
func Test_ExecuteJob_WhenAllSucceeds_ReportsCompletedWithStats(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runResult: restore.Result{PgRestoreExitCode: 0, DurationMs: 1234}}
stats := &fakeStats{stats: verifier.Stats{
DBSizeBytes: 9_000_000,
SchemaCount: 2,
TableCount: 3,
TableStats: []verifier.TableStat{{SchemaName: "public", Name: "t1", RowCount: 10}},
}}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return true }
r.executeJob(t.Context(), postgresJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusCompleted, report.Status)
require.NotNil(t, report.DBSizeBytesAfterRestore)
assert.Equal(t, int64(9_000_000), *report.DBSizeBytesAfterRestore)
require.NotNil(t, report.TableCount)
assert.Equal(t, 3, *report.TableCount)
require.Len(t, report.TableStats, 1)
assert.Equal(t, "public", report.TableStats[0].SchemaName)
}
func timescaleJob() *api.JobAssignment {
job := postgresJob()
job.TimescaledbVersion = "2.17.0"
return job
}
func Test_ExecuteJob_WhenTimescaleJobSucceeds_RunsPreRestoreThenRestoreThenPostRestore(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runResult: restore.Result{PgRestoreExitCode: 0}}
stats := &fakeStats{stats: verifier.Stats{TableCount: 1}}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return true }
r.executeJob(t.Context(), timescaleJob())
report, ok := apiClient.lastReport()
require.True(t, ok)
assert.Equal(t, api.VerificationStatusCompleted, report.Status)
assert.Equal(t, []string{"pre", "restore", "post"}, restorer.calls,
"timescaledb restore must enter restoring mode, restore, then leave it")
assert.Equal(t, 1, restorer.runParallelJobs,
"timescaledb restore must be single-threaded to keep _timescaledb_catalog FK order")
}
func Test_ExecuteJob_WhenNonTimescaleJob_DoesNotRunTimescaleHooks(t *testing.T) {
apiClient := &fakeAPI{downloadBody: []byte("ARCHIVE")}
restorer := &fakeRestorer{runResult: restore.Result{PgRestoreExitCode: 0}}
stats := &fakeStats{stats: verifier.Stats{TableCount: 1}}
r := newTestRunner(apiClient, okSpawner(), restorer, stats, newFakeRegistrar())
r.connAlive = func(context.Context, dbconn.Conn) bool { return true }
r.executeJob(t.Context(), postgresJob())
assert.Equal(t, []string{"restore"}, restorer.calls,
"a non-timescaledb restore must not run the timescaledb hooks")
assert.Equal(t, 2, restorer.runParallelJobs,
"a non-timescaledb restore keeps parallel jobs (CPUPerJob)")
}
func Test_ExecuteJob_WhenAbortedBeforeFailedReport_DoesNotReport(t *testing.T) {
apiClient := &fakeAPI{}
spawner := &fakeSpawner{err: errors.New("spawn failed")}
reg := newFakeRegistrar()
reg.aborted = true
r := newTestRunner(apiClient, spawner, &fakeRestorer{}, &fakeStats{}, reg)
r.executeJob(t.Context(), postgresJob())
assert.Equal(t, 0, apiClient.reportCalled,
"an aborted/cancelled verification must not get a spurious FAILED report")
}
func Test_Run_WhenContextCancelled_DrainsAndStops(t *testing.T) {
apiClient := &fakeAPI{}
r := newTestRunner(apiClient, okSpawner(), &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
ctx, cancel := context.WithCancel(t.Context())
done := make(chan struct{})
go func() {
r.Run(ctx)
close(done)
}()
cancel()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Run did not return after context cancellation")
}
}
func Test_Run_WhenCalledTwice_Panics(t *testing.T) {
r := newTestRunner(&fakeAPI{}, okSpawner(), &fakeRestorer{}, &fakeStats{}, newFakeRegistrar())
ctx, cancel := context.WithCancel(t.Context())
cancel()
r.Run(ctx)
assert.Panics(t, func() { r.Run(ctx) })
}