Skip to content

Commit aed7710

Browse files
committed
PCSM-219: Add integration tests for the CLI client
1 parent 2a0e34c commit aed7710

File tree

12 files changed

+1432
-132
lines changed

12 files changed

+1432
-132
lines changed

config/config.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
package config
33

44
import (
5+
"math"
56
"os"
67
"slices"
78
"strings"
89

10+
"github.com/dustin/go-humanize"
911
"github.com/spf13/cobra"
1012
"github.com/spf13/viper"
1113

@@ -45,7 +47,7 @@ func bindEnvVars() {
4547
_ = viper.BindEnv("target", "PCSM_TARGET_URI")
4648

4749
// MongoDB client timeout
48-
_ = viper.BindEnv("mongodb-cli-operation-timeout", "PCSM_MONGODB_CLI_OPERATION_TIMEOUT")
50+
_ = viper.BindEnv("mongodb-operation-timeout", "PCSM_MONGODB_OPERATION_TIMEOUT")
4951

5052
// Bulk write option (hidden, internal use)
5153
_ = viper.BindEnv("use-collection-bulk-write", "PCSM_USE_COLLECTION_BULK_WRITE")
@@ -72,3 +74,69 @@ func UseTargetClientCompressors() []string {
7274

7375
return rv
7476
}
77+
78+
// ResolveCloneSegmentSize resolves the clone segment size from an optional HTTP value
79+
// or falls back to the CLI config value. Both sources are validated.
80+
func ResolveCloneSegmentSize(cfg *Config, value *string) (int64, error) {
81+
if value != nil {
82+
return parseAndValidateCloneSegmentSize(*value)
83+
}
84+
85+
// Fall back to CLI config value and validate it
86+
sizeBytes := cfg.Clone.SegmentSizeBytes()
87+
88+
err := ValidateCloneSegmentSize(uint64(max(sizeBytes, 0))) //nolint:gosec
89+
if err != nil {
90+
return 0, errors.Wrap(err, "config clone-segment-size")
91+
}
92+
93+
return sizeBytes, nil
94+
}
95+
96+
// ResolveCloneReadBatchSize resolves the clone read batch size from an optional HTTP value
97+
// or falls back to the CLI config value. Both sources are validated.
98+
func ResolveCloneReadBatchSize(cfg *Config, value *string) (int32, error) {
99+
if value != nil {
100+
return parseAndValidateCloneReadBatchSize(*value)
101+
}
102+
103+
// Fall back to CLI config value and validate it
104+
sizeBytes := cfg.Clone.ReadBatchSizeBytes()
105+
106+
err := ValidateCloneReadBatchSize(uint64(max(sizeBytes, 0))) //nolint:gosec
107+
if err != nil {
108+
return 0, errors.Wrap(err, "config clone-read-batch-size")
109+
}
110+
111+
return sizeBytes, nil
112+
}
113+
114+
// parseAndValidateCloneSegmentSize parses a byte size string and validates it.
115+
func parseAndValidateCloneSegmentSize(value string) (int64, error) {
116+
sizeBytes, err := humanize.ParseBytes(value)
117+
if err != nil {
118+
return 0, errors.Wrapf(err, "invalid cloneSegmentSize value: %s", value)
119+
}
120+
121+
err = ValidateCloneSegmentSize(sizeBytes)
122+
if err != nil {
123+
return 0, err
124+
}
125+
126+
return int64(min(sizeBytes, math.MaxInt64)), nil //nolint:gosec
127+
}
128+
129+
// parseAndValidateCloneReadBatchSize parses a byte size string and validates it.
130+
func parseAndValidateCloneReadBatchSize(value string) (int32, error) {
131+
sizeBytes, err := humanize.ParseBytes(value)
132+
if err != nil {
133+
return 0, errors.Wrapf(err, "invalid cloneReadBatchSize value: %s", value)
134+
}
135+
136+
err = ValidateCloneReadBatchSize(sizeBytes)
137+
if err != nil {
138+
return 0, err
139+
}
140+
141+
return int32(min(sizeBytes, math.MaxInt32)), nil //nolint:gosec
142+
}

config/config_test.go

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package config_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/dustin/go-humanize"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/percona/percona-clustersync-mongodb/config"
12+
)
13+
14+
func TestResolveCloneSegmentSize(t *testing.T) {
15+
t.Parallel()
16+
17+
tests := []struct {
18+
name string
19+
cfg *config.Config
20+
value *string
21+
want int64
22+
wantErr string
23+
}{
24+
{
25+
name: "nil value - falls back to config default (empty)",
26+
cfg: &config.Config{},
27+
value: nil,
28+
want: config.AutoCloneSegmentSize,
29+
wantErr: "",
30+
},
31+
{
32+
name: "nil value - falls back to config value",
33+
cfg: &config.Config{
34+
Clone: config.CloneConfig{
35+
SegmentSize: "1GiB",
36+
},
37+
},
38+
value: nil,
39+
want: humanize.GiByte,
40+
wantErr: "",
41+
},
42+
{
43+
name: "valid size 500MB (above minimum)",
44+
cfg: &config.Config{},
45+
value: toPtr("500MB"),
46+
want: 500 * humanize.MByte,
47+
wantErr: "",
48+
},
49+
{
50+
name: "valid size 1GiB",
51+
cfg: &config.Config{},
52+
value: toPtr("1GiB"),
53+
want: humanize.GiByte,
54+
wantErr: "",
55+
},
56+
{
57+
name: "zero value (auto)",
58+
cfg: &config.Config{},
59+
value: toPtr("0"),
60+
want: 0,
61+
wantErr: "",
62+
},
63+
{
64+
name: "below minimum (100MB)",
65+
cfg: &config.Config{},
66+
value: toPtr("100MB"),
67+
wantErr: "cloneSegmentSize must be at least",
68+
},
69+
{
70+
name: "above maximum",
71+
cfg: &config.Config{},
72+
value: toPtr("100GiB"),
73+
wantErr: "cloneSegmentSize must be at most",
74+
},
75+
{
76+
name: "at minimum boundary (using exact bytes)",
77+
cfg: &config.Config{},
78+
value: toPtr(fmt.Sprintf("%dB", config.MinCloneSegmentSizeBytes)),
79+
want: int64(config.MinCloneSegmentSizeBytes),
80+
wantErr: "",
81+
},
82+
{
83+
name: "at maximum boundary",
84+
cfg: &config.Config{},
85+
value: toPtr("64GiB"),
86+
want: int64(config.MaxCloneSegmentSizeBytes),
87+
wantErr: "",
88+
},
89+
{
90+
name: "invalid format",
91+
cfg: &config.Config{},
92+
value: toPtr("abc"),
93+
wantErr: "invalid cloneSegmentSize value",
94+
},
95+
{
96+
name: "empty string",
97+
cfg: &config.Config{},
98+
value: toPtr(""),
99+
wantErr: "invalid cloneSegmentSize value",
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
t.Parallel()
106+
107+
got, err := config.ResolveCloneSegmentSize(tt.cfg, tt.value)
108+
109+
if tt.wantErr == "" {
110+
require.NoError(t, err)
111+
assert.Equal(t, tt.want, got)
112+
} else {
113+
require.Error(t, err)
114+
assert.Contains(t, err.Error(), tt.wantErr)
115+
}
116+
})
117+
}
118+
}
119+
120+
func TestResolveCloneReadBatchSize(t *testing.T) {
121+
t.Parallel()
122+
123+
tests := []struct {
124+
name string
125+
cfg *config.Config
126+
value *string
127+
want int32
128+
wantErr string
129+
}{
130+
{
131+
name: "nil value - falls back to config default (empty)",
132+
cfg: &config.Config{},
133+
value: nil,
134+
want: 0,
135+
wantErr: "",
136+
},
137+
{
138+
name: "nil value - falls back to config value",
139+
cfg: &config.Config{
140+
Clone: config.CloneConfig{
141+
ReadBatchSize: "32MiB",
142+
},
143+
},
144+
value: nil,
145+
want: 32 * humanize.MiByte,
146+
wantErr: "",
147+
},
148+
{
149+
name: "valid size 16MiB",
150+
cfg: &config.Config{},
151+
value: toPtr("16MiB"),
152+
want: 16 * humanize.MiByte,
153+
wantErr: "",
154+
},
155+
{
156+
name: "valid size 48MB",
157+
cfg: &config.Config{},
158+
value: toPtr("48MB"),
159+
want: 48 * humanize.MByte,
160+
wantErr: "",
161+
},
162+
{
163+
name: "zero value (auto)",
164+
cfg: &config.Config{},
165+
value: toPtr("0"),
166+
want: 0,
167+
wantErr: "",
168+
},
169+
{
170+
name: "below minimum",
171+
cfg: &config.Config{},
172+
value: toPtr("1KB"),
173+
wantErr: "cloneReadBatchSize must be at least",
174+
},
175+
{
176+
name: "at minimum boundary (using exact bytes)",
177+
cfg: &config.Config{},
178+
value: toPtr(fmt.Sprintf("%dB", config.MinCloneReadBatchSizeBytes)),
179+
want: config.MinCloneReadBatchSizeBytes,
180+
wantErr: "",
181+
},
182+
{
183+
name: "invalid format",
184+
cfg: &config.Config{},
185+
value: toPtr("xyz"),
186+
wantErr: "invalid cloneReadBatchSize value",
187+
},
188+
{
189+
name: "empty string",
190+
cfg: &config.Config{},
191+
value: toPtr(""),
192+
wantErr: "invalid cloneReadBatchSize value",
193+
},
194+
}
195+
196+
for _, tt := range tests {
197+
t.Run(tt.name, func(t *testing.T) {
198+
t.Parallel()
199+
200+
got, err := config.ResolveCloneReadBatchSize(tt.cfg, tt.value)
201+
202+
if tt.wantErr == "" {
203+
require.NoError(t, err)
204+
assert.Equal(t, tt.want, got)
205+
} else {
206+
require.Error(t, err)
207+
assert.Contains(t, err.Error(), tt.wantErr)
208+
}
209+
})
210+
}
211+
}
212+
213+
func TestUseTargetClientCompressors(t *testing.T) {
214+
tests := []struct {
215+
name string
216+
envVal string
217+
want []string
218+
wantNil bool
219+
}{
220+
{
221+
name: "empty env - returns nil",
222+
envVal: "",
223+
want: nil,
224+
wantNil: true,
225+
},
226+
{
227+
name: "single valid compressor zstd",
228+
envVal: "zstd",
229+
want: []string{"zstd"},
230+
},
231+
{
232+
name: "single valid compressor zlib",
233+
envVal: "zlib",
234+
want: []string{"zlib"},
235+
},
236+
{
237+
name: "single valid compressor snappy",
238+
envVal: "snappy",
239+
want: []string{"snappy"},
240+
},
241+
{
242+
name: "multiple valid compressors",
243+
envVal: "zstd,zlib,snappy",
244+
want: []string{"zstd", "zlib", "snappy"},
245+
},
246+
{
247+
name: "compressors with spaces",
248+
envVal: " zstd , zlib , snappy ",
249+
want: []string{"zstd", "zlib", "snappy"},
250+
},
251+
{
252+
name: "invalid compressor ignored",
253+
envVal: "zstd,invalid,zlib",
254+
want: []string{"zstd", "zlib"},
255+
},
256+
{
257+
name: "all invalid compressors - returns empty slice",
258+
envVal: "invalid,gzip,lz4",
259+
want: []string{},
260+
},
261+
{
262+
name: "duplicate compressors - deduplicated",
263+
envVal: "zstd,zstd,zlib,zstd",
264+
want: []string{"zstd", "zlib"},
265+
},
266+
{
267+
name: "whitespace only - returns nil",
268+
envVal: " ",
269+
want: nil,
270+
wantNil: true,
271+
},
272+
{
273+
name: "mixed valid and invalid with spaces",
274+
envVal: " zstd , invalid , snappy ",
275+
want: []string{"zstd", "snappy"},
276+
},
277+
}
278+
279+
for _, tt := range tests {
280+
t.Run(tt.name, func(t *testing.T) {
281+
t.Setenv("PCSM_DEV_TARGET_CLIENT_COMPRESSORS", tt.envVal)
282+
283+
got := config.UseTargetClientCompressors()
284+
285+
if tt.wantNil {
286+
assert.Nil(t, got)
287+
} else {
288+
assert.Equal(t, tt.want, got)
289+
}
290+
})
291+
}
292+
}
293+
294+
func toPtr(s string) *string { return &s }

config/const.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ const (
4444
DisconnectTimeout = 5 * time.Second
4545
// CloseCursorTimeout is the timeout duration for closing cursor.
4646
CloseCursorTimeout = 10 * time.Second
47-
// DefaultMongoDBCliOperationTimeout is the default timeout duration for MongoDB client
47+
// DefaultMongoDBOperationTimeout is the default timeout duration for MongoDB client
4848
// operations like insert, update, delete, etc. It can be overridden via
4949
// environment variable (see config.OperationMongoDBCliTimeout()).
50-
DefaultMongoDBCliOperationTimeout = 5 * time.Minute
50+
DefaultMongoDBOperationTimeout = 5 * time.Minute
5151
)
5252

5353
// Change stream and replication settings.

0 commit comments

Comments
 (0)