From e90c7ab8a7c18f13219576761b339f6413ec6459 Mon Sep 17 00:00:00 2001 From: Mr_xie <1084586616@qq.com> Date: Thu, 25 Jun 2026 17:48:43 +0800 Subject: [PATCH] refactor(tests): replace logger mocks with caplog (#37468) (#37922) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/workspace/test_workspace.py | 15 +++--- .../apps/advanced_chat/test_app_generator.py | 44 ++++++++--------- .../test_agent_chat_app_generator.py | 25 +++++----- .../core/app/apps/test_base_app_runner.py | 47 ++++++++++--------- .../test_generate_task_pipeline_core.py | 15 +++--- .../test_entities_provider_configuration.py | 8 ++-- .../test_plugin_auto_upgrade_service.py | 18 ++++--- ...est_clear_free_plan_tenant_expired_logs.py | 14 +++--- .../tasks/test_dataset_indexing_task.py | 18 +++++-- 9 files changed, 117 insertions(+), 87 deletions(-) diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py index 4663d503e3d..47e9f51fb27 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py @@ -1,4 +1,5 @@ import inspect +import logging from io import BytesIO from unittest.mock import MagicMock, patch @@ -151,7 +152,9 @@ class TestTenantListApi: get_plan_bulk_mock.assert_called_once_with(["t1", "t2"]) get_features_mock.assert_called_once_with("t2", exclude_vector_space=True) - def test_get_saas_path_falls_back_to_legacy_feature_path_on_bulk_error(self, app: Flask): + def test_get_saas_path_falls_back_to_legacy_feature_path_on_bulk_error( + self, app: Flask, caplog: pytest.LogCaptureFixture + ): """Test fallback to FeatureService when bulk billing returns empty result. BillingService.get_plan_bulk catches exceptions internally and returns empty dict, @@ -170,6 +173,7 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), + caplog.at_level(logging.WARNING, logger="controllers.console.workspace.workspace"), patch( "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", return_value=[(tenant1, make_membership()), (tenant2, make_membership())], @@ -185,7 +189,6 @@ class TestTenantListApi: "controllers.console.workspace.workspace.FeatureService.get_features", return_value=features, ) as get_features_mock, - patch("controllers.console.workspace.workspace.logger.warning") as logger_warning_mock, ): result, status = method(api, "t2", user) @@ -194,7 +197,7 @@ class TestTenantListApi: assert result["workspaces"][1]["plan"] == CloudPlan.TEAM get_plan_bulk_mock.assert_called_once_with(["t1", "t2"]) assert get_features_mock.call_count == 2 - logger_warning_mock.assert_called_once() + assert "get_plan_bulk returned empty result, falling back to legacy feature path" in caplog.messages def test_get_billing_disabled_community_path(self, app: Flask): api = TenantListApi() @@ -365,7 +368,7 @@ class TestTenantApi: with pytest.raises(Unauthorized): method(api, user) - def test_post_info_path(self, app: Flask): + def test_post_info_path(self, app: Flask, caplog: pytest.LogCaptureFixture): api = TenantApi() method = inspect.unwrap(api.post) @@ -374,15 +377,15 @@ class TestTenantApi: with ( app.test_request_context("/info"), + caplog.at_level(logging.WARNING, logger="controllers.console.workspace.workspace"), patch( "controllers.console.workspace.workspace.WorkspaceService.get_tenant_info", return_value={"id": "t1"}, ), - patch("controllers.console.workspace.workspace.logger.warning") as warn_mock, ): result, status = method(api, user) - warn_mock.assert_called_once() + assert "Deprecated URL /info was used." in caplog.messages assert status == 200 diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py index b644af083e5..cda5178e30c 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from contextlib import contextmanager from types import SimpleNamespace from unittest.mock import MagicMock @@ -961,7 +962,9 @@ class TestAdvancedChatAppGeneratorInternals: stream=False, ) - def test_handle_response_re_raises_value_error(self, monkeypatch: pytest.MonkeyPatch): + def test_handle_response_re_raises_value_error( + self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 app_config = self._build_app_config() @@ -986,29 +989,28 @@ class TestAdvancedChatAppGeneratorInternals: def process(self): raise ValueError("other error") - logger_exception = MagicMock() - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.logger.exception", logger_exception) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.AdvancedChatAppGenerateTaskPipeline", _Pipeline) - with pytest.raises(ValueError, match="other error"): - generator._handle_advanced_chat_response( - application_generate_entity=application_generate_entity, - workflow=WorkflowSnapshot(id="wf", tenant_id="tenant", features_dict={}), - queue_manager=SimpleNamespace(), - conversation=ConversationSnapshot(id="conv", mode=AppMode.ADVANCED_CHAT), - message=MessageSnapshot( - id="msg", - query="hello", - created_at=naive_utc_now(), - status=MessageStatus.NORMAL, - answer="", - ), - user=SimpleNamespace(), - draft_var_saver_factory=lambda **kwargs: None, - stream=False, - ) + with caplog.at_level(logging.ERROR, logger="core.app.apps.advanced_chat.app_generator"): + with pytest.raises(ValueError, match="other error"): + generator._handle_advanced_chat_response( + application_generate_entity=application_generate_entity, + workflow=WorkflowSnapshot(id="wf", tenant_id="tenant", features_dict={}), + queue_manager=SimpleNamespace(), + conversation=ConversationSnapshot(id="conv", mode=AppMode.ADVANCED_CHAT), + message=MessageSnapshot( + id="msg", + query="hello", + created_at=naive_utc_now(), + status=MessageStatus.NORMAL, + answer="", + ), + user=SimpleNamespace(), + draft_var_saver_factory=lambda **kwargs: None, + stream=False, + ) - logger_exception.assert_called_once() + assert "Failed to process generate task pipeline, conversation_id: conv" in caplog.messages def test_generate_worker_handles_invoke_auth_error(self, monkeypatch: pytest.MonkeyPatch): generator = AdvancedChatAppGenerator() diff --git a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py index 9ed7acd39cf..467d404b7db 100644 --- a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py @@ -1,4 +1,5 @@ import contextlib +import logging import pytest from pydantic import ValidationError @@ -274,7 +275,9 @@ class TestAgentChatAppGeneratorWorker: assert queue_manager.publish_error.called - def test_generate_worker_logs_value_error_when_debug(self, generator, mocker: MockerFixture): + def test_generate_worker_logs_value_error_when_debug( + self, generator, mocker: MockerFixture, caplog: pytest.LogCaptureFixture + ): queue_manager = mocker.MagicMock() generator._get_conversation = mocker.MagicMock(return_value=mocker.MagicMock()) generator._get_message = mocker.MagicMock(return_value=mocker.MagicMock()) @@ -285,15 +288,15 @@ class TestAgentChatAppGeneratorWorker: mocker.patch("core.app.apps.agent_chat.app_generator.db.session.close") mocker.patch("core.app.apps.agent_chat.app_generator.dify_config", new=mocker.MagicMock(DEBUG=True)) - logger = mocker.patch("core.app.apps.agent_chat.app_generator.logger") - generator._generate_worker( - flask_app=mocker.MagicMock(), - context=mocker.MagicMock(), - application_generate_entity=mocker.MagicMock(), - queue_manager=queue_manager, - conversation_id="conv", - message_id="msg", - ) + with caplog.at_level(logging.ERROR, logger="core.app.apps.agent_chat.app_generator"): + generator._generate_worker( + flask_app=mocker.MagicMock(), + context=mocker.MagicMock(), + application_generate_entity=mocker.MagicMock(), + queue_manager=queue_manager, + conversation_id="conv", + message_id="msg", + ) - logger.exception.assert_called_once() + assert "Error when generating" in caplog.messages diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_runner.py b/api/tests/unit_tests/core/app/apps/test_base_app_runner.py index cd1e5babf88..1b22412ba6b 100644 --- a/api/tests/unit_tests/core/app/apps/test_base_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/test_base_app_runner.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from types import SimpleNamespace from unittest.mock import MagicMock @@ -263,11 +264,11 @@ class TestAppRunner: files=[], ) - def test_handle_invoke_result_stream_routes_chunks_and_builds_message(self, monkeypatch: pytest.MonkeyPatch): + def test_handle_invoke_result_stream_routes_chunks_and_builds_message( + self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + ): runner = AppRunner() queue = _QueueRecorder() - warning_logger = MagicMock() - monkeypatch.setattr("core.app.apps.base_app_runner._logger.warning", warning_logger) image_content = ImagePromptMessageContent( url="https://example.com/image.png", format="png", mime_type="image/png" @@ -290,23 +291,24 @@ class TestAppRunner: ), ) - runner._handle_invoke_result( - invoke_result=_stream(), - queue_manager=queue, - stream=True, - agent=False, - ) + with caplog.at_level(logging.WARNING, logger="core.app.apps.base_app_runner"): + runner._handle_invoke_result( + invoke_result=_stream(), + queue_manager=queue, + stream=True, + agent=False, + ) assert isinstance(queue.events[0], QueueLLMChunkEvent) assert isinstance(queue.events[-1], QueueMessageEndEvent) assert queue.events[-1].llm_result.message.content == "abc" - warning_logger.assert_called_once() + assert "Received multimodal output but missing required parameters" in caplog.messages - def test_handle_invoke_result_stream_agent_mode_handles_multimodal_errors(self, monkeypatch: pytest.MonkeyPatch): + def test_handle_invoke_result_stream_agent_mode_handles_multimodal_errors( + self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + ): runner = AppRunner() queue = _QueueRecorder() - exception_logger = MagicMock() - monkeypatch.setattr("core.app.apps.base_app_runner._logger.exception", exception_logger) monkeypatch.setattr( runner, @@ -335,19 +337,20 @@ class TestAppRunner: ), ) - runner._handle_invoke_result_stream( - invoke_result=_stream(), - queue_manager=queue, - agent=True, - message_id="message-id", - user_id="user-id", - tenant_id="tenant-id", - ) + with caplog.at_level(logging.ERROR, logger="core.app.apps.base_app_runner"): + runner._handle_invoke_result_stream( + invoke_result=_stream(), + queue_manager=queue, + agent=True, + message_id="message-id", + user_id="user-id", + tenant_id="tenant-id", + ) assert isinstance(queue.events[0], QueueAgentMessageEvent) assert isinstance(queue.events[-1], QueueMessageEndEvent) assert queue.events[-1].llm_result.usage == usage - exception_logger.assert_called_once() + assert "Failed to handle multimodal image output" in caplog.messages def test_handle_invoke_result_stream_closes_generator_when_stopped(self): runner = AppRunner() diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py index 9a04014d620..04fe7a2ebed 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from contextlib import contextmanager from types import SimpleNamespace from unittest.mock import MagicMock @@ -639,7 +640,9 @@ class TestWorkflowGenerateTaskPipeline: assert sleep_spy assert any(isinstance(item, MessageAudioEndStreamResponse) for item in responses) - def test_wrapper_process_stream_response_handles_audio_exception(self, monkeypatch: pytest.MonkeyPatch): + def test_wrapper_process_stream_response_handles_audio_exception( + self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + ): pipeline = _make_pipeline() pipeline._workflow_features_dict = { "text_to_speech": {"enabled": True, "autoPlay": "enabled", "voice": "v", "language": "en"} @@ -659,20 +662,16 @@ class TestWorkflowGenerateTaskPipeline: def publish(self, message): _ = message - logger_exception = [] monkeypatch.setattr("core.app.apps.workflow.generate_task_pipeline.time.time", lambda: 0.0) - monkeypatch.setattr( - "core.app.apps.workflow.generate_task_pipeline.logger.exception", - lambda *args, **kwargs: logger_exception.append((args, kwargs)), - ) monkeypatch.setattr( "core.app.apps.workflow.generate_task_pipeline.AppGeneratorTTSPublisher", _Publisher, ) - responses = list(pipeline._wrapper_process_stream_response()) + with caplog.at_level(logging.ERROR, logger="core.app.apps.workflow.generate_task_pipeline"): + responses = list(pipeline._wrapper_process_stream_response()) - assert logger_exception + assert "Fails to get audio trunk, task_id: task" in caplog.messages assert any(isinstance(item, MessageAudioEndStreamResponse) for item in responses) def test_database_session_rolls_back_on_error(self, monkeypatch: pytest.MonkeyPatch): diff --git a/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py b/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py index dbc8f4969dc..bb473739c87 100644 --- a/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py +++ b/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py @@ -2042,7 +2042,9 @@ def test_get_custom_provider_models_skips_schema_models_with_mismatched_type() - assert all(model.model != "embed-model" for model in models) -def test_get_custom_provider_models_skips_custom_models_on_schema_error_or_none() -> None: +def test_get_custom_provider_models_skips_custom_models_on_schema_error_or_none( + caplog: pytest.LogCaptureFixture, +) -> None: configuration = _build_provider_configuration() configuration.custom_configuration.models = [ CustomModelConfiguration(model="error-custom", model_type=ModelType.LLM, credentials={"k": "v"}), @@ -2064,7 +2066,7 @@ def test_get_custom_provider_models_skips_custom_models_on_schema_error_or_none( return None return _build_ai_model(model) - with patch("core.entities.provider_configuration.logger.warning") as mock_warning: + with caplog.at_level(logging.WARNING, logger="core.entities.provider_configuration"): with patch.object(ProviderConfiguration, "get_model_schema", side_effect=_schema): models = configuration._get_custom_provider_models( model_types=[ModelType.LLM], @@ -2072,6 +2074,6 @@ def test_get_custom_provider_models_skips_custom_models_on_schema_error_or_none( model_setting_map={}, ) - assert mock_warning.call_count == 1 + assert "get custom model schema failed, boom" in caplog.messages assert any(model.model == "ok-custom" for model in models) assert all(model.model != "none-custom" for model in models) diff --git a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py index 88d5853a78b..e284c98079d 100644 --- a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py +++ b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py @@ -1,6 +1,9 @@ +import logging from types import SimpleNamespace from unittest.mock import MagicMock, patch +import pytest + from models.account import TenantPluginAutoUpgradeStrategy MODULE = "services.plugin.plugin_auto_upgrade_service" @@ -227,7 +230,7 @@ class TestBackfillStrategyCategories: assert default_time % (15 * 60) == 0 assert 0 <= default_time < 24 * 60 * 60 - def test_creates_missing_categories_and_splits_known_plugins(self): + def test_creates_missing_categories_and_splits_known_plugins(self, caplog: pytest.LogCaptureFixture): p1, session = _patched_session() tool_strategy = SimpleNamespace( category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL, @@ -260,7 +263,11 @@ class TestBackfillStrategyCategories: installer = MagicMock() installer.list_plugins.return_value = installed_plugins - with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer), patch(f"{MODULE}.logger") as logger: + with ( + p1, + patch(f"{MODULE}.PluginInstaller", return_value=installer), + caplog.at_level(logging.WARNING, logger=MODULE), + ): from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService result = PluginAutoUpgradeService.backfill_strategy_categories("t1") @@ -272,10 +279,7 @@ class TestBackfillStrategyCategories: assert tool_strategy.include_plugins == ["tool-plugin"] assert model_strategy.exclude_plugins == ["model-plugin"] assert model_strategy.include_plugins == ["model-plugin"] - logger.warning.assert_called_once_with( + assert ( "Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: " - "tenant_id=%s, field=%s, plugin_ids=%s", - "t1", - "exclude_plugins", - ["unknown-plugin"], + "tenant_id=t1, field=exclude_plugins, plugin_ids=['unknown-plugin']" in caplog.messages ) diff --git a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py index 6c54e9c5725..5d0a93e239d 100644 --- a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py +++ b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py @@ -1,4 +1,5 @@ import datetime +import logging from types import SimpleNamespace from unittest.mock import MagicMock, Mock, patch @@ -347,7 +348,9 @@ def test_serialize_record_falls_back_to_table_columns() -> None: } -def test_process_with_tenant_ids_filters_by_plan_and_logs_errors(monkeypatch: pytest.MonkeyPatch) -> None: +def test_process_with_tenant_ids_filters_by_plan_and_logs_errors( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: monkeypatch.setattr(service_module, "db", SimpleNamespace(engine=object())) # Total tenant count query @@ -381,14 +384,13 @@ def test_process_with_tenant_ids_filters_by_plan_and_logs_errors(monkeypatch: py process_tenant_mock = MagicMock(side_effect=lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("err"))) monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", process_tenant_mock) - logger_exc = MagicMock() - monkeypatch.setattr(service_module.logger, "exception", logger_exc) - - ClearFreePlanTenantExpiredLogs.process(days=7, batch=10, tenant_ids=["t_sandbox", "t_paid", "t_fail"]) + with caplog.at_level(logging.ERROR, logger=service_module.logger.name): + ClearFreePlanTenantExpiredLogs.process(days=7, batch=10, tenant_ids=["t_sandbox", "t_paid", "t_fail"]) # Only sandbox tenant should attempt processing, and its failure should be swallowed + logged. assert process_tenant_mock.call_count == 1 - assert logger_exc.call_count >= 1 + assert "Failed to process tenant t_sandbox" in caplog.messages + assert "Failed to process tenant t_fail" in caplog.messages def test_process_without_tenant_ids_batches_and_scales_interval(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py index b74079bd690..69b7a156ca8 100644 --- a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py +++ b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py @@ -9,6 +9,7 @@ This module tests the document indexing task functionality including: - Task cancellation and cleanup """ +import logging import uuid from contextlib import nullcontext from types import SimpleNamespace @@ -758,7 +759,15 @@ class TestErrorHandling: assert mock_db_session.close.called def test_tenant_queue_error_handling_still_processes_next_task( - self, tenant_id, dataset_id, document_ids, mock_redis, mock_db_session, mock_dataset, mock_indexing_runner + self, + tenant_id, + dataset_id, + document_ids, + mock_redis, + mock_db_session, + mock_dataset, + mock_indexing_runner, + caplog: pytest.LogCaptureFixture, ): """ Test that errors don't prevent processing next task in tenant queue. @@ -778,14 +787,17 @@ class TestErrorHandling: with patch("tasks.document_indexing_task._document_indexing") as mock_indexing: mock_indexing.side_effect = Exception("Processing failed") - # Patch logger to avoid format string issue in actual code - with patch("tasks.document_indexing_task.logger"): + with caplog.at_level(logging.ERROR, logger="tasks.document_indexing_task"): with patch("tasks.document_indexing_task.normal_document_indexing_task") as mock_task: # Act _document_indexing_with_tenant_queue(tenant_id, dataset_id, document_ids, mock_task) # Assert - Next task should still be enqueued despite error mock_task.apply_async.assert_called() + assert ( + f"Error processing document indexing {dataset_id} for tenant {tenant_id}: {document_ids}" + in caplog.messages + ) def test_concurrent_task_limit_respected( self, tenant_id, dataset_id, document_ids, mock_redis, mock_db_session, mock_dataset