import unittest import os import shutil import logging from typing import List # 调整导入路径以适应测试文件在 tests/ 目录下的情况 # 我们假设 tests/ 和 ddms_compliance_suite/ 在同一级别 (项目根目录下) import sys # 获取当前文件 (test_test_case_registry.py) 的目录 (tests/) current_file_dir = os.path.dirname(os.path.abspath(__file__)) # 获取项目根目录 (tests/ 的上一级) project_root = os.path.dirname(current_file_dir) # 将项目根目录添加到 sys.path 中,以便可以找到 ddms_compliance_suite 包 if project_root not in sys.path: sys.path.insert(0, project_root) from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity from ddms_compliance_suite.test_case_registry import TestCaseRegistry # 为了测试,我们需要一个临时的测试用例目录 TEMP_TEST_CASES_DIR = os.path.join(current_file_dir, "temp_custom_testcases_for_registry_test") # 禁用 TestCaseRegistry 和 BaseAPITestCase 在测试期间的 INFO 和 DEBUG 日志,除非特意捕获 # logging.getLogger("ddms_compliance_suite.test_case_registry").setLevel(logging.WARNING) # logging.getLogger("testcase").setLevel(logging.WARNING) # BaseAPITestCase uses testcase. class TestTestCaseRegistry(unittest.TestCase): def setUp(self): """在每个测试方法运行前创建临时测试用例目录。""" if os.path.exists(TEMP_TEST_CASES_DIR): shutil.rmtree(TEMP_TEST_CASES_DIR) os.makedirs(TEMP_TEST_CASES_DIR) self.registry = None # 确保每个测试都重新初始化registry def tearDown(self): """在每个测试方法运行后清理临时测试用例目录。""" if os.path.exists(TEMP_TEST_CASES_DIR): shutil.rmtree(TEMP_TEST_CASES_DIR) def _create_test_case_file(self, filename: str, content: str): """辅助方法,在临时目录中创建测试用例文件。""" with open(os.path.join(TEMP_TEST_CASES_DIR, filename), "w", encoding="utf-8") as f: f.write(content) def test_init_with_non_existent_dir(self): """测试使用不存在的目录初始化 TestCaseRegistry。""" non_existent_dir = os.path.join(TEMP_TEST_CASES_DIR, "_i_do_not_exist_") with self.assertLogs(level='WARNING') as log_watcher: registry = TestCaseRegistry(test_cases_dir=non_existent_dir) self.assertTrue(any(f"测试用例目录不存在或不是一个目录: {non_existent_dir}" in msg for msg in log_watcher.output)) self.assertEqual(len(registry.get_all_test_case_classes()), 0) def test_init_with_empty_dir(self): """测试使用空的目录初始化 TestCaseRegistry。""" registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) self.assertEqual(len(registry.get_all_test_case_classes()), 0) # 应该有一条INFO日志表明发现完成且数量为0 def test_discover_single_valid_test_case(self): """测试发现单个有效的测试用例。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class MyTest(BaseAPITestCase): id = "TC-001" name = "Test Case 1" description = "Desc 1" severity = TestSeverity.HIGH tags = ["tag1"] """ self._create_test_case_file("test_001.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) all_cases = registry.get_all_test_case_classes() self.assertEqual(len(all_cases), 1) self.assertEqual(all_cases[0].id, "TC-001") self.assertIsNotNone(registry.get_test_case_by_id("TC-001")) def test_discover_multiple_test_cases_in_one_file(self): """测试在单个文件中发现多个测试用例。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class TestA(BaseAPITestCase): id = "TC-A" name = "A" class TestB(BaseAPITestCase): id = "TC-B" name = "B" """ self._create_test_case_file("test_ab.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) all_cases = registry.get_all_test_case_classes() self.assertEqual(len(all_cases), 2) self.assertIsNotNone(registry.get_test_case_by_id("TC-A")) self.assertIsNotNone(registry.get_test_case_by_id("TC-B")) # 确保顺序与文件中定义的顺序(或至少是可预测的)一致,inspect.getmembers 通常按字母顺序 ids = sorted([case.id for case in all_cases]) self.assertEqual(ids, ["TC-A", "TC-B"]) def test_discover_test_cases_in_multiple_files(self): """测试在多个文件中发现测试用例。""" content1 = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class Test1(BaseAPITestCase): id = "TC-1" """ content2 = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class Test2(BaseAPITestCase): id = "TC-2" """ self._create_test_case_file("file1.py", content1) self._create_test_case_file("file2.py", content2) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) all_cases = registry.get_all_test_case_classes() self.assertEqual(len(all_cases), 2) self.assertIsNotNone(registry.get_test_case_by_id("TC-1")) self.assertIsNotNone(registry.get_test_case_by_id("TC-2")) def test_ignore_non_py_files_and_dunder_files(self): """测试忽略非.py文件和以__开头的文件。""" self._create_test_case_file("not_a_test.txt", "text content") self._create_test_case_file("__init__.py", "# I am an init file") content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class RealTest(BaseAPITestCase): id = "TC-REAL" """ self._create_test_case_file("real_test.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) all_cases = registry.get_all_test_case_classes() self.assertEqual(len(all_cases), 1) self.assertEqual(all_cases[0].id, "TC-REAL") def test_handle_import_error_in_test_file(self): """测试处理测试用例文件中的导入错误。""" content = "import non_existent_module\n" # This will cause ImportError self._create_test_case_file("importerror_test.py", content) with self.assertLogs(level='ERROR') as log_watcher: registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) self.assertTrue(any("导入模块 'importerror_test' 从" in msg and "失败" in msg for msg in log_watcher.output)) self.assertEqual(len(registry.get_all_test_case_classes()), 0) def test_handle_attribute_error_missing_id(self): """测试处理测试用例类缺少 'id' 属性的情况。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class MissingIdTest(BaseAPITestCase): name = "Missing ID" # id is missing """ self._create_test_case_file("missing_id.py", content) # AttributeError is caught by the generic Exception in discover_test_cases if not directly handled # It might also depend on when/how inspect.getmembers tries to access obj.id # Forcing access here to ensure the test scenario is valid if discovery itself doesn't raise it immediately with self.assertLogs(level='ERROR') as log_watcher: registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) # The error log in discover_test_cases for AttributeError on obj.id might be tricky to assert precisely # We'll check that no test cases were loaded from this problematic file, but other files might load. self.assertTrue(any("在模块 'missing_id'" in msg and "查找测试用例时出错" in msg for msg in log_watcher.output), msg=f"Did not find expected error log. Logs: {log_watcher.output}") # Ensure no test cases are registered if the only file has this error self.assertEqual(len(registry.get_all_test_case_classes()), 0) def test_duplicate_test_case_id(self): """测试发现重复的测试用例 ID。""" content1 = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class TestOne(BaseAPITestCase): id = "DUPLICATE-ID-001" name = "First with ID" """ content2 = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class TestTwo(BaseAPITestCase): id = "DUPLICATE-ID-001" # Same ID name = "Second with ID" """ self._create_test_case_file("file_one.py", content1) self._create_test_case_file("file_two.py", content2) with self.assertLogs(level='WARNING') as log_watcher: registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) self.assertTrue(any("发现重复的测试用例 ID: 'DUPLICATE-ID-001'" in msg for msg in log_watcher.output)) all_cases = registry.get_all_test_case_classes() # inspect.getmembers order is not guaranteed across files, so we can't be sure which one is kept. # However, the _registry (by ID) should have only one entry. self.assertEqual(len(registry._registry), 1) # The _test_case_classes list might have two if they are distinct class objects, but one overrides in _registry # Depending on load order, one will overwrite the other in _registry. Let's check the final one. registered_case = registry.get_test_case_by_id("DUPLICATE-ID-001") self.assertIsNotNone(registered_case) # We cannot reliably assert which name ('First with ID' or 'Second with ID') is kept due to file load order. # Check that _test_case_classes might have more if classes are distinct but _registry has one. # If the class objects are truly distinct, len(all_cases) could be 2. The important part is that by ID, only one is retrievable. # A more robust check for `_test_case_classes` would be to ensure it contains the class that `_registry` points to. self.assertIn(registered_case, all_cases) # If the goal is that _test_case_classes should also be unique by some criteria after discovery, that logic would need adjustment. # For now, get_all_test_case_classes returns all *discovered* classes that are BaseAPITestCase subclasses. # And get_test_case_by_id returns the one that won the ID race. # A typical use case iterates get_all_test_case_classes() for filtering, so this list should ideally be clean or documented. # For now, we accept it might contain classes whose IDs were superseded if they are distinct objects. def test_get_applicable_test_cases_no_restrictions(self): """测试 get_applicable_test_cases,当测试用例没有适用性限制时。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class NoRestrictionTest(BaseAPITestCase): id = "TC-NR-001" name = "No Restrictions" """ self._create_test_case_file("no_restriction.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) applicable = registry.get_applicable_test_cases("GET", "/api/items") self.assertEqual(len(applicable), 1) self.assertEqual(applicable[0].id, "TC-NR-001") def test_get_applicable_by_method(self): """测试根据 applicable_methods 进行筛选。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class GetOnlyTest(BaseAPITestCase): id = "TC-GET-ONLY" name = "GET Only" applicable_methods = ["GET", "HEAD"] class PostOnlyTest(BaseAPITestCase): id = "TC-POST-ONLY" name = "POST Only" applicable_methods = ["POST"] """ self._create_test_case_file("method_tests.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) applicable_get = registry.get_applicable_test_cases("GET", "/api/data") self.assertEqual(len(applicable_get), 1) self.assertEqual(applicable_get[0].id, "TC-GET-ONLY") applicable_post = registry.get_applicable_test_cases("POST", "/api/data") self.assertEqual(len(applicable_post), 1) self.assertEqual(applicable_post[0].id, "TC-POST-ONLY") applicable_put = registry.get_applicable_test_cases("PUT", "/api/data") self.assertEqual(len(applicable_put), 0) applicable_head = registry.get_applicable_test_cases("HEAD", "/api/data") # Case sensitive check for methods in list self.assertEqual(len(applicable_head), 1) self.assertEqual(applicable_head[0].id, "TC-GET-ONLY") def test_get_applicable_by_path_regex(self): """测试根据 applicable_paths_regex 进行筛选。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class UserPathTest(BaseAPITestCase): id = "TC-USER-PATH" name = "User Path" applicable_paths_regex = r"^/api/users/\\d+$" # Matches /api/users/ class OrderPathTest(BaseAPITestCase): id = "TC-ORDER-PATH" name = "Order Path" applicable_paths_regex = r"^/api/orders" """ self._create_test_case_file("path_tests.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) applicable_user = registry.get_applicable_test_cases("GET", "/api/users/123") self.assertEqual(len(applicable_user), 1) self.assertEqual(applicable_user[0].id, "TC-USER-PATH") applicable_order = registry.get_applicable_test_cases("POST", "/api/orders/new") self.assertEqual(len(applicable_order), 1) self.assertEqual(applicable_order[0].id, "TC-ORDER-PATH") applicable_none1 = registry.get_applicable_test_cases("GET", "/api/products/789") self.assertEqual(len(applicable_none1), 0) applicable_none2 = registry.get_applicable_test_cases("GET", "/api/users/profile") # Does not match \d+ self.assertEqual(len(applicable_none2), 0) def test_get_applicable_by_method_and_path(self): """测试同时根据方法和路径进行筛选。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class SpecificGetTest(BaseAPITestCase): id = "TC-SPECIFIC-GET" name = "Specific GET" applicable_methods = ["GET"] applicable_paths_regex = r"^/data/\\w+$" """ self._create_test_case_file("specific_get.py", content) registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) applicable = registry.get_applicable_test_cases("GET", "/data/item1") self.assertEqual(len(applicable), 1) self.assertEqual(applicable[0].id, "TC-SPECIFIC-GET") not_applicable_method = registry.get_applicable_test_cases("POST", "/data/item1") self.assertEqual(len(not_applicable_method), 0) not_applicable_path = registry.get_applicable_test_cases("GET", "/data/item1/details") self.assertEqual(len(not_applicable_path), 0) def test_invalid_path_regex_handling(self): """测试处理无效的路径正则表达式。""" content = """ from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity class InvalidRegexTest(BaseAPITestCase): id = "TC-INVALID-REGEX" name = "Invalid Regex" applicable_paths_regex = r"^/api/path_(" # Unbalanced parenthesis """ self._create_test_case_file("invalid_regex.py", content) with self.assertLogs(level='ERROR') as log_watcher: registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR) self.assertTrue(any("中的路径正则表达式 'invalid_regex.InvalidRegexTest' 无效" in msg for msg in log_watcher.output)) # The test case with invalid regex should not match any path applicable = registry.get_applicable_test_cases("GET", "/api/path_something") self.assertEqual(len(applicable), 0) if __name__ == '__main__': # Configure logging for detailed output when running directly # logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, # format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') unittest.main()