Skip to content

Commit 8ef42c7

Browse files
committed
use ginkgo
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 4bf190b commit 8ef42c7

11 files changed

Lines changed: 646 additions & 1011 deletions

File tree

core/http/endpoints/localai/nodes_test.go

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,32 @@ package localai
33
import (
44
"crypto/sha256"
55
"crypto/subtle"
6-
"testing"
7-
)
86

9-
// TestTokenValidation verifies the token hashing and comparison logic
10-
// used in RegisterNodeEndpoint.
11-
func TestTokenValidation(t *testing.T) {
12-
tests := []struct {
13-
name string
14-
expectedToken string
15-
providedToken string
16-
wantMatch bool
17-
}{
18-
{"matching tokens", "my-secret-token", "my-secret-token", true},
19-
{"mismatched tokens", "my-secret-token", "wrong-token", false},
20-
{"empty expected (no auth)", "", "any-token", true},
21-
{"empty provided when expected set", "my-secret-token", "", false},
22-
}
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
)
2310

24-
for _, tt := range tests {
25-
t.Run(tt.name, func(t *testing.T) {
26-
// Simulate the logic from RegisterNodeEndpoint
27-
if tt.expectedToken == "" {
28-
// No auth required — always matches
29-
if !tt.wantMatch {
30-
t.Error("expected no-auth to always pass")
31-
}
32-
return
33-
}
11+
var _ = DescribeTable("token validation",
12+
func(expectedToken, providedToken string, wantMatch bool) {
13+
if expectedToken == "" {
14+
// No auth required — always matches
15+
Expect(wantMatch).To(BeTrue(), "no-auth should always pass")
16+
return
17+
}
3418

35-
if tt.providedToken == "" {
36-
if tt.wantMatch {
37-
t.Error("expected empty token to be rejected")
38-
}
39-
return
40-
}
19+
if providedToken == "" {
20+
Expect(wantMatch).To(BeFalse(), "empty token should be rejected")
21+
return
22+
}
4123

42-
expectedHash := sha256.Sum256([]byte(tt.expectedToken))
43-
providedHash := sha256.Sum256([]byte(tt.providedToken))
44-
match := subtle.ConstantTimeCompare(expectedHash[:], providedHash[:]) == 1
24+
expectedHash := sha256.Sum256([]byte(expectedToken))
25+
providedHash := sha256.Sum256([]byte(providedToken))
26+
match := subtle.ConstantTimeCompare(expectedHash[:], providedHash[:]) == 1
4527

46-
if match != tt.wantMatch {
47-
t.Errorf("got match=%v, want %v", match, tt.wantMatch)
48-
}
49-
})
50-
}
51-
}
28+
Expect(match).To(Equal(wantMatch))
29+
},
30+
Entry("matching tokens", "my-secret-token", "my-secret-token", true),
31+
Entry("mismatched tokens", "my-secret-token", "wrong-token", false),
32+
Entry("empty expected (no auth)", "", "any-token", true),
33+
Entry("empty provided when expected set", "my-secret-token", "", false),
34+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package advisorylock
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestAdvisoryLock(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "AdvisoryLock test suite")
13+
}

core/services/advisorylock/advisorylock_test.go

Lines changed: 92 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"context"
55
"sync"
66
"sync/atomic"
7-
"testing"
87
"time"
98

9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
1012
"github.com/mudler/LocalAI/core/services/messaging"
1113

1214
"github.com/testcontainers/testcontainers-go"
@@ -17,11 +19,8 @@ import (
1719
"gorm.io/gorm/logger"
1820
)
1921

