Skip to content

Commit 9c26722

Browse files
port tests to pytest style
1 parent 006e5be commit 9c26722

File tree

93 files changed

+5392
-5377
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+5392
-5377
lines changed

README.md

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ This project follows library best practices for dependency management:
7777
# Run type checking
7878
uv run mypy --explicit-package-bases src/
7979

80-
# Run tests
81-
uv run python -m unittest discover -s tests
80+
# Run tests (with coverage)
81+
uv run scripts/run_tests.sh
82+
# Or directly:
83+
uv run pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
8284
```
8385

8486
3. **Build the project:**
@@ -95,14 +97,14 @@ We use a multi-tier testing strategy to ensure compatibility across SurrealDB ve
9597
```bash
9698
# Test with default version (latest stable)
9799
docker-compose up -d
98-
uv run python -m unittest discover -s tests
100+
uv run scripts/run_tests.sh
99101

100102
# Test against specific version
101103
./scripts/test-versions.sh v2.1.8
102104

103105
# Test against different v2.x versions
104-
SURREALDB_VERSION=v2.0.5 uv run python -m unittest discover -s tests
105-
SURREALDB_VERSION=v2.3.6 uv run python -m unittest discover -s tests
106+
SURREALDB_VERSION=v2.0.5 uv run scripts/run_tests.sh
107+
SURREALDB_VERSION=v2.3.6 uv run scripts/run_tests.sh
106108
```
107109

108110
### CI/CD Testing
@@ -239,18 +241,26 @@ bash scripts/term.sh
239241
You will now be running an interactive terminal through a python virtual environment with all the dependencies installed. We can now run the tests with the following command:
240242

241243
```bash
242-
python -m unittest discover
244+
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
243245
```
244246

245247
The number of tests might increase but at the time of writing this you should get a printout like the one below:
246248

247249
```bash
248-
.........................................................................................................................................Error in live subscription: sent 1000 (OK); no close frame received
249-
..........................................................................................
250-
----------------------------------------------------------------------
251-
Ran 227 tests in 6.313s
250+
================================ test session starts ================================
251+
platform ...
252+
collected 227 items
253+
254+
....................................................................................
255+
... (test output)
256+
257+
---------- coverage: platform ... -----------
258+
Name Stmts Miss Cover Missing
259+
---------------------------------------------------------
260+
src/surrealdb/....
261+
...
252262

253-
OK
263+
============================= 227 passed in 6.31s ================================
254264
```
255265
Finally, we clean up the database with the command below:
256266
```bash
@@ -274,11 +284,11 @@ Test against different SurrealDB versions using environment variables:
274284

