uri-register 0.3.0

A high-performance PostgreSQL-backed URI dictionary service for assigning unique integer IDs to URIs
Documentation
"""
Tests for Python bindings of uri-register

Prerequisites:
- PostgreSQL running locally
- Database initialized with schema.sql
- DATABASE_URL environment variable set

Run with: pytest tests/test_python_bindings.py
"""

import os

import pytest

# Skip all tests in this module if uri_register cannot be imported
# (This allows tests to run in environments where the Python extension isn't built)
pytest.importorskip("uri_register")

from uri_register import UriRegister  # noqa: E402

# ==================== Fixtures ====================


@pytest.fixture
def db_url():
    """Get database URL from environment"""
    return os.environ.get("DATABASE_URL", "postgres://localhost/access")


@pytest.fixture
def register_sync(db_url):
    """Create a synchronous UriRegister instance for testing"""
    return UriRegister(
        db_url,
        "uri_register",  # table_name
        20,  # max_connections
        10000,  # cache_size
        # cache_strategy defaults to "moka", use_tls defaults to False
    )


@pytest.fixture
async def register_async(db_url):
    """Create an asynchronous UriRegister instance for testing"""
    return await UriRegister.new_async(
        db_url,
        "uri_register",  # table_name
        20,  # max_connections
        10000,  # cache_size
        # cache_strategy defaults to "moka", use_tls defaults to False
    )


# ==================== Synchronous API Tests ====================


def test_sync_register_single_uri(register_sync):
    """Test registering a single URI synchronously"""
    uri = "http://example.org/test/sync/single"
    id1 = register_sync.register_uri(uri)

    assert isinstance(id1, int)
    assert id1 > 0

    # Registering the same URI again should return the same ID
    id2 = register_sync.register_uri(uri)
    assert id1 == id2


def test_sync_register_uri_batch(register_sync):
    """Test batch registration synchronously with order preservation"""
    uris = [
        "http://example.org/test/sync/batch/1",
        "http://example.org/test/sync/batch/2",
        "http://example.org/test/sync/batch/3",
    ]

    ids = register_sync.register_uri_batch(uris)

    assert isinstance(ids, list)
    assert len(ids) == len(uris)

    # All IDs should be positive integers
    for id in ids:
        assert isinstance(id, int)
        assert id > 0

    # Verify order preservation: each ID should match its URI
    for i, uri in enumerate(uris):
        individual_id = register_sync.register_uri(uri)
        assert ids[i] == individual_id


def test_sync_register_uri_batch_with_duplicates(register_sync):
    """Test batch registration handles duplicates correctly (sync)"""
    uris = [
        "http://example.org/test/sync/dup/1",
        "http://example.org/test/sync/dup/2",
        "http://example.org/test/sync/dup/1",  # Duplicate
    ]

    ids = register_sync.register_uri_batch(uris)

    assert len(ids) == 3
    # First and third should have the same ID (duplicates)
    assert ids[0] == ids[2]
    # Second should be different
    assert ids[1] != ids[0]


def test_sync_register_uri_batch_hashmap(register_sync):
    """Test batch registration returning a hashmap (sync)"""
    uris = [
        "http://example.org/test/sync/hashmap/1",
        "http://example.org/test/sync/hashmap/2",
        "http://example.org/test/sync/hashmap/1",  # Duplicate within batch
    ]

    mapping = register_sync.register_uri_batch_hashmap(uris)

    assert isinstance(mapping, dict)
    # Should only have 2 unique entries
    assert len(mapping) == 2

    # Check each unique URI is in the mapping
    assert "http://example.org/test/sync/hashmap/1" in mapping
    assert "http://example.org/test/sync/hashmap/2" in mapping

    # Values should be positive integers
    for _uri, id in mapping.items():
        assert isinstance(id, int)
        assert id > 0


def test_sync_stats(register_sync):
    """Test getting statistics synchronously"""
    # Register some URIs first
    register_sync.register_uri("http://example.org/test/sync/stats/1")
    register_sync.register_uri("http://example.org/test/sync/stats/2")

    stats = register_sync.stats()

    assert isinstance(stats, dict)
    assert "total_uris" in stats
    assert "size_bytes" in stats

    assert isinstance(stats["total_uris"], int)
    assert isinstance(stats["size_bytes"], int)

    assert stats["total_uris"] >= 2  # At least the 2 we just registered
    assert stats["size_bytes"] > 0


def test_sync_empty_batch(register_sync):
    """Test batch operations with empty input (sync)"""
    ids = register_sync.register_uri_batch([])
    assert ids == []

    mapping = register_sync.register_uri_batch_hashmap([])
    assert mapping == {}


def test_sync_batch_performance(register_sync):
    """Test that batch operations work with larger batches (sync)"""
    # Generate 100 URIs
    uris = [f"http://example.org/test/sync/perf/{i}" for i in range(100)]

    ids = register_sync.register_uri_batch(uris)

    assert len(ids) == 100
    # All IDs should be unique (no duplicates in input)
    assert len(set(ids)) == 100


