master
go 428 lines 11.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package confopt
4
5 import (
6 "encoding/json"
7 "fmt"
8 "math"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15
16 "gopkg.in/yaml.v2"
17 )
18
19 func TestParseDuration(t *testing.T) {
20 tests := map[string]struct {
21 input string
22 wantDuration time.Duration
23 wantErr bool
24 }{
25 "nanoseconds": {input: "10ns", wantDuration: 10 * time.Nanosecond},
26 "microseconds": {input: "10us", wantDuration: 10 * time.Microsecond},
27 "milliseconds": {input: "10ms", wantDuration: 10 * time.Millisecond},
28 "seconds": {input: "10s", wantDuration: 10 * time.Second},
29 "minutes": {input: "10m", wantDuration: 10 * time.Minute},
30 "hours": {input: "10h", wantDuration: 10 * time.Hour},
31 "days": {input: "10d", wantDuration: 10 * 24 * time.Hour},
32 "weeks (w)": {input: "10w", wantDuration: 10 * 7 * 24 * time.Hour},
33 "weeks (wk)": {input: "10wk", wantDuration: 10 * 7 * 24 * time.Hour},
34 "months (mo)": {input: "10mo", wantDuration: 10 * 30 * 24 * time.Hour},
35 "months (M)": {input: "10M", wantDuration: 10 * 30 * 24 * time.Hour},
36 "years": {input: "10y", wantDuration: 10 * 365 * 24 * time.Hour},
37 "negative units": {input: "-10d", wantDuration: -10 * 24 * time.Hour},
38 "mixed units": {
39 input: "1y2M3w4d5h6m7s8ms9us10ns",
40 wantDuration: (1 * 365 * 24 * time.Hour) +
41 (2 * 30 * 24 * time.Hour) +
42 (3 * 7 * 24 * time.Hour) +
43 (4 * 24 * time.Hour) +
44 (5 * time.Hour) +
45 (6 * time.Minute) +
46 (7 * time.Second) +
47 (8 * time.Millisecond) +
48 (9 * time.Microsecond) +
49 (10 * time.Nanosecond),
50 },
51 "mixed units with spaces": {
52 input: "1y 2M 3w 4d 5h 6m 7s 8ms 9us 10ns",
53 wantDuration: (1 * 365 * 24 * time.Hour) +
54 (2 * 30 * 24 * time.Hour) +
55 (3 * 7 * 24 * time.Hour) +
56 (4 * 24 * time.Hour) +
57 (5 * time.Hour) +
58 (6 * time.Minute) +
59 (7 * time.Second) +
60 (8 * time.Millisecond) +
61 (9 * time.Microsecond) +
62 (10 * time.Nanosecond),
63 },
64 "mixed units with decimals": {
65 input: "1.5y2.25M3.75w4.5d5.5h6.5m7.5s8.5ms9.5us10.5ns",
66 wantDuration: time.Duration(math.Floor(1.5*365*24*float64(time.Hour))) +
67 time.Duration(math.Floor(2.25*30*24*float64(time.Hour))) +
68 time.Duration(math.Floor(3.75*7*24*float64(time.Hour))) +
69 time.Duration(math.Floor(4.5*24*float64(time.Hour))) +
70 time.Duration(math.Floor(5.5*float64(time.Hour))) +
71 time.Duration(math.Floor(6.5*float64(time.Minute))) +
72 time.Duration(math.Floor(7.5*float64(time.Second))) +
73 time.Duration(math.Floor(8.5*float64(time.Millisecond))) +
74 time.Duration(math.Floor(9.5*float64(time.Microsecond))) +
75 time.Duration(math.Floor(10.5*float64(time.Nanosecond))),
76 },
77 }
78
79 for name, test := range tests {
80 t.Run(name, func(t *testing.T) {
81 dur, err := ParseDuration(test.input)
82
83 if test.wantErr {
84 assert.Error(t, err)
85 } else {
86 require.NoError(t, err)
87 assert.Equal(t, test.wantDuration, dur)
88 }
89 })
90 }
91 }
92
93 func TestDuration_MarshalYAML(t *testing.T) {
94 tests := map[string]struct {
95 d Duration
96 want string
97 }{
98 "1 second": {d: Duration(time.Second), want: "1"},
99 "1.5 seconds": {d: Duration(time.Second + time.Millisecond*500), want: "1.5"},
100 }
101
102 for name, test := range tests {
103 t.Run(name, func(t *testing.T) {
104 bs, err := yaml.Marshal(&test.d)
105 require.NoError(t, err)
106
107 assert.Equal(t, test.want, strings.TrimSpace(string(bs)))
108 })
109 }
110 }
111
112 func TestDuration_MarshalJSON(t *testing.T) {
113 tests := map[string]struct {
114 d Duration
115 want string
116 }{
117 "1 second": {d: Duration(time.Second), want: "1"},
118 "1.5 seconds": {d: Duration(time.Second + time.Millisecond*500), want: "1.5"},
119 }
120
121 for name, test := range tests {
122 t.Run(name, func(t *testing.T) {
123 bs, err := json.Marshal(&test.d)
124 require.NoError(t, err)
125
126 assert.Equal(t, test.want, strings.TrimSpace(string(bs)))
127 })
128 }
129 }
130
131 func TestDuration_UnmarshalYAML(t *testing.T) {
132 tests := map[string]struct {
133 input any
134 }{
135 "duration": {input: "300ms"},
136 "string int": {input: "1"},
137 "string float": {input: "1.1"},
138 "int": {input: 2},
139 "float": {input: 2.2},
140 }
141
142 var zero Duration
143
144 for name, test := range tests {
145 name = fmt.Sprintf("%s (%v)", name, test.input)
146 t.Run(name, func(t *testing.T) {
147 data, err := yaml.Marshal(test.input)
148 require.NoError(t, err)
149
150 var d Duration
151 require.NoError(t, yaml.Unmarshal(data, &d))
152 assert.NotEqual(t, zero.String(), d.String())
153 })
154 }
155 }
156
157 func TestDuration_UnmarshalJSON(t *testing.T) {
158 tests := map[string]struct {
159 input string
160 expected time.Duration
161 wantErr bool
162 }{
163 // JSON numbers (interpreted as seconds)
164 "json number int": {
165 input: `{"d": 30}`,
166 expected: 30 * time.Second,
167 },
168 "json number float": {
169 input: `{"d": 1.5}`,
170 expected: 1500 * time.Millisecond,
171 },
172 "json number zero": {
173 input: `{"d": 0}`,
174 expected: 0,
175 },
176
177 // JSON strings with duration format
178 "json string seconds": {
179 input: `{"d": "30s"}`,
180 expected: 30 * time.Second,
181 },
182 "json string minutes": {
183 input: `{"d": "5m"}`,
184 expected: 5 * time.Minute,
185 },
186 "json string hours": {
187 input: `{"d": "2h"}`,
188 expected: 2 * time.Hour,
189 },
190 "json string milliseconds": {
191 input: `{"d": "500ms"}`,
192 expected: 500 * time.Millisecond,
193 },
194 "json string combined": {
195 input: `{"d": "1h30m"}`,
196 expected: 90 * time.Minute,
197 },
198 "json string days": {
199 input: `{"d": "1d"}`,
200 expected: 24 * time.Hour,
201 },
202 "json string weeks": {
203 input: `{"d": "1w"}`,
204 expected: 7 * 24 * time.Hour,
205 },
206
207 // JSON strings with numeric values (interpreted as seconds)
208 "json string numeric int": {
209 input: `{"d": "30"}`,
210 expected: 30 * time.Second,
211 },
212 "json string numeric float": {
213 input: `{"d": "1.5"}`,
214 expected: 1500 * time.Millisecond,
215 },
216
217 // JSON null (results in zero value, not an error)
218 "json null": {
219 input: `{"d": null}`,
220 expected: 0,
221 },
222
223 // Errors
224 "json string invalid": {
225 input: `{"d": "invalid"}`,
226 wantErr: true,
227 },
228 }
229
230 for name, tc := range tests {
231 t.Run(name, func(t *testing.T) {
232 var result struct {
233 D Duration `json:"d"`
234 }
235
236 err := json.Unmarshal([]byte(tc.input), &result)
237
238 if tc.wantErr {
239 assert.Error(t, err)
240 return
241 }
242
243 require.NoError(t, err)
244 assert.Equal(t, tc.expected, result.D.Duration())
245 })
246 }
247 }
248
249 func TestFormatDuration(t *testing.T) {
250 tests := map[string]struct {
251 d time.Duration
252 want string
253 }{
254 "zero": {d: 0, want: "0s"},
255 "1 millisecond": {d: time.Millisecond, want: "1ms"},
256 "500 milliseconds": {d: 500 * time.Millisecond, want: "500ms"},
257 "1 second": {d: time.Second, want: "1s"},
258 "30 seconds": {d: 30 * time.Second, want: "30s"},
259 "1 minute": {d: time.Minute, want: "1m"},
260 "5 minutes": {d: 5 * time.Minute, want: "5m"},
261 "30 minutes": {d: 30 * time.Minute, want: "30m"},
262 "1 hour": {d: time.Hour, want: "1h"},
263 "12 hours": {d: 12 * time.Hour, want: "12h"},
264 "1 day": {d: 24 * time.Hour, want: "1d"},
265 "7 days": {d: 7 * 24 * time.Hour, want: "1w"},
266 "14 days": {d: 14 * 24 * time.Hour, want: "2w"},
267 "30 days": {d: 30 * 24 * time.Hour, want: "1mo"},
268 "365 days": {d: 365 * 24 * time.Hour, want: "1y"},
269 "2 years": {d: 2 * 365 * 24 * time.Hour, want: "2y"},
270 "negative 1 hour": {d: -time.Hour, want: "-1h"},
271 "negative 1 day": {d: -24 * time.Hour, want: "-1d"},
272 "1.5 seconds": {d: 1500 * time.Millisecond, want: "1.5s"},
273 "90 minutes": {d: 90 * time.Minute, want: "90m"},
274 "36 hours": {d: 36 * time.Hour, want: "36h"},
275 "sub-millisecond": {d: 100 * time.Microsecond, want: "100µs"},
276 "nanoseconds": {d: 50 * time.Nanosecond, want: "50ns"},
277 "negative sub-ms": {d: -100 * time.Microsecond, want: "-100µs"},
278 }
279
280 for name, tc := range tests {
281 t.Run(name, func(t *testing.T) {
282 got := formatDuration(tc.d)
283 assert.Equal(t, tc.want, got)
284 })
285 }
286 }
287
288 func TestDurationString_MarshalJSON(t *testing.T) {
289 tests := map[string]struct {
290 d LongDuration
291 want string
292 }{
293 "1 second": {d: LongDuration(time.Second), want: `"1s"`},
294 "30 seconds": {d: LongDuration(30 * time.Second), want: `"30s"`},
295 "2 minutes": {d: LongDuration(2 * time.Minute), want: `"2m"`},
296 "12 hours": {d: LongDuration(12 * time.Hour), want: `"12h"`},
297 "1 day": {d: LongDuration(24 * time.Hour), want: `"1d"`},
298 "1 week": {d: LongDuration(7 * 24 * time.Hour), want: `"1w"`},
299 "1 month": {d: LongDuration(30 * 24 * time.Hour), want: `"1mo"`},
300 "1 year": {d: LongDuration(365 * 24 * time.Hour), want: `"1y"`},
301 "1.5 seconds": {d: LongDuration(1500 * time.Millisecond), want: `"1.5s"`},
302 }
303
304 for name, tc := range tests {
305 t.Run(name, func(t *testing.T) {
306 bs, err := json.Marshal(&tc.d)
307 require.NoError(t, err)
308 assert.Equal(t, tc.want, string(bs))
309 })
310 }
311 }
312
313 func TestDurationString_MarshalYAML(t *testing.T) {
314 tests := map[string]struct {
315 d LongDuration
316 want string
317 }{
318 "1 second": {d: LongDuration(time.Second), want: "1s"},
319 "12 hours": {d: LongDuration(12 * time.Hour), want: "12h"},
320 "1 day": {d: LongDuration(24 * time.Hour), want: "1d"},
321 }
322
323 for name, tc := range tests {
324 t.Run(name, func(t *testing.T) {
325 bs, err := yaml.Marshal(&tc.d)
326 require.NoError(t, err)
327 assert.Equal(t, tc.want, strings.TrimSpace(string(bs)))
328 })
329 }
330 }
331
332 func TestDurationString_UnmarshalJSON(t *testing.T) {
333 tests := map[string]struct {
334 input string
335 expected time.Duration
336 wantErr bool
337 }{
338 // JSON numbers (interpreted as seconds)
339 "json number int": {
340 input: `{"d": 30}`,
341 expected: 30 * time.Second,
342 },
343 "json number float": {
344 input: `{"d": 1.5}`,
345 expected: 1500 * time.Millisecond,
346 },
347
348 // JSON strings with duration format
349 "json string seconds": {
350 input: `{"d": "30s"}`,
351 expected: 30 * time.Second,
352 },
353 "json string hours": {
354 input: `{"d": "12h"}`,
355 expected: 12 * time.Hour,
356 },
357 "json string days": {
358 input: `{"d": "1d"}`,
359 expected: 24 * time.Hour,
360 },
361 "json string weeks": {
362 input: `{"d": "1w"}`,
363 expected: 7 * 24 * time.Hour,
364 },
365 "json string months": {
366 input: `{"d": "1mo"}`,
367 expected: 30 * 24 * time.Hour,
368 },
369 "json string years": {
370 input: `{"d": "1y"}`,
371 expected: 365 * 24 * time.Hour,
372 },
373
374 // JSON strings with numeric values (interpreted as seconds)
375 "json string numeric": {
376 input: `{"d": "120"}`,
377 expected: 120 * time.Second,
378 },
379
380 // Errors
381 "json string invalid": {
382 input: `{"d": "invalid"}`,
383 wantErr: true,
384 },
385 }
386
387 for name, tc := range tests {
388 t.Run(name, func(t *testing.T) {
389 var result struct {
390 D LongDuration `json:"d"`
391 }
392
393 err := json.Unmarshal([]byte(tc.input), &result)
394
395 if tc.wantErr {
396 assert.Error(t, err)
397 return
398 }
399
400 require.NoError(t, err)
401 assert.Equal(t, tc.expected, result.D.Duration())
402 })
403 }
404 }
405
406 func TestDurationString_UnmarshalYAML(t *testing.T) {
407 tests := map[string]struct {
408 input string
409 expected time.Duration
410 }{
411 "duration string": {input: "d: 12h", expected: 12 * time.Hour},
412 "duration days": {input: "d: 1d", expected: 24 * time.Hour},
413 "numeric int": {input: "d: 120", expected: 120 * time.Second},
414 "numeric float": {input: "d: 1.5", expected: 1500 * time.Millisecond},
415 }
416
417 for name, tc := range tests {
418 t.Run(name, func(t *testing.T) {
419 var result struct {
420 D LongDuration `yaml:"d"`
421 }
422
423 err := yaml.Unmarshal([]byte(tc.input), &result)
424 require.NoError(t, err)
425 assert.Equal(t, tc.expected, result.D.Duration())
426 })
427 }
428 }