v / vlib / db / redis / redis_test.v
592 lines · 508 sloc · 14.77 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// vtest build: started_redis?
2import db.redis
3import os
4import rand
5import time
6
7const redis_password = os.getenv('VREDIS_PASSWORD')
8
9// Keys and channel names used by the tests
10const k_int = 'k:int'
11const k_string = 'k:string'
12const k_counter = 'k:counter'
13const r_k_map = 'r:k:map'
14const r_k_set = 'r:k:set'
15const r_k_pubch = 'r:k:pubch'
16
17// Helper publisher used by the pub/sub test. This helper swallows errors and logs
18// them locally so it can be spawned safely without propagating results.
19fn publish_after_delay() {
20 // small delay to ensure subscriber is ready
21 time.sleep(50 * time.millisecond)
22 mut conn := redis.connect() or {
23 eprintln('publish_after_delay: connect failed: ${err}')
24 return
25 }
26 defer { conn.close() or { eprintln('publish_after_delay: close failed: ${err}') } }
27 conn.cmd('PUBLISH', r_k_pubch, 'hello') or {
28 eprintln('publish_after_delay: publish failed: ${err}')
29 }
30}
31
32// -------------------- FAST / BASIC TESTS --------------------
33
34// Basic functionality smoke test (fast)
35fn test_redis_basic() {
36 mut db := redis.connect(password: redis_password)!
37 defer { db.close() or {} }
38
39 // Basic health check
40 assert db.ping()! == 'PONG'
41
42 // delete all keys first
43 db.cmd('FLUSHALL')!
44
45 // test set[T]
46 assert db.set('int', 123)! == 'OK'
47 assert db.set('string', 'abc')! == 'OK'
48 assert db.set('bin', [u8(0x00), 0x01, 0x02, 0x03])! == 'OK'
49
50 // test get[T]
51 assert db.get[i64]('int')! == 123
52 assert db.get[string]('string')! == 'abc'
53 assert db.get[[]u8]('bin')! == [u8(0x00), 0x01, 0x02, 0x03]
54
55 // test incr/decr
56 assert db.incr('int')! == 124
57 assert db.decr('int')! == 123
58
59 // test hset/hget/hgetall
60 m := {
61 'a': '1'
62 'b': '2'
63 'c': '3'
64 }
65 assert db.hset('map', m)! == m.len
66 assert db.hget[string]('map', 'a')! == '1'
67 assert db.hget[string]('map', 'b')! == '2'
68 assert db.hget[string]('map', 'c')! == '3'
69 assert db.hgetall[string]('map')! == m
70
71 // test expire
72 assert db.expire('int', 1)!
73 time.sleep(2 * time.second)
74 db.get[i64]('int') or {
75 // tolerate server-specific message wording; ensure some message exists
76 assert err.msg().len > 0
77 }
78
79 // test del
80 assert db.del('string')! == 1
81 db.get[string]('string') or {
82 // tolerate server-specific message wording; ensure some message exists
83 assert err.msg().len > 0
84 }
85
86 // test custom cmd
87 assert db.cmd('SET', 'bigint', '123456')! as string == 'OK'
88 assert db.cmd('GET', 'bigint')! as []u8 == [u8(49), 50, 51, 52, 53, 54]
89 db.close()!
90}
91
92// A concise pipeline test that exercises correctness but remains fast
93fn test_pipeline_small() {
94 mut db := redis.connect(password: redis_password)!
95 defer { db.close() or {} }
96
97 assert db.ping()! == 'PONG'
98
99 // start pipleline mode
100 db.pipeline_start()
101 // small pipeline of a few commands
102 db.cmd('FLUSHALL') or { eprintln('FLUSHALL failed (pipeline): ${err}') }
103 db.set(k_int, 1) or { eprintln('SET k_int failed (pipeline): ${err}') }
104 db.set(k_string, 'p') or { eprintln('SET k_string failed (pipeline): ${err}') }
105 db.get[i64](k_int) or { eprintln('GET k_int failed (pipeline): ${err}') }
106 db.get[string](k_string) or { eprintln('GET k_string failed (pipeline): ${err}') }
107 res := db.pipeline_execute()!
108
109 // Expect exactly the five results we queued and validate them precisely.
110 assert res.len == 5
111 match res[0] {
112 string { assert res[0] as string == 'OK' }
113 else { assert false }
114 }
115
116 match res[1] {
117 string { assert res[1] as string == 'OK' }
118 else { assert false }
119 }
120
121 match res[2] {
122 string { assert res[2] as string == 'OK' }
123 else { assert false }
124 }
125
126 match res[3] {
127 []u8 { assert (res[3] as []u8).bytestr() == '1' }
128 else { assert false }
129 }
130
131 match res[4] {
132 []u8 { assert (res[4] as []u8).bytestr() == 'p' }
133 else { assert false }
134 }
135}
136
137// Another small pipeline sequence test (fast)
138fn test_pipeline_sequence() {
139 mut db := redis.connect(password: redis_password)!
140 defer { db.close() or {} }
141
142 // keep operations minimal and deterministic
143 db.cmd('FLUSHALL') or { eprintln('FLUSHALL failed: ${err}') }
144 assert db.set(k_counter, '0')! == 'OK'
145
146 // restart pipeline again
147 db.pipeline_start()
148 // test set[T]
149 db.set('int', 123)!
150 db.set('string', 'abc')!
151 db.set('bin', [u8(0x00), 0x01, 0x02, 0x03])!
152
153 // execute the queued pipeline commands and collect results
154 res := db.pipeline_execute()!
155 // We queued 3 SET commands above; each should return "OK"
156 assert res.len == 3
157 match res[0] {
158 string { assert res[0] as string == 'OK' }
159 else { assert false }
160 }
161
162 match res[1] {
163 string { assert res[1] as string == 'OK' }
164 else { assert false }
165 }
166
167 match res[2] {
168 string { assert res[2] as string == 'OK' }
169 else { assert false }
170 }
171}
172
173// -------------------- RESP3 / PUBSUB SHAPE CHECKS (FAST) --------------------
174
175// Quick RESP3 ping and tiny roundtrip
176fn test_resp3_ping() {
177 mut db := redis.connect(password: redis_password)!
178 defer { db.close() or {} }
179
180 // Basic ping and quick SET/GET roundtrip
181 p := db.ping() or { panic(err) }
182 assert p == 'PONG'
183 db.cmd('DEL', r_k_map) or { eprintln('DEL r_k_map failed: ${err}') }
184 assert db.cmd('SET', r_k_map, 'temp')! as string == 'OK'
185 val := db.cmd('GET', r_k_map)!
186 match val {
187 []u8 { assert val.bytestr() == 'temp' }
188 string { assert val == 'temp' }
189 else { assert false }
190 }
191
192 db.cmd('DEL', r_k_map) or { eprintln('cleanup DEL r_k_map failed: ${err}') }
193}
194
195// Quick RESP3 HGETALL shape check (fast)
196fn test_resp3_hgetall_fast() {
197 mut db := redis.connect(password: redis_password)!
198 defer { db.close() or {} }
199
200 db.cmd('DEL', r_k_map)!
201 assert db.hset(r_k_map, {
202 'k1': 'v1'
203 'k2': 'v2'
204 })! >= 1
205 if db.version != 3 {
206 res := db.hgetall[string](r_k_map) or { panic(err) }
207 assert res['k1'] == 'v1'
208 db.cmd('DEL', r_k_map)!
209 return
210 }
211 r := db.cmd('HGETALL', r_k_map) or { panic(err) }
212 match r {
213 map[string]redis.RedisValue {
214 v1 := r['k1']!
215 v2 := r['k2']!
216 assert (v1 as []u8).bytestr() == 'v1'
217 assert (v2 as []u8).bytestr() == 'v2'
218 }
219 redis.RedisMap {
220 assert (r.pairs[1] as []u8).bytestr() == 'v1'
221 assert (r.pairs[3] as []u8).bytestr() == 'v2'
222 }
223 []redis.RedisValue {
224 assert (r[1] as []u8).bytestr() == 'v1'
225 assert (r[3] as []u8).bytestr() == 'v2'
226 }
227 else {
228 assert false
229 }
230 }
231
232 db.cmd('DEL', r_k_map)!
233}
234
235// Quick RESP3 SMEMBERS shape check (fast)
236fn test_resp3_smembers_fast() {
237 mut db := redis.connect(password: redis_password)!
238 defer { db.close() or {} }
239 db.cmd('DEL', r_k_set) or { eprintln('DEL r_k_set failed: ${err}') }
240 db.cmd('SADD', r_k_set, 'one', 'two') or { eprintln('SADD r_k_set failed: ${err}') }
241 r := db.cmd('SMEMBERS', r_k_set) or { panic(err) }
242 match r {
243 redis.RedisSet {
244 mut f1 := false
245 mut f2 := false
246 for el in r.elements {
247 el_str := (el as []u8).bytestr()
248 if el_str == 'one' {
249 f1 = true
250 }
251 if el_str == 'two' {
252 f2 = true
253 }
254 }
255 assert f1 && f2
256 }
257 []redis.RedisValue {
258 mut f1 := false
259 mut f2 := false
260 for el in r {
261 el_str := (el as []u8).bytestr()
262 if el_str == 'one' {
263 f1 = true
264 }
265 if el_str == 'two' {
266 f2 = true
267 }
268 }
269 assert f1 && f2
270 }
271 else {
272 assert false
273 }
274 }
275
276 db.cmd('DEL', r_k_set) or { eprintln('cleanup DEL r_k_set failed: ${err}') }
277}
278
279// Very small pub/sub smoke test (fast)
280fn test_resp3_pubsub_fast() {
281 mut sub := redis.connect(password: redis_password)!
282 defer { sub.close() or {} }
283 if sub.version != 3 {
284 return
285 }
286 spawn publish_after_delay()
287 sub.cmd('SUBSCRIBE', r_k_pubch) or { eprintln('SUBSCRIBE failed: ${err}') }
288 time.sleep(200 * time.millisecond)
289 sub.cmd('PING') or { eprintln('PING (pubsub) failed: ${err}') }
290}
291
292// -------------------- SLOW / HEAVY TESTS --------------------
293
294// Large binary bulk string roundtrip (~200 KiB)
295fn test_large_binary_bulk_string() ! {
296 mut db := redis.connect(password: redis_password)!
297 defer { db.close() or {} }
298
299 // ensure clean DB
300 db.cmd('FLUSHALL')!
301
302 // Create a large binary payload (~200 KB) with a repeating pattern,
303 // including zero bytes and CRLF sequences to exercise edge cases.
304 mut data := []u8{len: 204800, init: u8(index & 0xFF)}
305
306 // inject some CRLF and zero-byte sequences at several spots
307 data[50] = `\r`
308 data[51] = `\n`
309 data[1000] = 0
310 data[1001] = `\r`
311 data[1002] = `\n`
312
313 // store binary data
314 assert db.set('bigbin', data.clone())! == 'OK'
315
316 // retrieve as []u8 using typed get
317 got := db.get[[]u8]('bigbin')!
318 assert got == data
319
320 // also test with small binary containing embedded CRLF and NUL
321 small := [u8(0), `a`, `b`, `\r`, `\n`, `c`]
322 assert db.set('smallbin', small)! == 'OK'
323 got_small := db.get[[]u8]('smallbin')!
324 assert got_small == small
325
326 // cleanup
327 db.del('bigbin')!
328 db.del('smallbin')!
329}
330
331// Very large payload (~1.2 MiB) roundtrip
332fn test_very_large_payload() ! {
333 mut db := redis.connect(password: redis_password)!
334 defer { db.close() or {} }
335
336 // ensure clean DB
337 db.cmd('FLUSHALL')!
338
339 // Build a very large payload (> 1 MiB). Use ~1.2 MiB to be safely above 1 MiB.
340 size := 1_200_000
341 mut big := []u8{len: size, init: u8(index & 0xFF)}
342 // Put a few sentinel checks
343 big[0] = 0
344 big[1023] = `\r`
345 big[1024] = `\n`
346 big[size - 1] = 255
347
348 // store and retrieve
349 assert db.set('hugebin', big.clone())! == 'OK'
350 got := db.get[[]u8]('hugebin')!
351 assert got == big
352
353 // cleanup
354 db.del('hugebin')!
355}
356
357// Many pipeline commands to exercise batching behavior
358fn test_many_pipeline_commands() ! {
359 mut db := redis.connect(password: redis_password)!
360 defer { db.close() or {} }
361
362 // ensure clean DB
363 db.cmd('FLUSHALL')!
364
365 // We'll queue a moderate but substantial number of commands in pipeline to test behavior.
366 // Keep it reasonable for CI runtime; 300 pairs (SET, GET) should exercise pipeline thoroughly.
367 count := 300
368 db.pipeline_start()
369 for i in 0 .. count {
370 key := 'kp_${i}'
371 val := 'v${i}'
372 db.cmd('SET', key, val)!
373 db.cmd('GET', key)!
374 }
375 results := db.pipeline_execute()!
376 // Expect 2 * count responses
377 assert results.len == count * 2
378 for i in 0 .. count {
379 set_idx := i * 2
380 get_idx := i * 2 + 1
381 // SET should return OK
382 match results[set_idx] {
383 string { assert (results[set_idx] as string) == 'OK' }
384 else { assert false }
385 }
386
387 // GET should return the value as []u8
388 expected := ('v${i}').bytes()
389 match results[get_idx] {
390 []u8 { assert (results[get_idx] as []u8) == expected }
391 else { assert false }
392 }
393 }
394}
395
396// Larger randomized binary payloads (many iterations)
397fn test_fuzz_random_binary_many() ! {
398 mut db := redis.connect(password: redis_password)!
399 defer { db.close() or {} }
400
401 db.cmd('FLUSHALL')!
402
403 // LCG PRNG seed using process id
404 mut x := u64(os.getpid())
405
406 count := 50
407 for iter in 0 .. count {
408 // size up to ~200 KiB
409 size := int((x % 200_000) + 1)
410 mut data := []u8{len: size}
411 for i in 0 .. size {
412 data[i] = rand.u8()
413 }
414 key := 'fuzz_big_${iter}'
415 assert db.set(key, data.clone())! == 'OK'
416 got := db.get[[]u8](key)!
417 assert got == data
418 // clean up
419 db.del(key)!
420
421 x += u64(size) * u64(iter + 1)
422 }
423}
424
425// Randomized tests that ensure CRLF sequences embedded at random locations,
426// including chunk boundaries, are handled correctly.
427fn test_fuzz_crlf_random_positions() ! {
428 mut db := redis.connect(password: redis_password)!
429 defer { db.close() or {} }
430
431 db.cmd('FLUSHALL')!
432
433 seed := u64(os.getpid())
434 mut x := seed
435
436 trials := 30
437 for t in 0 .. trials {
438 // size between 1 KiB and 32 KiB
439 size := int((x % 31_744) + 1024)
440 mut data := []u8{len: size}
441 for i in 0 .. size {
442 data[i] = rand.u8()
443 }
444 // place a CRLF at a pseudo-random position; sometimes force it near boundaries
445 pos := int(x % u64(size - 2))
446 data[pos] = `\r`
447 data[pos + 1] = `\n`
448 // also occasionally place CRLF at a chunk boundary (4096)
449 if t % 5 == 0 && size > 4097 {
450 data[4095] = `\r`
451 data[4096] = `\n`
452 }
453 key := 'fuzz_crlf_${t}'
454 assert db.set(key, data.clone())! == 'OK'
455 got := db.get[[]u8](key)!
456 assert got == data
457 db.del(key)!
458 x += u64(pos) + 1
459 }
460}
461
462// CRLF crossing a read-chunk boundary
463fn test_crlf_at_chunk_boundary() {
464 mut db := redis.connect(password: redis_password)!
465 defer { db.close() or {} }
466
467 // ensure clean DB
468 db.cmd('FLUSHALL')!
469
470 // The driver's read buffer reads in 4096-byte chunks. Create a payload where CRLF
471 // crosses the 4096-byte boundary: place '\r' at index 4095 and '\n' at 4096.
472 chunk := 4096
473 size := chunk * 3 // a few chunks
474 mut data := []u8{len: size, init: u8((index * 37) % 256)}
475 if size > chunk + 1 {
476 data[chunk - 1] = `\r` // index 4095
477 data[chunk] = `\n` // index 4096
478 }
479 // store and retrieve
480 assert db.set('crlf_boundary', data.clone())! == 'OK'
481 got := db.get[[]u8]('crlf_boundary')!
482 assert got.len == data.len
483 // ensure boundary bytes preserved
484 assert got[chunk - 1] == `\r`
485 assert got[chunk] == `\n`
486 // cleanup
487 db.del('crlf_boundary')!
488}
489
490// Small randomized binary payloads (many repetitions).
491fn test_fuzz_random_binary_small() {
492 mut db := redis.connect(password: redis_password)!
493 defer { db.close() or {} }
494
495 db.cmd('FLUSHALL')!
496
497 // Simple LCG PRNG seeded from process id
498 seed := u64(os.getpid())
499 mut x := seed
500 for iter := 0; iter < 20; iter++ {
501 // pseudo-random size in [1, 4096]
502 size := int((x % 4096) + 1)
503 mut data := []u8{len: size}
504 for i in 0 .. size {
505 data[i] = rand.u8()
506 }
507 k := 'fuzz_small_${iter}'
508 assert db.set(k, data.clone())! == 'OK'
509 got := db.get[[]u8](k)!
510 assert got == data
511 // mutate seed slightly so sizes vary
512 x += u64(size)
513 }
514
515 db.pipeline_start()
516
517 // ensure we have a small map to use here (define inline to avoid depending on outer scope)
518 db.hset('map', {
519 'a': '1'
520 'b': '2'
521 'c': '3'
522 })!
523 db.hget[string]('map', 'a')!
524 db.hget[string]('map', 'b')!
525 db.hget[string]('map', 'c')!
526 db.hgetall[string]('map')!
527
528 // test custom cmd
529 db.cmd('SET', 'bigint', '123456')!
530 db.cmd('GET', 'bigint')!
531
532 b := db.pipeline_execute()!
533
534 // Validate elements individually to accommodate server-shape differences.
535 assert b.len == 7
536
537 match b[0] {
538 i64 { assert (b[0] as i64) == 3 }
539 else { assert false }
540 }
541
542 match b[1] {
543 []u8 { assert (b[1] as []u8) == [u8(49)] }
544 else { assert false }
545 }
546
547 match b[2] {
548 []u8 { assert (b[2] as []u8) == [u8(50)] }
549 else { assert false }
550 }
551
552 match b[3] {
553 []u8 { assert (b[3] as []u8) == [u8(51)] }
554 else { assert false }
555 }
556
557 // HGETALL may arrive as map[string]RedisValue, RedisMap, or array - accept common shapes.
558 match b[4] {
559 map[string]redis.RedisValue {
560 m := b[4] as map[string]redis.RedisValue
561 v := m['a'] or { panic('hgetall: missing a') }
562 assert (v as []u8) == [u8(49)]
563 }
564 redis.RedisMap {
565 rm := b[4] as redis.RedisMap
566 // pairs are interleaved key/value; expect at least 2 pairs and check the first value
567 assert rm.pairs.len >= 2
568 assert (rm.pairs[1] as []u8) == [u8(49)]
569 }
570 []redis.RedisValue {
571 arr := b[4] as []redis.RedisValue
572 // RESP2-style interleaved array; ensure element 1 (second item) is the value for 'a'
573 assert arr.len > 1
574 assert (arr[1] as []u8) == [u8(49)]
575 }
576 else {
577 assert false
578 }
579 }
580
581 match b[5] {
582 string { assert b[5] as string == 'OK' }
583 else { assert false }
584 }
585
586 match b[6] {
587 []u8 { assert (b[6] as []u8) == [u8(49), 50, 51, 52, 53, 54] }
588 else { assert false }
589 }
590
591 db.close()!
592}
593