# ==================== Asynchronous API Tests ====================


@pytest.mark.asyncio
async def test_async_register_single_uri(register_async):
    """Test registering a single URI asynchronously"""
    uri = "http://example.org/test/async/single"
    id1 = await register_async.register_uri_async(uri)

    assert isinstance(id1, int)
    assert id1 > 0

    # Registering the same URI again should return the same ID
    id2 = await register_async.register_uri_async(uri)
    assert id1 == id2


@pytest.mark.asyncio
async def test_async_register_uri_batch(register_async):
    """Test batch registration asynchronously with order preservation"""
    uris = [
        "http://example.org/test/async/batch/1",
        "http://example.org/test/async/batch/2",
        "http://example.org/test/async/batch/3",
    ]

    ids = await register_async.register_uri_batch_async(uris)

    assert isinstance(ids, list)
    assert len(ids) == len(uris)

    # All IDs should be positive integers
    for id in ids:
        assert isinstance(id, int)
        assert id > 0

    # Verify order preservation: each ID should match its URI
    for i, uri in enumerate(uris):
        individual_id = await register_async.register_uri_async(uri)
        assert ids[i] == individual_id


@pytest.mark.asyncio
async def test_async_register_uri_batch_with_duplicates(register_async):
    """Test batch registration handles duplicates correctly (async)"""
    uris = [
        "http://example.org/test/async/dup/1",
        "http://example.org/test/async/dup/2",
        "http://example.org/test/async/dup/1",  # Duplicate
    ]

    ids = await register_async.register_uri_batch_async(uris)

    assert len(ids) == 3
    # First and third should have the same ID (duplicates)
    assert ids[0] == ids[2]
    # Second should be different
    assert ids[1] != ids[0]


@pytest.mark.asyncio
async def test_async_register_uri_batch_hashmap(register_async):
    """Test batch registration returning a hashmap (async)"""
    uris = [
        "http://example.org/test/async/hashmap/1",
        "http://example.org/test/async/hashmap/2",
        "http://example.org/test/async/hashmap/1",  # Duplicate within batch
    ]

    mapping = await register_async.register_uri_batch_hashmap_async(uris)

    assert isinstance(mapping, dict)
    # Should only have 2 unique entries
    assert len(mapping) == 2

    # Check each unique URI is in the mapping
    assert "http://example.org/test/async/hashmap/1" in mapping
    assert "http://example.org/test/async/hashmap/2" in mapping

    # Values should be positive integers
    for _uri, id in mapping.items():
        assert isinstance(id, int)
        assert id > 0


@pytest.mark.asyncio
async def test_async_stats(register_async):
    """Test getting statistics asynchronously"""
    # Register some URIs first
    await register_async.register_uri_async("http://example.org/test/async/stats/1")
    await register_async.register_uri_async("http://example.org/test/async/stats/2")

    stats = await register_async.stats_async()

    assert isinstance(stats, dict)
    assert "total_uris" in stats
    assert "size_bytes" in stats

    assert isinstance(stats["total_uris"], int)
    assert isinstance(stats["size_bytes"], int)

    assert stats["total_uris"] >= 2  # At least the 2 we just registered
    assert stats["size_bytes"] > 0


@pytest.mark.asyncio
async def test_async_empty_batch(register_async):
    """Test batch operations with empty input (async)"""
    ids = await register_async.register_uri_batch_async([])
    assert ids == []

    mapping = await register_async.register_uri_batch_hashmap_async([])
    assert mapping == {}


@pytest.mark.asyncio
async def test_async_batch_performance(register_async):
    """Test that batch operations work with larger batches (async)"""
    # Generate 100 URIs
    uris = [f"http://example.org/test/async/perf/{i}" for i in range(100)]

    ids = await register_async.register_uri_batch_async(uris)

    assert len(ids) == 100
    # All IDs should be unique (no duplicates in input)
    assert len(set(ids)) == 100


# ==================== Mixed API Tests ====================


def test_sync_instance_can_use_sync_methods(register_sync):
    """Verify sync instance works with sync methods"""
    # This tests that the runtime is properly initialized and block_on works
    uri = "http://example.org/test/mixed/sync"
    id1 = register_sync.register_uri(uri)
    id2 = register_sync.register_uri(uri)
    assert id1 == id2


@pytest.mark.asyncio
async def test_async_instance_can_use_both_methods(register_async):
    """Verify async instance can use both sync and async methods"""
    uri_async = "http://example.org/test/mixed/async"
    uri_sync = "http://example.org/test/mixed/sync_from_async"

    # Use async method
    id_async = await register_async.register_uri_async(uri_async)
    assert id_async > 0

    # Use sync method on same instance
    id_sync = register_async.register_uri(uri_sync)
    assert id_sync > 0