From 3a5f01a63f028d6027fee2e77a4a23fcce9aaa47 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 08:36:53 +0200 Subject: [PATCH] test(cluster): cover weighted node pool selection NewNodePool/Get had no tests. Covers capability-slot miss, preferred- node override, and weight-based selection. Authored By: TDvorak Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- pkg/cluster/pool_test.go | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pkg/cluster/pool_test.go diff --git a/pkg/cluster/pool_test.go b/pkg/cluster/pool_test.go new file mode 100644 index 00000000..5fc2cc89 --- /dev/null +++ b/pkg/cluster/pool_test.go @@ -0,0 +1,88 @@ +package cluster + +import ( + "context" + "errors" + "path/filepath" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/ent/enttest" + "github.com/cloudreve/Cloudreve/v4/ent/node" + "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type stubSettings struct { + setting.Provider +} + +func capsOf(bits ...types.NodeCapability) *boolset.BooleanSet { + b := &boolset.BooleanSet{} + for _, c := range bits { + boolset.Set(int(c), true, b) + } + return b +} + +func newPoolFixture(t *testing.T) (*ent.Client, NodePool, context.Context) { + t.Helper() + ctx := context.Background() + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1") + t.Cleanup(func() { client.Close() }) + + logger := logging.NewConsoleLogger(logging.LevelError) + cfg, err := conf.NewIniConfigProvider(filepath.Join(t.TempDir(), "conf.ini"), logger) + require.NoError(t, err) + + pool, err := NewNodePool(ctx, logger, cfg, &stubSettings{}, inventory.NewNodeClient(client)) + require.NoError(t, err) + return client, pool, ctx +} + +func TestWeightedNodePoolGet(t *testing.T) { + a := assert.New(t) + client, pool, ctx := newPoolFixture(t) + + n1 := client.Node.Create(). + SetName("n1").SetType(node.TypeMaster).SetStatus(node.StatusActive). + SetServer("http://n1").SetCapabilities(capsOf(types.NodeCapabilityRemoteDownload)). + SetWeight(10).SaveX(ctx) + client.Node.Create(). + SetName("n2").SetType(node.TypeMaster).SetStatus(node.StatusActive). + SetServer("http://n2").SetCapabilities(capsOf(types.NodeCapabilityRemoteDownload)). + SetWeight(1).SaveX(ctx) + + pool.(*weightedNodePool).Upsert(ctx, n1) + // Upsert requires a full refresh — rebuild pool from the seeded DB instead. + pool, err := NewNodePool(ctx, logging.NewConsoleLogger(logging.LevelError), pool.(*weightedNodePool).conf, &stubSettings{}, inventory.NewNodeClient(client)) + require.NoError(t, err) + + // Capability with no matching nodes errors. + _, err = pool.Get(ctx, types.NodeCapabilityCreateArchive, 0) + a.True(errors.Is(err, ErrNoAvailableNode)) + + // Preferred node wins regardless of weight. + selected, err := pool.Get(ctx, types.NodeCapabilityRemoteDownload, n1.ID) + require.NoError(t, err) + a.Equal(n1.ID, selected.ID()) + + // Without preference, the heavier node wins the first pick. + selected, err = pool.Get(ctx, types.NodeCapabilityRemoteDownload, 0) + require.NoError(t, err) + a.Equal(n1.ID, selected.ID()) +} + +func TestWeightedNodePoolGetUnknownCapability(t *testing.T) { + a := assert.New(t) + _, pool, ctx := newPoolFixture(t) + + _, err := pool.Get(ctx, types.NodeCapabilityRemoteDownload, 0) + a.True(errors.Is(err, ErrNoAvailableNode)) +}