316 lines
8.9 KiB
Python
316 lines
8.9 KiB
Python
"""Unit tests for analytics service.
|
|
|
|
Tests analytics service functionality including download statistics,
|
|
series popularity tracking, storage analysis, and performance reporting.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.server.services.analytics_service import (
|
|
AnalyticsService,
|
|
DownloadStats,
|
|
PerformanceReport,
|
|
StorageAnalysis,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def analytics_service(tmp_path):
|
|
"""Create analytics service with temp directory."""
|
|
with patch("src.server.services.analytics_service.ANALYTICS_FILE",
|
|
tmp_path / "analytics.json"):
|
|
service = AnalyticsService()
|
|
yield service
|
|
|
|
|
|
@pytest.fixture
|
|
async def mock_db():
|
|
"""Create mock database session."""
|
|
db = AsyncMock(spec=AsyncSession)
|
|
return db
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_service_initialization(analytics_service):
|
|
"""Test analytics service initializes with default data."""
|
|
assert analytics_service.analytics_file.exists()
|
|
|
|
data = json.loads(analytics_service.analytics_file.read_text())
|
|
assert "created_at" in data
|
|
assert "download_stats" in data
|
|
assert "series_popularity" in data
|
|
assert data["download_stats"]["total_downloads"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_download_stats_no_data(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test download statistics with no download data."""
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
|
return_value=[]
|
|
)))
|
|
))
|
|
|
|
stats = await analytics_service.get_download_stats(mock_db)
|
|
|
|
assert isinstance(stats, DownloadStats)
|
|
assert stats.total_downloads == 0
|
|
assert stats.successful_downloads == 0
|
|
assert stats.success_rate == 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_download_stats_with_data(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test download statistics with download data."""
|
|
# Mock downloads - updated to use actual model fields
|
|
download1 = MagicMock()
|
|
download1.status = "completed"
|
|
download1.total_bytes = 1024 * 1024 * 100 # 100 MB
|
|
download1.download_speed = 1024 * 1024 * 10 # 10 MB/s
|
|
|
|
download2 = MagicMock()
|
|
download2.status = "failed"
|
|
download2.total_bytes = 0
|
|
download2.download_speed = None
|
|
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
|
return_value=[download1, download2]
|
|
)))
|
|
))
|
|
|
|
stats = await analytics_service.get_download_stats(mock_db)
|
|
|
|
assert stats.total_downloads == 2
|
|
assert stats.successful_downloads == 1
|
|
assert stats.failed_downloads == 1
|
|
assert stats.success_rate == 50.0
|
|
assert stats.total_bytes_downloaded == 1024 * 1024 * 100
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_series_popularity_empty(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test series popularity with no data."""
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
all=MagicMock(return_value=[])
|
|
))
|
|
|
|
popularity = await analytics_service.get_series_popularity(
|
|
mock_db, limit=10
|
|
)
|
|
|
|
assert isinstance(popularity, list)
|
|
assert len(popularity) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_series_popularity_with_data(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test series popularity with data."""
|
|
# Mock returns tuples:
|
|
# (series_name, download_count, total_size, last_download, successful)
|
|
row = (
|
|
"Test Anime",
|
|
5,
|
|
1024 * 1024 * 500,
|
|
datetime.now(),
|
|
4
|
|
)
|
|
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
all=MagicMock(return_value=[row])
|
|
))
|
|
|
|
popularity = await analytics_service.get_series_popularity(
|
|
mock_db, limit=10
|
|
)
|
|
|
|
assert len(popularity) == 1
|
|
assert popularity[0].series_name == "Test Anime"
|
|
assert popularity[0].download_count == 5
|
|
assert popularity[0].success_rate == 80.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_storage_analysis(analytics_service):
|
|
"""Test storage analysis retrieval."""
|
|
with patch("psutil.disk_usage") as mock_disk:
|
|
mock_disk.return_value = MagicMock(
|
|
total=1024 * 1024 * 1024 * 1024,
|
|
used=512 * 1024 * 1024 * 1024,
|
|
free=512 * 1024 * 1024 * 1024,
|
|
percent=50.0,
|
|
)
|
|
|
|
analysis = analytics_service.get_storage_analysis()
|
|
|
|
assert isinstance(analysis, StorageAnalysis)
|
|
assert analysis.total_storage_bytes > 0
|
|
assert analysis.storage_percent_used == 50.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_performance_report_no_data(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test performance report with no data."""
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
|
return_value=[]
|
|
)))
|
|
))
|
|
|
|
with patch("psutil.Process") as mock_process:
|
|
mock_process.return_value = MagicMock(
|
|
memory_info=MagicMock(
|
|
return_value=MagicMock(rss=100 * 1024 * 1024)
|
|
),
|
|
cpu_percent=MagicMock(return_value=10.0),
|
|
)
|
|
|
|
report = await analytics_service.get_performance_report(
|
|
mock_db, hours=24
|
|
)
|
|
|
|
assert isinstance(report, PerformanceReport)
|
|
assert report.downloads_per_hour == 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_record_performance_sample(analytics_service):
|
|
"""Test recording performance samples."""
|
|
analytics_service.record_performance_sample(
|
|
queue_size=5,
|
|
active_downloads=2,
|
|
cpu_percent=25.0,
|
|
memory_mb=512.0,
|
|
)
|
|
|
|
data = json.loads(
|
|
analytics_service.analytics_file.read_text()
|
|
)
|
|
assert len(data["performance_samples"]) == 1
|
|
sample = data["performance_samples"][0]
|
|
assert sample["queue_size"] == 5
|
|
assert sample["active_downloads"] == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_record_multiple_performance_samples(
|
|
analytics_service
|
|
):
|
|
"""Test recording multiple performance samples."""
|
|
for i in range(5):
|
|
analytics_service.record_performance_sample(
|
|
queue_size=i,
|
|
active_downloads=i % 2,
|
|
cpu_percent=10.0 + i,
|
|
memory_mb=256.0 + i * 50,
|
|
)
|
|
|
|
data = json.loads(
|
|
analytics_service.analytics_file.read_text()
|
|
)
|
|
assert len(data["performance_samples"]) == 5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_summary_report(
|
|
analytics_service, mock_db
|
|
):
|
|
"""Test generating comprehensive summary report."""
|
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
|
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
|
return_value=[]
|
|
))),
|
|
all=MagicMock(return_value=[]),
|
|
))
|
|
|
|
with patch("psutil.disk_usage") as mock_disk:
|
|
mock_disk.return_value = MagicMock(
|
|
total=1024 * 1024 * 1024,
|
|
used=512 * 1024 * 1024,
|
|
free=512 * 1024 * 1024,
|
|
percent=50.0,
|
|
)
|
|
|
|
with patch("psutil.Process"):
|
|
report = await analytics_service.generate_summary_report(
|
|
mock_db
|
|
)
|
|
|
|
assert "timestamp" in report
|
|
assert "download_stats" in report
|
|
assert "series_popularity" in report
|
|
assert "storage_analysis" in report
|
|
assert "performance_report" in report
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_dir_size(analytics_service, tmp_path):
|
|
"""Test directory size calculation."""
|
|
# Create test files
|
|
(tmp_path / "file1.txt").write_text("test content")
|
|
(tmp_path / "file2.txt").write_text("more test content")
|
|
subdir = tmp_path / "subdir"
|
|
subdir.mkdir()
|
|
(subdir / "file3.txt").write_text("nested content")
|
|
|
|
size = analytics_service._get_dir_size(tmp_path)
|
|
|
|
assert size > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_dir_size_nonexistent(analytics_service):
|
|
"""Test directory size for nonexistent directory."""
|
|
size = analytics_service._get_dir_size(
|
|
Path("/nonexistent/directory")
|
|
)
|
|
|
|
assert size == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_persistence(analytics_service):
|
|
"""Test analytics data persistence."""
|
|
analytics_service.record_performance_sample(
|
|
queue_size=10,
|
|
active_downloads=3,
|
|
cpu_percent=50.0,
|
|
memory_mb=1024.0,
|
|
)
|
|
|
|
# Create new service instance
|
|
analytics_service2 = AnalyticsService()
|
|
analytics_service2.analytics_file = analytics_service.analytics_file
|
|
|
|
data = json.loads(
|
|
analytics_service2.analytics_file.read_text()
|
|
)
|
|
assert len(data["performance_samples"]) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_service_singleton(analytics_service):
|
|
"""Test analytics service singleton pattern."""
|
|
from src.server.services.analytics_service import get_analytics_service
|
|
|
|
service1 = get_analytics_service()
|
|
service2 = get_analytics_service()
|
|
|
|
assert service1 is service2
|