Skip to content

Commit 89916de

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

File tree

8 files changed

+1346
-120
lines changed

8 files changed

+1346
-120
lines changed

config/config.go

Lines changed: 68 additions & 0 deletions
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

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

0 commit comments

Comments
 (0)