20-
func setupTestDB(t *testing.T) *gorm.DB {
21-
t.Helper()
22-
t.Skip("requires docker")
23-
24-
ctx := t.Context()
22+
func setupTestDB() *gorm.DB {
23+
ctx := context.Background()
2524

2625
pgC, err := tcpostgres.Run(ctx, "postgres:16",
2726
tcpostgres.WithDatabase("testdb"),
@@ -30,132 +29,109 @@ func setupTestDB(t *testing.T) *gorm.DB {
3029
testcontainers.WithWaitStrategyAndDeadline(60*time.Second,
3130
wait.ForLog("database system is ready to accept connections").WithOccurrence(2)),
3231
)
33-
if err != nil {
34-
t.Fatalf("start postgres container: %v", err)
35-
}
36-
t.Cleanup(func() { pgC.Terminate(context.Background()) })
32+
Expect(err).ToNot(HaveOccurred())
33+
DeferCleanup(func() { pgC.Terminate(context.Background()) })
3734

3835
connStr, err := pgC.ConnectionString(ctx, "sslmode=disable")
39-
if err != nil {
40-
t.Fatalf("connection string: %v", err)
41-
}
36+
Expect(err).ToNot(HaveOccurred())
4237

4338
db, err := gorm.Open(postgres.Open(connStr), &gorm.Config{
4439
Logger: logger.Default.LogMode(logger.Silent),
4540
})
46-
if err != nil {
47-
t.Fatalf("open gorm: %v", err)
48-
}
41+
Expect(err).ToNot(HaveOccurred())
4942
return db
5043
}
5144

52-
func TestWithLock_PreventsConcurrentExecution(t *testing.T) {
53-
db := setupTestDB(t)
54-
55-
const lockKey int64 = 999
56-
57-
var (
58-
mu sync.Mutex
59-
maxRunning int32
60-
running int32
61-
concurrency int32
62-
)
63-
64-
var wg sync.WaitGroup
65-
wg.Add(2)
66-
67-
for i := 0; i < 2; i++ {
68-
go func() {
69-
defer wg.Done()
70-
err := WithLock(db, lockKey, func() error {
71-
cur := atomic.AddInt32(&running, 1)
72-
mu.Lock()
73-
if cur > maxRunning {
74-
maxRunning = cur
75-
}
76-
if cur > 1 {
77-
atomic.AddInt32(&concurrency, 1)
78-
}
79-
mu.Unlock()
80-
81-
// Hold the lock briefly so the other goroutine has a chance to contend.
82-
time.Sleep(50 * time.Millisecond)
83-
84-
atomic.AddInt32(&running, -1)
85-
return nil
86-
})
87-
if err != nil {
88-
t.Errorf("WithLock returned error: %v", err)
45+
var _ = Describe("AdvisoryLock", func() {
46+
Context("PostgreSQL advisory locks", func() {
47+
var db *gorm.DB
48+
49+
BeforeEach(func() {
50+
db = setupTestDB()
51+
})
52+
53+
It("prevents concurrent execution with WithLock", func() {
54+
const lockKey int64 = 999
55+
56+
var (
57+
mu sync.Mutex
58+
maxRunning int32
59+
running int32
60+
concurrency int32
61+
)
62+
63+
var wg sync.WaitGroup
64+
wg.Add(2)
65+
66+
for i := 0; i < 2; i++ {
67+
go func() {
68+
defer GinkgoRecover()
69+
defer wg.Done()
70+
err := WithLock(db, lockKey, func() error {
71+
cur := atomic.AddInt32(&running, 1)
72+
mu.Lock()
73+
if cur > maxRunning {
74+
maxRunning = cur
75+
}
76+
if cur > 1 {
77+
atomic.AddInt32(&concurrency, 1)
78+
}
79+
mu.Unlock()
80+
81+
time.Sleep(50 * time.Millisecond)
82+
83+
atomic.AddInt32(&running, -1)
84+
return nil
85+
})
86+
Expect(err).ToNot(HaveOccurred())
87+
}()
8988
}
90-
}()
91-
}
92-
93-
wg.Wait()
94-
95-
if maxRunning > 1 {
96-
t.Errorf("expected max 1 goroutine inside lock at a time, observed %d", maxRunning)
97-
}
98-
if concurrency > 0 {
99-
t.Errorf("detected concurrent execution inside advisory lock")
100-
}
101-
}
102-
103-
func TestTryLock_AcquiresAndUnlocks(t *testing.T) {
104-
db := setupTestDB(t)
10589

106-
const lockKey int64 = 888
90+
wg.Wait()
10791

108-
// NOTE: TryLock and Unlock may use different pool connections,
109-
// so this test documents the API contract but may sometimes pass
110-
// even when the implementation has a connection-affinity bug.
111-
acquired := TryLock(db, lockKey)
112-
if !acquired {
113-
t.Fatal("expected TryLock to acquire the lock")
114-
}
92+
Expect(maxRunning).To(BeNumerically("<=", 1), "expected max 1 goroutine inside lock at a time")
93+
Expect(concurrency).To(BeZero(), "detected concurrent execution inside advisory lock")
94+
})
11595

116-
Unlock(db, lockKey)
96+
It("acquires and unlocks with TryLock", func() {
97+
const lockKey int64 = 888
11798

118-
reacquired := TryLock(db, lockKey)
119-
if !reacquired {
120-
t.Error("expected TryLock to re-acquire the lock after Unlock")
121-
}
99+
acquired := TryLock(db, lockKey)
100+
Expect(acquired).To(BeTrue(), "expected TryLock to acquire the lock")
122101

123-
// Clean up
124-
Unlock(db, lockKey)
125-
}
102+
Unlock(db, lockKey)
126103

127-
func TestKeyFromUUID_Deterministic(t *testing.T) {
128-
a := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
129-
b := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
104+
reacquired := TryLock(db, lockKey)
105+
Expect(reacquired).To(BeTrue(), "expected TryLock to re-acquire the lock after Unlock")
130106

131-
keyA1 := KeyFromUUID(a)
132-
keyA2 := KeyFromUUID(a)
133-
keyB := KeyFromUUID(b)
107+
Unlock(db, lockKey)
108+
})
109+
})
134110

135-
if keyA1 != keyA2 {
136-
t.Errorf("KeyFromUUID not deterministic: got %d and %d for same input", keyA1, keyA2)
137-
}
138-
if keyA1 == keyB {
139-
t.Errorf("KeyFromUUID returned same key %d for different inputs", keyA1)
140-
}
141-
}
111+
Context("pure logic", func() {
112+
It("KeyFromUUID is deterministic", func() {
113+
a := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
114+
b := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
115+
116+
keyA1 := KeyFromUUID(a)
117+
keyA2 := KeyFromUUID(a)
118+
keyB := KeyFromUUID(b)
119+
120+
Expect(keyA1).To(Equal(keyA2), "KeyFromUUID not deterministic")
121+
Expect(keyA1).ToNot(Equal(keyB), "KeyFromUUID returned same key for different inputs")
122+
})
123+
124+
It("all advisory lock keys are unique", func() {
125+
keys := map[int64]string{
126+
messaging.AdvisoryLockCronScheduler: "messaging.AdvisoryLockCronScheduler",
127+
messaging.AdvisoryLockStaleNodeCleanup: "messaging.AdvisoryLockStaleNodeCleanup",
128+
messaging.AdvisoryLockGalleryDedup: "messaging.AdvisoryLockGalleryDedup",
129+
messaging.AdvisoryLockAgentScheduler: "messaging.AdvisoryLockAgentScheduler",
130+
messaging.AdvisoryLockHealthCheck: "messaging.AdvisoryLockHealthCheck",
131+
messaging.AdvisoryLockSchemaMigrate: "messaging.AdvisoryLockSchemaMigrate",
132+
}
142133

143-
func TestAllLockKeysUnique(t *testing.T) {
144-
// Collect all advisory lock keys from both the advisorylock package
145-
// and the messaging package. They share the same PostgreSQL lock
146-
// namespace so every key must be unique.
147-
keys := map[int64]string{
148-
messaging.AdvisoryLockCronScheduler: "messaging.AdvisoryLockCronScheduler",
149-
messaging.AdvisoryLockStaleNodeCleanup: "messaging.AdvisoryLockStaleNodeCleanup",
150-
messaging.AdvisoryLockGalleryDedup: "messaging.AdvisoryLockGalleryDedup",
151-
messaging.AdvisoryLockAgentScheduler: "messaging.AdvisoryLockAgentScheduler",
152-
messaging.AdvisoryLockHealthCheck: "messaging.AdvisoryLockHealthCheck",
153-
messaging.AdvisoryLockSchemaMigrate: "messaging.AdvisoryLockSchemaMigrate",
154-
}
155-
156-
// If any keys collide, the map will have fewer entries than expected.
157-
expectedCount := 6
158-
if len(keys) != expectedCount {
159-
t.Errorf("expected %d unique advisory lock keys, got %d — some keys have the same value", expectedCount, len(keys))
160-
}
161-
}
134+
Expect(keys).To(HaveLen(6), "some advisory lock keys have the same value")
135+
})
136+
})
137+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package agents
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestAgents(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Agents test suite")
13+
}

0 commit comments

Comments
 (0)