Skip to content

Use pytest + coverage #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
run: uv sync --group dev

- name: Run unit tests
run: uv run python -m unittest discover -s tests
run: uv run pytest
env:
PYTHONPATH: ./src
SURREALDB_URL: http://localhost:8000
Expand Down Expand Up @@ -117,7 +117,7 @@ jobs:
run: uv sync --group dev

- name: Run unit tests
run: uv run python -m unittest discover -s tests
run: uv run pytest
env:
PYTHONPATH: ./src
SURREALDB_URL: http://localhost:8000
Expand Down
63 changes: 50 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ This project follows library best practices for dependency management:
# Run type checking
uv run mypy --explicit-package-bases src/

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

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

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

# Test against different v2.x versions
SURREALDB_VERSION=v2.0.5 uv run python -m unittest discover -s tests
SURREALDB_VERSION=v2.3.6 uv run python -m unittest discover -s tests
SURREALDB_VERSION=v2.0.5 uv run scripts/run_tests.sh
SURREALDB_VERSION=v2.3.6 uv run scripts/run_tests.sh
```

### CI/CD Testing
Expand Down Expand Up @@ -239,18 +241,26 @@ bash scripts/term.sh
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:

```bash
python -m unittest discover
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
```

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

```bash
.........................................................................................................................................Error in live subscription: sent 1000 (OK); no close frame received
..........................................................................................
----------------------------------------------------------------------
Ran 227 tests in 6.313s
================================ test session starts ================================
platform ...
collected 227 items

....................................................................................
... (test output)

---------- coverage: platform ... -----------
Name Stmts Miss Cover Missing
---------------------------------------------------------
src/surrealdb/....
...

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

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

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

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

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

# Running Tests and Coverage

To run all tests with coverage reporting:

```bash
uv run scripts/run_tests.sh
```

This will:
- Run all tests using pytest
- Show a coverage summary in the terminal
- Generate an HTML coverage report in the `htmlcov/` directory

You can also run tests directly with:

```bash
uv run pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
```

To test a specific file:

```bash
uv run pytest tests/unit_tests/connections/test_connection_constructor.py --cov=src/surrealdb
```

To view the HTML coverage report, open `htmlcov/index.html` in your browser after running the tests.
46 changes: 45 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,55 @@ ignore_missing_imports = true
module = "websockets.*"
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
"--tb=short",
]
filterwarnings = [
"ignore::pytest.PytestUnraisableExceptionWarning",
]

[tool.coverage.run]
source = ["src"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

[dependency-groups]
dev = [
{ include-group = "test" },
"mypy>=1.0.0",
"ruff>=0.12.0",
"types-requests>=2.25.0", # Type stubs for requests
]
test = ["hypothesis>=6.135.16"]
test = [
"coverage>=7.0.0",
"hypothesis>=6.135.16",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.0.0",
]
4 changes: 3 additions & 1 deletion scripts/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ cd ..
cd src
export PYTHONPATH=$(pwd)
cd ..
python -m unittest discover

# Run tests with coverage
pytest --cov=src/surrealdb --cov-report=term-missing --cov-report=html
55 changes: 42 additions & 13 deletions src/surrealdb/connections/async_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from uuid import UUID

import websockets # type: ignore
from websockets.exceptions import ConnectionClosed, WebSocketException

from surrealdb.connections.async_template import AsyncTemplate
from surrealdb.connections.url import Url
Expand Down Expand Up @@ -50,17 +51,33 @@ def __init__(

async def _recv_task(self):
assert self.socket
async for data in self.socket:
response = decode(data)
if response_id := response.get("id"):
if fut := self.qry.get(response_id):
fut.set_result(response)
elif response_result := response.get("result"):
live_id = str(response_result["id"])
for queue in self.live_queues.get(live_id, []):
queue.put_nowait(response_result)
else:
self.check_response_for_error(response, "_recv_task")
try:
async for data in self.socket:
response = decode(data)
if response_id := response.get("id"):
if fut := self.qry.get(response_id):
fut.set_result(response)
elif response_result := response.get("result"):
live_id = str(response_result["id"])
for queue in self.live_queues.get(live_id, []):
queue.put_nowait(response_result)
else:
self.check_response_for_error(response, "_recv_task")
except (ConnectionClosed, WebSocketException, asyncio.CancelledError):
# Connection was closed or cancelled, this is expected
pass
except Exception as e:
# Log unexpected errors but don't let them propagate
import logging

logger = logging.getLogger(__name__)
logger.debug(f"Unexpected error in _recv_task: {e}")
finally:
# Clean up any pending futures
for fut in self.qry.values():
if not fut.done():
fut.cancel()
self.qry.clear()

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

async def close(self):
if self.recv_task:
# Cancel the receive task first
if self.recv_task and not self.recv_task.done():
self.recv_task.cancel()
try:
await self.recv_task
except asyncio.CancelledError:
pass
except Exception:
# Ignore any other exceptions during cleanup
pass

# Close the WebSocket connection
if self.socket is not None:
await self.socket.close()
try:
await self.socket.close()
except Exception:
# Ignore exceptions during socket closure
pass
finally:
self.socket = None
self.recv_task = None

async def __aenter__(self) -> "AsyncWsSurrealConnection":
"""
Expand Down
8 changes: 7 additions & 1 deletion src/surrealdb/data/types/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def parse(value: Union[str, int], nanoseconds: int = 0) -> "Duration":
if isinstance(value, int):
return Duration(nanoseconds + value * UNITS["s"])
elif isinstance(value, str):
# Check for multi-character units first
for unit in ["ns", "us", "ms"]:
if value.endswith(unit):
num = int(value[: -len(unit)])
return Duration(num * UNITS[unit])
# Check for single-character units
unit = value[-1]
num = int(value[:-1])
if unit in UNITS:
Expand Down Expand Up @@ -75,7 +81,7 @@ def weeks(self) -> int:
return self.elapsed // UNITS["w"]

def to_string(self) -> str:
for unit in reversed(["w", "d", "h", "m", "s", "ms", "us", "ns"]):
for unit in ["w", "d", "h", "m", "s", "ms", "us", "ns"]:
value = self.elapsed // UNITS[unit]
if value > 0:
return f"{value}{unit}"
Expand Down
27 changes: 3 additions & 24 deletions tests/unit_tests/connections/authenticate/test_async_ws.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
from unittest import IsolatedAsyncioTestCase, main
import pytest

from surrealdb.connections.async_ws import AsyncWsSurrealConnection


class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.url = "ws://localhost:8000"
self.password = "root"
self.username = "root"
self.vars_params = {
"username": self.username,
"password": self.password,
}
self.database_name = "test_db"
self.namespace = "test_ns"
self.connection = AsyncWsSurrealConnection(self.url)
_ = await self.connection.signin(self.vars_params)
_ = await self.connection.use(
namespace=self.namespace, database=self.database_name
)

async def test_authenticate(self):
outcome = await self.connection.authenticate(token=self.connection.token)


if __name__ == "__main__":
main()
async def test_authenticate(async_ws_connection):
outcome = await async_ws_connection.authenticate(token=async_ws_connection.token)
Loading
Loading