Skip to content

Commit 417a91d

Browse files
committed
Add tests for the shared HTTP client modules
1 parent db2d13f commit 417a91d

4 files changed

Lines changed: 691 additions & 0 deletions

File tree

‎tests/client/test_options.py‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# -*- coding: utf-8; -*-
2+
#
3+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
4+
# license agreements. See the NOTICE file distributed with this work for
5+
# additional information regarding copyright ownership. Crate licenses
6+
# this file to you under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License. You may
8+
# obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
#
18+
# However, if you have executed another commercial license agreement
19+
# with Crate these terms will supersede the license and you may use the
20+
# software solely pursuant to the terms of the relevant commercial agreement.
21+
22+
import socket
23+
24+
import pytest
25+
26+
from crate.client._options import (
27+
credentials_from_url,
28+
keepalive_socket_options,
29+
server_url,
30+
sql_path,
31+
to_server_list,
32+
validate_compress,
33+
)
34+
35+
36+
@pytest.mark.parametrize(
37+
"server, expected",
38+
[
39+
("a", "http://a"),
40+
("a/path", "http://a/path"),
41+
("a:9345", "http://a:9345"),
42+
("a:9345/path", "http://a:9345/path"),
43+
("https://a:9345", "https://a:9345"),
44+
("https://a:9345/path", "https://a:9345/path"),
45+
("https://a", "https://a"),
46+
("https://a/path", "https://a/path"),
47+
("HTTPS://a", "https://a"),
48+
("demo.crate.io", "http://demo.crate.io"),
49+
],
50+
)
51+
def test_server_url(server, expected):
52+
assert server_url(server) == expected
53+
54+
55+
@pytest.mark.parametrize(
56+
"servers",
57+
[
58+
"localhost:4200 https://localhost:4201",
59+
["localhost:4200", "https://localhost:4201"],
60+
("localhost:4200", "https://localhost:4201"),
61+
],
62+
)
63+
def test_to_server_list(servers):
64+
assert to_server_list(servers) == [
65+
"http://localhost:4200",
66+
"https://localhost:4201",
67+
]
68+
69+
70+
@pytest.mark.parametrize(
71+
"url, username, password, expected",
72+
[
73+
("http://localhost:4200", None, None, (None, None)),
74+
("http://crate@localhost:4200", None, None, ("crate", None)),
75+
(
76+
"http://crate:s3cr3t@localhost:4200",
77+
None,
78+
None,
79+
("crate", "s3cr3t"),
80+
),
81+
("http://localhost:4200", None, "s3cr3t", (None, "s3cr3t")),
82+
("http://crate@localhost:4200", None, "s3cr3t", ("crate", "s3cr3t")),
83+
(
84+
"http://crate:from-url@localhost:4200",
85+
None,
86+
"s3cr3t",
87+
("crate", "from-url"),
88+
),
89+
("http://localhost:4200", "", None, ("", None)),
90+
],
91+
)
92+
def test_credentials_from_url(url, username, password, expected):
93+
assert credentials_from_url(url, username, password) == expected
94+
95+
96+
@pytest.mark.parametrize("compress", [True, False, 0, 8192])
97+
def test_validate_compress(compress):
98+
assert validate_compress(compress) is compress
99+
100+
101+
@pytest.mark.parametrize(
102+
"compress, type_name", [("8192", "'str'"), (None, "'NoneType'")]
103+
)
104+
def test_validate_compress_invalid(compress, type_name):
105+
with pytest.raises(
106+
TypeError, match=f"compress must be bool or int, got {type_name}"
107+
):
108+
validate_compress(compress)
109+
110+
111+
@pytest.mark.parametrize(
112+
"error_trace, expected",
113+
[
114+
(False, "/_sql?types=true"),
115+
(True, "/_sql?types=true&error_trace=true"),
116+
],
117+
)
118+
def test_sql_path(error_trace, expected):
119+
assert sql_path(error_trace) == expected
120+
121+
122+
def test_sql_path_with_given_path():
123+
assert sql_path(True, "/_sql") == "/_sql&error_trace=true"
124+
125+
126+
def test_keepalive_socket_options_off():
127+
assert keepalive_socket_options(False, 30, 10, 5) == []
128+
129+
130+
def test_keepalive_socket_options_defaults():
131+
assert keepalive_socket_options() == [
132+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
133+
]
134+
135+
136+
@pytest.mark.skipif(
137+
not all(
138+
hasattr(socket, name)
139+
for name in ("TCP_KEEPIDLE", "TCP_KEEPINTVL", "TCP_KEEPCNT")
140+
),
141+
reason="Platform does not support all TCP keepalive options",
142+
)
143+
def test_keepalive_socket_options_tcp():
144+
assert keepalive_socket_options(True, 30, 10, 5) == [
145+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
146+
(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30),
147+
(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10),
148+
(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5),
149+
]

