Skip to content

Commit 46aaa5d

Browse files
committed
Merge branch 'feature/improve-test-coverage' into 'master'
Improve unit test coverage from 19% to 26% See merge request postgres-ai/database-lab!1113
2 parents 0efd7af + a02a442 commit 46aaa5d

49 files changed

Lines changed: 5585 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
.env
44

55
engine/bin/
6+
engine/coverage.out
67
/db-lab-run/
78

89
# Deploy contains Kubernetes configs with secrets, remove from .gitignore when generalized.

engine/.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
run:
2-
timeout: 2m
2+
timeout: 5m
33
issues-exit-code: 1
44
tests: true
55
output:

engine/Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ build-dle: build build-image ## Build Database Lab Engine binary and Docker imag
7575
test: ## Run unit tests
7676
${GOTEST} ./...
7777

78+
test-coverage: ## Run unit tests with coverage report
79+
${GOTEST} -coverprofile=coverage.out ./...
80+
@go tool cover -func=coverage.out
81+
7882
test-ci-integration: ## Run integration tests
7983
GO111MODULE=on go test -race -tags=integration ./...
8084

@@ -100,4 +104,4 @@ run-dle: build-dle
100104
-p "2345:2345" \
101105
dblab_server:local
102106

103-
.PHONY: help all build test run-lint install-lint lint fmt clean build-image build-dle build-ci-checker build-client build-rds-refresh run-dle
107+
.PHONY: help all build test test-coverage run-lint install-lint lint fmt clean build-image build-dle build-ci-checker build-client build-rds-refresh run-dle

