diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md index e2c5484..4d74784 100644 --- a/.github/instructions/python.instructions.md +++ b/.github/instructions/python.instructions.md @@ -244,7 +244,9 @@ if response is None: ### Code Validation -- **Linting**: Use editor's Python extensions instead of command-line tools +- **Install Ruff**: Before making changes, install ruff linter/formatter: `pip3 install ruff` +- **Linting**: Run `ruff check --fix --unsafe-fixes .` before each commit to fix linting issues +- **Formatting**: Run `ruff format .` before each commit to format code consistently - **Type Checking**: Ensure Pylance compatibility - **Testing**: Run tests before committing changes - **Documentation**: Update relevant documentation when making changes diff --git a/ddns/config/__init__.py b/ddns/config/__init__.py index 8cfe00c..8d9e866 100644 --- a/ddns/config/__init__.py +++ b/ddns/config/__init__.py @@ -63,6 +63,13 @@ def _setup_logging(cli_config, env_config, all_json_configs): log_format = "%(asctime)s %(levelname)s: %(message)s" else: log_format = "%(asctime)s %(levelname)s [%(name)s]: %(message)s" + + # Create parent directory for log file if it doesn't exist + if global_conf.log_file: + log_dir = os.path.dirname(global_conf.log_file) + if log_dir and not os.path.exists(log_dir): + os.makedirs(log_dir) + logging.basicConfig( level=global_conf.log_level, format=log_format, datefmt=global_conf.log_datefmt, filename=global_conf.log_file ) diff --git a/tests/test_config_log_file_dir.py b/tests/test_config_log_file_dir.py new file mode 100644 index 0000000..1b3887d --- /dev/null +++ b/tests/test_config_log_file_dir.py @@ -0,0 +1,148 @@ +# coding=utf-8 +# type: ignore[index,operator,assignment] +""" +Unit tests for log file directory creation issue +Reproduces the issue where log_file path has non-existent parent directories +@author: GitHub Copilot +""" + +from __init__ import unittest +import tempfile +import json +import os +import sys +import shutil +from ddns.config import load_configs + + +class TestLogFileDirectory(unittest.TestCase): + """测试日志文件目录创建问题""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.original_argv = sys.argv[:] + + def tearDown(self): + sys.argv = self.original_argv + # Clean up logging handlers + import logging + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + # Clean up temp directory + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_log_file_with_nonexistent_directory_single_config(self): + """测试单个配置时,log文件所在目录不存在的情况""" + # 创建一个不存在的目录路径 + log_dir = os.path.join(self.temp_dir, "nonexistent", "subdir") + log_file = os.path.join(log_dir, "ddns.log") + + # 确保目录不存在 + self.assertFalse(os.path.exists(log_dir)) + + config_data = { + "dns": "debug", + "id": "test@example.com", + "token": "secret123", + "ipv4": ["test.example.com"], + "log": {"file": log_file, "level": "INFO"}, + } + + config_path = os.path.join(self.temp_dir, "config.json") + with open(config_path, "w") as f: + json.dump(config_data, f) + + sys.argv = ["ddns", "-c", config_path] + + # 加载配置,应该自动创建目录 + configs = load_configs("test", "1.0", "2023-01-01") + + # 验证目录被创建 + self.assertTrue(os.path.exists(log_dir), "Log directory should be created") + self.assertEqual(len(configs), 1) + self.assertEqual(configs[0].log_file, log_file) + + def test_log_file_with_nonexistent_directory_multi_provider(self): + """测试多个provider配置时,log文件所在目录不存在的情况(复现issue中的问题)""" + # 创建一个不存在的目录路径 + log_dir = os.path.join(self.temp_dir, "ddns") + log_file = os.path.join(log_dir, "ddns.log") + + # 确保目录不存在 + self.assertFalse(os.path.exists(log_dir)) + + # 模拟issue中的配置:多个provider,共享同一个log文件 + config_data = { + "$schema": "https://ddns.newfuture.cc/schema/v4.1.json", + "ssl": "auto", + "cache": os.path.join(self.temp_dir, "cache"), + "log": {"level": "INFO", "file": log_file}, + "index4": "default", + "index6": "default", + "providers": [ + {"provider": "debug", "token": "token1", "ipv6": ["test1.xyz"]}, + {"provider": "debug", "token": "token2", "ipv6": ["test2.org"]}, + ], + } + + config_path = os.path.join(self.temp_dir, "multi_config.json") + with open(config_path, "w") as f: + json.dump(config_data, f) + + sys.argv = ["ddns", "-c", config_path] + + # 加载配置,应该自动创建目录 + configs = load_configs("test", "1.0", "2023-01-01") + + # 验证目录被创建 + self.assertTrue(os.path.exists(log_dir), "Log directory should be created") + self.assertEqual(len(configs), 2) + + # 验证两个配置都使用同一个log文件 + for config in configs: + self.assertEqual(config.log_file, log_file) + + def test_log_file_with_nested_nonexistent_directory(self): + """测试log文件路径有多级不存在的目录""" + # 创建多级不存在的目录路径 + log_file = os.path.join(self.temp_dir, "a", "b", "c", "d", "ddns.log") + + config_data = {"dns": "debug", "id": "test@example.com", "token": "secret123", "log": {"file": log_file}} + + config_path = os.path.join(self.temp_dir, "config.json") + with open(config_path, "w") as f: + json.dump(config_data, f) + + sys.argv = ["ddns", "-c", config_path] + + # 加载配置,应该自动创建所有父目录 + configs = load_configs("test", "1.0", "2023-01-01") + + # 验证所有父目录被创建 + self.assertTrue(os.path.exists(os.path.dirname(log_file)), "All parent directories should be created") + self.assertEqual(configs[0].log_file, log_file) + + def test_log_file_with_existing_directory(self): + """测试log文件所在目录已存在的情况(不应该出错)""" + # 创建目录 + log_dir = os.path.join(self.temp_dir, "existing_dir") + os.makedirs(log_dir) + log_file = os.path.join(log_dir, "ddns.log") + + config_data = {"dns": "debug", "id": "test@example.com", "token": "secret123", "log": {"file": log_file}} + + config_path = os.path.join(self.temp_dir, "config.json") + with open(config_path, "w") as f: + json.dump(config_data, f) + + sys.argv = ["ddns", "-c", config_path] + + # 加载配置,应该正常工作 + configs = load_configs("test", "1.0", "2023-01-01") + + self.assertTrue(os.path.exists(log_dir)) + self.assertEqual(configs[0].log_file, log_file) + + +if __name__ == "__main__": + unittest.main()