From 12dc219c1a0b4f689ed9c80882424849545aec35 Mon Sep 17 00:00:00 2001 From: Oskar Skoglund Date: Mon, 5 Oct 2026 07:54:52 +0000 Subject: [PATCH 1/6] Fix Config.Equals comparing the node count against the same config The second check compared counter with len(config.Nodes) twice, so a config with fewer nodes than config2 was reported as equal. --- pkg/cluster/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cluster/config.go b/pkg/cluster/config.go index 792e4cd78..2420a9463 100644 --- a/pkg/cluster/config.go +++ b/pkg/cluster/config.go @@ -73,7 +73,7 @@ func (config *Config) Equals(config2 *Config) bool { } } - if counter == len(config.Nodes) && counter == len(config.Nodes) { + if counter == len(config.Nodes) && counter == len(config2.Nodes) { return true } From 984120b9fc40687d2f346096e5bfa5d6df7b7803 Mon Sep 17 00:00:00 2001 From: Oskar Skoglund <116717756+ossko@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:28:17 +0200 Subject: [PATCH 2/6] Test performance improvements Each test process now creates its own Postgres database and truncates tables between tests instead of recreating the entire schema. Paralellization is also enabled by dynamically assigning free ports + unique temp dirs allowing servers to run in parallel during testing. Benchmarking on my machine shows a reduction in time for a full test run from 15+ minutes to approx 3 minutes for a full test run. --- .github/workflows/go.yml | 2 +- CLAUDE.md | 10 +- Makefile | 53 +- internal/cryptolib.wasm/cryptolib.go | 2 + pkg/cluster/etcd_test.go | 42 +- pkg/cluster/relay_server_test.go | 7 +- pkg/cluster/test_helpers_test.go | 35 + pkg/constants/constants.go | 1 - pkg/database/postgresql/test_utils.go | 258 +++++- pkg/server/channel_integration_test.go | 62 +- pkg/server/controllers/mock_test.go | 867 ++++++++++--------- pkg/server/handlers/realtime/handler_test.go | 4 +- pkg/server/server_manager_test.go | 188 ++-- pkg/server/test_utils.go | 157 ++-- pkg/utils/free_ports.go | 108 +++ tests/smoke/main.go | 191 ---- 16 files changed, 1092 insertions(+), 895 deletions(-) create mode 100644 pkg/cluster/test_helpers_test.go create mode 100644 pkg/utils/free_ports.go delete mode 100644 tests/smoke/main.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b1973199c..8c5726a84 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -65,7 +65,7 @@ jobs: PKGS=$(go list ./pkg/... | grep -v '/monitoring$' | tr '\n' ' ') # Run tests with coverage # Use -coverpkg to measure client package coverage through server tests - go test -p 1 -race -coverprofile=coverage.raw -covermode=atomic -coverpkg=./pkg/... $PKGS + go test -race -coverprofile=coverage.raw -covermode=atomic -coverpkg=./pkg/... $PKGS # Filter out test utilities, mocks, and monitoring from coverage report grep -v 'pkg/server/test_utils.go' coverage.raw | \ grep -v 'pkg/monitoring/' | \ diff --git a/CLAUDE.md b/CLAUDE.md index 409246a47..ca46caa27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,10 +26,16 @@ The deployment docker-compose files expect `colonyos/colonies:latest`. Using a d ### Testing ```bash -make test # Run all tests (requires grc for colored output) -make github_test # Run tests without grc (for CI) +make test # Run all tests: needs Postgres on localhost:5432 (make startdb) + # and an S3 server on localhost:9000 for pkg/fs +make github_test # Alias for test (used by CI) ``` +Set a timezone when running tests, e.g. `export TZ=Europe/Stockholm` (the +database layer refuses to connect without TZ, and one core test expects a +non-UTC zone). Test packages use per-process databases and dynamic ports, so +they run in parallel. + ### Development Environment ```bash docker-compose up -d # Start Colonies server with dependencies (TimescaleDB, MinIO) diff --git a/Makefile b/Makefile index a644d3787..19490b69f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ all: build -.PHONY: all build +.PHONY: all build container container-multiplatform container-multiplatform-push push coverage test test-all test-compat github_test install startdb nukedb BUILD_IMAGE ?= colonyos/colonies PUSH_IMAGE ?= colonyos/colonies:v1.9.10 @@ -34,55 +34,18 @@ push: docker push $(PUSH_IMAGE) coverage: - ./buildtools/coverage.sh - ./buildtools/codecov + @go test -coverprofile=coverage.txt -covermode=atomic ./... build_cryptolib_ubuntu_2020: cd buildtools; ./build_cryptolib_ubuntu.sh +# Runs all tests: needs Postgres on localhost:5432 (make startdb) and an S3 +# server on localhost:9000 for pkg/fs. Test packages use per-process databases +# and dynamic ports, so they run in parallel. test: - @cd tests/reliability; go test -v --race - @cd internal/crypto; go test -v --race - @cd pkg/core; go test -v --race - @cd pkg/database/postgresql; go test -v --race - @cd pkg/rpc; go test -v --race - @cd pkg/security; go test -v --race - @cd pkg/security/crypto; go test -v --race - @cd pkg/security/validator; go test -v --race - @cd pkg/backends/gin; go test -v --race - @cd pkg/client; go test -v --race - @cd pkg/client/backends; go test -v --race - @cd pkg/client/gin; go test -v --race - @cd pkg/server; go test -v --race - @cd pkg/server/controllers; go test -v --race - @cd pkg/server/handlers/attribute; go test -v --race - @cd pkg/server/handlers/blueprint; go test -v --race - @cd pkg/server/handlers/channel; go test -v --race - @cd pkg/server/handlers/colony; go test -v --race - @cd pkg/server/handlers/cron; go test -v --race - @cd pkg/server/handlers/executor; go test -v --race - @cd pkg/server/handlers/file; go test -v --race - @cd pkg/server/handlers/function; go test -v --race - @cd pkg/server/handlers/generator; go test -v --race - @cd pkg/server/handlers/location; go test -v --race - @cd pkg/server/handlers/log; go test -v --race - @cd pkg/server/handlers/process; go test -v --race - @cd pkg/server/handlers/processgraph; go test -v --race - @cd pkg/server/handlers/security; go test -v --race - @cd pkg/server/handlers/server; go test -v --race - @cd pkg/server/handlers/snapshot; go test -v --race - @cd pkg/server/handlers/user; go test -v --race - @cd pkg/server/handlers/realtime; go test -v --race - @cd pkg/server/registry; go test -v --race - @cd pkg/server/utils; go test -v --race - @cd pkg/scheduler; go test -v --race - @cd pkg/parsers; go test -v --race - @cd pkg/utils; go test -v --race - @cd pkg/validate; go test -v --race - @cd pkg/channel; go test -v --race - @cd pkg/cluster; go test -v --race - @cd pkg/cron; go test -v --race - @cd pkg/fs; go test -v --race + @go test -race ./... + +github_test: test install: cp ./bin/colonies /usr/local/bin diff --git a/internal/cryptolib.wasm/cryptolib.go b/internal/cryptolib.wasm/cryptolib.go index 59a694e3f..9e12e9390 100644 --- a/internal/cryptolib.wasm/cryptolib.go +++ b/internal/cryptolib.wasm/cryptolib.go @@ -1,3 +1,5 @@ +//go:build js && wasm + package main import ( diff --git a/pkg/cluster/etcd_test.go b/pkg/cluster/etcd_test.go index 9ebaf0211..96118c93e 100644 --- a/pkg/cluster/etcd_test.go +++ b/pkg/cluster/etcd_test.go @@ -1,6 +1,7 @@ package cluster import ( + "fmt" "os" "testing" @@ -8,10 +9,8 @@ import ( ) func TestCreateEtcdCluster(t *testing.T) { - node1 := Node{Name: "etcd1", Host: "localhost", EtcdClientPort: 24100, EtcdPeerPort: 23100, RelayPort: 25100, APIPort: 26100} - node2 := Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 24200, EtcdPeerPort: 23200, RelayPort: 25200, APIPort: 26200} - node3 := Node{Name: "etcd3", Host: "localhost", EtcdClientPort: 24300, EtcdPeerPort: 23300, RelayPort: 25300, APIPort: 26300} - node4 := Node{Name: "etcd4", Host: "localhost", EtcdClientPort: 24400, EtcdPeerPort: 23400, RelayPort: 25400, APIPort: 26400} + nodes, releasePorts := testClusterNodes(t, "etcd1", "etcd2", "etcd3", "etcd4") + node1, node2, node3, node4 := nodes[0], nodes[1], nodes[2], nodes[3] config := Config{} config.AddNode(node1) @@ -19,13 +18,17 @@ func TestCreateEtcdCluster(t *testing.T) { config.AddNode(node3) config.AddNode(node4) - server1 := CreateEtcdServer(node1, config, ".") - server2 := CreateEtcdServer(node2, config, ".") - server3 := CreateEtcdServer(node3, config, ".") - server4 := CreateEtcdServer(node4, config, ".") + dataPath := t.TempDir() + server1 := CreateEtcdServer(node1, config, dataPath) + server2 := CreateEtcdServer(node2, config, dataPath) + server3 := CreateEtcdServer(node3, config, dataPath) + server4 := CreateEtcdServer(node4, config, dataPath) - assert.Equal(t, server1.buildInitialClusterStr(), "etcd1=http://localhost:23100,etcd2=http://localhost:23200,etcd3=http://localhost:23300,etcd4=http://localhost:23400") + expectedClusterStr := fmt.Sprintf("etcd1=http://localhost:%d,etcd2=http://localhost:%d,etcd3=http://localhost:%d,etcd4=http://localhost:%d", + node1.EtcdPeerPort, node2.EtcdPeerPort, node3.EtcdPeerPort, node4.EtcdPeerPort) + assert.Equal(t, server1.buildInitialClusterStr(), expectedClusterStr) + releasePorts() server1.Start() server2.Start() server3.Start() @@ -70,11 +73,13 @@ func TestCreateEtcdCluster(t *testing.T) { } func TestEtcdAssignmentsPauseResume(t *testing.T) { - node := Node{Name: "etcd1", Host: "localhost", EtcdClientPort: 24500, EtcdPeerPort: 23500, RelayPort: 25500, APIPort: 26500} + nodes, releasePorts := testClusterNodes(t, "etcd1") + node := nodes[0] config := Config{} config.AddNode(node) - server := CreateEtcdServer(node, config, ".") + server := CreateEtcdServer(node, config, t.TempDir()) + releasePorts() server.Start() server.WaitToStart() @@ -123,11 +128,12 @@ func TestEtcdAssignmentsPauseResume(t *testing.T) { } func TestEtcdAssignmentsPauseResumeWithoutClient(t *testing.T) { - node := Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 24600, EtcdPeerPort: 23600, RelayPort: 25600, APIPort: 26600} + nodes, _ := testClusterNodes(t, "etcd2") + node := nodes[0] config := Config{} config.AddNode(node) - server := CreateEtcdServer(node, config, ".") + server := CreateEtcdServer(node, config, t.TempDir()) colonyName := "test_colony" // Test methods fail when etcd client is not initialized @@ -146,16 +152,18 @@ func TestEtcdAssignmentsPauseResumeWithoutClient(t *testing.T) { } func TestEtcdAssignmentsPauseResumeMultiNode(t *testing.T) { - node1 := Node{Name: "etcd1", Host: "localhost", EtcdClientPort: 24700, EtcdPeerPort: 23700, RelayPort: 25700, APIPort: 26700} - node2 := Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 24800, EtcdPeerPort: 23800, RelayPort: 25800, APIPort: 26800} + nodes, releasePorts := testClusterNodes(t, "etcd1", "etcd2") + node1, node2 := nodes[0], nodes[1] config := Config{} config.AddNode(node1) config.AddNode(node2) - server1 := CreateEtcdServer(node1, config, ".") - server2 := CreateEtcdServer(node2, config, ".") + dataPath := t.TempDir() + server1 := CreateEtcdServer(node1, config, dataPath) + server2 := CreateEtcdServer(node2, config, dataPath) + releasePorts() server1.Start() server2.Start() server1.WaitToStart() diff --git a/pkg/cluster/relay_server_test.go b/pkg/cluster/relay_server_test.go index 11986e420..e48639fbc 100644 --- a/pkg/cluster/relay_server_test.go +++ b/pkg/cluster/relay_server_test.go @@ -12,15 +12,16 @@ func TestRelayServer(t *testing.T) { gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = ioutil.Discard - node1 := Node{Name: "etcd1", Host: "localhost", EtcdClientPort: 24100, EtcdPeerPort: 23100, RelayPort: 25100, APIPort: 26100} - node2 := Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 24200, EtcdPeerPort: 23200, RelayPort: 25200, APIPort: 26200} - node3 := Node{Name: "etcd3", Host: "localhost", EtcdClientPort: 24300, EtcdPeerPort: 23300, RelayPort: 25300, APIPort: 26300} + nodes, releasePorts := testClusterNodes(t, "etcd1", "etcd2", "etcd3") + node1, node2, node3 := nodes[0], nodes[1], nodes[2] config := Config{} config.AddNode(node1) config.AddNode(node2) config.AddNode(node3) + // CreateRelayServer binds the relay port + releasePorts() relayServer1 := CreateRelayServer(node1, config) relayServer2 := CreateRelayServer(node2, config) relayServer3 := CreateRelayServer(node3, config) diff --git a/pkg/cluster/test_helpers_test.go b/pkg/cluster/test_helpers_test.go new file mode 100644 index 000000000..899a32aaa --- /dev/null +++ b/pkg/cluster/test_helpers_test.go @@ -0,0 +1,35 @@ +package cluster + +import ( + "testing" + + "github.com/colonyos/colonies/pkg/utils" +) + +// testClusterNodes returns n cluster nodes with kernel-assigned free ports so +// tests never collide on fixed port numbers. The ports stay reserved until the +// returned release function is called, which tests should do right before the +// first component binds. Unreleased reservations are released when the test +// ends. +func testClusterNodes(t *testing.T, names ...string) ([]Node, func()) { + t.Helper() + reserved, err := utils.ReservePorts(4 * len(names)) + if err != nil { + t.Fatalf("failed to reserve ports: %v", err) + } + release := func() { utils.ReleasePorts(reserved) } + t.Cleanup(release) + + nodes := make([]Node, len(names)) + for i, name := range names { + nodes[i] = Node{ + Name: name, + Host: "localhost", + EtcdClientPort: reserved[4*i].Port(), + EtcdPeerPort: reserved[4*i+1].Port(), + RelayPort: reserved[4*i+2].Port(), + APIPort: reserved[4*i+3].Port(), + } + } + return nodes, release +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 20927eb8a..212a0da68 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -8,7 +8,6 @@ const MAX_LOG_COUNT = 500 // Maximum number of log entries that can be reque // Test Configuration - Default values used in test environments const TESTHOST = "localhost" // Default hostname for test servers -const TESTPORT = 28088 // Default port for test servers // Background Processing Periods - How frequently various system tasks run const RELEASE_PERIOD = 1 // Period in seconds when processes are checked for max exec time or max wait time diff --git a/pkg/database/postgresql/test_utils.go b/pkg/database/postgresql/test_utils.go index 31465ba2e..409344b67 100644 --- a/pkg/database/postgresql/test_utils.go +++ b/pkg/database/postgresql/test_utils.go @@ -1,21 +1,216 @@ package postgresql import ( - "io/ioutil" + "crypto/rand" + "database/sql" + "encoding/hex" + "errors" + "fmt" + "io" "log" - "math/rand" "os" - "time" + "strconv" + "strings" + "sync" + "syscall" ) +// testTables lists every table created by Initialize. Kept in sync with +// Initialize so test cleanup can truncate instead of recreating the schema. +var testTables = []string{ + "USERS", + "SERVER", + "COLONIES", + "EXECUTORS", + "NODES", + "FUNCTIONS", + "PROCESSES", + "LOGS", + "FILES", + "SNAPSHOTS", + "ATTRIBUTES", + "PROCESSGRAPHS", + "GENERATORS", + "GENERATORARGS", + "CRONS", + "BLUEPRINTDEFINITIONS", + "BLUEPRINTS", + "LOCATIONS", + "BLUEPRINT_HISTORY", +} + +var testSchemas = struct { + sync.Mutex + ready map[string]bool +}{ready: make(map[string]bool)} + +var testDatabase struct { + once sync.Once + name string + err error +} + +// pinnedTestConn holds one open connection to this process's test database for +// the lifetime of the process, so the database shows up in pg_stat_activity +// and is never considered stale by other test processes. +var pinnedTestConn *sql.DB + +const testDatabasePrefix = "colonies_test_" + +// testHostID identifies the host a test database was created on. Hostnames are +// reduced to lowercase alphanumerics so the result is a valid identifier +// fragment, and truncated to keep the database name well under Postgres's +// 63-character limit. +func testHostID() string { + hostname, err := os.Hostname() + if err != nil || hostname == "" { + return "unknownhost" + } + var b strings.Builder + for _, r := range strings.ToLower(hostname) { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { + b.WriteRune(r) + } + } + id := b.String() + if id == "" { + return "unknownhost" + } + if len(id) > 16 { + id = id[:16] + } + return id +} + +// newTestDatabaseName builds a name unique to this test process: +// colonies_test___. The host lets cleanup tell whether it +// can judge the owning process, the pid identifies that process, and the +// random suffix keeps names distinct across containers that share a PID space +// with the Postgres host or reuse PIDs quickly. +func newTestDatabaseName() (string, error) { + token := make([]byte, 4) + if _, err := rand.Read(token); err != nil { + return "", err + } + return fmt.Sprintf("%s%s_%d_%s", testDatabasePrefix, testHostID(), os.Getpid(), hex.EncodeToString(token)), nil +} + +// testDatabaseName returns the name of a database dedicated to this test +// process, creating it on first use. Per-process databases let test packages +// run in parallel against one Postgres instance without interfering. Stale +// databases from finished or crashed runs are dropped opportunistically: a +// database is stale only if it has no active connections AND it was created on +// this host by a process that is no longer alive. Databases created on other +// hosts are never dropped, because their owner's liveness cannot be checked +// from here. +func testDatabaseName(host string, port int, user string, password string) (string, error) { + testDatabase.once.Do(func() { + adminDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=disable", host, port, user, password) + admin, err := sql.Open("postgres", adminDSN) + if err != nil { + testDatabase.err = err + return + } + defer admin.Close() + + // Only client sessions count as activity; TimescaleDB keeps a + // background worker session open on every database + rows, err := admin.Query(`SELECT datname FROM pg_database WHERE datname LIKE '` + testDatabasePrefix + `%' AND datname NOT IN (SELECT datname FROM pg_stat_activity WHERE datname IS NOT NULL AND backend_type = 'client backend')`) + if err == nil { + var stale []string + for rows.Next() { + var name string + if rows.Scan(&name) == nil && !testDatabaseOwnerAlive(name) { + stale = append(stale, name) + } + } + rows.Close() + for _, name := range stale { + // Best effort; a database that just became active is skipped + admin.Exec(`DROP DATABASE IF EXISTS ` + name) + } + } + + name, err := newTestDatabaseName() + if err != nil { + testDatabase.err = err + return + } + if _, err := admin.Exec(`CREATE DATABASE ` + name); err != nil { + testDatabase.err = err + return + } + + // Pin a connection so the new database is visible as in-use + pinnedDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, name) + pinnedTestConn, err = sql.Open("postgres", pinnedDSN) + if err == nil { + pinnedTestConn.SetMaxIdleConns(1) + pinnedTestConn.SetConnMaxLifetime(0) + pinnedTestConn.SetConnMaxIdleTime(0) + testDatabase.err = pinnedTestConn.Ping() + if testDatabase.err != nil { + return + } + } + + testDatabase.name = name + }) + + return testDatabase.name, testDatabase.err +} + +// testDatabaseOwnerAlive reports whether the process that created a test +// database may still be running. It returns true (keep the database) whenever +// liveness cannot be determined: unrecognized names, names from other hosts, +// or processes owned by another user. +func testDatabaseOwnerAlive(datname string) bool { + parts := strings.Split(strings.TrimPrefix(datname, testDatabasePrefix), "_") + var pidStr string + switch len(parts) { + case 1: + // Legacy colonies_test_ name from before host ids were added; + // assume it was created on this host + pidStr = parts[0] + case 3: + if parts[0] != testHostID() { + // Created on another host; its owner cannot be checked from here + return true + } + pidStr = parts[1] + default: + // Unrecognized name; leave it alone + return true + } + pid, err := strconv.Atoi(pidStr) + if err != nil || pid <= 0 { + return true + } + return processAlive(pid) +} + +// processAlive reports whether a process with the given pid exists. Signal 0 +// performs the existence check without delivering anything; EPERM means the +// process exists but belongs to another user. +func processAlive(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + err = process.Signal(syscall.Signal(0)) + return err == nil || errors.Is(err, syscall.EPERM) +} + func PrepareTests() (*PQDatabase, error) { return PrepareTestsWithPrefix("TEST_") } +// PrepareTestsWithPrefix returns a connected database with an empty schema. +// The schema is dropped and recreated only on the first call per process and +// prefix; subsequent calls truncate all tables, which is roughly an order of +// magnitude faster than Drop+Initialize. func PrepareTestsWithPrefix(prefix string) (*PQDatabase, error) { - log.SetOutput(ioutil.Discard) - - rand.Seed(time.Now().UTC().UnixNano()) + log.SetOutput(io.Discard) dbHost := os.Getenv("COLONIES_DB_HOST") if dbHost == "" { @@ -30,18 +225,59 @@ func PrepareTestsWithPrefix(prefix string) (*PQDatabase, error) { if dbPassword == "" { dbPassword = "rFcLGNkgsNtksg6Pgtn9CumL4xXBQ7" } - dbName := "postgres" - dbPrefix := prefix + dbName, err := testDatabaseName(dbHost, dbPort, dbUser, dbPassword) + if err != nil { + return nil, err + } + + db := CreatePQDatabase(dbHost, dbPort, dbUser, dbPassword, dbName, prefix, false) - db := CreatePQDatabase(dbHost, dbPort, dbUser, dbPassword, dbName, dbPrefix, false) + // Keep pools small so parallel test packages do not exhaust the Postgres + // server's connection limit + if os.Getenv("COLONIES_DB_MAX_OPEN_CONNS") == "" { + os.Setenv("COLONIES_DB_MAX_OPEN_CONNS", "25") + } + if os.Getenv("COLONIES_DB_MAX_IDLE_CONNS") == "" { + os.Setenv("COLONIES_DB_MAX_IDLE_CONNS", "5") + } - err := db.Connect() + err = db.Connect() if err != nil { return nil, err } + testSchemas.Lock() + defer testSchemas.Unlock() + + if testSchemas.ready[prefix] { + return db, db.clearTestData() + } + db.Drop() err = db.Initialize() + if err != nil { + return nil, err + } + + testSchemas.ready[prefix] = true + + return db, nil +} + +// clearTestData empties all tables and resets the file sequence, giving each +// test a clean database without paying for schema recreation. +func (db *PQDatabase) clearTestData() error { + tables := make([]string, len(testTables)) + for i, table := range testTables { + tables[i] = db.dbPrefix + table + } + + _, err := db.postgresql.Exec(`TRUNCATE TABLE ` + strings.Join(tables, ", ")) + if err != nil { + return err + } + + _, err = db.postgresql.Exec(`ALTER SEQUENCE ` + db.dbPrefix + `FILE_SEQ RESTART WITH 1`) - return db, err + return err } diff --git a/pkg/server/channel_integration_test.go b/pkg/server/channel_integration_test.go index ea498328e..128d1a9ef 100644 --- a/pkg/server/channel_integration_test.go +++ b/pkg/server/channel_integration_test.go @@ -1,6 +1,8 @@ package server import ( + "errors" + "net/http" "testing" "time" @@ -29,20 +31,27 @@ func TestChannelEndToEndIntegration(t *testing.T) { assert.Nil(t, err) defer db.Close() - // Create server - port := 8081 + // Create server. Ports stay reserved until just before each component + // binds so parallel test processes cannot be handed the same port + reserved := utils.ReservePortsOrPanic(4) + t.Cleanup(func() { utils.ReleasePorts(reserved) }) + port := reserved[0].Port() thisNode := cluster.Node{ Name: "test-node", Host: "localhost", APIPort: port, - EtcdClientPort: 2379, - EtcdPeerPort: 2380, - RelayPort: 25100, + EtcdClientPort: reserved[1].Port(), + EtcdPeerPort: reserved[2].Port(), + RelayPort: reserved[3].Port(), } clusterConfig := cluster.Config{ Nodes: []cluster.Node{thisNode}, } + // CreateServer binds the etcd and relay ports + reserved[1].Release() + reserved[2].Release() + reserved[3].Release() server := CreateServer( db, port, @@ -51,7 +60,7 @@ func TestChannelEndToEndIntegration(t *testing.T) { "", thisNode, clusterConfig, - "/tmp/test-etcd-"+time.Now().Format("20060102150405"), // etcd path in /tmp + t.TempDir(), 10, // generator period 10, // cron period false, // exclusive assign @@ -63,7 +72,15 @@ func TestChannelEndToEndIntegration(t *testing.T) { ) // Start server in background - go server.ServeForever() + reserved[0].Release() + go func() { + err := server.ServeForever() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + // A silent bind failure would leave the client talking to some + // other process's server; fail loudly instead + panic("test server failed to serve: " + err.Error()) + } + }() time.Sleep(500 * time.Millisecond) // Wait for server to start // Create client @@ -218,20 +235,27 @@ func TestChannelCleanupOnProcessFail(t *testing.T) { assert.Nil(t, err) defer db.Close() - // Create server - port := 8082 + // Create server. Ports stay reserved until just before each component + // binds so parallel test processes cannot be handed the same port + reserved := utils.ReservePortsOrPanic(4) + t.Cleanup(func() { utils.ReleasePorts(reserved) }) + port := reserved[0].Port() thisNode := cluster.Node{ Name: "test-node", Host: "localhost", APIPort: port, - EtcdClientPort: 2379, - EtcdPeerPort: 2380, - RelayPort: 25101, + EtcdClientPort: reserved[1].Port(), + EtcdPeerPort: reserved[2].Port(), + RelayPort: reserved[3].Port(), } clusterConfig := cluster.Config{ Nodes: []cluster.Node{thisNode}, } + // CreateServer binds the etcd and relay ports + reserved[1].Release() + reserved[2].Release() + reserved[3].Release() server := CreateServer( db, port, @@ -240,7 +264,7 @@ func TestChannelCleanupOnProcessFail(t *testing.T) { "", thisNode, clusterConfig, - "/tmp/test-etcd-fail-"+time.Now().Format("20060102150405"), + t.TempDir(), 10, // generator period 10, // cron period false, // exclusive assign @@ -252,8 +276,16 @@ func TestChannelCleanupOnProcessFail(t *testing.T) { ) // Start server in background - go server.ServeForever() - time.Sleep(500 * time.Millisecond) + reserved[0].Release() + go func() { + err := server.ServeForever() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + // A silent bind failure would leave the client talking to some + // other process's server; fail loudly instead + panic("test server failed to serve: " + err.Error()) + } + }() + time.Sleep(500 * time.Millisecond) // Wait for server to start // Create client colonies := client.CreateColoniesClient("localhost", port, true, true) diff --git a/pkg/server/controllers/mock_test.go b/pkg/server/controllers/mock_test.go index 3fb809244..14eb00e27 100644 --- a/pkg/server/controllers/mock_test.go +++ b/pkg/server/controllers/mock_test.go @@ -3,620 +3,643 @@ package controllers import ( "errors" "fmt" + "os" "sync/atomic" "time" - "github.com/colonyos/colonies/pkg/backends" "github.com/colonyos/colonies/pkg/cluster" "github.com/colonyos/colonies/pkg/constants" "github.com/colonyos/colonies/pkg/core" + "github.com/colonyos/colonies/pkg/database" "github.com/colonyos/colonies/pkg/database/postgresql" + "github.com/colonyos/colonies/pkg/utils" ) // portCounter is used to allocate unique ports for each test to avoid port conflicts var portCounter int32 = 0 -// ControllerMock implements the Controller interface for testing -type ControllerMock struct { +// DatabaseMock implements database interfaces for testing +type DatabaseMock struct { ReturnError string ReturnValue string } -func (v *ControllerMock) GetCronPeriod() int { - return -1 +// Implement all database interfaces as no-ops for testing +// ColonyDatabase interface +func (db *DatabaseMock) AddColony(colony *core.Colony) error { + if db.ReturnError == "AddColony" { + return errors.New("mock error") + } + return nil } - -func (v *ControllerMock) GetGeneratorPeriod() int { - return -1 +func (db *DatabaseMock) GetColonies() ([]*core.Colony, error) { + if db.ReturnError == "GetColonies" { + return nil, errors.New("mock error") + } + return nil, nil } - -func (v *ControllerMock) GetEtcdServer() *cluster.EtcdServer { +func (db *DatabaseMock) GetColonyByID(id string) (*core.Colony, error) { + if db.ReturnError == "GetColonyByID" { + return nil, errors.New("mock error") + } + return nil, nil +} +func (db *DatabaseMock) GetColonyByName(name string) (*core.Colony, error) { + if db.ReturnError == "GetColonyByName" { + return nil, errors.New("mock error") + } + return nil, nil +} +func (db *DatabaseMock) RenameColony(colonyName string, newColonyName string) error { + if db.ReturnError == "RenameColony" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) GetEventHandler() backends.RealtimeEventHandler { +func (db *DatabaseMock) RemoveColonyByName(colonyName string) error { + if db.ReturnError == "RemoveColonyByName" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) GetThisNode() cluster.Node { - return cluster.Node{} +func (db *DatabaseMock) CountColonies() (int, error) { + if db.ReturnError == "CountColonies" { + return 0, errors.New("mock error") + } + return 0, nil } -func (v *ControllerMock) SubscribeProcesses(executorID string, subscription *backends.RealtimeSubscription) error { +// ExecutorDatabase interface +func (db *DatabaseMock) AddExecutor(executor *core.Executor) error { + if db.ReturnError == "AddExecutor" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) SubscribeProcess(executorID string, subscription *backends.RealtimeSubscription) error { +func (db *DatabaseMock) SetAllocations(colonyName string, executorName string, allocations core.Allocations) error { + if db.ReturnError == "SetAllocations" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) AddProcessToDB(process *core.Process) (*core.Process, error) { +func (db *DatabaseMock) GetExecutors() ([]*core.Executor, error) { + if db.ReturnError == "GetExecutors" { + return nil, errors.New("mock error") + } return nil, nil } - -func (v *ControllerMock) AddProcess(process *core.Process) (*core.Process, error) { +func (db *DatabaseMock) GetExecutorByID(executorID string) (*core.Executor, error) { + if db.ReturnError == "GetExecutorByID" { + return nil, errors.New("mock error") + } + // Return a dummy executor when no error is set + return &core.Executor{ID: executorID, ColonyName: "test-colony"}, nil +} +func (db *DatabaseMock) GetExecutorsByColonyName(colonyName string) ([]*core.Executor, error) { + if db.ReturnError == "GetExecutorsByColonyName" || db.ReturnError == "GetExecutorByColonyName" { + return nil, errors.New("mock error") + } return nil, nil } - -func (v *ControllerMock) AddChild(processGraphID string, parentProcessID string, childProcessID string, process *core.Process, executorID string, insert bool) (*core.Process, error) { +func (db *DatabaseMock) GetExecutorByName(colonyName string, executorName string) (*core.Executor, error) { + if db.ReturnError == "GetExecutorByName" { + return nil, errors.New("mock error") + } + if db.ReturnValue == "GetExecutorByName" { + return &core.Executor{}, nil + } return nil, nil } - -func (v *ControllerMock) UpdateProcessGraph(graph *core.ProcessGraph) error { +func (db *DatabaseMock) ApproveExecutor(executor *core.Executor) error { return nil } +func (db *DatabaseMock) RejectExecutor(executor *core.Executor) error { return nil } +func (db *DatabaseMock) MarkAlive(executor *core.Executor) error { return nil } +func (db *DatabaseMock) RemoveExecutorByName(colonyName string, executorName string) error { return nil } - -func (v *ControllerMock) CreateProcessGraph(workflowSpec *core.WorkflowSpec, args []interface{}, kwargs map[string]interface{}, rootInput []interface{}, recoveredID string) (*core.ProcessGraph, error) { - return nil, nil +func (db *DatabaseMock) RemoveExecutorsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) CountExecutors() (int, error) { return 0, nil } +func (db *DatabaseMock) CountExecutorsByColonyName(colonyName string) (int, error) { return 0, nil } +func (db *DatabaseMock) CountExecutorsByColonyNameAndState(colonyName string, state int) (int, error) { + return 0, nil } - -func (v *ControllerMock) SubmitWorkflowSpec(workflowSpec *core.WorkflowSpec, recoveredID string) (*core.ProcessGraph, error) { +func (db *DatabaseMock) GetExecutorsByBlueprintID(blueprintID string) ([]*core.Executor, error) { return nil, nil } - -func (v *ControllerMock) GetProcessGraphByID(processGraphID string) (*core.ProcessGraph, error) { - return nil, nil +func (db *DatabaseMock) UpdateExecutorCapabilities(colonyName string, executorName string, capabilities core.Capabilities) error { + return nil } -func (v *ControllerMock) FindWaitingProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { +// LocationDatabase interface +func (db *DatabaseMock) AddLocation(location *core.Location) error { return nil } +func (db *DatabaseMock) GetLocationsByColonyName(colonyName string) ([]*core.Location, error) { return nil, nil } - -func (v *ControllerMock) FindRunningProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { +func (db *DatabaseMock) GetLocationByID(locationID string) (*core.Location, error) { return nil, nil } +func (db *DatabaseMock) GetLocationByName(colonyName string, name string) (*core.Location, error) { return nil, nil } +func (db *DatabaseMock) RemoveLocationByID(locationID string) error { return nil } +func (db *DatabaseMock) RemoveLocationByName(colonyName string, name string) error { return nil } +func (db *DatabaseMock) RemoveLocationsByColonyName(colonyName string) error { return nil } -func (v *ControllerMock) FindSuccessfulProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { - return nil, nil +// ProcessDatabase interface +func (db *DatabaseMock) AddProcess(process *core.Process) error { + if db.ReturnError == "AddProcess" { + return errors.New("mock error") + } + return nil } - -func (v *ControllerMock) FindFailedProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { +func (db *DatabaseMock) GetProcesses() ([]*core.Process, error) { + if db.ReturnError == "GetProcesses" { + return nil, errors.New("mock error") + } return nil, nil } - -func (v *ControllerMock) CloseSuccessful(processID string, executorID string, output []interface{}) error { - return nil +func (db *DatabaseMock) GetProcessByID(processID string) (*core.Process, error) { + if db.ReturnError == "GetProcessByID" { + return nil, errors.New("mock error") + } + return nil, nil } - -func (v *ControllerMock) NotifyChildren(process *core.Process) error { +func (db *DatabaseMock) SetProcessState(processID string, state int) error { + if db.ReturnError == "SetProcessState" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) CloseFailed(processID string, errs []string) error { +func (db *DatabaseMock) SetWaitForParents(processID string, waitForParent bool) error { + if db.ReturnError == "SetWaitForParents" { + return errors.New("mock error") + } return nil } - -func (v *ControllerMock) HandleDefunctProcessgraph(processGraphID string, processID string, err error) error { - return nil +func (db *DatabaseMock) MarkSuccessful(processID string) (float64, float64, error) { + if db.ReturnError == "MarkSuccessful" { + return 0, 0, errors.New("mock error") + } + return 1.0, 1.0, nil } - -func (v *ControllerMock) Assign(executorID string, colonyName string, cpu int64, memory int64) (*AssignResult, error) { +func (db *DatabaseMock) FindProcessesByColonyName(colonyName string, seconds int, state int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) DistributedAssign(executor *core.Executor, colonyName string, cpu int64, memory int64, storage int64) (*AssignResult, error) { +func (db *DatabaseMock) FindProcessesByExecutorID(colonyName string, executorID string, seconds int, state int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) UnassignExecutor(processID string) error { - return nil +func (db *DatabaseMock) FindWaitingProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { + return nil, nil } - -func (v *ControllerMock) ResetProcess(processID string) error { - return nil +func (db *DatabaseMock) FindRunningProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { + return nil, nil } - -func (v *ControllerMock) AddGenerator(generator *core.Generator) (*core.Generator, error) { +func (db *DatabaseMock) FindSuccessfulProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) GetGenerator(generatorID string) (*core.Generator, error) { +func (db *DatabaseMock) FindFailedProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) ResolveGenerator(colonyName string, generatorName string) (*core.Generator, error) { +func (db *DatabaseMock) FindAllRunningProcesses() ([]*core.Process, error) { return nil, nil } +func (db *DatabaseMock) FindAllWaitingProcesses() ([]*core.Process, error) { return nil, nil } +func (db *DatabaseMock) FindCandidates(colonyName string, executorType string, executorLocationName string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) GetGenerators(colonyName string, count int) ([]*core.Generator, error) { +func (db *DatabaseMock) FindCandidatesByName(colonyName string, executorName string, executorType string, executorLocationName string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) ([]*core.Process, error) { return nil, nil } - -func (v *ControllerMock) PackGenerator(generatorID string, colonyName, arg string) error { +func (db *DatabaseMock) RemoveProcessByID(processID string) error { return nil } +func (db *DatabaseMock) RemoveAllProcesses() error { return nil } +func (db *DatabaseMock) RemoveAllWaitingProcessesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllRunningProcessesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllSuccessfulProcessesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllFailedProcessesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllProcessesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllProcessesByProcessGraphID(processGraphID string) error { return nil } +func (db *DatabaseMock) RemoveAllProcessesInProcessGraphsByColonyName(colonyName string) error { return nil } - -func (v *ControllerMock) GeneratorTriggerLoop() { -} - -func (v *ControllerMock) TriggerGenerators() { +func (db *DatabaseMock) ResetProcess(process *core.Process) error { return nil } +func (db *DatabaseMock) SetInput(processID string, input []interface{}) error { return nil } +func (db *DatabaseMock) SetOutput(processID string, output []interface{}) error { return nil } +func (db *DatabaseMock) SetErrors(processID string, errs []string) error { return nil } +func (db *DatabaseMock) SetParents(processID string, parents []string) error { return nil } +func (db *DatabaseMock) SetChildren(processID string, children []string) error { return nil } +func (db *DatabaseMock) Assign(executorID string, process *core.Process) error { return nil } +func (db *DatabaseMock) SelectAndAssign(colonyName string, executorID string, executorName string, executorType string, executorLocation string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) (*core.Process, error) { + return nil, nil } - -func (v *ControllerMock) SubmitWorkflow(generator *core.Generator, counter int, recoveredID string) { +func (db *DatabaseMock) Unassign(process *core.Process) error { return nil } +func (db *DatabaseMock) MarkFailed(processID string, errs []string) error { return nil } +func (db *DatabaseMock) CountProcesses() (int, error) { return 0, nil } +func (db *DatabaseMock) CountWaitingProcesses() (int, error) { return 0, nil } +func (db *DatabaseMock) CountRunningProcesses() (int, error) { return 0, nil } +func (db *DatabaseMock) CountSuccessfulProcesses() (int, error) { return 0, nil } +func (db *DatabaseMock) CountFailedProcesses() (int, error) { return 0, nil } +func (db *DatabaseMock) CountWaitingProcessesByColonyName(colonyName string) (int, error) { + return 0, nil } - -func (v *ControllerMock) AddCron(cron *core.Cron) (*core.Cron, error) { - return nil, nil +func (db *DatabaseMock) CountRunningProcessesByColonyName(colonyName string) (int, error) { + return 0, nil } - -func (v *ControllerMock) RemoveGenerator(generatorID string) error { - return nil +func (db *DatabaseMock) CountSuccessfulProcessesByColonyName(colonyName string) (int, error) { + return 0, nil } - -func (v *ControllerMock) GetCron(cronID string) (*core.Cron, error) { - return nil, nil +func (db *DatabaseMock) CountFailedProcessesByColonyName(colonyName string) (int, error) { + return 0, nil } -func (v *ControllerMock) GetCrons(colonyName string, count int) ([]*core.Cron, error) { +// UserDatabase interface +func (db *DatabaseMock) AddUser(user *core.User) error { return nil } +func (db *DatabaseMock) GetUserByName(colonyName string, name string) (*core.User, error) { return nil, nil } - -func (v *ControllerMock) GetCronByName(colonyName string, cronName string) (*core.Cron, error) { +func (db *DatabaseMock) GetUserByID(colonyName string, userID string) (*core.User, error) { return nil, nil } - -func (v *ControllerMock) RunCron(cronID string) (*core.Cron, error) { +func (db *DatabaseMock) GetUsersByColonyName(colonyName string) ([]*core.User, error) { return nil, nil } +func (db *DatabaseMock) RemoveUserByName(colonyName string, name string) error { return nil } +func (db *DatabaseMock) RemoveUserByID(colonyName string, userID string) error { return nil } +func (db *DatabaseMock) RemoveUsersByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) CountUsers() (int, error) { return 0, nil } -func (v *ControllerMock) RemoveCron(cronID string) error { - return nil -} - -func (v *ControllerMock) CalcNextRun(cron *core.Cron) time.Time { - return time.Time{} +// AttributeDatabase interface +func (db *DatabaseMock) AddAttribute(attribute core.Attribute) error { return nil } +func (db *DatabaseMock) AddAttributes(attributes []core.Attribute) error { return nil } +func (db *DatabaseMock) GetAttributeByID(attributeID string) (core.Attribute, error) { + return core.Attribute{}, nil } - -func (v *ControllerMock) StartCron(cron *core.Cron) { +func (db *DatabaseMock) GetAttributesByColonyName(colonyName string) ([]core.Attribute, error) { + return nil, nil } - -func (v *ControllerMock) TriggerCrons() { +func (db *DatabaseMock) GetAttribute(targetID string, key string, attributeType int) (core.Attribute, error) { + return core.Attribute{}, nil } - -func (v *ControllerMock) CronTriggerLoop() { +func (db *DatabaseMock) GetAttributes(targetID string) ([]core.Attribute, error) { return nil, nil } +func (db *DatabaseMock) GetAttributesByType(targetID string, attributeType int) ([]core.Attribute, error) { + return nil, nil } - -func (v *ControllerMock) ResetDatabase() error { +func (db *DatabaseMock) UpdateAttribute(attribute core.Attribute) error { return nil } +func (db *DatabaseMock) RemoveAttributeByID(attributeID string) error { return nil } +func (db *DatabaseMock) RemoveAllAttributesByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllAttributesByColonyNameWithState(colonyName string, state int) error { return nil } - -func (v *ControllerMock) PauseColonyAssignments(colonyName string) error { +func (db *DatabaseMock) RemoveAllAttributesByProcessGraphID(processGraphID string) error { return nil } +func (db *DatabaseMock) RemoveAllAttributesInProcessGraphsByColonyName(colonyName string) error { return nil } - -func (v *ControllerMock) ResumeColonyAssignments(colonyName string) error { +func (db *DatabaseMock) RemoveAllAttributesInProcessGraphsByColonyNameWithState(colonyName string, state int) error { return nil } - -func (v *ControllerMock) AreColonyAssignmentsPaused(colonyName string) (bool, error) { - return false, nil -} - -func (v *ControllerMock) Stop() { -} - -func (v *ControllerMock) IsLeader() bool { - return false -} - -func (v *ControllerMock) TryBecomeLeader() bool { - return false -} - -func (v *ControllerMock) TimeoutLoop() { -} - -func (v *ControllerMock) BlockingCmdQueueWorker() { -} - -func (v *ControllerMock) RetentionWorker() { -} - -func (v *ControllerMock) CmdQueueWorker() { -} - -// DatabaseMock implements database interfaces for testing -type DatabaseMock struct { - ReturnError string - ReturnValue string -} - -// Implement all database interfaces as no-ops for testing -// ColonyDatabase interface -func (db *DatabaseMock) AddColony(colony *core.Colony) error { - if db.ReturnError == "AddColony" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveAttributesByTargetID(targetID string, attributeType int) error { return nil } -func (db *DatabaseMock) GetColonies() ([]*core.Colony, error) { - if db.ReturnError == "GetColonies" { return nil, errors.New("mock error") } - return nil, nil -} -func (db *DatabaseMock) GetColonyByID(id string) (*core.Colony, error) { - if db.ReturnError == "GetColonyByID" { return nil, errors.New("mock error") } +func (db *DatabaseMock) RemoveAllAttributesByTargetID(targetID string) error { return nil } +func (db *DatabaseMock) RemoveAllAttributes() error { return nil } + +// FunctionDatabase interface +func (db *DatabaseMock) AddFunction(function *core.Function) error { return nil } +func (db *DatabaseMock) GetFunctionByID(functionID string) (*core.Function, error) { return nil, nil } +func (db *DatabaseMock) GetFunctionsByExecutorName(colonyName string, executorName string) ([]*core.Function, error) { return nil, nil } -func (db *DatabaseMock) GetColonyByName(name string) (*core.Colony, error) { - if db.ReturnError == "GetColonyByName" { return nil, errors.New("mock error") } +func (db *DatabaseMock) GetFunctionsByColonyName(colonyName string) ([]*core.Function, error) { return nil, nil } -func (db *DatabaseMock) RenameColony(colonyName string, newColonyName string) error { - if db.ReturnError == "RenameColony" { return errors.New("mock error") } - return nil -} -func (db *DatabaseMock) RemoveColonyByName(colonyName string) error { - if db.ReturnError == "RemoveColonyByName" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveFunctionByID(functionID string) error { return nil } +func (db *DatabaseMock) RemoveFunctionsByExecutorName(colonyName string, executorName string) error { return nil } -func (db *DatabaseMock) CountColonies() (int, error) { - if db.ReturnError == "CountColonies" { return 0, errors.New("mock error") } - return 0, nil +func (db *DatabaseMock) RemoveFunctionsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) GetFunctionsByExecutorAndName(colonyName string, executorName string, name string) (*core.Function, error) { + return nil, nil } - -// ExecutorDatabase interface -func (db *DatabaseMock) AddExecutor(executor *core.Executor) error { - if db.ReturnError == "AddExecutor" { return errors.New("mock error") } +func (db *DatabaseMock) UpdateFunctionStats(colonyName string, executorName string, name string, counter int, minWaitTime float64, maxWaitTime float64, minExecTime float64, maxExecTime float64, avgWaitTime float64, avgExecTime float64) error { return nil } -func (db *DatabaseMock) SetAllocations(colonyName string, executorName string, allocations core.Allocations) error { - if db.ReturnError == "SetAllocations" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveFunctionByName(colonyName string, executorName string, name string) error { return nil } -func (db *DatabaseMock) GetExecutors() ([]*core.Executor, error) { - if db.ReturnError == "GetExecutors" { return nil, errors.New("mock error") } +func (db *DatabaseMock) RemoveFunctions() error { return nil } +func (db *DatabaseMock) CountFunctions() (int, error) { return 0, nil } + +// ProcessGraphDatabase interface +func (db *DatabaseMock) AddProcessGraph(processGraph *core.ProcessGraph) error { return nil } +func (db *DatabaseMock) GetProcessGraphByID(processGraphID string) (*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) GetExecutorByID(executorID string) (*core.Executor, error) { - if db.ReturnError == "GetExecutorByID" { return nil, errors.New("mock error") } - // Return a dummy executor when no error is set - return &core.Executor{ID: executorID, ColonyName: "test-colony"}, nil +func (db *DatabaseMock) SetProcessGraphState(processGraphID string, state int) error { + if db.ReturnError == "SetProcessGraphState" { + return errors.New("mock error") + } + return nil } -func (db *DatabaseMock) GetExecutorsByColonyName(colonyName string) ([]*core.Executor, error) { - if db.ReturnError == "GetExecutorsByColonyName" || db.ReturnError == "GetExecutorByColonyName" { return nil, errors.New("mock error") } +func (db *DatabaseMock) FindWaitingProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) GetExecutorByName(colonyName string, executorName string) (*core.Executor, error) { - if db.ReturnError == "GetExecutorByName" { return nil, errors.New("mock error") } - if db.ReturnValue == "GetExecutorByName" { return &core.Executor{}, nil } +func (db *DatabaseMock) FindRunningProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) ApproveExecutor(executor *core.Executor) error { return nil } -func (db *DatabaseMock) RejectExecutor(executor *core.Executor) error { return nil } -func (db *DatabaseMock) MarkAlive(executor *core.Executor) error { return nil } -func (db *DatabaseMock) RemoveExecutorByName(colonyName string, executorName string) error { return nil } -func (db *DatabaseMock) RemoveExecutorsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) CountExecutors() (int, error) { return 0, nil } -func (db *DatabaseMock) CountExecutorsByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountExecutorsByColonyNameAndState(colonyName string, state int) (int, error) { return 0, nil } -func (db *DatabaseMock) GetExecutorsByBlueprintID(blueprintID string) ([]*core.Executor, error) { return nil, nil } -func (db *DatabaseMock) UpdateExecutorCapabilities(colonyName string, executorName string, capabilities core.Capabilities) error { return nil } - -// LocationDatabase interface -func (db *DatabaseMock) AddLocation(location *core.Location) error { return nil } -func (db *DatabaseMock) GetLocationsByColonyName(colonyName string) ([]*core.Location, error) { return nil, nil } -func (db *DatabaseMock) GetLocationByID(locationID string) (*core.Location, error) { return nil, nil } -func (db *DatabaseMock) GetLocationByName(colonyName string, name string) (*core.Location, error) { return nil, nil } -func (db *DatabaseMock) RemoveLocationByID(locationID string) error { return nil } -func (db *DatabaseMock) RemoveLocationByName(colonyName string, name string) error { return nil } -func (db *DatabaseMock) RemoveLocationsByColonyName(colonyName string) error { return nil } - -// ProcessDatabase interface -func (db *DatabaseMock) AddProcess(process *core.Process) error { - if db.ReturnError == "AddProcess" { return errors.New("mock error") } - return nil -} -func (db *DatabaseMock) GetProcesses() ([]*core.Process, error) { - if db.ReturnError == "GetProcesses" { return nil, errors.New("mock error") } +func (db *DatabaseMock) FindSuccessfulProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) GetProcessByID(processID string) (*core.Process, error) { - if db.ReturnError == "GetProcessByID" { return nil, errors.New("mock error") } +func (db *DatabaseMock) FindFailedProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) SetProcessState(processID string, state int) error { - if db.ReturnError == "SetProcessState" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveProcessGraphByID(processGraphID string) error { return nil } +func (db *DatabaseMock) RemoveAllProcessGraphsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllWaitingProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) SetWaitForParents(processID string, waitForParent bool) error { - if db.ReturnError == "SetWaitForParents" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveAllRunningProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) MarkSuccessful(processID string) (float64, float64, error) { - if db.ReturnError == "MarkSuccessful" { return 0, 0, errors.New("mock error") } - return 1.0, 1.0, nil -} -func (db *DatabaseMock) FindProcessesByColonyName(colonyName string, seconds int, state int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindProcessesByExecutorID(colonyName string, executorID string, seconds int, state int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindWaitingProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindRunningProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindSuccessfulProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindFailedProcesses(colonyName string, executorType string, label string, initiator string, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindAllRunningProcesses() ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindAllWaitingProcesses() ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindCandidates(colonyName string, executorType string, executorLocationName string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) FindCandidatesByName(colonyName string, executorName string, executorType string, executorLocationName string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) ([]*core.Process, error) { return nil, nil } -func (db *DatabaseMock) RemoveProcessByID(processID string) error { return nil } -func (db *DatabaseMock) RemoveAllProcesses() error { return nil } -func (db *DatabaseMock) RemoveAllWaitingProcessesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllRunningProcessesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllSuccessfulProcessesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllFailedProcessesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllProcessesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllProcessesByProcessGraphID(processGraphID string) error { return nil } -func (db *DatabaseMock) RemoveAllProcessesInProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) ResetProcess(process *core.Process) error { return nil } -func (db *DatabaseMock) SetInput(processID string, input []interface{}) error { return nil } -func (db *DatabaseMock) SetOutput(processID string, output []interface{}) error { return nil } -func (db *DatabaseMock) SetErrors(processID string, errs []string) error { return nil } -func (db *DatabaseMock) SetParents(processID string, parents []string) error { return nil } -func (db *DatabaseMock) SetChildren(processID string, children []string) error { return nil } -func (db *DatabaseMock) Assign(executorID string, process *core.Process) error { return nil } -func (db *DatabaseMock) SelectAndAssign(colonyName string, executorID string, executorName string, executorType string, executorLocation string, cpu int64, memory int64, storage int64, nodes int, processes int, processesPerNode int, count int) (*core.Process, error) { return nil, nil } -func (db *DatabaseMock) Unassign(process *core.Process) error { return nil } -func (db *DatabaseMock) MarkFailed(processID string, errs []string) error { return nil } -func (db *DatabaseMock) CountProcesses() (int, error) { return 0, nil } -func (db *DatabaseMock) CountWaitingProcesses() (int, error) { return 0, nil } -func (db *DatabaseMock) CountRunningProcesses() (int, error) { return 0, nil } -func (db *DatabaseMock) CountSuccessfulProcesses() (int, error) { return 0, nil } -func (db *DatabaseMock) CountFailedProcesses() (int, error) { return 0, nil } -func (db *DatabaseMock) CountWaitingProcessesByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountRunningProcessesByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountSuccessfulProcessesByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountFailedProcessesByColonyName(colonyName string) (int, error) { return 0, nil } - -// UserDatabase interface -func (db *DatabaseMock) AddUser(user *core.User) error { return nil } -func (db *DatabaseMock) GetUserByName(colonyName string, name string) (*core.User, error) { return nil, nil } -func (db *DatabaseMock) GetUserByID(colonyName string, userID string) (*core.User, error) { return nil, nil } -func (db *DatabaseMock) GetUsersByColonyName(colonyName string) ([]*core.User, error) { return nil, nil } -func (db *DatabaseMock) RemoveUserByName(colonyName string, name string) error { return nil } -func (db *DatabaseMock) RemoveUserByID(colonyName string, userID string) error { return nil } -func (db *DatabaseMock) RemoveUsersByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) CountUsers() (int, error) { return 0, nil } - -// AttributeDatabase interface -func (db *DatabaseMock) AddAttribute(attribute core.Attribute) error { return nil } -func (db *DatabaseMock) AddAttributes(attributes []core.Attribute) error { return nil } -func (db *DatabaseMock) GetAttributeByID(attributeID string) (core.Attribute, error) { return core.Attribute{}, nil } -func (db *DatabaseMock) GetAttributesByColonyName(colonyName string) ([]core.Attribute, error) { return nil, nil } -func (db *DatabaseMock) GetAttribute(targetID string, key string, attributeType int) (core.Attribute, error) { return core.Attribute{}, nil } -func (db *DatabaseMock) GetAttributes(targetID string) ([]core.Attribute, error) { return nil, nil } -func (db *DatabaseMock) GetAttributesByType(targetID string, attributeType int) ([]core.Attribute, error) { return nil, nil } -func (db *DatabaseMock) UpdateAttribute(attribute core.Attribute) error { return nil } -func (db *DatabaseMock) RemoveAttributeByID(attributeID string) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesByColonyNameWithState(colonyName string, state int) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesByProcessGraphID(processGraphID string) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesInProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesInProcessGraphsByColonyNameWithState(colonyName string, state int) error { return nil } -func (db *DatabaseMock) RemoveAttributesByTargetID(targetID string, attributeType int) error { return nil } -func (db *DatabaseMock) RemoveAllAttributesByTargetID(targetID string) error { return nil } -func (db *DatabaseMock) RemoveAllAttributes() error { return nil } - -// FunctionDatabase interface -func (db *DatabaseMock) AddFunction(function *core.Function) error { return nil } -func (db *DatabaseMock) GetFunctionByID(functionID string) (*core.Function, error) { return nil, nil } -func (db *DatabaseMock) GetFunctionsByExecutorName(colonyName string, executorName string) ([]*core.Function, error) { return nil, nil } -func (db *DatabaseMock) GetFunctionsByColonyName(colonyName string) ([]*core.Function, error) { return nil, nil } -func (db *DatabaseMock) RemoveFunctionByID(functionID string) error { return nil } -func (db *DatabaseMock) RemoveFunctionsByExecutorName(colonyName string, executorName string) error { return nil } -func (db *DatabaseMock) RemoveFunctionsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) GetFunctionsByExecutorAndName(colonyName string, executorName string, name string) (*core.Function, error) { return nil, nil } -func (db *DatabaseMock) UpdateFunctionStats(colonyName string, executorName string, name string, counter int, minWaitTime float64, maxWaitTime float64, minExecTime float64, maxExecTime float64, avgWaitTime float64, avgExecTime float64) error { return nil } -func (db *DatabaseMock) RemoveFunctionByName(colonyName string, executorName string, name string) error { return nil } -func (db *DatabaseMock) RemoveFunctions() error { return nil } -func (db *DatabaseMock) CountFunctions() (int, error) { return 0, nil } - -// ProcessGraphDatabase interface -func (db *DatabaseMock) AddProcessGraph(processGraph *core.ProcessGraph) error { return nil } -func (db *DatabaseMock) GetProcessGraphByID(processGraphID string) (*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) SetProcessGraphState(processGraphID string, state int) error { - if db.ReturnError == "SetProcessGraphState" { return errors.New("mock error") } +func (db *DatabaseMock) RemoveAllSuccessfulProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) FindWaitingProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) FindRunningProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) FindSuccessfulProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) FindFailedProcessGraphs(colonyName string, count int) ([]*core.ProcessGraph, error) { return nil, nil } -func (db *DatabaseMock) RemoveProcessGraphByID(processGraphID string) error { return nil } -func (db *DatabaseMock) RemoveAllProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllWaitingProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllRunningProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) RemoveAllSuccessfulProcessGraphsByColonyName(colonyName string) error { return nil } func (db *DatabaseMock) RemoveAllFailedProcessGraphsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) CountWaitingProcessGraphs() (int, error) { return 0, nil } -func (db *DatabaseMock) CountRunningProcessGraphs() (int, error) { return 0, nil } -func (db *DatabaseMock) CountSuccessfulProcessGraphs() (int, error) { return 0, nil } -func (db *DatabaseMock) CountFailedProcessGraphs() (int, error) { return 0, nil } -func (db *DatabaseMock) CountWaitingProcessGraphsByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountRunningProcessGraphsByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountSuccessfulProcessGraphsByColonyName(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountFailedProcessGraphsByColonyName(colonyName string) (int, error) { return 0, nil } +func (db *DatabaseMock) CountWaitingProcessGraphs() (int, error) { return 0, nil } +func (db *DatabaseMock) CountRunningProcessGraphs() (int, error) { return 0, nil } +func (db *DatabaseMock) CountSuccessfulProcessGraphs() (int, error) { return 0, nil } +func (db *DatabaseMock) CountFailedProcessGraphs() (int, error) { return 0, nil } +func (db *DatabaseMock) CountWaitingProcessGraphsByColonyName(colonyName string) (int, error) { + return 0, nil +} +func (db *DatabaseMock) CountRunningProcessGraphsByColonyName(colonyName string) (int, error) { + return 0, nil +} +func (db *DatabaseMock) CountSuccessfulProcessGraphsByColonyName(colonyName string) (int, error) { + return 0, nil +} +func (db *DatabaseMock) CountFailedProcessGraphsByColonyName(colonyName string) (int, error) { + return 0, nil +} // GeneratorDatabase interface func (db *DatabaseMock) AddGenerator(generator *core.Generator) error { - if db.ReturnError == "AddGenerator" { return errors.New("mock error") } + if db.ReturnError == "AddGenerator" { + return errors.New("mock error") + } return nil } func (db *DatabaseMock) GetGeneratorByID(generatorID string) (*core.Generator, error) { - if db.ReturnError == "GetGeneratorByID" { return nil, errors.New("mock error") } + if db.ReturnError == "GetGeneratorByID" { + return nil, errors.New("mock error") + } return &core.Generator{ID: generatorID, ColonyName: "test-colony"}, nil } func (db *DatabaseMock) GetGeneratorByName(colonyName string, generatorName string) (*core.Generator, error) { - if db.ReturnError == "GetGeneratorByName" { return nil, errors.New("mock error") } + if db.ReturnError == "GetGeneratorByName" { + return nil, errors.New("mock error") + } return &core.Generator{ID: generatorName, ColonyName: colonyName}, nil } func (db *DatabaseMock) FindAllGenerators() ([]*core.Generator, error) { return nil, nil } -func (db *DatabaseMock) FindGeneratorsByColonyName(colonyName string, count int) ([]*core.Generator, error) { return nil, nil } -func (db *DatabaseMock) RemoveGeneratorByID(generatorID string) error { return nil } +func (db *DatabaseMock) FindGeneratorsByColonyName(colonyName string, count int) ([]*core.Generator, error) { + return nil, nil +} +func (db *DatabaseMock) RemoveGeneratorByID(generatorID string) error { return nil } func (db *DatabaseMock) AddGeneratorArg(generatorArg *core.GeneratorArg) error { return nil } -func (db *DatabaseMock) GetGeneratorArgs(generatorID string, count int) ([]*core.GeneratorArg, error) { return nil, nil } -func (db *DatabaseMock) CountGeneratorArgs(generatorID string) (int, error) { return 0, nil } -func (db *DatabaseMock) RemoveGeneratorArgByID(generatorArgID string) error { return nil } -func (db *DatabaseMock) SetGeneratorLastRun(generatorID string) error { return nil } -func (db *DatabaseMock) SetGeneratorFirstPack(generatorID string) error { return nil } -func (db *DatabaseMock) RemoveAllGeneratorsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) GetGeneratorArgs(generatorID string, count int) ([]*core.GeneratorArg, error) { + return nil, nil +} +func (db *DatabaseMock) CountGeneratorArgs(generatorID string) (int, error) { return 0, nil } +func (db *DatabaseMock) RemoveGeneratorArgByID(generatorArgID string) error { return nil } +func (db *DatabaseMock) SetGeneratorLastRun(generatorID string) error { return nil } +func (db *DatabaseMock) SetGeneratorFirstPack(generatorID string) error { return nil } +func (db *DatabaseMock) RemoveAllGeneratorsByColonyName(colonyName string) error { return nil } func (db *DatabaseMock) RemoveAllGeneratorArgsByGeneratorID(generatorID string) error { return nil } -func (db *DatabaseMock) RemoveAllGeneratorArgsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) RemoveAllGeneratorArgsByColonyName(colonyName string) error { return nil } // CronDatabase interface func (db *DatabaseMock) AddCron(cron *core.Cron) error { - if db.ReturnError == "AddCron" { return errors.New("mock error") } + if db.ReturnError == "AddCron" { + return errors.New("mock error") + } return nil } func (db *DatabaseMock) GetCronByID(cronID string) (*core.Cron, error) { - if db.ReturnError == "GetCronByID" { return nil, errors.New("mock error") } + if db.ReturnError == "GetCronByID" { + return nil, errors.New("mock error") + } return &core.Cron{ID: cronID, ColonyName: "test-colony"}, nil } func (db *DatabaseMock) FindAllCrons() ([]*core.Cron, error) { return nil, nil } -func (db *DatabaseMock) FindCronsByColonyName(colonyName string, count int) ([]*core.Cron, error) { return nil, nil } +func (db *DatabaseMock) FindCronsByColonyName(colonyName string, count int) ([]*core.Cron, error) { + return nil, nil +} func (db *DatabaseMock) RemoveCronByID(cronID string) error { - if db.ReturnError == "RemoveCronByID" { return errors.New("mock error") } + if db.ReturnError == "RemoveCronByID" { + return errors.New("mock error") + } return nil } -func (db *DatabaseMock) UpdateCron(cronID string, nextRun time.Time, lastRun time.Time, processGraphID string) error { return nil } -func (db *DatabaseMock) GetCronByName(colonyName string, cronName string) (*core.Cron, error) { return nil, nil } +func (db *DatabaseMock) UpdateCron(cronID string, nextRun time.Time, lastRun time.Time, processGraphID string) error { + return nil +} +func (db *DatabaseMock) GetCronByName(colonyName string, cronName string) (*core.Cron, error) { + return nil, nil +} func (db *DatabaseMock) RemoveAllCronsByColonyName(colonyName string) error { return nil } -// LogDatabase interface -func (db *DatabaseMock) AddLog(processID string, colonyName string, executorName string, timestamp int64, msg string) error { return nil } -func (db *DatabaseMock) GetLogsByProcessID(processID string, limit int) ([]*core.Log, error) { return nil, nil } -func (db *DatabaseMock) GetLogsByProcessIDSince(processID string, limit int, since int64) ([]*core.Log, error) { return nil, nil } -func (db *DatabaseMock) GetLogsByExecutor(executorName string, limit int) ([]*core.Log, error) { return nil, nil } -func (db *DatabaseMock) GetLogsByExecutorSince(executorName string, limit int, since int64) ([]*core.Log, error) { return nil, nil } -func (db *DatabaseMock) GetLogsByProcessIDLatest(processID string, limit int) ([]*core.Log, error) { return nil, nil } -func (db *DatabaseMock) GetLogsByExecutorLatest(executorName string, limit int) ([]*core.Log, error) { return nil, nil } +// LogDatabase interface +func (db *DatabaseMock) AddLog(processID string, colonyName string, executorName string, timestamp int64, msg string) error { + return nil +} +func (db *DatabaseMock) GetLogsByProcessID(processID string, limit int) ([]*core.Log, error) { + return nil, nil +} +func (db *DatabaseMock) GetLogsByProcessIDSince(processID string, limit int, since int64) ([]*core.Log, error) { + return nil, nil +} +func (db *DatabaseMock) GetLogsByExecutor(executorName string, limit int) ([]*core.Log, error) { + return nil, nil +} +func (db *DatabaseMock) GetLogsByExecutorSince(executorName string, limit int, since int64) ([]*core.Log, error) { + return nil, nil +} +func (db *DatabaseMock) GetLogsByProcessIDLatest(processID string, limit int) ([]*core.Log, error) { + return nil, nil +} +func (db *DatabaseMock) GetLogsByExecutorLatest(executorName string, limit int) ([]*core.Log, error) { + return nil, nil +} func (db *DatabaseMock) RemoveLogsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) CountLogs(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) SearchLogs(colonyName string, text string, days int, count int) ([]*core.Log, error) { return nil, nil } +func (db *DatabaseMock) CountLogs(colonyName string) (int, error) { return 0, nil } +func (db *DatabaseMock) SearchLogs(colonyName string, text string, days int, count int) ([]*core.Log, error) { + return nil, nil +} // FileDatabase interface func (db *DatabaseMock) AddFile(file *core.File) error { return nil } -func (db *DatabaseMock) GetFileByID(colonyName string, fileID string) (*core.File, error) { return nil, nil } -func (db *DatabaseMock) GetLatestFileByName(colonyName string, label string, name string) ([]*core.File, error) { return nil, nil } -func (db *DatabaseMock) GetFileByName(colonyName string, label string, name string) ([]*core.File, error) { return nil, nil } -func (db *DatabaseMock) GetFilenamesByLabel(colonyName string, label string) ([]string, error) { return nil, nil } -func (db *DatabaseMock) GetFileDataByLabel(colonyName string, label string) ([]*core.FileData, error) { return nil, nil } +func (db *DatabaseMock) GetFileByID(colonyName string, fileID string) (*core.File, error) { + return nil, nil +} +func (db *DatabaseMock) GetLatestFileByName(colonyName string, label string, name string) ([]*core.File, error) { + return nil, nil +} +func (db *DatabaseMock) GetFileByName(colonyName string, label string, name string) ([]*core.File, error) { + return nil, nil +} +func (db *DatabaseMock) GetFilenamesByLabel(colonyName string, label string) ([]string, error) { + return nil, nil +} +func (db *DatabaseMock) GetFileDataByLabel(colonyName string, label string) ([]*core.FileData, error) { + return nil, nil +} func (db *DatabaseMock) GetFileLabels(colonyName string) ([]*core.Label, error) { return nil, nil } -func (db *DatabaseMock) GetFileLabelsByName(colonyName string, name string, exact bool) ([]*core.Label, error) { return nil, nil } -func (db *DatabaseMock) GetFilesByColonyName(colonyName string) ([]*core.File, error) { return nil, nil } -func (db *DatabaseMock) GetFilesByProcessGraphID(processGraphID string) ([]*core.File, error) { return nil, nil } -func (db *DatabaseMock) GetFiles() ([]*core.File, error) { return nil, nil } -func (db *DatabaseMock) UpdateFile(file *core.File) error { return nil } +func (db *DatabaseMock) GetFileLabelsByName(colonyName string, name string, exact bool) ([]*core.Label, error) { + return nil, nil +} +func (db *DatabaseMock) GetFilesByColonyName(colonyName string) ([]*core.File, error) { + return nil, nil +} +func (db *DatabaseMock) GetFilesByProcessGraphID(processGraphID string) ([]*core.File, error) { + return nil, nil +} +func (db *DatabaseMock) GetFiles() ([]*core.File, error) { return nil, nil } +func (db *DatabaseMock) UpdateFile(file *core.File) error { return nil } func (db *DatabaseMock) RemoveFileByID(colonyName string, fileID string) error { return nil } -func (db *DatabaseMock) RemoveFileByName(colonyName string, label string, name string) error { return nil } +func (db *DatabaseMock) RemoveFileByName(colonyName string, label string, name string) error { + return nil +} func (db *DatabaseMock) CountFiles(colonyName string) (int, error) { return 0, nil } -func (db *DatabaseMock) CountFilesWithLabel(colonyName string, label string) (int, error) { return 0, nil } - +func (db *DatabaseMock) CountFilesWithLabel(colonyName string, label string) (int, error) { + return 0, nil +} // SnapshotDatabase interface -func (db *DatabaseMock) CreateSnapshot(colonyName string, label string, name string) (*core.Snapshot, error) { return nil, nil } -func (db *DatabaseMock) GetSnapshotByID(colonyName string, snapshotID string) (*core.Snapshot, error) { return nil, nil } -func (db *DatabaseMock) GetSnapshotsByColonyName(colonyName string) ([]*core.Snapshot, error) { return nil, nil } +func (db *DatabaseMock) CreateSnapshot(colonyName string, label string, name string) (*core.Snapshot, error) { + return nil, nil +} +func (db *DatabaseMock) GetSnapshotByID(colonyName string, snapshotID string) (*core.Snapshot, error) { + return nil, nil +} +func (db *DatabaseMock) GetSnapshotsByColonyName(colonyName string) ([]*core.Snapshot, error) { + return nil, nil +} func (db *DatabaseMock) RemoveSnapshotByID(colonyName string, snapshotID string) error { return nil } -func (db *DatabaseMock) GetSnapshotByName(colonyName string, name string) (*core.Snapshot, error) { return nil, nil } +func (db *DatabaseMock) GetSnapshotByName(colonyName string, name string) (*core.Snapshot, error) { + return nil, nil +} func (db *DatabaseMock) RemoveSnapshotByName(colonyName string, name string) error { return nil } -func (db *DatabaseMock) RemoveSnapshotsByColonyName(colonyName string) error { return nil } -func (db *DatabaseMock) CountSnapshots() (int, error) { return 0, nil } - +func (db *DatabaseMock) RemoveSnapshotsByColonyName(colonyName string) error { return nil } +func (db *DatabaseMock) CountSnapshots() (int, error) { return 0, nil } // SecurityDatabase interface func (db *DatabaseMock) SetServerID(oldServerID, newServerID string) error { return nil } -func (db *DatabaseMock) GetServerID() (string, error) { return "", nil } -func (db *DatabaseMock) ChangeColonyID(colonyName string, oldColonyID, newColonyID string) error { return nil } -func (db *DatabaseMock) ChangeUserID(colonyName string, oldUserID, newUserID string) error { return nil } -func (db *DatabaseMock) ChangeExecutorID(colonyName string, oldExecutorID, newExecutorID string) error { return nil } +func (db *DatabaseMock) GetServerID() (string, error) { return "", nil } +func (db *DatabaseMock) ChangeColonyID(colonyName string, oldColonyID, newColonyID string) error { + return nil +} +func (db *DatabaseMock) ChangeUserID(colonyName string, oldUserID, newUserID string) error { + return nil +} +func (db *DatabaseMock) ChangeExecutorID(colonyName string, oldExecutorID, newExecutorID string) error { + return nil +} // BlueprintDatabase interface - BlueprintDefinition methods func (db *DatabaseMock) AddBlueprintDefinition(sd *core.BlueprintDefinition) error { return nil } -func (db *DatabaseMock) GetBlueprintDefinitionByID(id string) (*core.BlueprintDefinition, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintDefinitionByName(namespace, name string) (*core.BlueprintDefinition, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintDefinitions() ([]*core.BlueprintDefinition, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintDefinitionsByNamespace(namespace string) ([]*core.BlueprintDefinition, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintDefinitionsByGroup(group string) ([]*core.BlueprintDefinition, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintDefinitionByKind(kind string) (*core.BlueprintDefinition, error) { return nil, nil } +func (db *DatabaseMock) GetBlueprintDefinitionByID(id string) (*core.BlueprintDefinition, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintDefinitionByName(namespace, name string) (*core.BlueprintDefinition, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintDefinitions() ([]*core.BlueprintDefinition, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintDefinitionsByNamespace(namespace string) ([]*core.BlueprintDefinition, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintDefinitionsByGroup(group string) ([]*core.BlueprintDefinition, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintDefinitionByKind(kind string) (*core.BlueprintDefinition, error) { + return nil, nil +} func (db *DatabaseMock) UpdateBlueprintDefinition(sd *core.BlueprintDefinition) error { return nil } -func (db *DatabaseMock) RemoveBlueprintDefinitionByID(id string) error { return nil } +func (db *DatabaseMock) RemoveBlueprintDefinitionByID(id string) error { return nil } func (db *DatabaseMock) RemoveBlueprintDefinitionByName(namespace, name string) error { return nil } -func (db *DatabaseMock) CountBlueprintDefinitions() (int, error) { return 0, nil } +func (db *DatabaseMock) CountBlueprintDefinitions() (int, error) { return 0, nil } // BlueprintDatabase interface - Blueprint methods -func (db *DatabaseMock) AddBlueprint(blueprint *core.Blueprint) error { return nil } +func (db *DatabaseMock) AddBlueprint(blueprint *core.Blueprint) error { return nil } func (db *DatabaseMock) GetBlueprintByID(id string) (*core.Blueprint, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintByName(namespace, name string) (*core.Blueprint, error) { return nil, nil } +func (db *DatabaseMock) GetBlueprintByName(namespace, name string) (*core.Blueprint, error) { + return nil, nil +} func (db *DatabaseMock) GetBlueprints() ([]*core.Blueprint, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintsByNamespace(namespace string) ([]*core.Blueprint, error) { return nil, nil } +func (db *DatabaseMock) GetBlueprintsByNamespace(namespace string) ([]*core.Blueprint, error) { + return nil, nil +} func (db *DatabaseMock) GetBlueprintsByKind(kind string) ([]*core.Blueprint, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintsByNamespaceAndKind(namespace, kind string) ([]*core.Blueprint, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintsByNamespaceKindAndLocation(namespace, kind, locationName string) ([]*core.Blueprint, error) { return nil, nil } +func (db *DatabaseMock) GetBlueprintsByNamespaceAndKind(namespace, kind string) ([]*core.Blueprint, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintsByNamespaceKindAndLocation(namespace, kind, locationName string) ([]*core.Blueprint, error) { + return nil, nil +} func (db *DatabaseMock) UpdateBlueprint(blueprint *core.Blueprint) error { return nil } -func (db *DatabaseMock) UpdateBlueprintStatus(id string, status map[string]interface{}) error { return nil } -func (db *DatabaseMock) RemoveBlueprintByID(id string) error { return nil } -func (db *DatabaseMock) RemoveBlueprintByName(namespace, name string) error { return nil } -func (db *DatabaseMock) RemoveBlueprintsByNamespace(namespace string) error { return nil } -func (db *DatabaseMock) CountBlueprints() (int, error) { return 0, nil } +func (db *DatabaseMock) UpdateBlueprintStatus(id string, status map[string]interface{}) error { + return nil +} +func (db *DatabaseMock) RemoveBlueprintByID(id string) error { return nil } +func (db *DatabaseMock) RemoveBlueprintByName(namespace, name string) error { return nil } +func (db *DatabaseMock) RemoveBlueprintsByNamespace(namespace string) error { return nil } +func (db *DatabaseMock) CountBlueprints() (int, error) { return 0, nil } func (db *DatabaseMock) CountBlueprintsByNamespace(namespace string) (int, error) { return 0, nil } func (db *DatabaseMock) AddBlueprintHistory(history *core.BlueprintHistory) error { return nil } -func (db *DatabaseMock) GetBlueprintHistory(blueprintID string, limit int) ([]*core.BlueprintHistory, error) { return nil, nil } -func (db *DatabaseMock) GetBlueprintHistoryByGeneration(blueprintID string, generation int64) (*core.BlueprintHistory, error) { return nil, nil } +func (db *DatabaseMock) GetBlueprintHistory(blueprintID string, limit int) ([]*core.BlueprintHistory, error) { + return nil, nil +} +func (db *DatabaseMock) GetBlueprintHistoryByGeneration(blueprintID string, generation int64) (*core.BlueprintHistory, error) { + return nil, nil +} func (db *DatabaseMock) RemoveBlueprintHistory(blueprintID string) error { return nil } // Implement the database.Database interface -func (db *DatabaseMock) CreateTables() error { return nil } -func (db *DatabaseMock) DropTables() error { return nil } -func (db *DatabaseMock) Close() { } -func (db *DatabaseMock) Initialize() error { return nil } -func (db *DatabaseMock) Drop() error { return nil } -func (db *DatabaseMock) Lock(timeout int) error { return nil } -func (db *DatabaseMock) Unlock() error { return nil } +func (db *DatabaseMock) CreateTables() error { return nil } +func (db *DatabaseMock) DropTables() error { return nil } +func (db *DatabaseMock) Close() {} +func (db *DatabaseMock) Initialize() error { return nil } +func (db *DatabaseMock) Drop() error { return nil } +func (db *DatabaseMock) Lock(timeout int) error { return nil } +func (db *DatabaseMock) Unlock() error { return nil } func (db *DatabaseMock) ApplyRetentionPolicy(retentionPeriod int64) error { return nil } // Test utility functions -func createFakeColoniesController() (*ColoniesController, *DatabaseMock) { - // Use atomic counter to get unique ports for each test to avoid "address already in use" errors - portOffset := atomic.AddInt32(&portCounter, 1) - etcdClientPort := 24100 + int(portOffset)*10 - etcdPeerPort := 23100 + int(portOffset)*10 - relayPort := 25100 + int(portOffset)*10 - nodeName := fmt.Sprintf("etcd-%d", portOffset) - dataPath := fmt.Sprintf("/tmp/colonies/etcd-%d", portOffset) - - node := cluster.Node{Name: nodeName, Host: "localhost", EtcdClientPort: etcdClientPort, EtcdPeerPort: etcdPeerPort, RelayPort: relayPort, APIPort: constants.TESTPORT} +func newTestColoniesController(db database.Database, name string) *ColoniesController { + // Unique node names keep etcd data directories apart; dynamic ports and + // temp dirs let test packages run in parallel + offset := atomic.AddInt32(&portCounter, 1) + reserved := utils.ReservePortsOrPanic(4) + nodeName := fmt.Sprintf("%s-%d", name, offset) + dataPath, err := os.MkdirTemp("", "colonies-etcd-") + if err != nil { + utils.ReleasePorts(reserved) + panic(err) + } + + node := cluster.Node{Name: nodeName, Host: "localhost", EtcdClientPort: reserved[0].Port(), EtcdPeerPort: reserved[1].Port(), RelayPort: reserved[2].Port(), APIPort: reserved[3].Port()} clusterConfig := cluster.Config{} clusterConfig.AddNode(node) + // CreateColoniesController binds the etcd and relay ports; the API port is + // never bound by a bare controller + utils.ReleasePorts(reserved) + return CreateColoniesController(db, node, clusterConfig, dataPath, constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) +} + +func createFakeColoniesController() (*ColoniesController, *DatabaseMock) { dbMock := &DatabaseMock{} - return CreateColoniesController(dbMock, node, clusterConfig, dataPath, constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second), dbMock + return newTestColoniesController(dbMock, "etcd"), dbMock } func createTestColoniesController(db *postgresql.PQDatabase) *ColoniesController { - node := cluster.Node{Name: "test", Host: "localhost", EtcdClientPort: 24101, EtcdPeerPort: 23101, RelayPort: 25101, APIPort: constants.TESTPORT} - clusterConfig := cluster.Config{} - clusterConfig.AddNode(node) - return CreateColoniesController(db, node, clusterConfig, "/tmp/colonies/etcd_test", constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) + return newTestColoniesController(db, "test") } func createTestColoniesController2(db *postgresql.PQDatabase) *ColoniesController { - node := cluster.Node{Name: "test2", Host: "localhost", EtcdClientPort: 24102, EtcdPeerPort: 23102, RelayPort: 25102, APIPort: constants.TESTPORT} - clusterConfig := cluster.Config{} - clusterConfig.AddNode(node) - return CreateColoniesController(db, node, clusterConfig, "/tmp/colonies/etcd_test2", constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) -} \ No newline at end of file + return newTestColoniesController(db, "test2") +} diff --git a/pkg/server/handlers/realtime/handler_test.go b/pkg/server/handlers/realtime/handler_test.go index 895381436..8710157c9 100644 --- a/pkg/server/handlers/realtime/handler_test.go +++ b/pkg/server/handlers/realtime/handler_test.go @@ -190,7 +190,9 @@ func TestSubscribeChangeStateProcess2(t *testing.T) { err = client.Close(assignedProcess.ID, env.Executor1PrvKey) assert.Nil(t, err) - time.Sleep(5 * time.Second) + // The process state is committed once Close returns; a short pause lets + // any in-flight events drain before subscribing after the fact + time.Sleep(500 * time.Millisecond) subscription, err := client.SubscribeProcess(env.Colony1Name, addedProcess.ID, diff --git a/pkg/server/server_manager_test.go b/pkg/server/server_manager_test.go index 1b921075a..41acf3a3e 100644 --- a/pkg/server/server_manager_test.go +++ b/pkg/server/server_manager_test.go @@ -9,22 +9,39 @@ import ( "github.com/colonyos/colonies/pkg/constants" "github.com/colonyos/colonies/pkg/database/postgresql" "github.com/colonyos/colonies/pkg/security/crypto" + "github.com/colonyos/colonies/pkg/utils" "github.com/stretchr/testify/assert" ) +// testServerManagerNode returns a single-node cluster config with +// kernel-assigned ports so tests can run in parallel. The ports stay reserved +// until the returned release function is called, which tests should do right +// before StartAll so no other process is handed the same port in between. +// Unreleased reservations are released when the test ends. +func testServerManagerNode(t *testing.T) (cluster.Node, func()) { + t.Helper() + reserved, err := utils.ReservePorts(4) + if err != nil { + t.Fatalf("failed to reserve ports: %v", err) + } + release := func() { utils.ReleasePorts(reserved) } + t.Cleanup(release) + return cluster.Node{ + Name: "test-node", + Host: "localhost", + EtcdClientPort: reserved[0].Port(), + EtcdPeerPort: reserved[1].Port(), + RelayPort: reserved[2].Port(), + APIPort: reserved[3].Port(), + }, release +} + func TestServerManagerCreation(t *testing.T) { db, err := postgresql.PrepareTests() assert.Nil(t, err) defer db.Close() - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT, - } + node, _ := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -32,7 +49,7 @@ func TestServerManagerCreation(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -46,14 +63,7 @@ func TestServerManagerBackendFactoryRegistration(t *testing.T) { assert.Nil(t, err) defer db.Close() - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT, - } + node, _ := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -61,7 +71,7 @@ func TestServerManagerBackendFactoryRegistration(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -88,14 +98,7 @@ func TestServerManagerConfigManagement(t *testing.T) { assert.Nil(t, err) defer db.Close() - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT, - } + node, _ := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -103,7 +106,7 @@ func TestServerManagerConfigManagement(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -111,16 +114,16 @@ func TestServerManagerConfigManagement(t *testing.T) { // Add gin server config ginConfig := &ServerConfig{ BackendType: GinBackendType, - Port: constants.TESTPORT + 100, - TLS: false, - TLSPrivateKeyPath: "", - TLSCertPath: "", - ExclusiveAssign: true, + Port: node.APIPort, + TLS: false, + TLSPrivateKeyPath: "", + TLSCertPath: "", + ExclusiveAssign: true, AllowExecutorReregister: false, - Retention: false, - RetentionPolicy: -1, - RetentionPeriod: 500, - Enabled: true, + Retention: false, + RetentionPolicy: -1, + RetentionPeriod: 500, + Enabled: true, } err = sm.AddServerConfig(ginConfig) @@ -135,7 +138,7 @@ func TestServerManagerConfigManagement(t *testing.T) { sm.running = true err = sm.AddServerConfig(&ServerConfig{ BackendType: "test", - Enabled: true, + Enabled: true, }) assert.NotNil(t, err) assert.Contains(t, err.Error(), "cannot add server config while server manager is running") @@ -155,14 +158,7 @@ func TestServerManagerLifecycle(t *testing.T) { err = db.SetServerID("", serverID) assert.Nil(t, err) - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT + 300, - } + node, releasePorts := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -170,7 +166,7 @@ func TestServerManagerLifecycle(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -182,22 +178,23 @@ func TestServerManagerLifecycle(t *testing.T) { ginConfig := &ServerConfig{ BackendType: GinBackendType, - Port: constants.TESTPORT + 300, - TLS: false, - TLSPrivateKeyPath: "", - TLSCertPath: "", - ExclusiveAssign: true, + Port: node.APIPort, + TLS: false, + TLSPrivateKeyPath: "", + TLSCertPath: "", + ExclusiveAssign: true, AllowExecutorReregister: false, - Retention: false, - RetentionPolicy: -1, - RetentionPeriod: 500, - Enabled: true, + Retention: false, + RetentionPolicy: -1, + RetentionPeriod: 500, + Enabled: true, } err = sm.AddServerConfig(ginConfig) assert.Nil(t, err) // Start all servers + releasePorts() err = sm.StartAll() assert.Nil(t, err) assert.True(t, sm.IsRunning()) @@ -217,7 +214,7 @@ func TestServerManagerLifecycle(t *testing.T) { assert.True(t, exists) assert.Equal(t, GinBackendType, ginStatus.BackendType) assert.True(t, ginStatus.Running) - assert.Equal(t, constants.TESTPORT+300, ginStatus.Port) + assert.Equal(t, node.APIPort, ginStatus.Port) // Get specific server ginServer, exists := sm.GetServer(GinBackendType) @@ -226,6 +223,7 @@ func TestServerManagerLifecycle(t *testing.T) { assert.True(t, ginServer.IsRunning()) // Try to start again - should fail + releasePorts() err = sm.StartAll() assert.NotNil(t, err) assert.Contains(t, err.Error(), "already running") @@ -245,14 +243,7 @@ func TestServerManagerMissingFactory(t *testing.T) { assert.Nil(t, err) defer db.Close() - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT + 400, - } + node, releasePorts := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -260,7 +251,7 @@ func TestServerManagerMissingFactory(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -268,14 +259,15 @@ func TestServerManagerMissingFactory(t *testing.T) { // Add config without registering factory ginConfig := &ServerConfig{ BackendType: GinBackendType, - Port: constants.TESTPORT + 400, - Enabled: true, + Port: node.APIPort, + Enabled: true, } err = sm.AddServerConfig(ginConfig) assert.Nil(t, err) // Start should fail due to missing factory + releasePorts() err = sm.StartAll() assert.NotNil(t, err) assert.Contains(t, err.Error(), "no factory registered") @@ -295,14 +287,7 @@ func TestServerManagerStopTimeout(t *testing.T) { err = db.SetServerID("", serverID) assert.Nil(t, err) - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT + 500, - } + node, releasePorts := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -310,7 +295,7 @@ func TestServerManagerStopTimeout(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -322,22 +307,23 @@ func TestServerManagerStopTimeout(t *testing.T) { ginConfig := &ServerConfig{ BackendType: GinBackendType, - Port: constants.TESTPORT + 500, - TLS: false, - TLSPrivateKeyPath: "", - TLSCertPath: "", - ExclusiveAssign: true, + Port: node.APIPort, + TLS: false, + TLSPrivateKeyPath: "", + TLSCertPath: "", + ExclusiveAssign: true, AllowExecutorReregister: false, - Retention: false, - RetentionPolicy: -1, - RetentionPeriod: 500, - Enabled: true, + Retention: false, + RetentionPolicy: -1, + RetentionPeriod: 500, + Enabled: true, } err = sm.AddServerConfig(ginConfig) assert.Nil(t, err) // Start servers + releasePorts() err = sm.StartAll() assert.Nil(t, err) @@ -364,14 +350,7 @@ func TestServerManagerHealthCheck(t *testing.T) { err = db.SetServerID("", serverID) assert.Nil(t, err) - node := cluster.Node{ - Name: "test-node", - Host: "localhost", - EtcdClientPort: 24100, - EtcdPeerPort: 23100, - RelayPort: 25100, - APIPort: constants.TESTPORT + 600, - } + node, releasePorts := testServerManagerNode(t) clusterConfig := cluster.Config{} clusterConfig.AddNode(node) @@ -379,7 +358,7 @@ func TestServerManagerHealthCheck(t *testing.T) { db, node, clusterConfig, - "/tmp/colonies/etcd", + t.TempDir(), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, ) @@ -397,21 +376,22 @@ func TestServerManagerHealthCheck(t *testing.T) { ginConfig := &ServerConfig{ BackendType: GinBackendType, - Port: constants.TESTPORT + 600, - TLS: false, - TLSPrivateKeyPath: "", - TLSCertPath: "", - ExclusiveAssign: true, + Port: node.APIPort, + TLS: false, + TLSPrivateKeyPath: "", + TLSCertPath: "", + ExclusiveAssign: true, AllowExecutorReregister: false, - Retention: false, - RetentionPolicy: -1, - RetentionPeriod: 500, - Enabled: true, + Retention: false, + RetentionPolicy: -1, + RetentionPeriod: 500, + Enabled: true, } err = sm.AddServerConfig(ginConfig) assert.Nil(t, err) + releasePorts() err = sm.StartAll() assert.Nil(t, err) @@ -424,4 +404,4 @@ func TestServerManagerHealthCheck(t *testing.T) { // Cleanup err = sm.StopAll(5 * time.Second) assert.Nil(t, err) -} \ No newline at end of file +} diff --git a/pkg/server/test_utils.go b/pkg/server/test_utils.go index fc21fba9c..0ade6d535 100644 --- a/pkg/server/test_utils.go +++ b/pkg/server/test_utils.go @@ -2,11 +2,13 @@ package server import ( "context" + "errors" "fmt" "io/ioutil" "math/rand" - "os" + "net/http" "strconv" + "strings" "testing" "time" @@ -18,7 +20,6 @@ import ( "github.com/colonyos/colonies/pkg/database/postgresql" "github.com/colonyos/colonies/pkg/rpc" "github.com/colonyos/colonies/pkg/security/crypto" - "github.com/colonyos/colonies/pkg/server/controllers" "github.com/colonyos/colonies/pkg/utils" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -206,8 +207,16 @@ func prepareTests(t *testing.T) (*client.ColoniesClient, *Server, string, chan b } func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesClient, *Server, string, chan bool) { - os.RemoveAll("/tmp/colonies") - client := client.CreateColoniesClient(constants.TESTHOST, constants.TESTPORT, Insecure, SkipTLSVerify) + // Dynamic ports and a per-test etcd data directory allow test packages to + // run in parallel without colliding on fixed ports or /tmp/colonies. The + // ports stay reserved until just before each component binds, so parallel + // test processes cannot be handed the same port during the slow parts of + // the setup (database preparation, etcd startup). + reserved := utils.ReservePortsOrPanic(4) + apiPort, etcdClientPort, etcdPeerPort, relayPort := reserved[0].Port(), reserved[1].Port(), reserved[2].Port(), reserved[3].Port() + etcdDataPath := t.TempDir() + + client := client.CreateColoniesClient(constants.TESTHOST, apiPort, Insecure, SkipTLSVerify) db, err := postgresql.PrepareTests() assert.Nil(t, err) @@ -221,14 +230,23 @@ func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesCl err = db.SetServerID("", serverID) assert.Nil(t, err) - node := cluster.Node{Name: "etcd", Host: "localhost", EtcdClientPort: 24100, EtcdPeerPort: 23100, RelayPort: 25100, APIPort: constants.TESTPORT} + node := cluster.Node{Name: "etcd", Host: "localhost", EtcdClientPort: etcdClientPort, EtcdPeerPort: etcdPeerPort, RelayPort: relayPort, APIPort: apiPort} clusterConfig := cluster.Config{} clusterConfig.AddNode(node) - server := CreateServer(db, constants.TESTPORT, EnableTLS, "", "", node, clusterConfig, "/tmp/colonies/etcd", constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, false, retention, 1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) + reserved[1].Release() + reserved[2].Release() + reserved[3].Release() + server := CreateServer(db, apiPort, EnableTLS, "", "", node, clusterConfig, etcdDataPath, constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, false, retention, 1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) done := make(chan bool) + reserved[0].Release() go func() { - server.ServeForever() + err := server.ServeForever() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + // A silent bind failure would leave the client talking to some + // other process's server; fail loudly instead + panic("test server failed to serve: " + err.Error()) + } db.Close() done <- true }() @@ -236,20 +254,6 @@ func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesCl return client, server, serverPrvKey, done } -func createTestColoniesController(db database.Database) *controllers.ColoniesController { - node := cluster.Node{Name: "etcd", Host: "localhost", EtcdClientPort: 24100, EtcdPeerPort: 23100, RelayPort: 25100, APIPort: constants.TESTPORT} - clusterConfig := cluster.Config{} - clusterConfig.AddNode(node) - return controllers.CreateColoniesController(db, node, clusterConfig, "/tmp/colonies/etcd", constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) -} - -func createTestColoniesController2(db database.Database) *controllers.ColoniesController { - node := cluster.Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 26100, EtcdPeerPort: 27100, RelayPort: 28100, APIPort: constants.TESTPORT} - clusterConfig := cluster.Config{} - clusterConfig.AddNode(node) - return controllers.CreateColoniesController(db, node, clusterConfig, "/tmp/colonies/etcd", constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) -} - func GenerateDiamondtWorkflowSpec(colonyName string) *core.WorkflowSpec { // task1 // / \ @@ -371,71 +375,34 @@ type ServerInfo struct { } func StartCluster(t *testing.T, db database.Database, size int) []ServerInfo { - os.RemoveAll("/tmp/colonies") - gin.SetMode(gin.ReleaseMode) - gin.DefaultWriter = ioutil.Discard - - clusterConfig := cluster.Config{} - for i := 0; i < size; i++ { - node := cluster.Node{ - Name: "etcd" + strconv.Itoa(i), - Host: "localhost", - EtcdClientPort: 21000 + i, - EtcdPeerPort: 22000 + i, - RelayPort: 23000 + i, - APIPort: 24000 + i} - clusterConfig.AddNode(node) - } - - crypto := crypto.CreateCrypto() - serverPrvKey, err := crypto.GeneratePrivateKey() - assert.Nil(t, err) - serverID, err := crypto.GenerateID(serverPrvKey) - assert.Nil(t, err) - - db.SetServerID("", serverID) - - sChan := make(chan ServerInfo) - for i, node := range clusterConfig.Nodes { - go func(i int, node cluster.Node) { - log.WithFields(log.Fields{"APIPort": node.APIPort}).Info("Starting ColoniesServer") - server := CreateServer(db, node.APIPort, false, "", "", node, clusterConfig, "/tmp/colonies/etcd"+strconv.Itoa(i), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, true, false, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) - done := make(chan struct{}) - s := ServerInfo{ServerID: serverID, ServerPrvKey: serverPrvKey, Server: server, Node: node, Done: done} - go func(i int) { - log.Info("ColoniesServer serving") - server.ServeForever() - log.Info("ColoniesServer stopped") - done <- struct{}{} - }(i) - sChan <- s - }(i, node) - } - - var servers []ServerInfo - for range clusterConfig.Nodes { - s := <-sChan - servers = append(servers, s) - } - - return servers + return startCluster(t, db, size, true) } // StartClusterDistributed creates a cluster with ExclusiveAssign=false for testing distributed assignment func StartClusterDistributed(t *testing.T, db database.Database, size int) []ServerInfo { - os.RemoveAll("/tmp/colonies") + return startCluster(t, db, size, false) +} + +func startCluster(t *testing.T, db database.Database, size int, exclusiveAssign bool) []ServerInfo { gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = ioutil.Discard + etcdDataPath := t.TempDir() + + // Ports stay reserved until just before each component binds so parallel + // test processes cannot be handed the same port while the cluster starts + reserved := utils.ReservePortsOrPanic(4 * size) + t.Cleanup(func() { utils.ReleasePorts(reserved) }) + clusterConfig := cluster.Config{} for i := 0; i < size; i++ { node := cluster.Node{ Name: "etcd" + strconv.Itoa(i), Host: "localhost", - EtcdClientPort: 21000 + i, - EtcdPeerPort: 22000 + i, - RelayPort: 23000 + i, - APIPort: 24000 + i} + EtcdClientPort: reserved[4*i].Port(), + EtcdPeerPort: reserved[4*i+1].Port(), + RelayPort: reserved[4*i+2].Port(), + APIPort: reserved[4*i+3].Port()} clusterConfig.AddNode(node) } @@ -451,13 +418,22 @@ func StartClusterDistributed(t *testing.T, db database.Database, size int) []Ser for i, node := range clusterConfig.Nodes { go func(i int, node cluster.Node) { log.WithFields(log.Fields{"APIPort": node.APIPort}).Info("Starting ColoniesServer") - // ExclusiveAssign=false for distributed assignment - server := CreateServer(db, node.APIPort, false, "", "", node, clusterConfig, "/tmp/colonies/etcd"+strconv.Itoa(i), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, false, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) + // CreateServer binds the etcd and relay ports + reserved[4*i].Release() + reserved[4*i+1].Release() + reserved[4*i+2].Release() + server := CreateServer(db, node.APIPort, false, "", "", node, clusterConfig, etcdDataPath+"/etcd"+strconv.Itoa(i), constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, exclusiveAssign, false, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) done := make(chan struct{}) s := ServerInfo{ServerID: serverID, ServerPrvKey: serverPrvKey, Server: server, Node: node, Done: done} + reserved[4*i+3].Release() go func(i int) { log.Info("ColoniesServer serving") - server.ServeForever() + err := server.ServeForever() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + // A silent bind failure would let WaitForCluster count + // another process's server as ours; fail loudly instead + panic("cluster server " + strconv.Itoa(i) + " failed to serve: " + err.Error()) + } log.Info("ColoniesServer stopped") done <- struct{}{} }(i) @@ -505,18 +481,35 @@ func WaitForServerToDie(t *testing.T, s ServerInfo) { } } +// transientProcessGraphError is the message the server returns when a graph is +// visible before all of its processes are. CreateProcessGraph inserts the graph +// first, so a poll can briefly land in that window; the assignment path retries +// graph resolution for the same reason. +const transientProcessGraphError = "Failed to iterate processgraph, process with ID=" + +// WaitForProcessGraphs polls until at least threshold waiting process graphs +// exist or the deadline passes, and returns the number of graphs seen on the +// last poll. The transient missing-process error is retried until the deadline; +// any other error fails the test immediately. func WaitForProcessGraphs(t *testing.T, c *client.ColoniesClient, colonyName string, generatorID string, executorPrvKey string, threshold int) int { var graphs []*core.ProcessGraph var err error - retries := 40 - for i := 0; i < retries; i++ { + deadline := time.Now().Add(40 * time.Second) + for { graphs, err = c.GetWaitingProcessGraphs(colonyName, 100, executorPrvKey) - assert.Nil(t, err) - if len(graphs) >= threshold { + if err != nil && !strings.Contains(err.Error(), transientProcessGraphError) { + assert.Nil(t, err) + break + } + if err == nil && len(graphs) >= threshold { + break + } + if time.Now().After(deadline) { + assert.Nil(t, err, "process graphs still not consistent at deadline") break } - time.Sleep(1 * time.Second) + time.Sleep(50 * time.Millisecond) } return len(graphs) diff --git a/pkg/utils/free_ports.go b/pkg/utils/free_ports.go new file mode 100644 index 000000000..e9c1ac0d2 --- /dev/null +++ b/pkg/utils/free_ports.go @@ -0,0 +1,108 @@ +package utils + +import ( + "net" + "strconv" +) + +// Ports are probed on the IPv4 wildcard address because that is where etcd, +// the relay server and the HTTP API bind. Probing localhost only would miss +// ports that are already in use on another interface. +const portProbeNetwork = "tcp4" +const portProbeAddr = "0.0.0.0:0" + +// FreePorts asks the kernel for n distinct free TCP ports. The listeners are +// closed before returning, so there is a small window in which another process +// could grab a port. Prefer ReservePorts when there is slow setup work between +// allocating a port and binding it. +func FreePorts(n int) ([]int, error) { + reservations, err := ReservePorts(n) + if err != nil { + return nil, err + } + + ports := make([]int, 0, n) + for _, r := range reservations { + ports = append(ports, r.Port()) + } + ReleasePorts(reservations) + + return ports, nil +} + +// FreePort returns a single free TCP port. +func FreePort() (int, error) { + ports, err := FreePorts(1) + if err != nil { + return 0, err + } + return ports[0], nil +} + +// FreePortsOrPanic is a convenience wrapper for test setup code without error +// returns. +func FreePortsOrPanic(n int) []int { + ports, err := FreePorts(n) + if err != nil { + panic("failed to allocate free ports: " + err.Error() + " (n=" + strconv.Itoa(n) + ")") + } + return ports +} + +// ReservedPort is a free TCP port that stays bound until Release is called, so +// no other process can be handed the same port in the meantime. Test setups +// that do slow work between allocating a port and binding it (starting etcd, +// preparing a database) should hold a reservation and release it just before +// the component binds, keeping the unbound window to microseconds. +type ReservedPort struct { + listener net.Listener + port int +} + +// Port returns the reserved port number. The port stays bound until Release. +func (r *ReservedPort) Port() int { + return r.port +} + +// Release closes the reservation so the port can be bound. Safe to call more +// than once. +func (r *ReservedPort) Release() { + if r.listener != nil { + r.listener.Close() + r.listener = nil + } +} + +// ReservePorts asks the kernel for n distinct free TCP ports and keeps them +// bound until each reservation is released. +func ReservePorts(n int) ([]*ReservedPort, error) { + reservations := make([]*ReservedPort, 0, n) + + for i := 0; i < n; i++ { + l, err := net.Listen(portProbeNetwork, portProbeAddr) + if err != nil { + ReleasePorts(reservations) + return nil, err + } + reservations = append(reservations, &ReservedPort{listener: l, port: l.Addr().(*net.TCPAddr).Port}) + } + + return reservations, nil +} + +// ReservePortsOrPanic is a convenience wrapper for test setup code without +// error returns. +func ReservePortsOrPanic(n int) []*ReservedPort { + reservations, err := ReservePorts(n) + if err != nil { + panic("failed to reserve free ports: " + err.Error() + " (n=" + strconv.Itoa(n) + ")") + } + return reservations +} + +// ReleasePorts releases every reservation in the slice. +func ReleasePorts(reservations []*ReservedPort) { + for _, r := range reservations { + r.Release() + } +} diff --git a/tests/smoke/main.go b/tests/smoke/main.go deleted file mode 100644 index 4040c959c..000000000 --- a/tests/smoke/main.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "os" - "strconv" - "time" - - "github.com/colonyos/colonies/pkg/client" - "github.com/colonyos/colonies/pkg/core" - "github.com/colonyos/colonies/pkg/security/crypto" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randStringRunes(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} - -func checkError(err error) { - if err != nil { - fmt.Println(err) - os.Exit(-1) - } -} - -func submitProcess(client *client.ColoniesClient, colonyID string, executorPrvKey string) { - funcSpec := core.FunctionSpec{ - Func: "test_func", - Args: []string{"arg1"}, - MaxWaitTime: 100, - MaxExecTime: 2, - MaxRetries: 10, - Conditions: core.Conditions{ColonyName: colonyID, ExecutorType: "bemisexecutor"}, - Env: make(map[string]string)} - - client.Submit(&funcSpec, executorPrvKey) -} - -func startCron(client *client.ColoniesClient, colonyID string, executorPrvKey string) { - funcSpec1 := core.FunctionSpec{ - Name: "cron_task1", - Func: "cron_test_func", - Args: []string{"arg1"}, - MaxWaitTime: -1, - MaxExecTime: 2, - MaxRetries: 10, - Conditions: core.Conditions{ColonyName: colonyID, ExecutorType: "bemisexecutor"}, - Env: make(map[string]string)} - - funcSpec2 := core.FunctionSpec{ - Name: "cron_task2", - Func: "cron_test_func", - Args: []string{"arg1"}, - MaxWaitTime: -1, - MaxExecTime: 2, - MaxRetries: 30, - Conditions: core.Conditions{ColonyName: colonyID, ExecutorType: "bemisexecutor"}, - Env: make(map[string]string)} - - workflowSpec := core.CreateWorkflowSpec(colonyID) - funcSpec2.AddDependency("cron_task1") - workflowSpec.AddFunctionSpec(&funcSpec1) - workflowSpec.AddFunctionSpec(&funcSpec2) - jsonStr, err := workflowSpec.ToJSON() - checkError(err) - - cron := core.CreateCron(colonyID, "test_cron1"+core.GenerateRandomID(), "1 * * * * *", -1, false, jsonStr) - _, err = client.AddCron(cron, executorPrvKey) - checkError(err) -} - -func startGenerator(client *client.ColoniesClient, colonyID string, executorPrvKey string) { - funcSpec1 := core.FunctionSpec{ - Name: "gen_task1", - Func: "gen_test_func", - Args: []string{"arg1"}, - MaxWaitTime: -1, - MaxExecTime: 2, - MaxRetries: 10, - Conditions: core.Conditions{ColonyName: colonyID, ExecutorType: "bemisexecutor"}, - Env: make(map[string]string)} - - funcSpec2 := core.FunctionSpec{ - Name: "gen_task2", - Func: "gen_test_func", - Args: []string{"arg1"}, - MaxWaitTime: -1, - MaxExecTime: 2, - MaxRetries: 30, - Conditions: core.Conditions{ColonyName: colonyID, ExecutorType: "bemisexecutor"}, - Env: make(map[string]string)} - - workflowSpec := core.CreateWorkflowSpec(colonyID) - funcSpec2.AddDependency("gen_task1") - workflowSpec.AddFunctionSpec(&funcSpec1) - workflowSpec.AddFunctionSpec(&funcSpec2) - jsonStr, err := workflowSpec.ToJSON() - checkError(err) - generator := core.CreateGenerator(colonyID, "test_genname"+core.GenerateRandomID(), jsonStr, 10) - - generator, err = client.AddGenerator(generator, executorPrvKey) - checkError(err) - - go func() { - for { - client.PackGenerator(generator.ID, randStringRunes(10), executorPrvKey) - } - }() -} - -func startExecutor(client *client.ColoniesClient, colonyID string, colonyPrvKey string) { - crypto := crypto.CreateCrypto() - executorPrvKey, err := crypto.GeneratePrivateKey() - checkError(err) - executorID, err := crypto.GenerateID(executorPrvKey) - checkError(err) - - executor := core.CreateExecutor(executorID, "bemisexecutor", core.GenerateRandomID(), colonyID, time.Now(), time.Now()) - - _, err = client.AddExecutor(executor, colonyPrvKey) - checkError(err) - - err = client.ApproveExecutor(executorID, colonyPrvKey) - checkError(err) - - go func() { - for { - assignedProcess, err := client.Assign(colonyID, 10, executorPrvKey) - if err == nil { - time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond) - client.Close(assignedProcess.ID, executorPrvKey) - } - } - }() -} - -func main() { - colonyID := os.Getenv("COLONIES_COLONY_ID") - colonyPrvKey := os.Getenv("COLONIES_COLONY_PRVKEY") - - serverHost := os.Getenv("COLONIES_SERVER_HOST") - serverPortEnvStr := os.Getenv("COLONIES_SERVER_PORT") - serverPort := -1 - var err error - if serverPortEnvStr != "" { - serverPort, err = strconv.Atoi(serverPortEnvStr) - checkError(err) - } - - tlsEnv := os.Getenv("COLONIES_TLS") - insecure := true - if tlsEnv == "true" { - insecure = false - } else if tlsEnv == "false" { - insecure = true - } - - executorPrvKey := os.Getenv("COLONIES_EXECUTOR_PRVKEY") - - client := client.CreateColoniesClient(serverHost, serverPort, insecure, true) - - for i := 0; i < 20; i++ { - startExecutor(client, colonyID, colonyPrvKey) - } - - startGenerator(client, colonyID, executorPrvKey) - startCron(client, colonyID, executorPrvKey) - - //start := time.Now().UnixNano() - for i := 0; i < 100000000; i++ { - //now := time.Now().UnixNano() - // delta := now - start - // start = time.Now().UnixNano() - //fmt.Println(i, " ", delta) - - submitProcess(client, colonyID, executorPrvKey) - } - - done := make(chan struct{}) - <-done -} From 2613f8106138c22962a40c8d1da5ae09dbe27258 Mon Sep 17 00:00:00 2001 From: Oskar Skoglund Date: Thu, 3 Sep 2026 13:48:52 +0200 Subject: [PATCH 3/6] Run CI on public GitHub runners Only run coverage on pushes to main, this keeps PR runs fast and prevents runs from failing in forks due to lacking API keys to publish the results. --- .github/workflows/go.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8c5726a84..16c699f12 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,12 +4,12 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, dev ] jobs: # Label of the container job runner-job: - runs-on: self-hosted + runs-on: ubuntu-latest # Service containers to run with `runner-job` services: @@ -36,10 +36,10 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v7 with: go-version: 1.25.5 @@ -59,7 +59,10 @@ jobs: COLONIES_DB_PASSWORD: rFcLGNkgsNtksg6Pgtn9CumL4xXBQ7 COLONIES_DB_HOST: localhost + # Coverage runs the suite a second time, so it is limited to pushes to main + # and skipped on pull requests to keep PR runs fast - name: Generate coverage + if: github.event_name == 'push' run: | # Get package list excluding monitoring PKGS=$(go list ./pkg/... | grep -v '/monitoring$' | tr '\n' ' ') @@ -84,11 +87,11 @@ jobs: COLONIES_DB_HOST: localhost - name: Upload coverage to Codecov + if: github.event_name == 'push' uses: codecov/codecov-action@v4 with: files: ./coverage.out fail_ci_if_error: false verbose: true env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file From f10e6fbb732134a84b7a46df8e987f76830567fc Mon Sep 17 00:00:00 2001 From: Oskar Skoglund Date: Thu, 24 Sep 2026 12:34:59 +0000 Subject: [PATCH 4/6] Remove the dependency on minio in favor of seaweedfs and replace the complex devcontainer setup with a more streamlined version matching what a developer without a devcontainer based workflow would use --- .devcontainer/devcontainer-lock.json | 9 ++++ .devcontainer/devcontainer.json | 32 ++++++++++--- .devcontainer/devenv.sh | 19 -------- .devcontainer/docker-compose.yml | 64 ------------------------- .devcontainer/init-entrypoint.sh | 35 -------------- .devcontainer/minio-entrypoint.sh | 4 -- .devcontainer/timescaledb-entrypoint.sh | 5 -- .github/workflows/go.yml | 20 ++++---- CLAUDE.md | 8 +++- Makefile | 16 ++++++- README.md | 2 +- docker-compose.env | 4 +- docker-compose.yml | 52 ++++++-------------- windowsenv.bat | 4 +- 14 files changed, 86 insertions(+), 188 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json delete mode 100755 .devcontainer/devenv.sh delete mode 100644 .devcontainer/docker-compose.yml delete mode 100755 .devcontainer/init-entrypoint.sh delete mode 100755 .devcontainer/minio-entrypoint.sh delete mode 100644 .devcontainer/timescaledb-entrypoint.sh diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..3b970b72c --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:4": { + "version": "4.1.1", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88", + "integrity": "sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a0ae74b89..48fe048cf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,9 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go . { - "name": "ColonyOS ", - "dockerComposeFile": [ - "docker-compose.yml" - ], - "service": "devcontainer", - "workspaceFolder": "/workspaces", - "postCreateCommand": "if [ -f .devcontainer/devenv.sh ]; then .devcontainer/devenv.sh; elif [ -f colonies/.devcontainer/devenv.sh ]; then colonies/.devcontainer/devenv.sh; fi" + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:2-1.27-trixie", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:4": { + "azureDnsAutoDetection": true, + "installDockerBuildx": true, + "iptablesSwitchAtRuntime": true, + "version": "latest", + "dockerDashComposeVersion": "latest", + "moby": false + } + } + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } \ No newline at end of file diff --git a/.devcontainer/devenv.sh b/.devcontainer/devenv.sh deleted file mode 100755 index 3e5f41d9b..000000000 --- a/.devcontainer/devenv.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -source /configuration/colonyos.env -source /configuration/minio.env - -echo "source /configuration/colonyos.env" >> /home/vscode/.bashrc -echo "source /configuration/minio.env" >> /home/vscode/.bashrc - -echo "loaded environment variables" - -go install github.com/minio/mc@latest - -until (mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}); do echo 'Waiting for MinIO...'; sleep 3; done; -echo 'MinIO is ready. Setting up...'; -mc admin user add myminio ${AWS_S3_ACCESSKEY} ${AWS_S3_SECRETKEY}; -mc admin policy attach myminio readwrite --user=${AWS_S3_ACCESSKEY}; -mc mb myminio/${AWS_S3_BUCKET}; -echo 'MinIO setup completed.'; \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index b942bd61f..000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,64 +0,0 @@ -services: - init: - image: colonyos/colonies:v1.9.0 - volumes: - - configuration:/configuration - - ./init-entrypoint.sh:/init-entrypoint.sh - command: ["/init-entrypoint.sh"] - - timescaledb: - image: timescale/timescaledb:latest-pg16 - hostname: timescaledb - environment: - POSTGRES_USER: colonies - PGDATA: /var/lib/postgresql/data - TZ: Europe/Stockholm - volumes: - - timescaledb_data:/var/lib/postgresql/data - - configuration:/configuration - - ./timescaledb-entrypoint.sh:/timescaledb-entrypoint.sh - depends_on: - init: - condition: service_completed_successfully - entrypoint: ["/bin/bash", "./timescaledb-entrypoint.sh"] - - minio: - image: minio/minio:latest - environment: - MINIO_ROOT_USER: colonies - volumes: - - minio_data:/var/lib/minio/data - - configuration:/configuration - - ./minio-entrypoint.sh:/minio-entrypoint.sh - depends_on: - init: - condition: service_completed_successfully - entrypoint: ["/bin/sh", "minio-entrypoint.sh"] - - devcontainer: - image: mcr.microsoft.com/devcontainers/go:1-1.24 - environment: - COLONIES_DB_HOST: timescaledb - COLONIES_DB_USER: colonies - COLONIES_SERVER_TLS: false - COLONIES_SERVER_PORT: 50080 - COLONIES_SERVER_HOST: localhost - COLONIES_COLONY_NAME: dev - MINIO_ROOT_USER: colonies - AWS_S3_ENDPOINT: minio:9000 - AWS_S3_BUCKET: colonies-dev - AWS_S3_TLS: false - AWS_S3_SKIPVERIFY: false - TZ: Europe/Stockholm - volumes: - - ../:/workspaces:cached - - configuration:/configuration - depends_on: - - timescaledb - - minio - command: ["sleep", "infinity"] - -volumes: - timescaledb_data: - configuration: - minio_data: diff --git a/.devcontainer/init-entrypoint.sh b/.devcontainer/init-entrypoint.sh deleted file mode 100755 index f55b32ede..000000000 --- a/.devcontainer/init-entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh - -if [ -f /configuration/postgres.env ]; then - exit 0 -fi - -DB_PASSWORD=$(head -c 16 /dev/urandom | base64) - -echo "export POSTGRES_PASSWORD=$DB_PASSWORD" >> /configuration/postgres.env -echo "export COLONIES_DB_PASSWORD=$DB_PASSWORD" >> /configuration/colonyos.env - -COLONIES_PRVKEY=$(colonies security generate 2>&1 | grep 'PrvKey=' | awk -F'PrvKey=' '{print $2}') -echo "export COLONIES_PRVKEY=$COLONIES_PRVKEY" >> /configuration/colonyos.env - -SERVER_KEYPAIR=$(colonies security generate 2>&1) -SERVER_ID=$(echo "$SERVER_KEYPAIR" | grep -o 'Id=[^ ]*' | awk -F'=' '{print $2}') -SERVER_PRVKEY=$(echo "$SERVER_KEYPAIR" | grep -o 'PrvKey=[^ ]*' | awk -F'=' '{print $2}') - -echo "export COLONIES_SERVER_ID=$SERVER_ID" >> /configuration/colonyos.env -echo "export COLONIES_SERVER_PRVKEY=$SERVER_PRVKEY" >> /configuration/colonyos.env - -COLONY_KEYPAIR=$(colonies security generate 2>&1) -COLONY_ID=$(echo "$COLONY_KEYPAIR" | grep -o 'Id=[^ ]*' | awk -F'=' '{print $2}') -COLONY_PRVKEY=$(echo "$COLONY_KEYPAIR" | grep -o 'PrvKey=[^ ]*' | awk -F'=' '{print $2}') - -echo "export COLONIES_COLONY_ID=$COLONY_ID" >> /configuration/colonyos.env -echo "export COLONIES_COLONY_PRVKEY=$COLONY_PRVKEY" >> /configuration/colonyos.env - -MINIO_ROOT_PASSWORD=$(head -c 16 /dev/urandom | base64) -AWS_S3_ACCESSKEY=$(head -c 16 /dev/urandom | base64 | tr -d '=.' ) -AWS_S3_SECRETKEY=$(head -c 32 /dev/urandom | base64 | tr -d '=.' ) - -echo "export MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD" >> /configuration/minio.env -echo "export AWS_S3_ACCESSKEY=$AWS_S3_ACCESSKEY" >> /configuration/minio.env -echo "export AWS_S3_SECRETKEY=$AWS_S3_SECRETKEY" >> /configuration/minio.env diff --git a/.devcontainer/minio-entrypoint.sh b/.devcontainer/minio-entrypoint.sh deleted file mode 100755 index 70ae776d0..000000000 --- a/.devcontainer/minio-entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -source /configuration/minio.env -minio server /var/lib/minio/data --console-address :9001 \ No newline at end of file diff --git a/.devcontainer/timescaledb-entrypoint.sh b/.devcontainer/timescaledb-entrypoint.sh deleted file mode 100644 index 60e8ea88e..000000000 --- a/.devcontainer/timescaledb-entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e - -source /configuration/postgres.env -exec docker-entrypoint.sh postgres diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 16c699f12..74816e972 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,12 +13,16 @@ jobs: # Service containers to run with `runner-job` services: - minio: - image: fclairamb/minio-github-actions + # S3 server for pkg/fs. SeaweedFS "mini" mode seeds the credentials and + # bucket from these variables and serves S3 on 8333 + seaweedfs: + image: chrislusf/seaweedfs:4.47 ports: - - 9000:9000 + - 9000:8333 env: - MINIO_DOMAIN: localhost + AWS_ACCESS_KEY_ID: testaccesskey + AWS_SECRET_ACCESS_KEY: testsecretkey + S3_BUCKET: test # Label used to access the service container postgres: # Docker Hub image @@ -50,8 +54,8 @@ jobs: run: TZ=Europe/Stockholm make test env: AWS_S3_ENDPOINT: localhost:9000 - AWS_S3_ACCESSKEY: minioadmin - AWS_S3_SECRETKEY: minioadmin + AWS_S3_ACCESSKEY: testaccesskey + AWS_S3_SECRETKEY: testsecretkey AWS_S3_TLS: false AWS_S3_SKIPVERIFY: false AWS_S3_BUCKET: test @@ -77,8 +81,8 @@ jobs: rm coverage.raw env: AWS_S3_ENDPOINT: localhost:9000 - AWS_S3_ACCESSKEY: minioadmin - AWS_S3_SECRETKEY: minioadmin + AWS_S3_ACCESSKEY: testaccesskey + AWS_S3_SECRETKEY: testsecretkey AWS_S3_TLS: false AWS_S3_SKIPVERIFY: false AWS_S3_BUCKET: test diff --git a/CLAUDE.md b/CLAUDE.md index ca46caa27..bd081d696 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ The deployment docker-compose files expect `colonyos/colonies:latest`. Using a d ### Testing ```bash make test # Run all tests: needs Postgres on localhost:5432 (make startdb) - # and an S3 server on localhost:9000 for pkg/fs + # and an S3 server on localhost:9000 for pkg/fs (make starts3) make github_test # Alias for test (used by CI) ``` @@ -36,9 +36,13 @@ database layer refuses to connect without TZ, and one core test expects a non-UTC zone). Test packages use per-process databases and dynamic ports, so they run in parallel. +The S3 server is SeaweedFS in mini mode. `make starts3` prints the `AWS_S3_*` +variables to export before running `make test`; without them the `pkg/fs` +tests fail with an empty endpoint error. + ### Development Environment ```bash -docker-compose up -d # Start Colonies server with dependencies (TimescaleDB, MinIO) +docker-compose up -d # Start Colonies server with dependencies (TimescaleDB, SeaweedFS) docker-compose down # Stop all services docker-compose logs -f # View logs ``` diff --git a/Makefile b/Makefile index 19490b69f..d7d6cb309 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,8 @@ build_cryptolib_ubuntu_2020: cd buildtools; ./build_cryptolib_ubuntu.sh # Runs all tests: needs Postgres on localhost:5432 (make startdb) and an S3 -# server on localhost:9000 for pkg/fs. Test packages use per-process databases +# server on localhost:9000 for pkg/fs (make starts3, then export the AWS_S3_* +# variables printed by that target). Test packages use per-process databases # and dynamic ports, so they run in parallel. test: @go test -race ./... @@ -61,3 +62,16 @@ nukedb: @docker rm $$(docker ps -aq --filter ancestor=timescale/timescaledb:latest-pg16) 2>/dev/null || true @docker volume rm $$(docker volume ls -q --filter dangling=true) 2>/dev/null || true @echo "TimescaleDB containers and volumes destroyed" + +# S3 server for pkg/fs tests. SeaweedFS mini mode seeds the credentials and +# bucket from the environment and serves S3 on 8333, mapped to 9000 here. +starts3: + docker run -d -p 9000:8333 -e AWS_ACCESS_KEY_ID=testaccesskey -e AWS_SECRET_ACCESS_KEY=testsecretkey -e S3_BUCKET=test --restart unless-stopped chrislusf/seaweedfs:4.47 + @echo "Export before running tests:" + @echo " export AWS_S3_ENDPOINT=localhost:9000 AWS_S3_ACCESSKEY=testaccesskey AWS_S3_SECRETKEY=testsecretkey AWS_S3_TLS=false AWS_S3_SKIPVERIFY=false AWS_S3_BUCKET=test" + +nukes3: + @echo "Nuking SeaweedFS containers..." + @docker stop $$(docker ps -aq --filter ancestor=chrislusf/seaweedfs:4.47) 2>/dev/null || true + @docker rm $$(docker ps -aq --filter ancestor=chrislusf/seaweedfs:4.47) 2>/dev/null || true + @echo "SeaweedFS containers destroyed" diff --git a/README.md b/README.md index ef0d45170..9b0087e05 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The [Colonies Dashboard](https://github.com/colonyos/dashboard) provides a web U - [Executors](https://github.com/colonyos/executors) - Pre-built executor implementations ## Development -The repository contains a development container configuration to simplify development environment setup. You can use it locally or in a [GitHub Codespace](https://docs.github.com/en/codespaces). The configuration will launch a TimescaleDB-insance for the ColonyOS database, a MinIO instance for the ColonyOS file system and the actual development container. It will automatically generate required credentials and keys unique to your environment, no furhter configuration needed. +The repository contains a development container configuration to simplify development environment setup. You can use it locally or in a [GitHub Codespace](https://docs.github.com/en/codespaces). It provides a Go toolchain with Docker-in-Docker, so the dependencies can be started inside the container: `make startdb` for TimescaleDB, `make starts3` for the SeaweedFS S3 server used by the file system, or `docker compose up -d` for the full Colonies stack. **Local Development (VS Code):** diff --git a/docker-compose.env b/docker-compose.env index aa23b5afd..6a8baabb1 100644 --- a/docker-compose.env +++ b/docker-compose.env @@ -58,8 +58,8 @@ export EXECUTOR_GPU="false" # ============================================================================ # COLONYFS # ============================================================================ -export MINIO_USER="admin" -export MINIO_PASSWORD="admin12345" +export S3_ADMIN_USER="admin" +export S3_ADMIN_PASSWORD="admin12345" export AWS_S3_ENDPOINT="localhost:9000" export AWS_S3_ACCESSKEY="RrXN2vcLeHjBptG8a3Ay" export AWS_S3_SECRETKEY="ivwLB0Luqomq65nNVmoo8fTBgxXgNvqYGC50VQN6" diff --git a/docker-compose.yml b/docker-compose.yml index 1069ab23f..650b7207f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,45 +10,21 @@ services: volumes: - timescaledb_data:/var/lib/postgresql/data - minio: - image: minio/minio:latest + # S3-compatible object store for the Colonies file system. SeaweedFS mini + # mode seeds the access key, secret and bucket from the environment, serves + # S3 on 8333 and an admin UI on 23646 + seaweedfs: + image: chrislusf/seaweedfs:4.47 environment: - MINIO_ROOT_USER: ${MINIO_USER} - MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD} + AWS_ACCESS_KEY_ID: ${AWS_S3_ACCESSKEY} + AWS_SECRET_ACCESS_KEY: ${AWS_S3_SECRETKEY} + S3_BUCKET: ${AWS_S3_BUCKET} volumes: - - minio_data:/var/lib/minio/data - command: minio server /var/lib/minio/data --console-address :9001 + - seaweedfs_data:/data + command: mini -dir=/data -admin.user=${S3_ADMIN_USER} -admin.password=${S3_ADMIN_PASSWORD} ports: - - "9000:9000" - - "9001:9001" - - minio-setup: - image: minio/mc - depends_on: - - minio - environment: - AWS_S3_ENDPOINT: minio:9000 - AWS_S3_ACCESSKEY: RrXN2vcLeHjBptG8a3Ay - AWS_S3_SECRETKEY: ivwLB0Luqomq65nNVmoo8fTBgxXgNvqYGC50VQN6 - AWS_S3_REGION_KEY: "" - AWS_S3_BUCKET: colonies-prod - AWS_S3_TLS: "false" - AWS_S3_SKIPVERIFY: "false" - MINIO_USER: ${MINIO_USER} - MINIO_PASSWORD: ${MINIO_PASSWORD} - entrypoint: > - /bin/sh -c " - echo 'Executing: /usr/bin/mc alias set myminio http://minio:9000 ${MINIO_USER} ${MINIO_PASSWORD}'; - until (/usr/bin/mc alias set myminio http://minio:9000 ${MINIO_USER} ${MINIO_PASSWORD}); do echo 'Waiting for MinIO...'; sleep 3; done; - echo 'MinIO is ready. Setting up...'; - echo 'Executing: /usr/bin/mc admin user add myminio ${AWS_S3_ACCESSKEY} ${AWS_S3_SECRETKEY}'; - /usr/bin/mc admin user add myminio ${AWS_S3_ACCESSKEY} ${AWS_S3_SECRETKEY}; - echo 'Executing: /usr/bin/mc admin policy attach myminio readwrite --user=${AWS_S3_ACCESSKEY}'; - /usr/bin/mc admin policy attach myminio readwrite --user=${AWS_S3_ACCESSKEY}; - echo 'Executing: /usr/bin/mc mb myminio/${AWS_S3_BUCKET}'; - /usr/bin/mc mb myminio/${AWS_S3_BUCKET}; - echo 'MinIO setup completed.'; - " + - "9000:8333" + - "9001:23646" colonies-server: image: colonyos/colonies:v1.9.10 @@ -130,7 +106,7 @@ services: COLONIES_EXECUTOR_NAME: ${COLONIES_EXECUTOR_NAME:-dev-docker} AWS_S3_TLS: ${AWS_S3_TLS} AWS_S3_SKIPVERIFY: ${AWS_S3_SKIPVERIFY} - AWS_S3_ENDPOINT: minio:9000 + AWS_S3_ENDPOINT: seaweedfs:8333 AWS_S3_ACCESSKEY: ${AWS_S3_ACCESSKEY} AWS_S3_SECRETKEY: ${AWS_S3_SECRETKEY} AWS_S3_REGION_KEY: ${AWS_S3_REGION_KEY} @@ -168,5 +144,5 @@ services: volumes: timescaledb_data: - minio_data: + seaweedfs_data: colonies_etcd: diff --git a/windowsenv.bat b/windowsenv.bat index 7c0ac8103..91c518ad1 100644 --- a/windowsenv.bat +++ b/windowsenv.bat @@ -33,8 +33,8 @@ set COLONIES_RETENTION_POLICY=200 set COLONIES_SERVER_PROFILER=false set COLONIES_SERVER_PROFILER_PORT=6060 set COLONIES_VERBOSE=false -set MINIO_USER=admin -set MINIO_PASSWORD=admin12345 +set S3_ADMIN_USER=admin +set S3_ADMIN_PASSWORD=admin12345 set AWS_S3_ENDPOINT=localhost:9000 set AWS_S3_ACCESSKEY=RrXN2vcLeHjBptG8a3Ay set AWS_S3_SECRETKEY=ivwLB0Luqomq65nNVmoo8fTBgxXgNvqYGC50VQN6 From 158d5c8ca79f32fac8b086aec7e8cd9a481f6de4 Mon Sep 17 00:00:00 2001 From: Oskar Skoglund Date: Fri, 25 Sep 2026 13:15:56 +0200 Subject: [PATCH 5/6] make tests robust under load --- Makefile | 7 ++- README.md | 16 ++++++ pkg/constants/constants.go | 8 +-- pkg/database/postgresql/executors_test.go | 9 ++-- pkg/fs/main_test.go | 17 ++++++ pkg/server/handlers/cron/handlers_test.go | 54 ++++++++++++++----- .../handlers/realtime/event_handler_test.go | 45 ++++++++++++++-- pkg/server/retention_test.go | 22 +++++--- pkg/server/test_utils.go | 54 +++++++++++++++---- 9 files changed, 187 insertions(+), 45 deletions(-) create mode 100644 pkg/fs/main_test.go diff --git a/Makefile b/Makefile index d7d6cb309..0fda28620 100644 --- a/Makefile +++ b/Makefile @@ -53,8 +53,11 @@ install: cp ./lib/libcryptolib.so /usr/local/lib cp ./lib/libcfslib.so /usr/local/lib -startdb: - docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=rFcLGNkgsNtksg6Pgtn9CumL4xXBQ7 --restart unless-stopped timescale/timescaledb:latest-pg16 +# max_connections is raised from the default 100 because go test runs many +# packages in parallel, each with its own database, connection pool and +# TimescaleDB background worker +startdb: + docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=rFcLGNkgsNtksg6Pgtn9CumL4xXBQ7 --restart unless-stopped timescale/timescaledb:latest-pg16 -c max_connections=300 nukedb: @echo "Nuking TimescaleDB containers and volumes..." diff --git a/README.md b/README.md index 9b0087e05..db39ca97c 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,27 @@ For detailed instructions on building containers including multi-platform builds ### Testing +The tests need a PostgreSQL server on `localhost:5432` and an S3 server on +`localhost:9000`, both of which can be started with Docker: + +```bash +make startdb # Start TimescaleDB +make starts3 # Start SeaweedFS and print the AWS_S3_* variables to export +``` + +Export the `AWS_S3_*` variables printed by `make starts3` and set a timezone, +then run the tests: + ```bash +export TZ=Europe/Stockholm make test # Run all tests make github_test # Run tests for CI (no color output) ``` +Test packages use per-process databases and dynamic ports, so they run in +parallel. To stress the suite under load, use for example +`go test -race -count=10 -timeout 0 ./...`. + ### Code Coverage ```bash diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 212a0da68..0219a6564 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -2,9 +2,9 @@ package constants // API Limits - Maximum values for API requests to prevent abuse -const MAX_COUNT = 100 // Maximum number of items that can be requested in list operations -const MAX_DAYS = 30 // Maximum number of days for log search operations -const MAX_LOG_COUNT = 500 // Maximum number of log entries that can be requested at once +const MAX_COUNT = 100 // Maximum number of items that can be requested in list operations +const MAX_DAYS = 30 // Maximum number of days for log search operations +const MAX_LOG_COUNT = 500 // Maximum number of log entries that can be requested at once // Test Configuration - Default values used in test environments const TESTHOST = "localhost" // Default hostname for test servers @@ -35,4 +35,4 @@ const CHANNEL_MAX_LOG_ENTRIES = 10000 // Maximum entries per channel, oldest rem const CHANNEL_MAX_CHANNELS_PER_PROCESS = 100 // Maximum channels a single process can have // Executor Cleanup - Configuration for automatic stale executor removal -const DEFAULT_STALE_EXECUTOR_DURATION = 600 // Default duration in seconds (10 minutes) before an executor is considered stale \ No newline at end of file +const DEFAULT_STALE_EXECUTOR_DURATION = 600 // Default duration in seconds (10 minutes) before an executor is considered stale diff --git a/pkg/database/postgresql/executors_test.go b/pkg/database/postgresql/executors_test.go index d73018e58..fc70b6da5 100644 --- a/pkg/database/postgresql/executors_test.go +++ b/pkg/database/postgresql/executors_test.go @@ -1,6 +1,7 @@ package postgresql import ( + "strings" "testing" "time" @@ -194,8 +195,11 @@ func TestAddDuplicateExecutorConcurrentRejected(t *testing.T) { successCount++ } else { failureCount++ - // All failures should be "already exists" errors - assert.Contains(t, err.Error(), "already exists") + // A loser either fails the name pre-check ("already exists") or, + // if it passed the check before the winner committed, the unique + // constraint ("not unique"); both are correct rejections + msg := err.Error() + assert.True(t, strings.Contains(msg, "already exists") || strings.Contains(msg, "not unique"), msg) } } @@ -945,4 +949,3 @@ func TestChangeExecutorID(t *testing.T) { defer db.Close() } - diff --git a/pkg/fs/main_test.go b/pkg/fs/main_test.go new file mode 100644 index 000000000..6c97cefac --- /dev/null +++ b/pkg/fs/main_test.go @@ -0,0 +1,17 @@ +package fs + +import ( + "fmt" + "os" + "testing" +) + +func TestMain(m *testing.M) { + if os.Getenv("AWS_S3_ENDPOINT") == "" { + fmt.Fprintln(os.Stderr, "pkg/fs tests need an S3 server (make starts3) and the AWS_S3_* variables, for example:") + fmt.Fprintln(os.Stderr, " export AWS_S3_ENDPOINT=localhost:9000 AWS_S3_ACCESSKEY=testaccesskey AWS_S3_SECRETKEY=testsecretkey AWS_S3_TLS=false AWS_S3_SKIPVERIFY=false AWS_S3_BUCKET=test") + os.Exit(1) + } + + os.Exit(m.Run()) +} diff --git a/pkg/server/handlers/cron/handlers_test.go b/pkg/server/handlers/cron/handlers_test.go index 1c95a9911..01465d47a 100644 --- a/pkg/server/handlers/cron/handlers_test.go +++ b/pkg/server/handlers/cron/handlers_test.go @@ -5,11 +5,31 @@ import ( "testing" "time" + "github.com/colonyos/colonies/pkg/client" + "github.com/colonyos/colonies/pkg/core" "github.com/colonyos/colonies/pkg/server" "github.com/colonyos/colonies/pkg/utils" "github.com/stretchr/testify/assert" ) +// cronSettleTime is how long a test waits after the cron has fired to make +// sure it does not fire again. It covers the one second cron interval used by +// the tests plus a few trigger periods of slack. +const cronSettleTime = 1500 * time.Millisecond + +// waitForWaitingProcesses polls until the colony has exactly n waiting +// processes and returns their IDs. Polling instead of sleeping a fixed time +// keeps the tests fast and tolerant of a slow machine. +func waitForWaitingProcesses(t *testing.T, client *client.ColoniesClient, env *server.TestEnv2, n int) []*core.Process { + var processes []*core.Process + assert.Eventually(t, func() bool { + var err error + processes, err = client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) + return err == nil && len(processes) == n + }, 10*time.Second, 100*time.Millisecond, "expected %d waiting processes, got %d", n, len(processes)) + return processes +} + func TestAddCronDebug(t *testing.T) { env, client, server, _, done := server.SetupTestEnv2(t) @@ -40,7 +60,7 @@ func TestAddCronRemoveAllProcesses(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, addedCron) - time.Sleep(2 * time.Second) + waitForWaitingProcesses(t, client, env, 2) // The workflow has two tasks err = client.RemoveAllProcesses(env.ColonyName, env.ColonyPrvKey) assert.Nil(t, err) @@ -65,7 +85,7 @@ func TestAddCronRemoveAllProcessGraphs(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, addedCron) - time.Sleep(2 * time.Second) + waitForWaitingProcesses(t, client, env, 2) // The workflow has two tasks err = client.RemoveAllProcessGraphs(env.ColonyName, env.ColonyPrvKey) assert.Nil(t, err) @@ -117,10 +137,12 @@ func TestAddCronWaitForPrevProcessGraph(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, addedCron) - // Wait for 5 seconds, we should only have 1 cron workflow since WaitForPrevProcessGraph is true - time.Sleep(5 * time.Second) + // Wait for the first cron workflow, then give the cron a chance to + // misbehave: there should still be only one since WaitForPrevProcessGraph is true + processes := waitForWaitingProcesses(t, client, env, 1) + time.Sleep(cronSettleTime) - processes, err := client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) + processes, err = client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) assert.Nil(t, err) assert.Len(t, processes, 1) @@ -137,7 +159,9 @@ func TestAddCronWaitForPrevProcessGraph(t *testing.T) { err = client.Close(process.ID, env.ExecutorPrvKey) assert.Nil(t, err) - time.Sleep(5 * time.Second) + // The next cron workflow may only start now that the previous one finished + processes = waitForWaitingProcesses(t, client, env, 1) + time.Sleep(cronSettleTime) processes, err = client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) assert.Nil(t, err) @@ -168,10 +192,12 @@ func TestAddCronWaitForPrevProcessGraphFail(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, addedCron) - // Wait for 5 seconds, we should only have 1 cron workflow since WaitForPrevProcessGraph is true - time.Sleep(5 * time.Second) + // Wait for the first cron workflow, then give the cron a chance to + // misbehave: there should still be only one since WaitForPrevProcessGraph is true + processes := waitForWaitingProcesses(t, client, env, 1) + time.Sleep(cronSettleTime) - processes, err := client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) + processes, err = client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) assert.Nil(t, err) assert.Len(t, processes, 1) @@ -188,7 +214,9 @@ func TestAddCronWaitForPrevProcessGraphFail(t *testing.T) { err = client.Fail(process.ID, []string{""}, env.ExecutorPrvKey) assert.Nil(t, err) - time.Sleep(5 * time.Second) + // The next cron workflow may only start now that the previous one finished + processes = waitForWaitingProcesses(t, client, env, 1) + time.Sleep(cronSettleTime) processes, err = client.GetWaitingProcesses(env.ColonyName, "", "", "", 100, env.ExecutorPrvKey) assert.Nil(t, err) @@ -559,9 +587,9 @@ func TestAddCronRandomWithCronExpression(t *testing.T) { env, client, server, _, done := server.SetupTestEnv2(t) cron := utils.FakeCron(t, env.ColonyName, env.ExecutorID, env.ExecutorName) - cron.Interval = -1 // Use cron expression - cron.CronExpression = "0/1 * * * * *" // Every second - cron.Random = true // Invalid with cron expression + cron.Interval = -1 // Use cron expression + cron.CronExpression = "0/1 * * * * *" // Every second + cron.Random = true // Invalid with cron expression _, err := client.AddCron(cron, env.ExecutorPrvKey) assert.NotNil(t, err) diff --git a/pkg/server/handlers/realtime/event_handler_test.go b/pkg/server/handlers/realtime/event_handler_test.go index 9de99d9d1..856540f8a 100644 --- a/pkg/server/handlers/realtime/event_handler_test.go +++ b/pkg/server/handlers/realtime/event_handler_test.go @@ -2,14 +2,16 @@ package realtime_test import ( "context" + "net" + "strconv" "sync" "sync/atomic" "testing" "time" + backendGin "github.com/colonyos/colonies/pkg/backends/gin" "github.com/colonyos/colonies/pkg/cluster" "github.com/colonyos/colonies/pkg/core" - backendGin "github.com/colonyos/colonies/pkg/backends/gin" "github.com/colonyos/colonies/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -402,19 +404,54 @@ func TestEventHandlerSubscribeProcessIDFailed(t *testing.T) { assert.NotNil(t, retVal.err) // Not OK, will timeout } +// waitForRelayServers blocks until every node's relay port accepts +// connections. The relay servers listen in a goroutine, so signalling before +// they are bound would silently lose the broadcast. +func waitForRelayServers(t *testing.T, nodes ...cluster.Node) { + t.Helper() + for _, node := range nodes { + addr := net.JoinHostPort(node.Host, strconv.Itoa(node.RelayPort)) + assert.Eventually(t, func() bool { + conn, err := net.DialTimeout("tcp", addr, 100*time.Millisecond) + if err != nil { + return false + } + conn.Close() + return true + }, 10*time.Second, 20*time.Millisecond, "relay server %s did not start", node.Name) + } +} + func TestEventHandleRelayServer(t *testing.T) { - node1 := cluster.Node{Name: "etcd1", Host: "localhost", EtcdClientPort: 24100, EtcdPeerPort: 23100, RelayPort: 25100, APIPort: 26100} - node2 := cluster.Node{Name: "etcd2", Host: "localhost", EtcdClientPort: 24200, EtcdPeerPort: 23200, RelayPort: 25200, APIPort: 26200} - node3 := cluster.Node{Name: "etcd3", Host: "localhost", EtcdClientPort: 24300, EtcdPeerPort: 23300, RelayPort: 25300, APIPort: 26300} + // Kernel-assigned ports keep this test from colliding with other test + // processes and with earlier iterations of itself in the same process + reserved := utils.ReservePortsOrPanic(12) + nodes := make([]cluster.Node, 3) + for i, name := range []string{"etcd1", "etcd2", "etcd3"} { + nodes[i] = cluster.Node{ + Name: name, + Host: "localhost", + EtcdClientPort: reserved[4*i].Port(), + EtcdPeerPort: reserved[4*i+1].Port(), + RelayPort: reserved[4*i+2].Port(), + APIPort: reserved[4*i+3].Port(), + } + } + node1, node2, node3 := nodes[0], nodes[1], nodes[2] config := cluster.Config{} config.AddNode(node1) config.AddNode(node2) config.AddNode(node3) + utils.ReleasePorts(reserved) relayServer1 := cluster.CreateRelayServer(node1, config) relayServer2 := cluster.CreateRelayServer(node2, config) relayServer3 := cluster.CreateRelayServer(node3, config) + defer relayServer1.Shutdown() + defer relayServer2.Shutdown() + defer relayServer3.Shutdown() + waitForRelayServers(t, node1, node2, node3) factory1 := backendGin.NewFactory() handler1 := factory1.CreateTestableEventHandler(relayServer1) diff --git a/pkg/server/retention_test.go b/pkg/server/retention_test.go index 649c326fc..f1886c032 100644 --- a/pkg/server/retention_test.go +++ b/pkg/server/retention_test.go @@ -42,14 +42,20 @@ func TestRetention(t *testing.T) { assert.Nil(t, err) stat, err := client.ColonyStatistics(colony.Name, executorPrvKey) - assert.Equal(t, stat.SuccessfulWorkflows, 1) - assert.Equal(t, stat.SuccessfulProcesses, 1) - - time.Sleep(2 * time.Second) - - stat, err = client.ColonyStatistics(colony.Name, executorPrvKey) - assert.Equal(t, stat.SuccessfulWorkflows, 0) - assert.Equal(t, stat.SuccessfulProcesses, 0) + assert.Nil(t, err) + assert.Equal(t, 1, stat.SuccessfulWorkflows) + assert.Equal(t, 1, stat.SuccessfulProcesses) + + // The retention worker deletes the process and workflow once they are + // older than the policy; poll instead of sleeping a fixed time so the + // test tolerates a slow leader election or database under load + assert.Eventually(t, func() bool { + stat, err := client.ColonyStatistics(colony.Name, executorPrvKey) + if err != nil { + return false + } + return stat.SuccessfulWorkflows == 0 && stat.SuccessfulProcesses == 0 + }, 4*testRetentionPolicySeconds*time.Second, 250*time.Millisecond) server.Shutdown() <-done diff --git a/pkg/server/test_utils.go b/pkg/server/test_utils.go index 0ade6d535..1e3c31fb1 100644 --- a/pkg/server/test_utils.go +++ b/pkg/server/test_utils.go @@ -3,7 +3,6 @@ package server import ( "context" "errors" - "fmt" "io/ioutil" "math/rand" "net/http" @@ -206,6 +205,21 @@ func prepareTests(t *testing.T) (*client.ColoniesClient, *Server, string, chan b return prepareTestsWithRetention(t, false) } +// Retention settings for test servers created with retention enabled. The +// policy must leave enough room for a test to submit, assign, close and +// inspect a process before it is deleted, even under heavy load (for example +// go test -race -count=10 ./...), so it is much longer than the worker period. +const ( + testRetentionPolicySeconds = 5 + testRetentionPeriodMillis = 500 +) + +// Cron trigger period for single-node test servers. Production evaluates crons +// once a second, which makes every cron test wait two to three seconds for the +// first trigger. A shorter tick keeps the same semantics (the one second cron +// interval is still the floor) while cutting the waiting to just over a second. +const testCronPeriodMillis = 100 + func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesClient, *Server, string, chan bool) { // Dynamic ports and a per-test etcd data directory allow test packages to // run in parallel without colliding on fixed ports or /tmp/colonies. The @@ -236,7 +250,7 @@ func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesCl reserved[1].Release() reserved[2].Release() reserved[3].Release() - server := CreateServer(db, apiPort, EnableTLS, "", "", node, clusterConfig, etcdDataPath, constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, false, retention, 1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) + server := CreateServer(db, apiPort, EnableTLS, "", "", node, clusterConfig, etcdDataPath, constants.GENERATOR_TRIGGER_PERIOD, testCronPeriodMillis, false, false, retention, testRetentionPolicySeconds, testRetentionPeriodMillis, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) done := make(chan bool) reserved[0].Release() @@ -251,9 +265,27 @@ func prepareTestsWithRetention(t *testing.T, retention bool) (*client.ColoniesCl done <- true }() + waitForServer(t, client) + return client, server, serverPrvKey, done } +// waitForServer blocks until the test server answers health checks. The server +// listens in a goroutine, so without this a test on a loaded machine can send +// its first request before the port is bound and fail with a connection error. +func waitForServer(t *testing.T, client *client.ColoniesClient) { + deadline := time.Now().Add(30 * time.Second) + var err error + for time.Now().Before(deadline) { + err = client.CheckHealth() + if err == nil { + return + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("test server did not become ready: %v", err) +} + func GenerateDiamondtWorkflowSpec(colonyName string) *core.WorkflowSpec { // task1 // / \ @@ -450,22 +482,22 @@ func startCluster(t *testing.T, db database.Database, size int, exclusiveAssign return servers } +// WaitForCluster blocks until every server in the cluster answers health +// checks in the same pass, so one healthy node polled repeatedly cannot +// satisfy the wait on behalf of nodes that are still starting. func WaitForCluster(t *testing.T, cluster []ServerInfo) { - serverReady := 0 for { + serverReady := 0 for _, s := range cluster { client := client.CreateColoniesClient("localhost", s.Node.APIPort, true, true) - err := client.CheckHealth() - if err == nil { + if err := client.CheckHealth(); err == nil { serverReady++ - } else { - time.Sleep(50 * time.Millisecond) - fmt.Println(err) - } - if serverReady == len(cluster) { - return } } + if serverReady == len(cluster) { + return + } + time.Sleep(50 * time.Millisecond) } } From e5c0f0659b7e4e28cbcf1290f75741b92b2cac25 Mon Sep 17 00:00:00 2001 From: Oskar Skoglund Date: Mon, 5 Oct 2026 07:59:58 +0000 Subject: [PATCH 6/6] Address review comments - Run CI on pushes to dev and limit coverage to pushes to main, since coverage repeats the suite - Remove the unused FreePorts, FreePort and FreePortsOrPanic helpers, which kept the release-before-bind race that ReservePorts avoids - Use t.TempDir in newTestColoniesController instead of os.MkdirTemp, which never removed its directory The Config.Equals fix was split out of "Test performance improvements" into its own commit earlier in the branch. --- .github/workflows/go.yml | 8 +-- .../controllers/colonies_controller_test.go | 50 +++++++++---------- .../colonies_controller_worker_test.go | 6 +-- pkg/server/controllers/mock_test.go | 23 ++++----- pkg/utils/free_ports.go | 38 -------------- 5 files changed, 42 insertions(+), 83 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 74816e972..0159a278a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,7 +2,7 @@ name: Go on: push: - branches: [ main ] + branches: [ main, dev ] pull_request: branches: [ main, dev ] @@ -64,9 +64,9 @@ jobs: COLONIES_DB_HOST: localhost # Coverage runs the suite a second time, so it is limited to pushes to main - # and skipped on pull requests to keep PR runs fast + # and skipped on dev pushes and pull requests to keep those runs fast - name: Generate coverage - if: github.event_name == 'push' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | # Get package list excluding monitoring PKGS=$(go list ./pkg/... | grep -v '/monitoring$' | tr '\n' ' ') @@ -91,7 +91,7 @@ jobs: COLONIES_DB_HOST: localhost - name: Upload coverage to Codecov - if: github.event_name == 'push' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: codecov/codecov-action@v4 with: files: ./coverage.out diff --git a/pkg/server/controllers/colonies_controller_test.go b/pkg/server/controllers/colonies_controller_test.go index dd97a6b35..919191f83 100644 --- a/pkg/server/controllers/colonies_controller_test.go +++ b/pkg/server/controllers/colonies_controller_test.go @@ -13,7 +13,7 @@ import ( ) func TestColoniesControllerInvalidDB(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) dbMock.ReturnError = "GetProcessByID" err := controller.SubscribeProcess("invalid_id", &backends.RealtimeSubscription{}) @@ -38,7 +38,7 @@ func TestColoniesControllerAddProcess(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -62,7 +62,7 @@ func TestColoniesControllerAssignExecutor(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -93,9 +93,9 @@ func TestColoniesControllerAssignExecutorConcurrency(t *testing.T) { processCount := 100 - controller1 := createTestColoniesController(db) + controller1 := createTestColoniesController(t, db) defer controller1.Stop() - controller2 := createTestColoniesController2(db) + controller2 := createTestColoniesController2(t, db) defer controller2.Stop() colonyName := core.GenerateRandomID() @@ -151,7 +151,7 @@ func TestColoniesControllerAssignExecutorConcurrency(t *testing.T) { } func TestColoniesControllerPauseResumeAssignments(t *testing.T) { - controller, _ := createFakeColoniesController() + controller, _ := createFakeColoniesController(t) defer controller.Stop() colonyName := "test_colony" @@ -175,7 +175,7 @@ func TestColoniesControllerPauseResumeAssignmentsWithEtcdServer(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := "test_colony" @@ -206,7 +206,7 @@ func TestColoniesControllerPauseResumeAssignmentsWithEtcdServer(t *testing.T) { // Test getter methods func TestColoniesControllerGetters(t *testing.T) { - controller, _ := createFakeColoniesController() + controller, _ := createFakeColoniesController(t) defer controller.Stop() // Test GetCronPeriod @@ -233,7 +233,7 @@ func TestColoniesControllerGetters(t *testing.T) { // Test ProcessGraphStorage adapter func TestColoniesControllerProcessGraphStorage(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() storage := controller.GetProcessGraphStorage() @@ -278,7 +278,7 @@ func TestColoniesControllerProcessGraphStorage(t *testing.T) { // Test SubscribeProcesses method func TestColoniesControllerSubscribeProcesses(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() subscription := &backends.RealtimeSubscription{} @@ -300,7 +300,7 @@ func TestColoniesControllerProcessOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -320,7 +320,7 @@ func TestColoniesControllerProcessOperations(t *testing.T) { // Test more controller methods with mocks func TestColoniesControllerAdditionalMethods(t *testing.T) { - controller, _ := createFakeColoniesController() + controller, _ := createFakeColoniesController(t) defer controller.Stop() // Test IsLeader @@ -332,7 +332,7 @@ func TestColoniesControllerAdditionalMethods(t *testing.T) { // Test error conditions and edge cases func TestColoniesControllerErrorHandling(t *testing.T) { - controller, _ := createFakeColoniesController() + controller, _ := createFakeColoniesController(t) defer controller.Stop() // Test nil process @@ -342,7 +342,7 @@ func TestColoniesControllerErrorHandling(t *testing.T) { // Test websocket subscriptions with better error handling func TestColoniesControllerWebSocketHandling(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() // Test SubscribeProcess with invalid process ID @@ -366,7 +366,7 @@ func TestColoniesControllerProcessGraphOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() // Test GetProcessGraphByID with non-existent ID @@ -381,7 +381,7 @@ func TestColoniesControllerAssignmentOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -414,7 +414,7 @@ func TestColoniesControllerAssignmentOperations(t *testing.T) { // Test cron functionality with mocks func TestColoniesControllerCronOperations(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() cronSpec := &core.Cron{ @@ -451,7 +451,7 @@ func TestColoniesControllerCronOperations(t *testing.T) { // Test generator functionality with mocks func TestColoniesControllerGeneratorOperations(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() generator := &core.Generator{ @@ -479,7 +479,7 @@ func TestColoniesControllerProcessLifecycleOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -529,7 +529,7 @@ func TestColoniesControllerProcessGraphOperations2(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -584,7 +584,7 @@ func TestColoniesControllerAssignmentOperations2(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -623,7 +623,7 @@ func TestColoniesControllerAttributeOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -656,7 +656,7 @@ func TestColoniesControllerFunctionOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -691,7 +691,7 @@ func TestColoniesControllerDatabaseAndUtilityOperations(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() // Test ResetDatabase @@ -701,7 +701,7 @@ func TestColoniesControllerDatabaseAndUtilityOperations(t *testing.T) { // Test additional methods with mocks - safer approach func TestColoniesControllerSafeMockTests(t *testing.T) { - controller, dbMock := createFakeColoniesController() + controller, dbMock := createFakeColoniesController(t) defer controller.Stop() // Test basic operations that are safe with mocks diff --git a/pkg/server/controllers/colonies_controller_worker_test.go b/pkg/server/controllers/colonies_controller_worker_test.go index d26038d98..4d79c8360 100644 --- a/pkg/server/controllers/colonies_controller_worker_test.go +++ b/pkg/server/controllers/colonies_controller_worker_test.go @@ -15,7 +15,7 @@ func TestCleanupStaleExecutors_SkipsZeroLastHeardFrom(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -49,7 +49,7 @@ func TestCleanupStaleExecutors_RemovesStaleExecutor(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() @@ -92,7 +92,7 @@ func TestCleanupStaleExecutors_KeepsRecentExecutor(t *testing.T) { defer db.Close() assert.Nil(t, err) - controller := createTestColoniesController(db) + controller := createTestColoniesController(t, db) defer controller.Stop() colonyName := core.GenerateRandomID() diff --git a/pkg/server/controllers/mock_test.go b/pkg/server/controllers/mock_test.go index 14eb00e27..ef9c0d8e2 100644 --- a/pkg/server/controllers/mock_test.go +++ b/pkg/server/controllers/mock_test.go @@ -3,8 +3,8 @@ package controllers import ( "errors" "fmt" - "os" "sync/atomic" + "testing" "time" "github.com/colonyos/colonies/pkg/cluster" @@ -610,17 +610,14 @@ func (db *DatabaseMock) Unlock() error { retu func (db *DatabaseMock) ApplyRetentionPolicy(retentionPeriod int64) error { return nil } // Test utility functions -func newTestColoniesController(db database.Database, name string) *ColoniesController { +func newTestColoniesController(t *testing.T, db database.Database, name string) *ColoniesController { + t.Helper() // Unique node names keep etcd data directories apart; dynamic ports and // temp dirs let test packages run in parallel offset := atomic.AddInt32(&portCounter, 1) reserved := utils.ReservePortsOrPanic(4) nodeName := fmt.Sprintf("%s-%d", name, offset) - dataPath, err := os.MkdirTemp("", "colonies-etcd-") - if err != nil { - utils.ReleasePorts(reserved) - panic(err) - } + dataPath := t.TempDir() node := cluster.Node{Name: nodeName, Host: "localhost", EtcdClientPort: reserved[0].Port(), EtcdPeerPort: reserved[1].Port(), RelayPort: reserved[2].Port(), APIPort: reserved[3].Port()} clusterConfig := cluster.Config{} @@ -631,15 +628,15 @@ func newTestColoniesController(db database.Database, name string) *ColoniesContr return CreateColoniesController(db, node, clusterConfig, dataPath, constants.GENERATOR_TRIGGER_PERIOD, constants.CRON_TRIGGER_PERIOD, false, -1, 500, time.Duration(constants.DEFAULT_STALE_EXECUTOR_DURATION)*time.Second) } -func createFakeColoniesController() (*ColoniesController, *DatabaseMock) { +func createFakeColoniesController(t *testing.T) (*ColoniesController, *DatabaseMock) { dbMock := &DatabaseMock{} - return newTestColoniesController(dbMock, "etcd"), dbMock + return newTestColoniesController(t, dbMock, "etcd"), dbMock } -func createTestColoniesController(db *postgresql.PQDatabase) *ColoniesController { - return newTestColoniesController(db, "test") +func createTestColoniesController(t *testing.T, db *postgresql.PQDatabase) *ColoniesController { + return newTestColoniesController(t, db, "test") } -func createTestColoniesController2(db *postgresql.PQDatabase) *ColoniesController { - return newTestColoniesController(db, "test2") +func createTestColoniesController2(t *testing.T, db *postgresql.PQDatabase) *ColoniesController { + return newTestColoniesController(t, db, "test2") } diff --git a/pkg/utils/free_ports.go b/pkg/utils/free_ports.go index e9c1ac0d2..a0dd34fc5 100644 --- a/pkg/utils/free_ports.go +++ b/pkg/utils/free_ports.go @@ -11,44 +11,6 @@ import ( const portProbeNetwork = "tcp4" const portProbeAddr = "0.0.0.0:0" -// FreePorts asks the kernel for n distinct free TCP ports. The listeners are -// closed before returning, so there is a small window in which another process -// could grab a port. Prefer ReservePorts when there is slow setup work between -// allocating a port and binding it. -func FreePorts(n int) ([]int, error) { - reservations, err := ReservePorts(n) - if err != nil { - return nil, err - } - - ports := make([]int, 0, n) - for _, r := range reservations { - ports = append(ports, r.Port()) - } - ReleasePorts(reservations) - - return ports, nil -} - -// FreePort returns a single free TCP port. -func FreePort() (int, error) { - ports, err := FreePorts(1) - if err != nil { - return 0, err - } - return ports[0], nil -} - -// FreePortsOrPanic is a convenience wrapper for test setup code without error -// returns. -func FreePortsOrPanic(n int) []int { - ports, err := FreePorts(n) - if err != nil { - panic("failed to allocate free ports: " + err.Error() + " (n=" + strconv.Itoa(n) + ")") - } - return ports -} - // ReservedPort is a free TCP port that stays bound until Release is called, so // no other process can be handed the same port in the meantime. Test setups // that do slow work between allocating a port and binding it (starting etcd,