Skip to content

Commit 3bc0165

Browse files
committed
Add resp encoder class to tests
1 parent 509c77c commit 3bc0165

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

tests/resp.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import itertools
2+
from types import NoneType
3+
from typing import Any, Optional
4+
5+
6+
class RespEncoder:
7+
"""
8+
A class for simple RESP protocol encodign for unit tests
9+
"""
10+
11+
def __init__(self, protocol: int = 2, encoding: str = "utf-8") -> None:
12+
self.protocol = protocol
13+
self.encoding = encoding
14+
15+
def encode(self, data: Any, hint: Optional[str] = None) -> bytes:
16+
if isinstance(data, dict):
17+
if self.protocol > 2:
18+
result = f"%{len(data)}\r\n".encode()
19+
for key, val in data.items():
20+
result += self.encode(key) + self.encode(val)
21+
return result
22+
else:
23+
# Automatically encode dicts as flattened key, value arrays
24+
mylist = list(
25+
itertools.chain(*((key, val) for (key, val) in data.items()))
26+
)
27+
return self.encode(mylist)
28+
29+
elif isinstance(data, list):
30+
result = f"*{len(data)}\r\n".encode()
31+
for val in data:
32+
result += self.encode(val)
33+
return result
34+
35+
elif isinstance(data, set):
36+
if self.protocol > 2:
37+
result = f"~{len(data)}\r\n".encode()
38+
for val in data:
39+
result += self.encode(val)
40+
return result
41+
else:
42+
return self.encode(list(data))
43+
44+
elif isinstance(data, str):
45+
enc = data.encode(self.encoding)
46+
# long strings or strings with control characters must be encoded as bulk
47+
# strings
48+
if hint or len(enc) > 20 or b"\r" in enc or b"\n" in enc:
49+
return self.encode_bulkstr(enc, hint)
50+
return b"+" + enc + b"\r\n"
51+
52+
elif isinstance(data, bytes):
53+
return self.encode_bulkstr(data, hint)
54+
55+
elif isinstance(data, bool):
56+
if self.protocol == 2:
57+
return b":1\r\n" if data else b":0\r\n"
58+
else:
59+
return b"t\r\n" if data else b"f\r\n"
60+
61+
elif isinstance(data, int):
62+
if (data > 2**63 - 1) or (data < -(2**63)):
63+
if self.protocol > 2:
64+
return f"({data}\r\n".encode() # resp3 big int
65+
else:
66+
return f"+{data}\r\n".encode() # force to simple string
67+
return f":{data}\r\n".encode()
68+
elif isinstance(data, float):
69+
if self.protocol > 2:
70+
return f",{data}\r\n".encode() # resp3 double
71+
else:
72+
return f"+{data}\r\n".encode() # simple string
73+
74+
elif isinstance(data, NoneType):
75+
if self.protocol > 2:
76+
return b"_\r\n" # resp3 null
77+
else:
78+
return b"$-1\r\n" # Null bulk string
79+
# some commands return null array: b"*-1\r\n"
80+
81+
else:
82+
raise NotImplementedError
83+
84+
def encode_bulkstr(self, bstr: bytes, hint: Optional[str]) -> bytes:
85+
if self.protocol > 2 and hint is not None:
86+
# a resp3 verbatim string
87+
return f"={len(bstr)}\r\n{hint}:".encode() + bstr + b"\r\n"
88+
else:
89+
return f"${len(bstr)}\r\n".encode() + bstr + b"\r\n"
90+
91+
92+
def encode(value: Any, protocol: int = 2, hint: Optional[str] = None) -> bytes:
93+
return RespEncoder(protocol).encode(value, hint)

tests/test_resp.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from .resp import encode
2+
3+
import pytest
4+
5+
6+
@pytest.fixture(params=[2, 3])
7+
def resp_version(request):
8+
return request.param
9+
10+
11+
class TestEncoder:
12+
def test_simple_str(self):
13+
assert encode("foo") == b"+foo\r\n"
14+
15+
def test_long_str(self):
16+
text = "fooling around with the sword in the mud"
17+
assert len(text) == 40
18+
assert encode(text) == b"$40\r\n" + text.encode() + b"\r\n"
19+
20+
# test strings with control characters
21+
def test_str_with_ctrl_chars(self):
22+
text = "foo\r\nbar"
23+
assert encode(text) == b"$8\r\nfoo\r\nbar\r\n"
24+
25+
def test_bytes(self):
26+
assert encode(b"foo") == b"$3\r\nfoo\r\n"
27+
28+
def test_int(self):
29+
assert encode(123) == b":123\r\n"
30+
31+
def test_float(self, resp_version):
32+
data = encode(1.23, protocol=resp_version)
33+
if resp_version == 2:
34+
assert data == b"+1.23\r\n"
35+
else:
36+
assert data == b",1.23\r\n"
37+
38+
def test_large_int(self, resp_version):
39+
data = encode(2**63, protocol=resp_version)
40+
if resp_version == 2:
41+
assert data == b"+9223372036854775808\r\n"
42+
else:
43+
assert data == b"(9223372036854775808\r\n"
44+
45+
def test_array(self):
46+
assert encode([1, 2, 3]) == b"*3\r\n:1\r\n:2\r\n:3\r\n"
47+
48+
def test_set(self, resp_version):
49+
data = encode({1, 2, 3}, protocol=resp_version)
50+
if resp_version == 2:
51+
assert data == b"*3\r\n:1\r\n:2\r\n:3\r\n"
52+
else:
53+
assert data == b"~3\r\n:1\r\n:2\r\n:3\r\n"
54+
55+
def test_map(self, resp_version):
56+
data = encode({1: 2, 3: 4}, protocol=resp_version)
57+
if resp_version == 2:
58+
assert data == b"*4\r\n:1\r\n:2\r\n:3\r\n:4\r\n"
59+
else:
60+
assert data == b"%2\r\n:1\r\n:2\r\n:3\r\n:4\r\n"
61+
62+
def test_nested_array(self):
63+
assert encode([1, [2, 3]]) == b"*2\r\n:1\r\n*2\r\n:2\r\n:3\r\n"
64+
65+
def test_nested_map(self, resp_version):
66+
data = encode({1: {2: 3}}, protocol=resp_version)
67+
if resp_version == 2:
68+
assert data == b"*2\r\n:1\r\n*2\r\n:2\r\n:3\r\n"
69+
else:
70+
assert data == b"%1\r\n:1\r\n%1\r\n:2\r\n:3\r\n"
71+
72+
def test_null(self, resp_version):
73+
data = encode(None, protocol=resp_version)
74+
if resp_version == 2:
75+
assert data == b"$-1\r\n"
76+
else:
77+
assert data == b"_\r\n"
78+
79+
def test_mixed_array(self, resp_version):
80+
data = encode([1, "foo", 2.3, None, True], protocol=resp_version)
81+
if resp_version == 2:
82+
assert data == b"*5\r\n:1\r\n+foo\r\n+2.3\r\n$-1\r\n:1\r\n"
83+
else:
84+
assert data == b"*5\r\n:1\r\n+foo\r\n,2.3\r\n_\r\nt\r\n"
85+
86+
def test_bool(self, resp_version):
87+
data = encode(True, protocol=resp_version)
88+
if resp_version == 2:
89+
assert data == b":1\r\n"
90+
else:
91+
assert data == b"t\r\n"
92+
93+
data = encode(False, resp_version)
94+
if resp_version == 2:
95+
assert data == b":0\r\n"
96+
else:
97+
assert data == b"f\r\n"

0 commit comments

Comments
 (0)