Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Unit tests for FastDeploy logging infrastructure.

Tests cover:
- get_logger() returns loggers with correct naming
- FD_LOG_LEVEL env var controls log level
- FastDeployLogger singleton behavior
- Console handler presence in unified logger
- Legacy get_logger(name, file) backward compatibility
"""

import logging
import os
import unittest
from unittest.mock import patch

try:
import paddle # noqa: F401

HAS_PADDLE = True
except ImportError:
HAS_PADDLE = False

SKIP_MSG = "PaddlePaddle is not installed"


@unittest.skipUnless(HAS_PADDLE, SKIP_MSG)
class TestGetLogger(unittest.TestCase):
"""Tests for the fastdeploy.logger.get_logger convenience function."""

def test_get_logger_returns_logger_instance(self):
from fastdeploy.logger import get_logger

logger = get_logger("test_module")
self.assertIsInstance(logger, logging.Logger)

def test_get_logger_prefixes_with_fastdeploy(self):
from fastdeploy.logger import get_logger

logger = get_logger("test_module")
self.assertTrue(
logger.name.startswith("fastdeploy"),
f"Expected logger name to start with 'fastdeploy', got '{logger.name}'",
)

def test_get_logger_none_returns_root_fastdeploy(self):
from fastdeploy.logger import get_logger

logger = get_logger(None)
self.assertEqual(logger.name, "fastdeploy")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug get_logger(None) 实际返回名为 "fastdeploy.main" 的 logger,而非 "fastdeploy"

原因:_get_channel_logger(None, "main") 中,当 name is None 时,返回的是 channel_logger,其名称为 f"fastdeploy.{channel}""fastdeploy.main"

建议修复:

# 修改断言以匹配实际命名
self.assertEqual(logger.name, "fastdeploy.main")


def test_get_logger_already_namespaced(self):
from fastdeploy.logger import get_logger

logger = get_logger("fastdeploy.engine")
self.assertEqual(logger.name, "fastdeploy.engine")

def test_get_logger_adds_prefix_for_plain_name(self):
from fastdeploy.logger import get_logger

logger = get_logger("scheduler")
self.assertEqual(logger.name, "fastdeploy.scheduler")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug get_logger("scheduler") 实际返回名为 "fastdeploy.main.scheduler" 的 logger,而非 "fastdeploy.scheduler"

原因:_get_channel_logger 中普通名称被拼接为 f"{channel_root_name}.{name}",即 "fastdeploy.main.scheduler"

建议修复:

# 修改断言以匹配实际命名
self.assertEqual(logger.name, "fastdeploy.main.scheduler")



@unittest.skipUnless(HAS_PADDLE, SKIP_MSG)
class TestFDLogLevel(unittest.TestCase):
"""Tests for FD_LOG_LEVEL environment variable."""

def test_fd_log_level_default_is_info(self):
with patch.dict(os.environ, {}, clear=False):
# Remove FD_LOG_LEVEL and FD_DEBUG if set
os.environ.pop("FD_LOG_LEVEL", None)
os.environ.pop("FD_DEBUG", None)
# envs uses lazy lambdas, so reading the attribute re-evaluates os.getenv
from fastdeploy import envs

level = envs.FD_LOG_LEVEL
self.assertEqual(level, "INFO")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug envs.FD_LOG_LEVEL 的默认值为 None,而非 "INFO"

原因:envs.pyFD_LOG_LEVEL 定义为 lambda: os.getenv("FD_LOG_LEVEL", None),未设置时返回 None

建议修复(二选一):

  1. 修改断言:self.assertIsNone(level)
  2. 或在测试中读取 resolve_log_level() 函数而非 envs.FD_LOG_LEVEL


def test_fd_log_level_debug_when_fd_debug_set(self):
with patch.dict(os.environ, {"FD_DEBUG": "1"}, clear=False):
os.environ.pop("FD_LOG_LEVEL", None)
from fastdeploy import envs

level = envs.FD_LOG_LEVEL
self.assertEqual(level, "DEBUG")

def test_fd_log_level_explicit_overrides_fd_debug(self):
with patch.dict(os.environ, {"FD_DEBUG": "1", "FD_LOG_LEVEL": "WARNING"}, clear=False):
from fastdeploy import envs

level = envs.FD_LOG_LEVEL
self.assertEqual(level, "WARNING")

def test_fd_log_level_accepts_error(self):
with patch.dict(os.environ, {"FD_LOG_LEVEL": "ERROR"}, clear=False):
from fastdeploy import envs

level = envs.FD_LOG_LEVEL
self.assertEqual(level, "ERROR")


@unittest.skipUnless(HAS_PADDLE, SKIP_MSG)
class TestFastDeployLoggerSingleton(unittest.TestCase):
"""Tests for FastDeployLogger singleton pattern."""

def test_singleton_returns_same_instance(self):
from fastdeploy.logger import FastDeployLogger

instance1 = FastDeployLogger()
instance2 = FastDeployLogger()
self.assertIs(instance1, instance2)


@unittest.skipUnless(HAS_PADDLE, SKIP_MSG)
class TestConsoleHandler(unittest.TestCase):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug setup_logging() 无返回值(返回 None),对 None 调用 .handlers 将抛出 AttributeError: 'NoneType' object has no attribute 'handlers'

原因:setup_logging 函数本身不创建或返回 logger 对象,仅做目录初始化和可选的 dictConfig。

建议修复:

from fastdeploy.logger.setup_logging import setup_logging
from fastdeploy.logger.logger import FastDeployLogger

setup_logging()
fd_logger = FastDeployLogger()._get_channel_logger(None, "console")
handler_classes = [type(h).__name__ for h in fd_logger.handlers]

"""Tests for console handler presence in unified logger setup."""

def setUp(self):
"""Reset setup_logging state for clean test isolation."""
from fastdeploy.logger.setup_logging import setup_logging

setup_logging._configured = False

def test_unified_logger_has_console_handler(self):
from fastdeploy.logger.setup_logging import setup_logging

fd_logger = setup_logging()

handler_classes = [type(h).__name__ for h in fd_logger.handlers]
self.assertTrue(
any("StreamHandler" in cls for cls in handler_classes),
f"Expected a StreamHandler (console) among handlers, got: {handler_classes}",
)

def tearDown(self):
"""Reset setup_logging state after test."""
from fastdeploy.logger.setup_logging import setup_logging

setup_logging._configured = False


@unittest.skipUnless(HAS_PADDLE, SKIP_MSG)
class TestLegacyGetLogger(unittest.TestCase):
"""Tests for backward compatibility with legacy get_logger(name, file)."""

def test_legacy_get_logger_still_works(self):
from fastdeploy.utils import get_logger

logger = get_logger("test_legacy", "test_legacy.log")
self.assertIsInstance(logger, logging.Logger)
# Legacy loggers use "legacy." prefix namespace
self.assertTrue(
logger.name.startswith("legacy."),
f"Expected legacy logger to have 'legacy.' prefix, got '{logger.name}'",
)

def test_legacy_prebuilt_loggers_accessible(self):
from fastdeploy.utils import llm_logger, scheduler_logger

self.assertIsInstance(llm_logger, logging.Logger)
self.assertIsInstance(scheduler_logger, logging.Logger)


if __name__ == "__main__":
unittest.main()
Loading