1package sqlite_test
2
3import (
4 "path/filepath"
5 "sync"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func TestLazyDB_NotInitializedByDefault(t *testing.T) {
15 dbPath := filepath.Join(t.TempDir(), "test.db")
16 lazy := sqlite.NewLazyDB(dbPath)
17
18 assert.False(t, lazy.IsInitialized(), "LazyDB should not be initialized before DB() is called")
19}
20
21func TestLazyDB_InitializesOnFirstAccess(t *testing.T) {
22 ctx := testCtx()
23 dbPath := filepath.Join(t.TempDir(), "test.db")
24 lazy := sqlite.NewLazyDB(dbPath)
25
26 db, err := lazy.DB(ctx)
27 require.NoError(t, err)
28 require.NotNil(t, db)
29
30 assert.True(t, lazy.IsInitialized(), "LazyDB should be initialized after DB() is called")
31
32 // Cleanup
33 require.NoError(t, lazy.Close())
34}
35
36func TestLazyDB_ReturnsSameConnection(t *testing.T) {
37 ctx := testCtx()
38 dbPath := filepath.Join(t.TempDir(), "test.db")
39 lazy := sqlite.NewLazyDB(dbPath)
40
41 db1, err := lazy.DB(ctx)
42 require.NoError(t, err)
43
44 db2, err := lazy.DB(ctx)
45 require.NoError(t, err)
46
47 assert.Same(t, db1, db2, "DB() should return the same connection instance")
48
49 require.NoError(t, lazy.Close())
50}
51
52func TestLazyDB_ConcurrentAccess(t *testing.T) {
53 ctx := testCtx()
54 dbPath := filepath.Join(t.TempDir(), "test.db")
55 lazy := sqlite.NewLazyDB(dbPath)
56
57 const goroutines = 10
58 var wg sync.WaitGroup
59 wg.Add(goroutines)
60
61 results := make(chan *struct {
62 db any
63 err error
64 }, goroutines)
65
66 for range goroutines {
67 go func() {
68 defer wg.Done()
69 db, err := lazy.DB(ctx)
70 results <- &struct {
71 db any
72 err error
73 }{db, err}
74 }()
75 }
76
77 wg.Wait()
78 close(results)
79
80 var firstDB any
81 for result := range results {
82 require.NoError(t, result.err)
83 require.NotNil(t, result.db)
84
85 if firstDB == nil {
86 firstDB = result.db
87 } else {
88 assert.Same(t, firstDB, result.db, "All goroutines should receive the same DB instance")
89 }
90 }
91
92 require.NoError(t, lazy.Close())
93}
94
95func TestLazyDB_CloseBeforeInit(t *testing.T) {
96 dbPath := filepath.Join(t.TempDir(), "test.db")
97 lazy := sqlite.NewLazyDB(dbPath)
98
99 // Close without ever calling DB() seals the provider without triggering
100 // initialization; late callers are rejected with the closed error.
101 require.NoError(t, lazy.Close())
102 assert.False(t, lazy.IsInitialized(), "Close must not trigger initialization")
103 _, err := lazy.DB(testCtx())
104 require.ErrorIs(t, err, sqlite.ErrLazyDBClosed)
105}
106
107func TestLazyDB_Path(t *testing.T) {
108 dbPath := "/some/path/to/db.sqlite"
109 lazy := sqlite.NewLazyDB(dbPath)
110
111 assert.Equal(t, dbPath, lazy.Path())
112}
113
114func TestLazyDB_DBIsUsable(t *testing.T) {
115 ctx := testCtx()
116 dbPath := filepath.Join(t.TempDir(), "test.db")
117 lazy := sqlite.NewLazyDB(dbPath)
118
119 db, err := lazy.DB(ctx)
120 require.NoError(t, err)
121
122 // Verify we can execute a simple query
123 var result int
124 err = db.QueryRowContext(ctx, "SELECT 1").Scan(&result)
125 require.NoError(t, err)
126 assert.Equal(t, 1, result)
127
128 require.NoError(t, lazy.Close())
129}
130
131func TestLazyDB_WarmupInitializesInBackground(t *testing.T) {
132 ctx := testCtx()
133 dbPath := filepath.Join(t.TempDir(), "test.db")
134 lazy := sqlite.NewLazyDB(dbPath)
135
136 // Warmup returns immediately and initializes the provider concurrently.
137 lazy.Warmup(ctx)
138
139 require.Eventually(t, func() bool {
140 return lazy.IsInitialized()
141 }, 10*time.Second, 5*time.Millisecond, "warmup should initialize the provider without a direct DB call")
142
143 db, err := lazy.DB(ctx)
144 require.NoError(t, err)
145 require.NotNil(t, db)
146 require.NoError(t, lazy.Close())
147}
148
149func TestLazyDB_CloseWaitsForInflightInit(t *testing.T) {
150 ctx := testCtx()
151 // Sealing and settlement: whether the warmup task is admitted or loses
152 // the admission race, an immediate Close leaves no usable connection
153 // behind and every later call fails closed.
154 dbPath := filepath.Join(t.TempDir(), "test.db")
155 lazy := sqlite.NewLazyDB(dbPath)
156
157 lazy.Warmup(ctx)
158 require.NoError(t, lazy.Close())
159
160 _, err := lazy.DB(ctx)
161 require.ErrorIs(t, err, sqlite.ErrLazyDBClosed)
162 require.NoError(t, lazy.Close(), "repeat Close must be idempotent")
163}
164
165func TestLazyDB_ConcurrentWarmupAccessAndClose(t *testing.T) {
166 ctx := testCtx()
167 dbPath := filepath.Join(t.TempDir(), "test.db")
168 lazy := sqlite.NewLazyDB(dbPath)
169
170 const workers = 8
171 var wg sync.WaitGroup
172 wg.Add(workers + 1)
173 for range workers {
174 go func() {
175 defer wg.Done()
176 _, _ = lazy.DB(ctx)
177 }()
178 }
179 go func() {
180 defer wg.Done()
181 lazy.Warmup(ctx)
182 }()
183 wg.Wait()
184
185 db, err := lazy.DB(ctx)
186 require.NoError(t, err)
187 require.NotNil(t, db)
188 require.NoError(t, lazy.Close())
189}
190
191func TestLazyDB_InitErrorPropagated(t *testing.T) {
192 ctx := testCtx()
193 // An empty path always fails connection setup; the error must be cached
194 // and reported to every later caller without panicking.
195 lazy := sqlite.NewLazyDB("")
196
197 _, err := lazy.DB(ctx)
198 require.Error(t, err, "initialization failure must propagate to the first caller")
199
200 _, err = lazy.DB(ctx)
201 require.Error(t, err, "initialization failure must propagate to later callers")
202
203 // Closing a failed provider must not error: there is no connection.
204 require.NoError(t, lazy.Close())
205 assert.False(t, lazy.IsInitialized())
206}