325 lines
16 KiB
Python
325 lines
16 KiB
Python
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.<id>
|
||
|
||
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/<number>
|
||
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() |