Skip to content

Commit a890614

Browse files
Use pytest + coverage (#190)
1 parent fe6c99b commit a890614

File tree

97 files changed

+6371
-6004
lines changed

Some content is hidden

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

97 files changed

+6371
-6004
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
run: uv sync --group dev
7272

7373
- name: Run unit tests
74-
run: uv run python -m unittest discover -s tests
74+
run: uv run pytest
7575
env:
7676
PYTHONPATH: ./src
7777
SURREALDB_URL: http://localhost:8000
@@ -117,7 +117,7 @@ jobs:
117117
run: uv sync --group dev
118118

119119
- name: Run unit tests
120-
run: uv run python -m unittest discover -s tests
120+
run: uv run pytest
121121
env:
122122
PYTHONPATH: ./src
123123
SURREALDB_URL: http://localhost:8000

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.

pyproject.toml

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,55 @@ ignore_missing_imports = true
7878
module = "websockets.*"
7979
ignore_missing_imports = true
8080

81+
[tool.pytest.ini_options]
82+
testpaths = ["tests"]
83+
python_files = ["test_*.py"]
84+
python_classes = ["Test*"]
85+
python_functions = ["test_*"]
86+
asyncio_mode = "auto"
87+
addopts = [
88+
"--strict-markers",
89+
"--strict-config",
90+
"--verbose",
91+
"--tb=short",
92+
]
93+
filterwarnings = [
94+
"ignore::pytest.PytestUnraisableExceptionWarning",
95+
]
96+
97+
[tool.coverage.run]
98+
source = ["src"]
99+
omit = [
100+
"*/tests/*",
101+
"*/test_*",
102+
"*/__pycache__/*",
103+
]
104+
105+
[tool.coverage.report]
106+
exclude_lines = [
107+
"pragma: no cover",
108+
"def __repr__",
109+
"if self.debug:",
110+
"if settings.DEBUG",
111+
"raise AssertionError",
112+
"raise NotImplementedError",
113+
"if 0:",
114+
"if __name__ == .__main__.:",
115+
"class .*\\bProtocol\\):",
116+
"@(abc\\.)?abstractmethod",
117+
]
118+
81119
[dependency-groups]
82120
dev = [
83121
{ include-group = "test" },
84122
"mypy>=1.0.0",
85123
"ruff>=0.12.0",
86124
"types-requests>=2.25.0", # Type stubs for requests
87125
]
88-
test = ["hypothesis>=6.135.16"]
126+
test = [
127+
"coverage>=7.0.0",
128+
"hypothesis>=6.135.16",
129+
"pytest>=7.0.0",
130+
"pytest-asyncio>=0.21.0",
131+
"pytest-cov>=4.0.0",
132+
]

scripts/run_tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ cd ..
88
cd src
99
export PYTHONPATH=$(pwd)
1010
cd ..
11-
python -m unittest discover
11+
12+
# Run tests with coverage
13+
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html

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)

0 commit comments

Comments
 (0)