275285
```bash
276286
# Test with latest v2.x (default: v2.3.6)
277-
uv run python -m unittest discover -s tests
287+
uv run scripts/run_tests.sh
278288

279289
# Test with specific v2.x version
280290
SURREALDB_VERSION=v2.1.8 docker-compose up -d surrealdb
281-
uv run python -m unittest discover -s tests
291+
uv run scripts/run_tests.sh
282292

283293
# Use different profiles for testing specific v2.x versions
284294
docker-compose --profile v2-0 up -d # v2.0.5 on port 8020
@@ -498,3 +508,30 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid
498508
## License
499509

500510
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
511+
512+
# Running Tests and Coverage
513+
514+
To run all tests with coverage reporting:
515+
516+
```bash
517+
uv run scripts/run_tests.sh
518+
```
519+
520+
This will:
521+
- Run all tests using pytest
522+
- Show a coverage summary in the terminal
523+
- Generate an HTML coverage report in the `htmlcov/` directory
524+
525+
You can also run tests directly with:
526+
527+
```bash
528+
uv run pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
529+
```
530+
531+
To test a specific file:
532+
533+
```bash
534+
uv run pytest tests/unit_tests/connections/test_connection_constructor.py --cov=src/surrealdb
535+
```
536+
537+
To view the HTML coverage report, open `htmlcov/index.html` in your browser after running the tests.

src/surrealdb/connections/async_ws.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from uuid import UUID
1010

1111
import websockets # type: ignore
12+
from websockets.exceptions import ConnectionClosed, WebSocketException
1213

1314
from surrealdb.connections.async_template import AsyncTemplate
1415
from surrealdb.connections.url import Url
@@ -50,17 +51,33 @@ def __init__(
5051

5152
async def _recv_task(self):
5253
assert self.socket
53-
async for data in self.socket:
54-
response = decode(data)
55-
if response_id := response.get("id"):
56-
if fut := self.qry.get(response_id):
57-
fut.set_result(response)
58-
elif response_result := response.get("result"):
59-
live_id = str(response_result["id"])
60-
for queue in self.live_queues.get(live_id, []):
61-
queue.put_nowait(response_result)
62-
else:
63-
self.check_response_for_error(response, "_recv_task")
54+
try:
55+
async for data in self.socket:
56+
response = decode(data)
57+
if response_id := response.get("id"):
58+
if fut := self.qry.get(response_id):
59+
fut.set_result(response)
60+
elif response_result := response.get("result"):
61+
live_id = str(response_result["id"])
62+
for queue in self.live_queues.get(live_id, []):
63+
queue.put_nowait(response_result)
64+
else:
65+
self.check_response_for_error(response, "_recv_task")
66+
except (ConnectionClosed, WebSocketException, asyncio.CancelledError):
67+
# Connection was closed or cancelled, this is expected
68+
pass
69+
except Exception as e:
70+
# Log unexpected errors but don't let them propagate
71+
import logging
72+
73+
logger = logging.getLogger(__name__)
74+
logger.debug(f"Unexpected error in _recv_task: {e}")
75+
finally:
76+
# Clean up any pending futures
77+
for fut in self.qry.values():
78+
if not fut.done():
79+
fut.cancel()
80+
self.qry.clear()
6481

6582
async def _send(
6683
self, message: RequestMessage, process: str, bypass: bool = False
@@ -306,15 +323,27 @@ async def upsert(
306323
return response["result"]
307324

308325
async def close(self):
309-
if self.recv_task:
326+
# Cancel the receive task first
327+
if self.recv_task and not self.recv_task.done():
310328
self.recv_task.cancel()
311329
try:
312330
await self.recv_task
313331
except asyncio.CancelledError:
314332
pass
333+
except Exception:
334+
# Ignore any other exceptions during cleanup
335+
pass
315336

337+
# Close the WebSocket connection
316338
if self.socket is not None:
317-
await self.socket.close()
339+
try:
340+
await self.socket.close()
341+
except Exception:
342+
# Ignore exceptions during socket closure
343+
pass
344+
finally:
345+
self.socket = None
346+
self.recv_task = None
318347

319348
async def __aenter__(self) -> "AsyncWsSurrealConnection":
320349
"""

src/surrealdb/data/types/duration.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ def parse(value: Union[str, int], nanoseconds: int = 0) -> "Duration":
2323
if isinstance(value, int):
2424
return Duration(nanoseconds + value * UNITS["s"])
2525
elif isinstance(value, str):
26+
# Check for multi-character units first
27+
for unit in ["ns", "us", "ms"]:
28+
if value.endswith(unit):
29+
num = int(value[: -len(unit)])
30+
return Duration(num * UNITS[unit])
31+
# Check for single-character units
2632
unit = value[-1]
2733
num = int(value[:-1])
2834
if unit in UNITS:
@@ -75,7 +81,7 @@ def weeks(self) -> int:
7581
return self.elapsed // UNITS["w"]
7682

7783
def to_string(self) -> str:
78-
for unit in reversed(["w", "d", "h", "m", "s", "ms", "us", "ns"]):
84+
for unit in ["w", "d", "h", "m", "s", "ms", "us", "ns"]:
7985
value = self.elapsed // UNITS[unit]
8086
if value > 0:
8187
return f"{value}{unit}"
Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,7 @@
1-
from unittest import IsolatedAsyncioTestCase, main
1+
import pytest
22

33
from surrealdb.connections.async_ws import AsyncWsSurrealConnection
44

55

6-
class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase):
7-
async def asyncSetUp(self):
8-
self.url = "ws://localhost:8000"
9-
self.password = "root"
10-
self.username = "root"
11-
self.vars_params = {
12-
"username": self.username,
13-
"password": self.password,
14-
}
15-
self.database_name = "test_db"
16-
self.namespace = "test_ns"
17-
self.connection = AsyncWsSurrealConnection(self.url)
18-
_ = await self.connection.signin(self.vars_params)
19-
_ = await self.connection.use(
20-
namespace=self.namespace, database=self.database_name
21-
)
22-
23-
async def test_authenticate(self):
24-
outcome = await self.connection.authenticate(token=self.connection.token)
25-
26-
27-
if __name__ == "__main__":
28-
main()
6+
async def test_authenticate(async_ws_connection):
7+
outcome = await async_ws_connection.authenticate(token=async_ws_connection.token)
Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,30 @@
11
import asyncio
22
import os
33
import sys
4-
from unittest import IsolatedAsyncioTestCase, main
54

6-
from surrealdb.connections.async_ws import AsyncWsSurrealConnection
5+
import pytest
76

87

9-
class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase):
10-
async def asyncSetUp(self):
11-
self.url = "ws://localhost:8000"
12-
self.password = "root"
13-
self.username = "root"
14-
self.vars_params = {
15-
"username": self.username,
16-
"password": self.password,
17-
}
18-
self.database_name = "test_db"
19-
self.namespace = "test_ns"
20-
self.data = {
21-
"username": self.username,
22-
"password": self.password,
23-
}
24-
self.connection = AsyncWsSurrealConnection(self.url)
25-
_ = await self.connection.signin(self.vars_params)
26-
_ = await self.connection.use(
27-
namespace=self.namespace, database=self.database_name
8+
async def test_batch(async_ws_connection):
9+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
10+
# async batching doesn't work for surrealDB v2.1.0" or lower
11+
if os.environ.get("SURREALDB_VERSION") == "v2.1.0":
12+
pass
13+
elif python_version == "3.9" or python_version == "3.10":
14+
print(
15+
"async batching is being bypassed due to python versions 3.9 and 3.10 not supporting async task group"
2816
)
29-
30-
async def test_batch(self):
31-
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
32-
# async batching doesn't work for surrealDB v2.1.0" or lower
33-
if os.environ.get("SURREALDB_VERSION") == "v2.1.0":
34-
pass
35-
elif python_version == "3.9" or python_version == "3.10":
36-
print(
37-
"async batching is being bypassed due to python versions 3.9 and 3.10 not supporting async task group"
38-
)
39-
else:
40-
async with asyncio.TaskGroup() as tg:
41-
tasks = [
42-
tg.create_task(
43-
self.connection.query(
44-
"RETURN sleep(duration::from::millis($d)) or $p**2",
45-
dict(d=10 if num % 2 else 0, p=num),
46-
)
17+
else:
18+
async with asyncio.TaskGroup() as tg:
19+
tasks = [
20+
tg.create_task(
21+
async_ws_connection.query(
22+
"RETURN sleep(duration::from::millis($d)) or $p**2",
23+
dict(d=10 if num % 2 else 0, p=num),
4724
)
48-
for num in range(5)
49-
]
50-
51-
outcome = [t.result() for t in tasks]
52-
self.assertEqual([0, 1, 4, 9, 16], outcome)
53-
await self.connection.close()
54-
25+
)
26+
for num in range(5)
27+
]
5528

56-
if __name__ == "__main__":
57-
main()
29+
outcome = [t.result() for t in tasks]
30+
assert [0 == 1, 4, 9, 16], outcome

0 commit comments

Comments
 (0)