‎tests/client/test_pool.py‎

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# -*- coding: utf-8; -*-
2+
#
3+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
4+
# license agreements. See the NOTICE file distributed with this work for
5+
# additional information regarding copyright ownership. Crate licenses
6+
# this file to you under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License. You may
8+
# obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
#
18+
# However, if you have executed another commercial license agreement
19+
# with Crate these terms will supersede the license and you may use the
20+
# software solely pursuant to the terms of the relevant commercial agreement.
21+
22+
import logging
23+
24+
import pytest
25+
26+
from crate.client import exceptions
27+
from crate.client._pool import _ServerPool
28+
from crate.client.http import Client
29+
30+
NODE1 = "http://localhost:4200"
31+
NODE2 = "http://localhost:4201"
32+
NODE3 = "http://localhost:4202"
33+
34+
35+
class FakeClock:
36+
def __init__(self, now: float = 1_700_000_000.0):
37+
self.now = now
38+
39+
def __call__(self) -> float:
40+
return self.now
41+
42+
43+
@pytest.fixture
44+
def clock():
45+
return FakeClock()
46+
47+
48+
def test_round_robin(clock):
49+
pool = _ServerPool([NODE1, NODE2, NODE3], clock=clock)
50+
picked = [pool._get_server() for _ in range(4)]
51+
assert picked == [NODE1, NODE2, NODE3, NODE1]
52+
53+
54+
def test_active_servers_is_a_copy(clock):
55+
pool = _ServerPool([NODE1, NODE2], clock=clock)
56+
active = pool.active_servers
57+
active.clear()
58+
assert pool.active_servers == [NODE1, NODE2]
59+
60+
61+
def test_drop_server(clock):
62+
pool = _ServerPool([NODE1, NODE2], clock=clock)
63+
pool._drop_server(NODE1, "Connection refused")
64+
assert pool.active_servers == [NODE2]
65+
assert pool._inactive_servers == [(clock.now, NODE1, "Connection refused")]
66+
assert [pool._get_server() for _ in range(2)] == [NODE2, NODE2]
67+
68+
69+
def test_drop_unknown_server_is_ignored(clock):
70+
pool = _ServerPool([NODE1, NODE2], clock=clock)
71+
pool._drop_server(NODE3, "Connection refused")
72+
assert pool.active_servers == [NODE1, NODE2]
73+
assert pool._inactive_servers == []
74+
75+
76+
def test_drop_last_server_raises(clock):
77+
pool = _ServerPool([NODE1, NODE2], clock=clock)
78+
pool._drop_server(NODE1, "Connection refused")
79+
with pytest.raises(
80+
exceptions.ConnectionError,
81+
match="No more Servers available, exception from last server: "
82+
"Read timed out",
83+
):
84+
pool._drop_server(NODE2, "Read timed out")
85+
assert pool.active_servers == []
86+
87+
88+
def test_server_restored_after_retry_interval(clock):
89+
pool = _ServerPool([NODE1, NODE2], clock=clock)
90+
pool.retry_interval = 5
91+
pool._drop_server(NODE1, "Connection refused")
92+
93+
clock.now += 4
94+
pool._get_server()
95+
assert pool.active_servers == [NODE2]
96+
97+
clock.now += 1
98+
assert pool._get_server() == NODE2
99+
assert pool.active_servers == [NODE1, NODE2]
100+
assert pool._inactive_servers == []
101+
102+
103+
def test_retry_interval_class_default(clock):
104+
pool = _ServerPool([NODE1, NODE2], clock=clock)
105+
pool._drop_server(NODE1, "Connection refused")
106+
107+
clock.now += 29
108+
pool._get_server()
109+
assert pool.active_servers == [NODE2]
110+
111+
clock.now += 1
112+
pool._get_server()
113+
assert sorted(pool.active_servers) == [NODE1, NODE2]
114+
115+
116+
def test_oldest_server_restored_when_none_is_active(clock):
117+
pool = _ServerPool([NODE1, NODE2, NODE3], clock=clock)
118+
pool._drop_server(NODE2, "Connection refused")
119+
clock.now += 1
120+
pool._drop_server(NODE1, "Connection refused")
121+
pool._active_servers = []
122+
123+
assert pool._get_server() == NODE2
124+
assert pool.active_servers == [NODE2]
125+
assert pool._inactive_servers == [(clock.now, NODE1, "Connection refused")]
126+
127+
128+
def test_pool_logs_on_subclass_logger(clock, caplog):
129+
class Pool(_ServerPool):
130+
_logger = logging.getLogger("crate.client.test")
131+
132+
pool = Pool([NODE1, NODE2], clock=clock)
133+
pool.retry_interval = 5
134+
with caplog.at_level(logging.WARNING):
135+
pool._drop_server(NODE1, "Connection refused")
136+
clock.now += 5
137+
pool._get_server()
138+
assert [(r.name, r.getMessage()) for r in caplog.records] == [
139+
("crate.client.test", f"Removed server {NODE1} from active pool"),
140+
("crate.client.test", f"Restored server {NODE1} into active pool"),
141+
]
142+
143+
144+
def test_client_logs_on_http_logger(caplog):
145+
client = Client(servers=[NODE1, NODE2])
146+
with caplog.at_level(logging.WARNING):
147+
client._drop_server(NODE1, "Connection refused")
148+
assert [r.name for r in caplog.records] == ["crate.client.http"]

0 commit comments

Comments
 (0)