mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
fix(channelserver): eliminate test log spam, schema errors, and slow setup
Three fixes for the channelserver test suite: - Add net.ErrClosed check in acceptClients() so a closed listener breaks the loop immediately instead of spinning and logging warnings. This is correct production behavior too. - Remove -c flag from pg_restore that conflicted with CleanTestDB's prior DROP, causing "gook DROP CONSTRAINT" errors. Clean the schema with DROP SCHEMA CASCADE instead of per-table drops. - Use sync.Once to apply the test schema once per binary run, then TRUNCATE tables for isolation. Reduces ~60 pg_restore + 29-patch cycles to a single setup pass.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -210,7 +211,7 @@ func (s *Server) acceptClients() {
|
|||||||
shutdown := s.isShuttingDown
|
shutdown := s.isShuttingDown
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
if shutdown {
|
if shutdown || errors.Is(err, net.ErrClosed) {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
s.logger.Warn("Error accepting client", zap.Error(err))
|
s.logger.Warn("Error accepting client", zap.Error(err))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"erupe-ce/server/channelserver/compression/nullcomp"
|
"erupe-ce/server/channelserver/compression/nullcomp"
|
||||||
@@ -14,6 +15,12 @@ import (
|
|||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testDBOnce sync.Once
|
||||||
|
testDB *sqlx.DB
|
||||||
|
testDBSetupFailed bool
|
||||||
|
)
|
||||||
|
|
||||||
// TestDBConfig holds the configuration for the test database
|
// TestDBConfig holds the configuration for the test database
|
||||||
type TestDBConfig struct {
|
type TestDBConfig struct {
|
||||||
Host string
|
Host string
|
||||||
@@ -42,52 +49,55 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupTestDB creates a connection to the test database and applies the schema
|
// SetupTestDB creates a connection to the test database and applies the schema.
|
||||||
|
// The schema is applied only once per test binary via sync.Once. Subsequent calls
|
||||||
|
// only TRUNCATE data for test isolation, avoiding expensive pg_restore + patch cycles.
|
||||||
func SetupTestDB(t *testing.T) *sqlx.DB {
|
func SetupTestDB(t *testing.T) *sqlx.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
config := DefaultTestDBConfig()
|
testDBOnce.Do(func() {
|
||||||
connStr := fmt.Sprintf(
|
config := DefaultTestDBConfig()
|
||||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
connStr := fmt.Sprintf(
|
||||||
config.Host, config.Port, config.User, config.Password, config.DBName,
|
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
)
|
config.Host, config.Port, config.User, config.Password, config.DBName,
|
||||||
|
)
|
||||||
|
|
||||||
db, err := sqlx.Open("postgres", connStr)
|
db, err := sqlx.Open("postgres", connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("Failed to connect to test database: %v. Run: docker compose -f docker/docker-compose.test.yml up -d", err)
|
testDBSetupFailed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
testDBSetupFailed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the database and apply schema once
|
||||||
|
CleanTestDB(t, db)
|
||||||
|
ApplyTestSchema(t, db)
|
||||||
|
|
||||||
|
testDB = db
|
||||||
|
})
|
||||||
|
|
||||||
|
if testDBSetupFailed || testDB == nil {
|
||||||
|
t.Skipf("Test database not available. Run: docker compose -f docker/docker-compose.test.yml up -d")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test connection
|
// Truncate all data for test isolation (schema stays intact)
|
||||||
if err := db.Ping(); err != nil {
|
truncateAllTables(t, testDB)
|
||||||
_ = db.Close()
|
|
||||||
t.Skipf("Test database not available: %v. Run: docker compose -f docker/docker-compose.test.yml up -d", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the database before tests
|
return testDB
|
||||||
CleanTestDB(t, db)
|
|
||||||
|
|
||||||
// Apply schema
|
|
||||||
ApplyTestSchema(t, db)
|
|
||||||
|
|
||||||
return db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanTestDB drops all tables to ensure a clean state
|
// CleanTestDB drops all objects in the public schema to ensure a clean state
|
||||||
func CleanTestDB(t *testing.T, db *sqlx.DB) {
|
func CleanTestDB(t *testing.T, db *sqlx.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Drop all tables in the public schema
|
// Drop and recreate the public schema to remove all objects (tables, types, sequences, etc.)
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`)
|
||||||
DO $$ DECLARE
|
|
||||||
r RECORD;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
|
|
||||||
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Warning: Failed to clean database: %v", err)
|
t.Logf("Warning: Failed to clean database: %v", err)
|
||||||
}
|
}
|
||||||
@@ -113,19 +123,19 @@ func ApplyTestSchema(t *testing.T, db *sqlx.DB) {
|
|||||||
"-d", config.DBName,
|
"-d", config.DBName,
|
||||||
"--no-owner",
|
"--no-owner",
|
||||||
"--no-acl",
|
"--no-acl",
|
||||||
"-c", // clean (drop) before recreating
|
|
||||||
schemaPath,
|
schemaPath,
|
||||||
)
|
)
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", config.Password))
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", config.Password))
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// pg_restore may error on first run (no tables to drop), that's usually ok
|
out := string(output)
|
||||||
t.Logf("pg_restore output: %s", string(output))
|
// pg_restore reports non-fatal warnings (version mismatches, already exists) as errors.
|
||||||
// Check if it's a fatal error
|
// Only fail if we see no "errors ignored on restore" summary, which means a real failure.
|
||||||
if !strings.Contains(string(output), "does not exist") {
|
if !strings.Contains(out, "errors ignored on restore") {
|
||||||
t.Logf("pg_restore error (may be non-fatal): %v", err)
|
t.Fatalf("pg_restore failed: %v\n%s", err, out)
|
||||||
}
|
}
|
||||||
|
t.Logf("pg_restore completed with non-fatal warnings (ignored)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the 9.2 update schema (init.sql bootstraps to 9.1.0)
|
// Apply the 9.2 update schema (init.sql bootstraps to 9.1.0)
|
||||||
@@ -239,12 +249,37 @@ func findProjectRoot(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeardownTestDB closes the database connection
|
// truncateAllTables truncates all tables in the public schema for test isolation.
|
||||||
|
func truncateAllTables(t *testing.T, db *sqlx.DB) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rows, err := db.Query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list tables for truncation: %v", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var tables []string
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
t.Fatalf("Failed to scan table name: %v", err)
|
||||||
|
}
|
||||||
|
tables = append(tables, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tables) > 0 {
|
||||||
|
_, err := db.Exec("TRUNCATE " + strings.Join(tables, ", ") + " CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to truncate tables: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeardownTestDB is a no-op. The shared DB connection is reused across tests
|
||||||
|
// and closed automatically at process exit.
|
||||||
func TeardownTestDB(t *testing.T, db *sqlx.DB) {
|
func TeardownTestDB(t *testing.T, db *sqlx.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if db != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTestUser creates a test user and returns the user ID
|
// CreateTestUser creates a test user and returns the user ID
|
||||||
|
|||||||
Reference in New Issue
Block a user