engine/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ require (
66
github.com/AlekSi/pointer v1.2.0
77
github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26
88
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
9-
github.com/aws/aws-sdk-go v1.44.309
9+
github.com/aws/aws-sdk-go v1.55.8
1010
github.com/aws/aws-sdk-go-v2 v1.41.0
1111
github.com/aws/aws-sdk-go-v2/config v1.32.5
1212
github.com/aws/aws-sdk-go-v2/service/rds v1.113.1
1313
github.com/containerd/errdefs v1.0.0
1414
github.com/docker/cli v27.1.1+incompatible
15-
github.com/docker/docker v28.5.1+incompatible
15+
github.com/docker/docker v28.5.2+incompatible
1616
github.com/docker/go-connections v0.6.0
1717
github.com/docker/go-units v0.5.0
1818
github.com/dustin/go-humanize v1.0.1

engine/go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:pzStYMLAXM7
1414
github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ilK+u7u1HoqaDk0mjhh27QJB7PyWMreGffEvOCoEKiY=
1515
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
1616
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
17-
github.com/aws/aws-sdk-go v1.44.309 h1:IPJOFBzXekakxmEpDwd4RTKmmBR6LIAiXgNsM51bWbU=
18-
github.com/aws/aws-sdk-go v1.44.309/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
17+
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
18+
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
1919
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
2020
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
2121
github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
@@ -78,8 +78,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
7878
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
7979
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
8080
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
81-
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
82-
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
81+
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
82+
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
8383
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
8484
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
8585
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -372,7 +372,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
372372
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
373373
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
374374
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
375-
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
376375
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
377376
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
378377
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
@@ -402,7 +401,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
402401
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
403402
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
404403
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
405-
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
406404
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
407405
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
408406
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -412,7 +410,6 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
412410
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
413411
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
414412
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
415-
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
416413
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
417414
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
418415
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
@@ -424,7 +421,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
424421
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
425422
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
426423
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
427-
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
428424
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
429425
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
430426
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

engine/internal/cloning/base.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"gitlab.com/postgres-ai/database-lab/v3/internal/provision"
2525
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
26+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/db"
2627
"gitlab.com/postgres-ai/database-lab/v3/internal/telemetry"
2728
"gitlab.com/postgres-ai/database-lab/v3/internal/webhooks"
2829
"gitlab.com/postgres-ai/database-lab/v3/pkg/client/dblabapi/types"
@@ -308,8 +309,10 @@ func (c *Base) ConnectToClone(ctx context.Context, cloneID string) (pgxtype.Quer
308309
}
309310

310311
func connectionString(host, port, username, dbname string) string {
311-
return fmt.Sprintf("host=%s port=%s user=%s database='%s'",
312-
host, port, username, dbname)
312+
return fmt.Sprintf("host='%s' port=%s user='%s' database='%s'",
313+
db.EscapeLibpqValue(host), port,
314+
db.EscapeLibpqValue(username),
315+
db.EscapeLibpqValue(dbname))
313316
}
314317

315318
// DestroyClone destroys clone.
@@ -752,6 +755,12 @@ func (c *Base) deleteClone(cloneID string) {
752755
c.cloneMutex.Unlock()
753756
}
754757

758+
func (c *Base) resetClones() {
759+
c.cloneMutex.Lock()
760+
c.clones = make(map[string]*CloneWrapper)
761+
c.cloneMutex.Unlock()
762+
}
763+
755764
// lenClones returns the number of clones.
756765
func (c *Base) lenClones() int {
757766
c.cloneMutex.RLock()

engine/internal/cloning/base_test.go

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ func (s *BaseCloningSuite) SetupSuite() {
3535
}
3636

3737
func (s *BaseCloningSuite) TearDownTest() {
38-
s.cloning.clones = make(map[string]*CloneWrapper)
39-
s.cloning.snapshotBox = SnapshotBox{items: make(map[string]*models.Snapshot)}
38+
s.cloning.resetClones()
39+
s.cloning.resetSnapshots(make(map[string]*models.Snapshot), nil)
4040
}
4141

4242
func (s *BaseCloningSuite) TestFindWrapper() {
@@ -217,3 +217,123 @@ func TestCalculateProtectionTime_EdgeCases(t *testing.T) {
217217
func ptrUint(v uint) *uint {
218218
return &v
219219
}
220+
221+
func (s *BaseCloningSuite) TestGetExpectedCloningTime() {
222+
t := s.T()
223+
224+
result := s.cloning.getExpectedCloningTime()
225+
assert.Equal(t, 0.0, result, "expected zero when no clones exist")
226+
227+
s.cloning.setWrapper("clone1", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 2.0}}})
228+
s.cloning.setWrapper("clone2", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 4.0}}})
229+
s.cloning.setWrapper("clone3", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 6.0}}})
230+
231+
result = s.cloning.getExpectedCloningTime()
232+
assert.Equal(t, 4.0, result, "expected average of 2, 4, 6")
233+
}
234+
235+
func (s *BaseCloningSuite) TestGetExpectedCloningTimeSingleClone() {
236+
t := s.T()
237+
238+
s.cloning.setWrapper("clone1", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 3.5}}})
239+
240+
result := s.cloning.getExpectedCloningTime()
241+
assert.Equal(t, 3.5, result)
242+
}
243+
244+
func (s *BaseCloningSuite) TestGetExpectedCloningTimeZeroValues() {
245+
t := s.T()
246+
247+
s.cloning.setWrapper("clone1", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 0.0}}})
248+
s.cloning.setWrapper("clone2", &CloneWrapper{Clone: &models.Clone{Metadata: models.CloneMetadata{CloningTime: 0.0}}})
249+
250+
result := s.cloning.getExpectedCloningTime()
251+
assert.Equal(t, 0.0, result)
252+
}
253+
254+
func (s *BaseCloningSuite) TestUpdateCloneStatusNotFound() {
255+
t := s.T()
256+
257+
err := s.cloning.UpdateCloneStatus("nonexistent", models.Status{Code: models.StatusOK, Message: models.CloneMessageOK})
258+
assert.Error(t, err)
259+
assert.Contains(t, err.Error(), "not found")
260+
}
261+
262+
func (s *BaseCloningSuite) TestUpdateCloneSnapshot() {
263+
t := s.T()
264+
265+
originalSnapshot := &models.Snapshot{ID: "snap1"}
266+
newSnapshot := &models.Snapshot{ID: "snap2"}
267+
268+
s.cloning.setWrapper("clone1", &CloneWrapper{Clone: &models.Clone{ID: "clone1", Snapshot: originalSnapshot}})
269+
270+
err := s.cloning.UpdateCloneSnapshot("clone1", newSnapshot)
271+
require.NoError(t, err)
272+
273+
w, ok := s.cloning.findWrapper("clone1")
274+
require.True(t, ok)
275+
assert.Equal(t, "snap2", w.Clone.Snapshot.ID)
276+
}
277+
278+
func (s *BaseCloningSuite) TestUpdateCloneSnapshotNotFound() {
279+
t := s.T()
280+
281+
err := s.cloning.UpdateCloneSnapshot("nonexistent", &models.Snapshot{ID: "snap1"})
282+
assert.Error(t, err)
283+
assert.Contains(t, err.Error(), "not found")
284+
}
285+
286+
func TestConnectionString(t *testing.T) {
287+
tests := []struct {
288+
name string
289+
host string
290+
port string
291+
username string
292+
dbname string
293+
expected string
294+
}{
295+
{name: "standard values", host: "localhost", port: "5432", username: "postgres", dbname: "testdb", expected: "host='localhost' port=5432 user='postgres' database='testdb'"},
296+
{name: "custom host and port", host: "10.0.0.1", port: "6000", username: "admin", dbname: "mydb", expected: "host='10.0.0.1' port=6000 user='admin' database='mydb'"},
297+
{name: "socket path host", host: "/var/run/postgres", port: "5433", username: "user1", dbname: "db1", expected: "host='/var/run/postgres' port=5433 user='user1' database='db1'"},
298+
{name: "empty dbname", host: "localhost", port: "5432", username: "postgres", dbname: "", expected: "host='localhost' port=5432 user='postgres' database=''"},
299+
{name: "dbname with single quote", host: "localhost", port: "5432", username: "postgres", dbname: "test'db", expected: "host='localhost' port=5432 user='postgres' database='test''db'"},
300+
{name: "dbname with spaces", host: "localhost", port: "5432", username: "postgres", dbname: "my database", expected: "host='localhost' port=5432 user='postgres' database='my database'"},
301+
{name: "dbname with backslash", host: "localhost", port: "5432", username: "postgres", dbname: `test\db`, expected: `host='localhost' port=5432 user='postgres' database='test\\db'`},
302+
{name: "username with single quote", host: "localhost", port: "5432", username: "user'name", dbname: "testdb", expected: `host='localhost' port=5432 user='user''name' database='testdb'`},
303+
{name: "username with backslash", host: "localhost", port: "5432", username: `user\name`, dbname: "testdb", expected: `host='localhost' port=5432 user='user\\name' database='testdb'`},
304+
{name: "combined quote and backslash in dbname", host: "localhost", port: "5432", username: "postgres", dbname: `test\'db`, expected: `host='localhost' port=5432 user='postgres' database='test\\''db'`},
305+
{name: "host with single quote", host: "host'name", port: "5432", username: "postgres", dbname: "testdb", expected: `host='host''name' port=5432 user='postgres' database='testdb'`},
306+
}
307+
308+
for _, tt := range tests {
309+
t.Run(tt.name, func(t *testing.T) {
310+
result := connectionString(tt.host, tt.port, tt.username, tt.dbname)
311+
assert.Equal(t, tt.expected, result)
312+
})
313+
}
314+
}
315+
316+
func TestGetCloningState(t *testing.T) {
317+
cfg := &Config{ProtectionLeaseDurationMinutes: 60, ProtectionMaxDurationMinutes: 120}
318+
base := &Base{
319+
config: cfg,
320+
clones: make(map[string]*CloneWrapper),
321+
snapshotBox: SnapshotBox{items: make(map[string]*models.Snapshot)},
322+
}
323+
324+
state := base.GetCloningState()
325+
assert.Equal(t, uint64(0), state.NumClones)
326+
assert.Equal(t, 0.0, state.ExpectedCloningTime)
327+
assert.Equal(t, uint(60), state.ProtectionLeaseDurationMinutes)
328+
assert.Equal(t, uint(120), state.ProtectionMaxDurationMinutes)
329+
assert.Empty(t, state.Clones)
330+
331+
base.setWrapper("c1", &CloneWrapper{Clone: &models.Clone{
332+
CreatedAt: &models.LocalTime{Time: time.Now()},
333+
Metadata: models.CloneMetadata{CloningTime: 1.5},
334+
}})
335+
336+
state = base.GetCloningState()
337+
assert.Equal(t, uint64(1), state.NumClones)
338+
assert.Equal(t, 1.5, state.ExpectedCloningTime)
339+
}

0 commit comments

Comments
 (0)