From 2b524bcfe555523708470baa81047f04be69d19e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 8 Mar 2026 14:15:26 +0200 Subject: [PATCH 1/2] chore: switch to SynchronizedBeforeSuite --- tests/context_test.go | 4 +- tests/e2e/suite_test.go | 14 ++- tests/setup/common.go | 2 +- tests/setup/template.go | 241 ++++++++++++++++++++++++++++++++++++++++ tests/suite_test.go | 14 ++- 5 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 tests/setup/template.go diff --git a/tests/context_test.go b/tests/context_test.go index fb7ae4e61..ea7fb0b52 100644 --- a/tests/context_test.go +++ b/tests/context_test.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/otel/trace" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/flanksource/commons/logger" "github.com/flanksource/duty/context" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -40,7 +41,8 @@ var _ = Describe("Context", func() { Expect(c.GetObjectMeta().Name).To(Equal("test")) Expect(c.IsDebug()).To(BeTrue()) - Expect(c.IsTrace()).To(BeFalse()) + + Expect(c.IsTrace()).To(Equal(logger.IsTraceEnabled())) Expect(c.GetName()).To(Equal("test")) Expect(c.GetNamespace()).To(Equal("default")) diff --git a/tests/e2e/suite_test.go b/tests/e2e/suite_test.go index 7a03388a5..259f232bb 100644 --- a/tests/e2e/suite_test.go +++ b/tests/e2e/suite_test.go @@ -17,8 +17,14 @@ func TestE2E(t *testing.T) { ginkgo.RunSpecs(t, "E2E Suite") } -var _ = ginkgo.BeforeSuite(func() { - DefaultContext = setup.BeforeSuiteFn() -}) +var setupOpts = setup.SetupOpts{DummyData: true} -var _ = ginkgo.AfterSuite(setup.AfterSuiteFn) +var _ = ginkgo.SynchronizedBeforeSuite( + func() []byte { return setup.SetupTemplate(setupOpts) }, + func(data []byte) { DefaultContext = setup.SetupNode(data, setupOpts) }, +) + +var _ = ginkgo.SynchronizedAfterSuite( + setup.SynchronizedAfterSuiteAllNodes, + setup.SynchronizedAfterSuiteNode1, +) diff --git a/tests/setup/common.go b/tests/setup/common.go index 83963a248..ba6029156 100644 --- a/tests/setup/common.go +++ b/tests/setup/common.go @@ -191,7 +191,7 @@ func SetupDB(dbName string, args ...interface{}) (context.Context, error) { return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err) } - shutdown.AddHookWithPriority("remote postgres", shutdown.PriorityCritical, func() { + shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() { if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil { logger.Errorf("execPostgres: %v", err) } diff --git a/tests/setup/template.go b/tests/setup/template.go new file mode 100644 index 000000000..02a3770ff --- /dev/null +++ b/tests/setup/template.go @@ -0,0 +1,241 @@ +package setup + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + embeddedPG "github.com/fergusstrange/embedded-postgres" + "github.com/flanksource/commons/logger" + "github.com/flanksource/commons/properties" + "github.com/flanksource/duty" + "github.com/flanksource/duty/context" + dutyKubernetes "github.com/flanksource/duty/kubernetes" + "github.com/flanksource/duty/shutdown" + "github.com/flanksource/duty/telemetry" + "github.com/flanksource/duty/tests/fixtures/dummy" + "github.com/onsi/ginkgo/v2" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +type SetupOpts struct { + DummyData bool +} + +type templateInfo struct { + AdminURL string `json:"admin_url"` + TemplateDB string `json:"template_db"` + Port int `json:"port"` +} + +func (t templateInfo) Marshal() []byte { + data, err := json.Marshal(t) + if err != nil { + panic(fmt.Sprintf("failed to marshal templateInfo: %v", err)) + } + return data +} + +func unmarshalTemplateInfo(data []byte) templateInfo { + var info templateInfo + if err := json.Unmarshal(data, &info); err != nil { + panic(fmt.Sprintf("failed to unmarshal templateInfo: %v", err)) + } + return info +} + +var ( + adminURL string + nodeDBName string +) + +func SetupTemplate(opts SetupOpts) []byte { + if err := properties.LoadFile(findFileInPath("test.properties", 2)); err != nil { + logger.Errorf("Failed to load test properties: %v", err) + } + + defer telemetry.InitTracer() + + var port int + if val, ok := os.LookupEnv(TEST_DB_PORT); ok { + parsed, err := strconv.ParseInt(val, 10, 32) + if err != nil { + panic(fmt.Sprintf("failed to parse TEST_DB_PORT: %v", err)) + } + port = int(parsed) + } else { + port = duty.FreePort() + } + + templateDB := "duty_test_template" + + url := os.Getenv(DUTY_DB_URL) + if url != "" && !recreateDatabase { + // DUTY_DB_CREATE=false: use direct connection, no template + PgUrl = url + return templateInfo{AdminURL: url, TemplateDB: "", Port: port}.Marshal() + } + + adminConn, err := ensurePostgres(port) + if err != nil { + panic(fmt.Sprintf("failed to start postgres: %v", err)) + } + adminURL = adminConn + + // Always recreate — dummy data uses uuid.New() so a cached template has stale UUIDs + _ = execPostgres(adminConn, fmt.Sprintf("ALTER DATABASE %s WITH is_template = false", templateDB)) + _ = execPostgres(adminConn, fmt.Sprintf( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", templateDB)) + _ = execPostgres(adminConn, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", templateDB)) + + if err := execPostgres(adminConn, fmt.Sprintf("CREATE DATABASE %s", templateDB)); err != nil { + panic(fmt.Sprintf("failed to create template db: %v", err)) + } + + templateURL := strings.Replace(adminConn, "/postgres", "/"+templateDB, 1) + if !strings.Contains(adminConn, "/postgres") { + templateURL = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/%s?sslmode=disable", port, templateDB) + } + + dbOptions := []duty.StartOption{duty.DisablePostgrest, duty.RunMigrations, duty.WithUrl(templateURL)} + if !disableRLS { + dbOptions = append(dbOptions, duty.EnableRLS) + } + + ctx, stop, err := duty.Start(templateDB, dbOptions...) + if err != nil { + panic(fmt.Sprintf("failed to start duty for template: %v", err)) + } + + if err := ctx.DB().Exec("SET TIME ZONE 'UTC'").Error; err != nil { + panic(fmt.Sprintf("failed to set timezone: %v", err)) + } + + if opts.DummyData { + dummyData = dummy.GetStaticDummyData(ctx.DB()) + if err := dummyData.Delete(ctx.DB()); err != nil { + logger.Errorf(err.Error()) + } + if err := dummyData.Populate(ctx); err != nil { + panic(fmt.Sprintf("failed to populate dummy data: %v", err)) + } + logger.Infof("Created dummy data in template (%d checks)", len(dummyData.Checks)) + } + + // Close all connections so the DB can be used as a template + stop() + _ = execPostgres(adminConn, fmt.Sprintf( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", templateDB)) + + if err := execPostgres(adminConn, fmt.Sprintf("ALTER DATABASE %s WITH is_template = true", templateDB)); err != nil { + panic(fmt.Sprintf("failed to mark template db: %v", err)) + } + + return templateInfo{AdminURL: adminConn, TemplateDB: templateDB, Port: port}.Marshal() +} + +func SetupNode(data []byte, opts SetupOpts) context.Context { + info := unmarshalTemplateInfo(data) + + if info.TemplateDB == "" { + // Direct connection mode (DUTY_DB_CREATE=false) + PgUrl = info.AdminURL + ctx, _, err := duty.Start("direct", duty.ClientOnly, duty.WithUrl(PgUrl)) + if err != nil { + panic(fmt.Sprintf("failed to connect to db: %v", err)) + } + return setupNodeContext(ctx, "direct") + } + + adminURL = info.AdminURL + nodeDBName = fmt.Sprintf("duty_test_node%d", ginkgo.GinkgoParallelProcess()) + + // Drop and clone from template + _ = execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", nodeDBName)) + + // Terminate any lingering connections to the template before cloning + _ = execPostgres(adminURL, fmt.Sprintf( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", info.TemplateDB)) + + // Unmark template temporarily for cloning (some pg versions need this) + _ = execPostgres(adminURL, fmt.Sprintf("ALTER DATABASE %s WITH is_template = false", info.TemplateDB)) + if err := execPostgres(adminURL, fmt.Sprintf("CREATE DATABASE %s TEMPLATE %s", nodeDBName, info.TemplateDB)); err != nil { + panic(fmt.Sprintf("failed to clone template: %v", err)) + } + _ = execPostgres(adminURL, fmt.Sprintf("ALTER DATABASE %s WITH is_template = true", info.TemplateDB)) + + // Build node connection URL + if strings.Contains(adminURL, "/postgres") { + PgUrl = strings.Replace(adminURL, "/postgres", "/"+nodeDBName, 1) + } else { + PgUrl = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/%s?sslmode=disable", info.Port, nodeDBName) + } + + // Skip migrations — the clone is byte-for-byte identical to the template + ctx, _, err := duty.Start(nodeDBName, duty.ClientOnly, duty.WithUrl(PgUrl)) + if err != nil { + panic(fmt.Sprintf("failed to connect to node db: %v", err)) + } + + return setupNodeContext(ctx, nodeDBName) +} + +func setupNodeContext(ctx context.Context, dbName string) context.Context { + if err := ctx.DB().Exec("SET TIME ZONE 'UTC'").Error; err != nil { + panic(fmt.Sprintf("failed to set timezone: %v", err)) + } + + ctx = ctx.WithValue("db_name", dbName).WithValue("db_url", PgUrl) + + clientset := fake.NewClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "default"}, + Data: map[string]string{"foo": "bar"}, + }, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "default"}, + Data: map[string][]byte{"foo": []byte("secret")}, + }) + + return ctx.WithLocalKubernetes(dutyKubernetes.NewKubeClient(logger.GetLogger("k8s"), clientset, nil)) +} + +func SynchronizedAfterSuiteAllNodes() { + if nodeDBName != "" && adminURL != "" { + if err := execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", nodeDBName)); err != nil { + logger.Errorf("failed to drop node db: %v", err) + } + } +} + +func SynchronizedAfterSuiteNode1() { + shutdown.Shutdown() +} + + +func ensurePostgres(port int) (string, error) { + url := os.Getenv(DUTY_DB_URL) + if url != "" { + postgresDBUrl = url + return url, nil + } + + if postgresServer == nil { + config, _ := GetEmbeddedPGConfig("postgres", port) + + if v, ok := os.LookupEnv(DUTY_DB_DATA_DIR); ok { + config = config.DataPath(v) + } + + postgresServer = embeddedPG.NewDatabase(config) + logger.Infof("starting embedded postgres on port %d", port) + if err := postgresServer.Start(); err != nil { + return "", err + } + logger.Infof("Started postgres on port %d", port) + } + + return fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port), nil +} diff --git a/tests/suite_test.go b/tests/suite_test.go index a30126769..21ef679f2 100644 --- a/tests/suite_test.go +++ b/tests/suite_test.go @@ -16,8 +16,14 @@ func TestDuty(t *testing.T) { ginkgo.RunSpecs(t, "Duty Suite") } -var _ = ginkgo.BeforeSuite(func() { - DefaultContext = setup.BeforeSuiteFn() -}) +var setupOpts = setup.SetupOpts{DummyData: true} -var _ = ginkgo.AfterSuite(setup.AfterSuiteFn) +var _ = ginkgo.SynchronizedBeforeSuite( + func() []byte { return setup.SetupTemplate(setupOpts) }, + func(data []byte) { DefaultContext = setup.SetupNode(data, setupOpts) }, +) + +var _ = ginkgo.SynchronizedAfterSuite( + setup.SynchronizedAfterSuiteAllNodes, + setup.SynchronizedAfterSuiteNode1, +) From 027187842ec563a6022baebf51009c582bb6fc19 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 8 Mar 2026 14:35:06 +0200 Subject: [PATCH 2/2] chore: upodate make test --- Makefile | 6 +++++- go.mod | 1 + go.sum | 2 ++ tests/e2e/loki_test.go | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d149c5998..6eb32ca01 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,11 @@ ginkgo: go install github.com/onsi/ginkgo/v2/ginkgo test: ginkgo - ginkgo -r -v --skip-package=tests/e2e + ginkgo -r -v --skip-package=tests/e2e --skip-package=bench --label-filter "!e2e" + +test-concurrent: ginkgo + ginkgo -r -v --nodes=4 --skip-package=bench --label-filter "!e2e" + .PHONY: test-e2e test-e2e: ginkgo diff --git a/go.mod b/go.mod index b6a6311e3..6c502dc23 100644 --- a/go.mod +++ b/go.mod @@ -111,6 +111,7 @@ require ( ) require ( + github.com/clipperhouse/stringish v0.1.1 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index 0dccf9a96..39819e86d 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,8 @@ github.com/clarkmcc/gorm-sqlite v0.0.0-20240426202654-00ed082c0311/go.mod h1:HrR github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= diff --git a/tests/e2e/loki_test.go b/tests/e2e/loki_test.go index 19143fd28..313e2ab14 100644 --- a/tests/e2e/loki_test.go +++ b/tests/e2e/loki_test.go @@ -19,7 +19,7 @@ import ( "github.com/flanksource/duty/logs/loki" ) -var _ = ginkgo.Describe("Loki Integration", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("Loki Integration", ginkgo.Ordered, ginkgo.Label("e2e"), func() { var ( lokiURL string ctx context.Context