Format tests

This commit is contained in:
itqop 2025-07-12 11:47:31 +03:00
parent c1cb0f46a4
commit fef5023d74
5 changed files with 86 additions and 58 deletions

View File

@ -24,17 +24,19 @@ def level_to_int(logger, method_name, event_dict):
pass pass
return event_dict return event_dict
@pytest.fixture(autouse=True, scope="session") @pytest.fixture(autouse=True, scope="session")
def configure_structlog(): def configure_structlog():
import tenacity import tenacity
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
structlog.configure( structlog.configure(
processors=[ processors=[
level_to_int, level_to_int,
structlog.processors.TimeStamper(fmt="iso"), structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer() structlog.dev.ConsoleRenderer(),
], ],
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG) wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
) )
tenacity.logger = structlog.get_logger("tenacity") tenacity.logger = structlog.get_logger("tenacity")

View File

@ -185,7 +185,11 @@ class TestLLMProviderAdapter:
long_text = "word " * 2000 long_text = "word " * 2000
with patch.object(adapter, "_check_rpm_limit"): with patch.object(adapter, "_check_rpm_limit"):
with patch.object(adapter, "count_tokens", return_value=50000): with patch.object(adapter, "count_tokens", return_value=50000):
with patch.object(adapter, "_make_completion_request", side_effect=LLMTokenLimitError("Token limit exceeded")): with patch.object(
adapter,
"_make_completion_request",
side_effect=LLMTokenLimitError("Token limit exceeded"),
):
with pytest.raises(LLMTokenLimitError): with pytest.raises(LLMTokenLimitError):
await adapter.simplify_text("Test", long_text, "template") await adapter.simplify_text("Test", long_text, "template")
@ -193,7 +197,9 @@ class TestLLMProviderAdapter:
async def test_simplify_text_success(self, test_config, mock_openai_response): async def test_simplify_text_success(self, test_config, mock_openai_response):
adapter = LLMProviderAdapter(test_config) adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create: with patch.object(
adapter.client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_openai_response mock_create.return_value = mock_openai_response
with patch.object(adapter, "_check_rpm_limit"): with patch.object(adapter, "_check_rpm_limit"):
@ -215,7 +221,7 @@ class TestLLMProviderAdapter:
from tenacity import AsyncRetrying, before_sleep_log from tenacity import AsyncRetrying, before_sleep_log
import structlog import structlog
import logging import logging
adapter = LLMProviderAdapter(test_config) adapter = LLMProviderAdapter(test_config)
good_logger = structlog.get_logger("tenacity") good_logger = structlog.get_logger("tenacity")
@ -230,9 +236,13 @@ class TestLLMProviderAdapter:
**{**kwargs, "before_sleep": fixed_before_sleep_log(good_logger, logging.WARNING)} **{**kwargs, "before_sleep": fixed_before_sleep_log(good_logger, logging.WARNING)}
) )
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create: with patch.object(
adapter.client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_response = MagicMock() mock_response = MagicMock()
mock_create.side_effect = RateLimitError("Rate limit exceeded", response=mock_response, body=None) mock_create.side_effect = RateLimitError(
"Rate limit exceeded", response=mock_response, body=None
)
with patch.object(adapter, "_check_rpm_limit"): with patch.object(adapter, "_check_rpm_limit"):
with pytest.raises(LLMRateLimitError): with pytest.raises(LLMRateLimitError):
await adapter.simplify_text( await adapter.simplify_text(
@ -272,7 +282,9 @@ class TestLLMProviderAdapter:
async def test_health_check_success(self, test_config, mock_openai_response): async def test_health_check_success(self, test_config, mock_openai_response):
adapter = LLMProviderAdapter(test_config) adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create: with patch.object(
adapter.client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_openai_response mock_create.return_value = mock_openai_response
result = await adapter.health_check() result = await adapter.health_check()
@ -281,7 +293,9 @@ class TestLLMProviderAdapter:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_health_check_failure(self, test_config): async def test_health_check_failure(self, test_config):
adapter = LLMProviderAdapter(test_config) adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create: with patch.object(
adapter.client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_request = MagicMock() mock_request = MagicMock()
mock_create.side_effect = APIError("API Error", body=None, request=mock_request) mock_create.side_effect = APIError("API Error", body=None, request=mock_request)
result = await adapter.health_check() result = await adapter.health_check()

View File

@ -113,7 +113,7 @@ class TestRepositoryIntegration:
article2 = await repository.create_article( article2 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2", url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2", title="Test 2",
raw_text="Content 2", raw_text="Content 2",
) )
@ -217,13 +217,12 @@ class TestAsyncOperations:
async def test_concurrent_read_operations(self, multiple_articles_in_db): async def test_concurrent_read_operations(self, multiple_articles_in_db):
articles = multiple_articles_in_db articles = multiple_articles_in_db
repository = articles[0].__class__.__module__ repository = articles[0].__class__.__module__
async def read_article(article_id: int): async def read_article(article_id: int):
pass pass
class TestSystemIntegration: class TestSystemIntegration:
@pytest.mark.asyncio @pytest.mark.asyncio
@ -281,7 +280,9 @@ class TestSystemIntegration:
mock_llm_instance.simplify_text.return_value = ("Упрощённый текст", 100, 50) mock_llm_instance.simplify_text.return_value = ("Упрощённый текст", 100, 50)
mock_llm_instance.count_tokens.return_value = 100 mock_llm_instance.count_tokens.return_value = 100
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f: with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
f.write("### role: user\n{wiki_source_text}") f.write("### role: user\n{wiki_source_text}")
test_config.prompt_template_path = f.name test_config.prompt_template_path = f.name
@ -364,7 +365,9 @@ class TestSystemIntegration:
mock_llm_instance.simplify_text.side_effect = delayed_simplify mock_llm_instance.simplify_text.side_effect = delayed_simplify
mock_llm_instance.count_tokens.return_value = 100 mock_llm_instance.count_tokens.return_value = 100
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f: with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
f.write("### role: user\n{wiki_source_text}") f.write("### role: user\n{wiki_source_text}")
test_config.prompt_template_path = f.name test_config.prompt_template_path = f.name

View File

@ -75,15 +75,13 @@ class TestAppConfig:
def test_app_config_defaults(self): def test_app_config_defaults(self):
from pathlib import Path from pathlib import Path
import os import os
from unittest.mock import patch from unittest.mock import patch
with patch.dict(os.environ, {}, clear=True): with patch.dict(os.environ, {}, clear=True):
config = AppConfig(openai_api_key="test-key") config = AppConfig(openai_api_key="test-key")
assert isinstance(config.db_path, str) assert isinstance(config.db_path, str)
assert Path(config.db_path).suffix == ".db" assert Path(config.db_path).suffix == ".db"
assert isinstance(config.openai_model, str) assert isinstance(config.openai_model, str)

View File

@ -29,7 +29,7 @@ class TestDatabaseService:
async def test_health_check_failure(self, test_config): async def test_health_check_failure(self, test_config):
test_config.db_path = "/invalid/path/database.db" test_config.db_path = "/invalid/path/database.db"
service = DatabaseService(test_config) service = DatabaseService(test_config)
result = await service.health_check() result = await service.health_check()
assert result is False assert result is False
@ -52,14 +52,13 @@ class TestArticleRepository:
async def test_create_duplicate_article(self, repository: ArticleRepository): async def test_create_duplicate_article(self, repository: ArticleRepository):
url = "https://ru.ruwiki.ru/wiki/Test" url = "https://ru.ruwiki.ru/wiki/Test"
await repository.create_article( await repository.create_article(
url=url, url=url,
title="Test Article", title="Test Article",
raw_text="Test content", raw_text="Test content",
) )
with pytest.raises(ValueError, match="уже существует"): with pytest.raises(ValueError, match="уже существует"):
await repository.create_article( await repository.create_article(
url=url, url=url,
@ -69,7 +68,7 @@ class TestArticleRepository:
async def test_get_by_id(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO): async def test_get_by_id(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO):
article = sample_article_in_db article = sample_article_in_db
retrieved = await repository.get_by_id(article.id) retrieved = await repository.get_by_id(article.id)
assert retrieved is not None assert retrieved is not None
assert retrieved.id == article.id assert retrieved.id == article.id
@ -79,9 +78,11 @@ class TestArticleRepository:
result = await repository.get_by_id(99999) result = await repository.get_by_id(99999)
assert result is None assert result is None
async def test_get_by_url(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO): async def test_get_by_url(
self, repository: ArticleRepository, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
retrieved = await repository.get_by_url(article.url) retrieved = await repository.get_by_url(article.url)
assert retrieved is not None assert retrieved is not None
assert retrieved.id == article.id assert retrieved.id == article.id
@ -91,14 +92,16 @@ class TestArticleRepository:
result = await repository.get_by_url("https://ru.ruwiki.ru/wiki/NonExistent") result = await repository.get_by_url("https://ru.ruwiki.ru/wiki/NonExistent")
assert result is None assert result is None
async def test_update_article(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO): async def test_update_article(
self, repository: ArticleRepository, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
article.simplified_text = "Simplified content" article.simplified_text = "Simplified content"
article.status = ArticleStatus.SIMPLIFIED article.status = ArticleStatus.SIMPLIFIED
updated = await repository.update_article(article) updated = await repository.update_article(article)
assert updated.simplified_text == "Simplified content" assert updated.simplified_text == "Simplified content"
assert updated.status == ArticleStatus.SIMPLIFIED assert updated.status == ArticleStatus.SIMPLIFIED
assert updated.updated_at is not None assert updated.updated_at is not None
@ -112,7 +115,7 @@ class TestArticleRepository:
status=ArticleStatus.PENDING, status=ArticleStatus.PENDING,
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
) )
with pytest.raises(ValueError, match="не найдена"): with pytest.raises(ValueError, match="не найдена"):
await repository.update_article(article) await repository.update_article(article)
@ -122,20 +125,20 @@ class TestArticleRepository:
title="Test 1", title="Test 1",
raw_text="Content 1", raw_text="Content 1",
) )
article2 = await repository.create_article( article2 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2", url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2", title="Test 2",
raw_text="Content 2", raw_text="Content 2",
) )
article2.status = ArticleStatus.SIMPLIFIED article2.status = ArticleStatus.SIMPLIFIED
await repository.update_article(article2) await repository.update_article(article2)
pending = await repository.get_articles_by_status(ArticleStatus.PENDING) pending = await repository.get_articles_by_status(ArticleStatus.PENDING)
assert len(pending) == 1 assert len(pending) == 1
assert pending[0].id == article1.id assert pending[0].id == article1.id
simplified = await repository.get_articles_by_status(ArticleStatus.SIMPLIFIED) simplified = await repository.get_articles_by_status(ArticleStatus.SIMPLIFIED)
assert len(simplified) == 1 assert len(simplified) == 1
assert simplified[0].id == article2.id assert simplified[0].id == article2.id
@ -154,16 +157,18 @@ class TestArticleRepository:
title="Test 2", title="Test 2",
raw_text="Content 2", raw_text="Content 2",
) )
count = await repository.count_by_status(ArticleStatus.PENDING) count = await repository.count_by_status(ArticleStatus.PENDING)
assert count == 2 assert count == 2
async def test_delete_article(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO): async def test_delete_article(
self, repository: ArticleRepository, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
deleted = await repository.delete_article(article.id) deleted = await repository.delete_article(article.id)
assert deleted is True assert deleted is True
retrieved = await repository.get_by_id(article.id) retrieved = await repository.get_by_id(article.id)
assert retrieved is None assert retrieved is None
@ -191,20 +196,24 @@ class TestAsyncWriteQueue:
await queue.stop() await queue.stop()
assert queue._worker_task.done() assert queue._worker_task.done()
async def test_update_article_operation(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO): async def test_update_article_operation(
self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
article.simplified_text = "Updated content" article.simplified_text = "Updated content"
await write_queue.update_article(article) await write_queue.update_article(article)
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
retrieved = await write_queue.repository.get_by_id(article.id) retrieved = await write_queue.repository.get_by_id(article.id)
assert retrieved.simplified_text == "Updated content" assert retrieved.simplified_text == "Updated content"
async def test_update_from_result_success(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO): async def test_update_from_result_success(
self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
result = ProcessingResult.success_result( result = ProcessingResult.success_result(
url=article.url, url=article.url,
title=article.title, title=article.title,
@ -214,27 +223,29 @@ class TestAsyncWriteQueue:
token_count_simplified=50, token_count_simplified=50,
processing_time_seconds=1.0, processing_time_seconds=1.0,
) )
updated_article = await write_queue.update_from_result(result) updated_article = await write_queue.update_from_result(result)
assert updated_article.simplified_text == "Processed content" assert updated_article.simplified_text == "Processed content"
assert updated_article.status == ArticleStatus.SIMPLIFIED assert updated_article.status == ArticleStatus.SIMPLIFIED
async def test_update_from_result_failure(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO): async def test_update_from_result_failure(
self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO
):
article = sample_article_in_db article = sample_article_in_db
result = ProcessingResult.failure_result( result = ProcessingResult.failure_result(
url=article.url, url=article.url,
error_message="Processing failed", error_message="Processing failed",
) )
updated_article = await write_queue.update_from_result(result) updated_article = await write_queue.update_from_result(result)
assert updated_article.status == ArticleStatus.FAILED assert updated_article.status == ArticleStatus.FAILED
async def test_queue_stats(self, write_queue: AsyncWriteQueue): async def test_queue_stats(self, write_queue: AsyncWriteQueue):
stats = write_queue.stats stats = write_queue.stats
assert "total_operations" in stats assert "total_operations" in stats
assert "failed_operations" in stats assert "failed_operations" in stats
assert "success_rate" in stats assert "success_rate" in stats
@ -247,7 +258,7 @@ class TestSimplifyService:
ruwiki_adapter = AsyncMock() ruwiki_adapter = AsyncMock()
llm_adapter = AsyncMock() llm_adapter = AsyncMock()
write_queue = AsyncMock() write_queue = AsyncMock()
service = SimplifyService( service = SimplifyService(
config=test_config, config=test_config,
ruwiki_adapter=ruwiki_adapter, ruwiki_adapter=ruwiki_adapter,
@ -255,7 +266,7 @@ class TestSimplifyService:
repository=repository, repository=repository,
write_queue=write_queue, write_queue=write_queue,
) )
return service return service
async def test_get_prompt_template(self, simplify_service: SimplifyService): async def test_get_prompt_template(self, simplify_service: SimplifyService):
@ -274,28 +285,28 @@ class TestSimplifyService:
status=ArticleStatus.SIMPLIFIED, status=ArticleStatus.SIMPLIFIED,
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
) )
simplify_service.repository.get_by_url = AsyncMock(return_value=existing_article) simplify_service.repository.get_by_url = AsyncMock(return_value=existing_article)
result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test") result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test")
assert result is not None assert result is not None
assert result.success is True assert result.success is True
assert result.simplified_text == "Simplified" assert result.simplified_text == "Simplified"
async def test_check_existing_article_not_found(self, simplify_service: SimplifyService): async def test_check_existing_article_not_found(self, simplify_service: SimplifyService):
simplify_service.repository.get_by_url = AsyncMock(return_value=None) simplify_service.repository.get_by_url = AsyncMock(return_value=None)
result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test") result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test")
assert result is None assert result is None
async def test_health_check(self, simplify_service: SimplifyService): async def test_health_check(self, simplify_service: SimplifyService):
simplify_service.ruwiki_adapter.health_check = AsyncMock() simplify_service.ruwiki_adapter.health_check = AsyncMock()
simplify_service.llm_adapter.health_check = AsyncMock() simplify_service.llm_adapter.health_check = AsyncMock()
checks = await simplify_service.health_check() checks = await simplify_service.health_check()
assert "ruwiki" in checks assert "ruwiki" in checks
assert "llm" in checks assert "llm" in checks
assert "prompt_template" in checks assert "prompt_template" in checks