Skip to content

Commit 2edb3c0

Browse files
Add unit tests across mongo, stats, webui, and benchmark; introduce integration test foundation; add docker-based mongo smoke test setup and docs
1 parent 7ac19e4 commit 2edb3c0

File tree

17 files changed

+2449
-37
lines changed

17 files changed

+2449
-37
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ Download the [`config.yaml`](./config.yaml) and make the necessary adjustments.
8383
./bin/plgm --webui
8484
```
8585

86+
### 4. Testing
87+
88+
For unit and integration test instructions (including Docker-based MongoDB setup), see [`TESTING.md`](./TESTING.md).
89+
8690
## The Interactive UI
8791

8892
`plgm` features a completely embedded Web UI. It allows you to configure your database connection, upload custom workload schemas, adjust operation ratios, and monitor real-time throughput and latency without ever touching a YAML file. It has the same functionality as the CLI version, but with an awesome UI.
@@ -1010,4 +1014,4 @@ Further configuration and examples specific to [Kubernetes environments setup wi
10101014
10111015
# Disclaimer
10121016
1013-
This application is not supported by Percona. It has been provided as a community contribution and is not covered under any Percona services agreement.
1017+
This application is not supported by Percona. It has been provided as a community contribution and is not covered under any Percona services agreement.

TESTING.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Testing Guide
2+
3+
This repository has two test layers:
4+
5+
1. Unit tests (default, fast, no MongoDB required)
6+
2. Integration tests (optional, require a real MongoDB instance)
7+
8+
## Unit Tests
9+
10+
Run all unit tests:
11+
12+
```bash
13+
go test ./...
14+
```
15+
16+
Unit tests are the default and are intentionally independent of local MongoDB availability.
17+
18+
## Integration Tests
19+
20+
Integration tests are behind the `integration` build tag and are skipped from default runs.
21+
22+
Current integration smoke test:
23+
24+
- `internal/mongo/TestRunWorkloadIntegration_OneShotExecutesFindUpdateAndSkipsInsert`
25+
26+
### Option A: Start MongoDB with Docker Compose (recommended)
27+
28+
From repository root:
29+
30+
```bash
31+
docker compose -f docker-compose.integration.yml up -d
32+
```
33+
34+
Then run the integration smoke test:
35+
36+
```bash
37+
go test -tags=integration ./internal/mongo -run TestRunWorkloadIntegration_OneShotExecutesFindUpdateAndSkipsInsert -v
38+
```
39+
40+
Stop MongoDB when done:
41+
42+
```bash
43+
docker compose -f docker-compose.integration.yml down -v
44+
```
45+
46+
### Option B: Use an existing MongoDB endpoint
47+
48+
Set the URI via environment variable:
49+
50+
```bash
51+
PLGM_IT_MONGO_URI='mongodb://127.0.0.1:30777' \
52+
go test -tags=integration ./internal/mongo -run TestRunWorkloadIntegration_OneShotExecutesFindUpdateAndSkipsInsert -v
53+
```
54+
55+
If your MongoDB requires authentication, include credentials in `PLGM_IT_MONGO_URI`.
56+
57+
## Run Everything Available
58+
59+
```bash
60+
# 1) Unit tests
61+
go test ./...
62+
63+
# 2) Integration smoke test(s)
64+
go test -tags=integration ./internal/mongo -run TestRunWorkloadIntegration_OneShotExecutesFindUpdateAndSkipsInsert -v
65+
```
66+
67+
## CI Notes (Optional)
68+
69+
- Keep `go test ./...` as the required fast gate.
70+
- Run `-tags=integration` tests in a separate optional/extended CI job with Docker services.

docker-compose.integration.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
mongo:
3+
image: mongo:7
4+
container_name: plgm-it-mongo
5+
ports:
6+
- "30777:27017"
7+
command: ["--bind_ip_all"]
8+
healthcheck:
9+
test: ["CMD", "mongosh", "--quiet", "--eval", "db.runCommand({ ping: 1 }).ok"]
10+
interval: 2s
11+
timeout: 2s
12+
retries: 20

internal/benchmark/runner_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package benchmark
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
"unicode"
7+
)
8+
9+
func TestGeneratePayload(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
size int
13+
want int
14+
}{
15+
{name: "zero_uses_default", size: 0, want: 1024},
16+
{name: "negative_uses_default", size: -10, want: 1024},
17+
{name: "custom_size", size: 64, want: 64},
18+
}
19+
20+
for _, tc := range tests {
21+
t.Run(tc.name, func(t *testing.T) {
22+
payload := GeneratePayload(tc.size)
23+
if len(payload) != tc.want {
24+
t.Fatalf("expected payload size %d, got %d", tc.want, len(payload))
25+
}
26+
})
27+
}
28+
}
29+
30+
func TestGenerateRandomString(t *testing.T) {
31+
if got := GenerateRandomString(0); got != "" {
32+
t.Fatalf("expected empty string for size 0, got %q", got)
33+
}
34+
35+
s := GenerateRandomString(32)
36+
if len(s) != 32 {
37+
t.Fatalf("expected len 32, got %d", len(s))
38+
}
39+
for _, r := range s {
40+
if !unicode.IsDigit(r) && !unicode.IsLetter(r) {
41+
t.Fatalf("expected alphanumeric characters only, got %q", string(r))
42+
}
43+
}
44+
}
45+
46+
func TestBuildBatchArray(t *testing.T) {
47+
template := []byte{0x11, 0x22, 0x33}
48+
magic := []byte{0x22}
49+
arr, offsets, err := buildBatchArray(template, 4, magic)
50+
if err != nil {
51+
t.Fatalf("buildBatchArray() error = %v", err)
52+
}
53+
if len(offsets) != 4 {
54+
t.Fatalf("expected 4 offsets, got %d", len(offsets))
55+
}
56+
for _, off := range offsets {
57+
if off < 0 || off >= len(arr) {
58+
t.Fatalf("invalid offset %d", off)
59+
}
60+
if !bytes.Equal(arr[off:off+1], magic) {
61+
t.Fatalf("offset %d does not point to magic byte", off)
62+
}
63+
}
64+
65+
_, _, err = buildBatchArray(template, 1, []byte{0xFF})
66+
if err == nil {
67+
t.Fatalf("expected error when magic byte is absent")
68+
}
69+
70+
arr, offsets, err = buildBatchArray(template, 0, magic)
71+
if err != nil {
72+
t.Fatalf("buildBatchArray(count=0) error = %v", err)
73+
}
74+
if len(offsets) != 0 {
75+
t.Fatalf("expected no offsets for empty batch, got %d", len(offsets))
76+
}
77+
if len(arr) == 0 {
78+
t.Fatalf("expected valid BSON bytes for empty batch")
79+
}
80+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestParseCollectionsBytes(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
wantLen int
14+
wantErr bool
15+
}{
16+
{
17+
name: "wrapped_format",
18+
input: `{"collections":[{"database":"db1","collection":"c1","fields":{}}]}`,
19+
wantLen: 1,
20+
},
21+
{
22+
name: "array_format",
23+
input: `[{"database":"db2","collection":"c2","fields":{}}]`,
24+
wantLen: 1,
25+
},
26+
{
27+
name: "invalid_format",
28+
input: `{"collections":[]}`,
29+
wantErr: true,
30+
},
31+
}
32+
33+
for _, tc := range tests {
34+
t.Run(tc.name, func(t *testing.T) {
35+
got, err := parseCollectionsBytes([]byte(tc.input))
36+
if tc.wantErr {
37+
if err == nil {
38+
t.Fatalf("expected error")
39+
}
40+
return
41+
}
42+
if err != nil {
43+
t.Fatalf("parseCollectionsBytes() error = %v", err)
44+
}
45+
if len(got.Collections) != tc.wantLen {
46+
t.Fatalf("expected %d collections, got %d", tc.wantLen, len(got.Collections))
47+
}
48+
})
49+
}
50+
}
51+
52+
func TestLoadCollectionsDirectoryFiltering(t *testing.T) {
53+
dir := t.TempDir()
54+
defaultJSON := `{"collections":[{"database":"db","collection":"default_col","fields":{}}]}`
55+
customJSON := `{"collections":[{"database":"db","collection":"custom_col","fields":{}}]}`
56+
57+
if err := os.WriteFile(filepath.Join(dir, "default.json"), []byte(defaultJSON), 0o644); err != nil {
58+
t.Fatalf("write default: %v", err)
59+
}
60+
if err := os.WriteFile(filepath.Join(dir, "custom.json"), []byte(customJSON), 0o644); err != nil {
61+
t.Fatalf("write custom: %v", err)
62+
}
63+
if err := os.WriteFile(filepath.Join(dir, "ignore.txt"), []byte("x"), 0o644); err != nil {
64+
t.Fatalf("write ignore: %v", err)
65+
}
66+
67+
defaultOnly, err := LoadCollections(dir, true)
68+
if err != nil {
69+
t.Fatalf("LoadCollections(default) error = %v", err)
70+
}
71+
if len(defaultOnly.Collections) != 1 || defaultOnly.Collections[0].Name != "default_col" {
72+
t.Fatalf("expected only default collection, got %+v", defaultOnly.Collections)
73+
}
74+
75+
nonDefault, err := LoadCollections(dir, false)
76+
if err != nil {
77+
t.Fatalf("LoadCollections(non-default) error = %v", err)
78+
}
79+
if len(nonDefault.Collections) != 1 || nonDefault.Collections[0].Name != "custom_col" {
80+
t.Fatalf("expected only custom collection, got %+v", nonDefault.Collections)
81+
}
82+
}
83+
84+
func TestLoadCollectionsMissingPathFallsBackToEmbeddedDefault(t *testing.T) {
85+
cfg, err := LoadCollections(filepath.Join(t.TempDir(), "does-not-exist"), true)
86+
if err != nil {
87+
t.Fatalf("LoadCollections() error = %v", err)
88+
}
89+
if len(cfg.Collections) == 0 {
90+
t.Fatalf("expected embedded default collections")
91+
}
92+
}
93+
94+
func TestLoadCollectionsValidationErrorForMissingNames(t *testing.T) {
95+
file := filepath.Join(t.TempDir(), "bad.json")
96+
bad := `{"collections":[{"database":"","collection":"","fields":{}}]}`
97+
if err := os.WriteFile(file, []byte(bad), 0o644); err != nil {
98+
t.Fatalf("write bad file: %v", err)
99+
}
100+
101+
_, err := LoadCollections(file, false)
102+
if err == nil {
103+
t.Fatalf("expected validation error for empty names")
104+
}
105+
}

0 commit comments

Comments
 (0)