exchange-data-manager/tests/test_async_database.py

214 lines
7.9 KiB
Python

"""Tests for async database cache."""
import pytest
import tempfile
import os
from exchange_data_manager.candles.models import Candle
from exchange_data_manager.cache.async_database import AsyncDatabaseCache
@pytest.fixture
async def async_db_cache():
"""Create a temporary async database for testing."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
cache = AsyncDatabaseCache(db_path=db_path)
await cache.initialize()
yield cache
# Cleanup
if os.path.exists(db_path):
os.unlink(db_path)
class TestAsyncDatabaseCache:
"""Tests for AsyncDatabaseCache class."""
@pytest.mark.asyncio
async def test_put_and_get_candles(self, async_db_cache):
"""Test storing and retrieving candles asynchronously."""
candles = [
Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0),
Candle(time=1709337660, open=50050.0, high=50200.0, low=50000.0, close=50150.0, volume=15.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
result, gaps = await async_db_cache.get("binance", "BTC/USDT", "1m")
assert len(result) == 2
assert result[0].time == 1709337600
assert result[1].time == 1709337660
@pytest.mark.asyncio
async def test_get_empty_returns_empty_list(self, async_db_cache):
"""Test that getting non-existent data returns empty list."""
result, gaps = await async_db_cache.get("binance", "BTC/USDT", "1m")
assert result == []
@pytest.mark.asyncio
async def test_get_with_time_range(self, async_db_cache):
"""Test getting candles within a time range."""
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337660, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337720, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337780, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
result, gaps = await async_db_cache.get(
"binance", "BTC/USDT", "1m",
start=1709337660,
end=1709337720,
)
assert len(result) == 2
assert result[0].time == 1709337660
assert result[1].time == 1709337720
@pytest.mark.asyncio
async def test_upsert_updates_existing(self, async_db_cache):
"""Test that storing duplicate timestamps updates existing records."""
candles1 = [
Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles1)
# Store updated candle with same timestamp
candles2 = [
Candle(time=1709337600, open=51000.0, high=51100.0, low=50900.0, close=51050.0, volume=20.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles2)
result, gaps = await async_db_cache.get("binance", "BTC/USDT", "1m")
# Should only have one candle with updated values
assert len(result) == 1
assert result[0].open == 51000.0
assert result[0].volume == 20.0
@pytest.mark.asyncio
async def test_delete_all(self, async_db_cache):
"""Test deleting all data."""
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
await async_db_cache.put("binance", "ETH/USDT", "1m", candles)
await async_db_cache.delete()
result1, _ = await async_db_cache.get("binance", "BTC/USDT", "1m")
result2, _ = await async_db_cache.get("binance", "ETH/USDT", "1m")
assert result1 == []
assert result2 == []
@pytest.mark.asyncio
async def test_delete_filtered(self, async_db_cache):
"""Test deleting filtered data."""
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
await async_db_cache.put("binance", "ETH/USDT", "1m", candles)
# Only delete BTC
await async_db_cache.delete(exchange="binance", symbol="BTC/USDT")
result1, _ = await async_db_cache.get("binance", "BTC/USDT", "1m")
result2, _ = await async_db_cache.get("binance", "ETH/USDT", "1m")
assert result1 == []
assert len(result2) == 1
@pytest.mark.asyncio
async def test_stats(self, async_db_cache):
"""Test database statistics."""
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337660, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
stats = await async_db_cache.stats()
assert stats["total_candles"] == 2
assert stats["num_entries"] == 1
@pytest.mark.asyncio
async def test_get_time_range(self, async_db_cache):
"""Test getting the time range of cached data."""
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337660, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
Candle(time=1709337720, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
time_range = await async_db_cache.get_time_range("binance", "BTC/USDT", "1m")
assert time_range is not None
assert time_range[0] == 1709337600 # min
assert time_range[1] == 1709337720 # max
@pytest.mark.asyncio
async def test_get_time_range_no_data(self, async_db_cache):
"""Test getting time range when no data exists."""
time_range = await async_db_cache.get_time_range("binance", "BTC/USDT", "1m")
assert time_range is None
@pytest.mark.asyncio
async def test_single_candle_gap_detection(self, async_db_cache):
"""Test that a single missing candle is detected as a gap."""
# Candles with 1m interval but missing the second one
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
# Missing: time=1709337660
Candle(time=1709337720, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await async_db_cache.put("binance", "BTC/USDT", "1m", candles)
result, gaps = await async_db_cache.get(
"binance", "BTC/USDT", "1m",
start=1709337600,
end=1709337720,
)
assert len(result) == 2
# Should detect the gap for the single missing candle
assert len(gaps) == 1
assert gaps[0] == (1709337660, 1709337660) # Single candle gap
@pytest.mark.asyncio
async def test_lazy_initialization(self):
"""Test that database is lazily initialized on first operation."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
# Don't call initialize explicitly
cache = AsyncDatabaseCache(db_path=db_path)
assert cache._initialized is False
# First operation should initialize
candles = [
Candle(time=1709337600, open=50000.0, high=50000.0, low=50000.0, close=50000.0, volume=1.0),
]
await cache.put("binance", "BTC/USDT", "1m", candles)
assert cache._initialized is True
# Cleanup
os.unlink(db_path)