mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor(string): split utilities by responsibility
This commit is contained in:
@@ -46,6 +46,7 @@ RETIRED_CANONICAL_FILES = (
|
||||
"app/security/url_safety.py",
|
||||
"app/domain/mediaserver.py",
|
||||
"app/domain/nfo.py",
|
||||
"app/domain/string.py",
|
||||
"app/log.py",
|
||||
"app/foundation/diagnostics.py",
|
||||
"app/infrastructure/log.py",
|
||||
@@ -226,6 +227,16 @@ def test_legacy_roots_contain_no_python_sources():
|
||||
assert leftovers == []
|
||||
|
||||
|
||||
def test_legacy_source_directories_do_not_exist():
|
||||
"""core/helper/utils 物理目录应完全退役,旧导入只由虚拟兼容包解析。"""
|
||||
leftovers = [
|
||||
root_name
|
||||
for root_name in ("core", "helper", "utils")
|
||||
if (APP_ROOT / root_name).exists()
|
||||
]
|
||||
assert leftovers == []
|
||||
|
||||
|
||||
def test_retired_canonical_filenames_do_not_return():
|
||||
"""能力包应使用包内语境明确的短文件名,避免再次出现冗余角色后缀。"""
|
||||
leftovers = [
|
||||
@@ -326,6 +337,31 @@ def test_capability_packages_do_not_import_forbidden_upper_layers():
|
||||
assert violations == {}
|
||||
|
||||
|
||||
def test_site_domain_uses_foundation_dom_boundary():
|
||||
"""站点领域规则应依赖 DOM 原语,不得重新耦合聚合字符串工具。"""
|
||||
modules = _discover_modules()
|
||||
dependencies = _resolve_imports(
|
||||
"app.domain.site",
|
||||
modules["app.domain.site"],
|
||||
set(modules),
|
||||
)
|
||||
assert "app.foundation.dom" in dependencies
|
||||
assert "app.domain.string" not in dependencies
|
||||
|
||||
|
||||
def test_host_code_does_not_use_string_utils_facade():
|
||||
"""聚合 StringUtils 只服务插件兼容,宿主实现必须使用拆分后的能力。"""
|
||||
violations: list[str] = []
|
||||
for path in APP_ROOT.rglob("*.py"):
|
||||
relative = path.relative_to(APP_ROOT)
|
||||
if relative.parts[0] in {"plugins", "sdk"}:
|
||||
continue
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
if any(isinstance(node, ast.Name) and node.id == "StringUtils" for node in ast.walk(tree)):
|
||||
violations.append(str(relative))
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_runtime_log_is_a_dependency_leaf():
|
||||
"""底层可引用运行时日志,但日志模块本身不得反向导入应用模块。"""
|
||||
modules = _discover_modules()
|
||||
|
||||
@@ -101,6 +101,13 @@ def _load_transmission_module():
|
||||
app_module.__path__ = []
|
||||
core_module = types.ModuleType("app.core")
|
||||
core_module.__path__ = []
|
||||
domain_module = types.ModuleType("app.domain")
|
||||
domain_module.__path__ = []
|
||||
foundation_module = types.ModuleType("app.foundation")
|
||||
foundation_module.__path__ = []
|
||||
torrent_rules_module = types.ModuleType("app.domain.torrent")
|
||||
size_tools_module = types.ModuleType("app.foundation.size")
|
||||
temporal_tools_module = types.ModuleType("app.foundation.temporal")
|
||||
cache_module = types.ModuleType("app.runtime.cache")
|
||||
modules_module = types.ModuleType("app.modules")
|
||||
modules_module.__path__ = []
|
||||
@@ -112,9 +119,6 @@ def _load_transmission_module():
|
||||
config_module = types.ModuleType("app.runtime.config")
|
||||
metainfo_module = types.ModuleType("app.domain.metainfo")
|
||||
log_module = types.ModuleType("app.runtime.log")
|
||||
utils_module = types.ModuleType("app.utils")
|
||||
utils_module.__path__ = []
|
||||
string_module = types.ModuleType("app.domain.string")
|
||||
transmission_rpc_module = types.ModuleType("transmission_rpc")
|
||||
torrentool_module = types.ModuleType("torrentool")
|
||||
torrentool_module.__path__ = []
|
||||
@@ -172,22 +176,17 @@ def _load_transmission_module():
|
||||
self.season_episode = ""
|
||||
self.episode_list = []
|
||||
|
||||
class _StringUtils:
|
||||
@staticmethod
|
||||
def is_magnet_link(value):
|
||||
return isinstance(value, str) and value.startswith("magnet:")
|
||||
def _is_magnet_link(value):
|
||||
"""按生产领域规则识别测试磁力链接。"""
|
||||
return isinstance(value, str) and value.startswith("magnet:")
|
||||
|
||||
@staticmethod
|
||||
def generate_random_str(_length):
|
||||
return "tmp-tag-01"
|
||||
def _format_size(value):
|
||||
"""返回隔离测试需要的简化容量文本。"""
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def str_filesize(value):
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def str_secends(value):
|
||||
return str(value)
|
||||
def _format_duration(value):
|
||||
"""返回隔离测试需要的简化时长文本。"""
|
||||
return str(value)
|
||||
|
||||
class _FileCache:
|
||||
def get(self, *_args, **_kwargs):
|
||||
@@ -211,28 +210,38 @@ def _load_transmission_module():
|
||||
log_module.logger = _Logger()
|
||||
modules_module._ModuleBase = _ModuleBase
|
||||
modules_module._DownloaderBase = _DownloaderBase
|
||||
string_module.StringUtils = _StringUtils
|
||||
torrent_rules_module.is_magnet_link = _is_magnet_link
|
||||
size_tools_module.format_compact_size = _format_size
|
||||
temporal_tools_module.format_duration = _format_duration
|
||||
transmission_rpc_module.File = object
|
||||
torrentool_torrent_module.Torrent = SimpleNamespace(
|
||||
from_string=lambda _content: SimpleNamespace(name="test", total_size=1)
|
||||
)
|
||||
|
||||
app_module.core = core_module
|
||||
app_module.domain = domain_module
|
||||
app_module.foundation = foundation_module
|
||||
app_module.modules = modules_module
|
||||
app_module.schemas = schemas_module
|
||||
app_module.utils = utils_module
|
||||
domain_module.torrent = torrent_rules_module
|
||||
foundation_module.size = size_tools_module
|
||||
foundation_module.temporal = temporal_tools_module
|
||||
core_module.cache = cache_module
|
||||
core_module.config = config_module
|
||||
core_module.metainfo = metainfo_module
|
||||
modules_module.transmission = transmission_package_module
|
||||
transmission_package_module.transmission = transmission_client_module
|
||||
schemas_module.types = schema_types_module
|
||||
utils_module.string = string_module
|
||||
torrentool_module.torrent = torrentool_torrent_module
|
||||
|
||||
stub_modules = {
|
||||
"app": app_module,
|
||||
"app.core": core_module,
|
||||
"app.domain": domain_module,
|
||||
"app.domain.torrent": torrent_rules_module,
|
||||
"app.foundation": foundation_module,
|
||||
"app.foundation.size": size_tools_module,
|
||||
"app.foundation.temporal": temporal_tools_module,
|
||||
"app.runtime.cache": cache_module,
|
||||
"app.runtime.config": config_module,
|
||||
"app.domain.metainfo": metainfo_module,
|
||||
@@ -242,8 +251,6 @@ def _load_transmission_module():
|
||||
"app.modules.transmission.transmission": transmission_client_module,
|
||||
"app.schemas": schemas_module,
|
||||
"app.schemas.types": schema_types_module,
|
||||
"app.utils": utils_module,
|
||||
"app.domain.string": string_module,
|
||||
"transmission_rpc": transmission_rpc_module,
|
||||
"torrentool": torrentool_module,
|
||||
"torrentool.torrent": torrentool_torrent_module,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from app.modules.indexer.parser.nexus_audiences import NexusAudiencesSiteUserInfo
|
||||
from app.domain.string import StringUtils
|
||||
from app.foundation.size import parse_size
|
||||
|
||||
|
||||
def test_audiences_userbar_metrics_override_generic_nexus_regex():
|
||||
@@ -38,8 +38,8 @@ def test_audiences_userbar_metrics_override_generic_nexus_regex():
|
||||
assert parser.userid == "18978"
|
||||
assert parser.username == "jxxghp"
|
||||
assert parser.user_level == "(江湖儿女)Elite User"
|
||||
assert parser.upload == StringUtils.num_filesize("10.150 TB")
|
||||
assert parser.download == StringUtils.num_filesize("3.624 TB")
|
||||
assert parser.upload == parse_size("10.150 TB")
|
||||
assert parser.download == parse_size("3.624 TB")
|
||||
assert parser.ratio == 2.801
|
||||
assert parser.bonus == 1973896.2
|
||||
assert parser.seeding == 355
|
||||
|
||||
@@ -19,13 +19,15 @@ def test_sdk_exports_canonical_plugin_interfaces():
|
||||
from app.domain.meta.metamusic import MetaMusic as CanonicalMetaMusic
|
||||
from app.domain.metainfo import MetaInfo as CanonicalMetaInfo
|
||||
from app.domain.scraper import NfoReader as CanonicalNfoReader
|
||||
from app.domain.string import StringUtils as CanonicalStringUtils
|
||||
LegacyDomainStringUtils = importlib.import_module(
|
||||
"app.domain.string"
|
||||
).StringUtils
|
||||
from app.foundation.crypto import CryptoJsUtils
|
||||
from app.runtime.extensions.module_manager import ModuleManager as CanonicalModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager as CanonicalPluginManager
|
||||
from app.adapters.network.http import RequestUtils as CanonicalRequestUtils
|
||||
from app.application.rss import RssHelper as CanonicalRssHelper
|
||||
from app.application.site.sites import SitesHelper as CanonicalSitesHelper
|
||||
from app.application.site.sites import SitesHelper as CanonicalSitesHelper # pylint: disable=no-name-in-module
|
||||
from app.runtime.cache import Cache as CanonicalCache
|
||||
from app.runtime.cache import cached as canonical_cached
|
||||
from app.runtime.config import settings as canonical_settings
|
||||
@@ -49,7 +51,7 @@ def test_sdk_exports_canonical_plugin_interfaces():
|
||||
assert RssHelper is CanonicalRssHelper
|
||||
assert SitesHelper is CanonicalSitesHelper
|
||||
assert NotificationHelper is CanonicalNotificationHelper
|
||||
assert UtilityStringUtils is CanonicalStringUtils
|
||||
assert UtilityStringUtils is LegacyDomainStringUtils
|
||||
assert decrypt is CryptoJsUtils.decrypt
|
||||
assert encrypt is CryptoJsUtils.encrypt
|
||||
assert ModuleManager is CanonicalModuleManager
|
||||
|
||||
@@ -13,8 +13,15 @@ def _load_qbittorrent_modules():
|
||||
app_module.__path__ = []
|
||||
core_module = types.ModuleType("app.core")
|
||||
core_module.__path__ = []
|
||||
utils_module = types.ModuleType("app.utils")
|
||||
utils_module.__path__ = []
|
||||
domain_module = types.ModuleType("app.domain")
|
||||
domain_module.__path__ = []
|
||||
foundation_module = types.ModuleType("app.foundation")
|
||||
foundation_module.__path__ = []
|
||||
torrent_rules_module = types.ModuleType("app.domain.torrent")
|
||||
size_tools_module = types.ModuleType("app.foundation.size")
|
||||
temporal_tools_module = types.ModuleType("app.foundation.temporal")
|
||||
text_tools_module = types.ModuleType("app.foundation.text")
|
||||
url_tools_module = types.ModuleType("app.foundation.url")
|
||||
modules_module = types.ModuleType("app.modules")
|
||||
modules_module.__path__ = []
|
||||
qbittorrent_package_module = types.ModuleType("app.modules.qbittorrent")
|
||||
@@ -25,7 +32,6 @@ def _load_qbittorrent_modules():
|
||||
metainfo_module = types.ModuleType("app.domain.metainfo")
|
||||
schemas_module = types.ModuleType("app.schemas")
|
||||
schema_types_module = types.ModuleType("app.schemas.types")
|
||||
string_module = types.ModuleType("app.domain.string")
|
||||
torrentool_module = types.ModuleType("torrentool")
|
||||
torrentool_module.__path__ = []
|
||||
torrentool_torrent_module = types.ModuleType("torrentool.torrent")
|
||||
@@ -46,28 +52,27 @@ def _load_qbittorrent_modules():
|
||||
def error(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
class _StringUtils:
|
||||
@staticmethod
|
||||
def get_domain_address(address, prefix=False):
|
||||
return address, 8080
|
||||
def _is_magnet_link(value):
|
||||
"""按生产领域规则识别测试磁力链接。"""
|
||||
if isinstance(value, bytes):
|
||||
return value.startswith(b"magnet:")
|
||||
return isinstance(value, str) and value.startswith("magnet:")
|
||||
|
||||
@staticmethod
|
||||
def is_magnet_link(value):
|
||||
if isinstance(value, bytes):
|
||||
return value.startswith(b"magnet:")
|
||||
return isinstance(value, str) and value.startswith("magnet:")
|
||||
def _parse_address(address, include_scheme=False):
|
||||
"""返回隔离测试使用的主机和固定端口。"""
|
||||
return address, 8080
|
||||
|
||||
@staticmethod
|
||||
def generate_random_str(_length):
|
||||
return "tmp-tag-01"
|
||||
def _random_string(_length):
|
||||
"""生成可断言的固定临时标签。"""
|
||||
return "tmp-tag-01"
|
||||
|
||||
@staticmethod
|
||||
def str_filesize(value):
|
||||
return str(value)
|
||||
def _format_size(value):
|
||||
"""返回隔离测试需要的简化容量文本。"""
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def str_secends(value):
|
||||
return str(value)
|
||||
def _format_duration(value):
|
||||
"""返回隔离测试需要的简化时长文本。"""
|
||||
return str(value)
|
||||
|
||||
class _FileCache:
|
||||
def get(self, *_args, **_kwargs):
|
||||
@@ -131,7 +136,11 @@ def _load_qbittorrent_modules():
|
||||
schema_types_module.DownloadTaskState = DownloadTaskState
|
||||
schema_types_module.ModuleType = ModuleType
|
||||
schema_types_module.DownloaderType = DownloaderType
|
||||
string_module.StringUtils = _StringUtils
|
||||
torrent_rules_module.is_magnet_link = _is_magnet_link
|
||||
url_tools_module.parse_address = _parse_address
|
||||
text_tools_module.random_string = _random_string
|
||||
size_tools_module.format_compact_size = _format_size
|
||||
temporal_tools_module.format_duration = _format_duration
|
||||
modules_module._ModuleBase = _ModuleBase
|
||||
modules_module._DownloaderBase = _DownloaderBase
|
||||
torrentool_torrent_module.Torrent = _Torrent
|
||||
@@ -145,14 +154,19 @@ def _load_qbittorrent_modules():
|
||||
qbittorrentapi_transfer_module.TransferInfoDictionary = dict
|
||||
|
||||
app_module.core = core_module
|
||||
app_module.domain = domain_module
|
||||
app_module.foundation = foundation_module
|
||||
app_module.log = log_module
|
||||
app_module.modules = modules_module
|
||||
app_module.schemas = schemas_module
|
||||
app_module.utils = utils_module
|
||||
domain_module.torrent = torrent_rules_module
|
||||
foundation_module.size = size_tools_module
|
||||
foundation_module.temporal = temporal_tools_module
|
||||
foundation_module.text = text_tools_module
|
||||
foundation_module.url = url_tools_module
|
||||
core_module.cache = cache_module
|
||||
core_module.config = config_module
|
||||
core_module.metainfo = metainfo_module
|
||||
utils_module.string = string_module
|
||||
schemas_module.types = schema_types_module
|
||||
modules_module.qbittorrent = qbittorrent_package_module
|
||||
torrentool_module.torrent = torrentool_torrent_module
|
||||
@@ -160,6 +174,13 @@ def _load_qbittorrent_modules():
|
||||
stub_modules = {
|
||||
"app": app_module,
|
||||
"app.core": core_module,
|
||||
"app.domain": domain_module,
|
||||
"app.domain.torrent": torrent_rules_module,
|
||||
"app.foundation": foundation_module,
|
||||
"app.foundation.size": size_tools_module,
|
||||
"app.foundation.temporal": temporal_tools_module,
|
||||
"app.foundation.text": text_tools_module,
|
||||
"app.foundation.url": url_tools_module,
|
||||
"app.runtime.cache": cache_module,
|
||||
"app.runtime.config": config_module,
|
||||
"app.domain.metainfo": metainfo_module,
|
||||
@@ -168,8 +189,6 @@ def _load_qbittorrent_modules():
|
||||
"app.modules.qbittorrent": qbittorrent_package_module,
|
||||
"app.schemas": schemas_module,
|
||||
"app.schemas.types": schema_types_module,
|
||||
"app.utils": utils_module,
|
||||
"app.domain.string": string_module,
|
||||
"qbittorrentapi": qbittorrentapi_module,
|
||||
"qbittorrentapi.client": qbittorrentapi_client_module,
|
||||
"qbittorrentapi.transfer": qbittorrentapi_transfer_module,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from importlib.machinery import EXTENSION_SUFFIXES, PathFinder
|
||||
from pathlib import Path
|
||||
|
||||
from app.application.site import _include_legacy_resource_directory
|
||||
from app.runtime.config import settings
|
||||
from app.adapters.system.resource import (
|
||||
ResourceHelper,
|
||||
@@ -13,41 +11,6 @@ from app.startup import modules_initializer
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_legacy_docker_updater_resource_directory_remains_importable(tmp_path):
|
||||
"""旧镜像把资源写入 helper 时,canonical 站点包仍应找到对应扩展。"""
|
||||
package_dir = tmp_path / "app" / "application" / "site"
|
||||
legacy_dir = tmp_path / "app" / "helper"
|
||||
package_dir.mkdir(parents=True)
|
||||
legacy_dir.mkdir(parents=True)
|
||||
extension_path = legacy_dir / f"sites{EXTENSION_SUFFIXES[0]}"
|
||||
extension_path.touch()
|
||||
package_paths = [str(package_dir)]
|
||||
|
||||
_include_legacy_resource_directory(package_paths, package_dir)
|
||||
|
||||
assert (ROOT_DIR / "app" / "helper" / ".resource-compat").is_file()
|
||||
assert package_paths == [str(package_dir), str(legacy_dir)]
|
||||
spec = PathFinder.find_spec("app.application.site.sites", package_paths)
|
||||
assert spec is not None
|
||||
assert spec.origin == str(extension_path)
|
||||
|
||||
|
||||
def test_canonical_site_extension_takes_priority_over_legacy_directory(tmp_path):
|
||||
"""canonical 扩展存在时不得把旧资源目录加入站点包搜索路径。"""
|
||||
package_dir = tmp_path / "app" / "application" / "site"
|
||||
legacy_dir = tmp_path / "app" / "helper"
|
||||
package_dir.mkdir(parents=True)
|
||||
legacy_dir.mkdir(parents=True)
|
||||
extension_name = f"sites{EXTENSION_SUFFIXES[0]}"
|
||||
(package_dir / extension_name).touch()
|
||||
(legacy_dir / extension_name).touch()
|
||||
package_paths = [str(package_dir)]
|
||||
|
||||
_include_legacy_resource_directory(package_paths, package_dir)
|
||||
|
||||
assert package_paths == [str(package_dir)]
|
||||
|
||||
|
||||
def test_resource_helper_uses_v3_only():
|
||||
"""在线资源更新器必须只请求 V3 清单、目录和站点索引文件。"""
|
||||
assert settings.VERSION_FLAG == "v3"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from lxml import etree
|
||||
|
||||
from app.domain.site import SiteUtils
|
||||
from app.foundation.dom import DomUtils
|
||||
from app.sdk.string import StringUtils
|
||||
|
||||
|
||||
def test_dom_child_element_check_preserves_existing_semantics():
|
||||
"""DOM 基础判断应区分空树和至少包含一个子元素的树。"""
|
||||
empty_tree = etree.HTML("<html></html>")
|
||||
populated_tree = etree.HTML("<html><body></body></html>")
|
||||
|
||||
assert DomUtils.has_child_elements(None) is False
|
||||
assert DomUtils.has_child_elements(empty_tree) is False
|
||||
assert DomUtils.has_child_elements(populated_tree) is True
|
||||
|
||||
|
||||
def test_plugin_string_facade_delegates_to_dom_primitive():
|
||||
"""存量 StringUtils 调用应继续获得与 DOM 原语一致的结果。"""
|
||||
html = etree.HTML("<html><body></body></html>")
|
||||
|
||||
assert StringUtils.is_valid_html_element(html) is DomUtils.has_child_elements(html)
|
||||
|
||||
|
||||
def test_site_login_state_is_derived_from_html_markers():
|
||||
"""站点登录规则应识别退出入口并拒绝密码登录页。"""
|
||||
logged_in_html = '<html><body><a href="/logout.php">退出</a></body></html>'
|
||||
login_form_html = '<html><body><input type="password"></body></html>'
|
||||
|
||||
assert SiteUtils.is_logged_in(logged_in_html) is True
|
||||
assert SiteUtils.is_logged_in(login_form_html) is False
|
||||
assert SiteUtils.is_logged_in("") is False
|
||||
|
||||
|
||||
def test_site_checkin_state_is_derived_from_html_markers():
|
||||
"""站点签到规则应把仍存在签到入口的页面识别为未签到。"""
|
||||
pending_html = '<html><body><a href="/attendance.php">签到</a></body></html>'
|
||||
completed_html = '<html><body><a href="/logout.php">退出</a></body></html>'
|
||||
|
||||
assert SiteUtils.is_checkin(pending_html) is False
|
||||
assert SiteUtils.is_checkin(completed_html) is True
|
||||
assert SiteUtils.is_checkin("") is False
|
||||
+19
-18
@@ -1,26 +1,27 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from app.domain.string import StringUtils
|
||||
from app.domain.title import is_media_title_like
|
||||
|
||||
|
||||
class StringUtilsTest(TestCase):
|
||||
class MediaTitleTest(TestCase):
|
||||
"""验证媒体标题候选规则。"""
|
||||
|
||||
def test_is_media_title_like_true(self):
|
||||
self.assertTrue(StringUtils.is_media_title_like("盗梦空间"))
|
||||
self.assertTrue(StringUtils.is_media_title_like("The Lord of the Rings"))
|
||||
self.assertTrue(StringUtils.is_media_title_like("庆余年 第2季"))
|
||||
self.assertTrue(StringUtils.is_media_title_like("The Office S01E01"))
|
||||
self.assertTrue(StringUtils.is_media_title_like("权力的游戏 Game of Thrones"))
|
||||
self.assertTrue(StringUtils.is_media_title_like("Spider-Man: No Way Home 2021"))
|
||||
self.assertTrue(is_media_title_like("盗梦空间"))
|
||||
self.assertTrue(is_media_title_like("The Lord of the Rings"))
|
||||
self.assertTrue(is_media_title_like("庆余年 第2季"))
|
||||
self.assertTrue(is_media_title_like("The Office S01E01"))
|
||||
self.assertTrue(is_media_title_like("权力的游戏 Game of Thrones"))
|
||||
self.assertTrue(is_media_title_like("Spider-Man: No Way Home 2021"))
|
||||
|
||||
def test_is_media_title_like_false(self):
|
||||
self.assertFalse(StringUtils.is_media_title_like(""))
|
||||
self.assertFalse(StringUtils.is_media_title_like(" "))
|
||||
self.assertFalse(StringUtils.is_media_title_like("a"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("第2季"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("S01E01"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("#推荐电影"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("请帮我推荐一部电影"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("盗梦空间怎么样?"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("我想看盗梦空间"))
|
||||
self.assertFalse(StringUtils.is_media_title_like("继续"))
|
||||
self.assertFalse(is_media_title_like(""))
|
||||
self.assertFalse(is_media_title_like(" "))
|
||||
self.assertFalse(is_media_title_like("a"))
|
||||
self.assertFalse(is_media_title_like("第2季"))
|
||||
self.assertFalse(is_media_title_like("S01E01"))
|
||||
self.assertFalse(is_media_title_like("#推荐电影"))
|
||||
self.assertFalse(is_media_title_like("请帮我推荐一部电影"))
|
||||
self.assertFalse(is_media_title_like("盗梦空间怎么样?"))
|
||||
self.assertFalse(is_media_title_like("我想看盗梦空间"))
|
||||
self.assertFalse(is_media_title_like("继续"))
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from app.sdk.string import StringUtils
|
||||
|
||||
|
||||
EXPECTED_METHODS = {
|
||||
"clear",
|
||||
"clear_file_name",
|
||||
"clear_upper",
|
||||
"compare_version",
|
||||
"count_words",
|
||||
"diff_time_str",
|
||||
"escape_markdown",
|
||||
"find_common_prefix",
|
||||
"format_ep",
|
||||
"format_size",
|
||||
"format_timestamp",
|
||||
"generate_random_str",
|
||||
"get_base_url",
|
||||
"get_domain_address",
|
||||
"get_idlist",
|
||||
"get_keyword",
|
||||
"get_time",
|
||||
"get_url_domain",
|
||||
"get_url_host",
|
||||
"get_url_netloc",
|
||||
"get_url_sld",
|
||||
"is_all_chinese",
|
||||
"is_chinese",
|
||||
"is_english_word",
|
||||
"is_japanese",
|
||||
"is_korean",
|
||||
"is_link",
|
||||
"is_magnet_link",
|
||||
"is_media_title_like",
|
||||
"is_number",
|
||||
"is_valid_html_element",
|
||||
"md5_hash",
|
||||
"natural_sort_key",
|
||||
"num_filesize",
|
||||
"safe_strip",
|
||||
"split_text",
|
||||
"str_amount",
|
||||
"str_filesize",
|
||||
"str_float",
|
||||
"str_from_cookiejar",
|
||||
"str_int",
|
||||
"str_secends",
|
||||
"str_series",
|
||||
"str_timehours",
|
||||
"str_timelong",
|
||||
"str_title",
|
||||
"str_to_timestamp",
|
||||
"to_bool",
|
||||
"unify_datetime_str",
|
||||
"url_equal",
|
||||
}
|
||||
|
||||
|
||||
def test_string_utils_keeps_complete_plugin_method_surface():
|
||||
"""SDK 门面必须保留拆分前全部静态方法名称。"""
|
||||
assert EXPECTED_METHODS <= set(dir(StringUtils))
|
||||
|
||||
|
||||
def test_legacy_string_modules_share_sdk_facade_identity():
|
||||
"""旧 utils/domain 路径与 SDK 应解析到同一个轻量兼容模块。"""
|
||||
sdk_module = importlib.import_module("app.sdk.string")
|
||||
legacy_utils = importlib.import_module("app.utils.string")
|
||||
legacy_domain = importlib.import_module("app.domain.string")
|
||||
|
||||
assert legacy_utils is sdk_module
|
||||
assert legacy_domain is sdk_module
|
||||
assert legacy_utils.StringUtils is StringUtils
|
||||
assert legacy_domain.StringUtils is StringUtils
|
||||
|
||||
|
||||
def test_string_utils_preserves_legacy_keyword_arguments():
|
||||
"""插件按旧参数名调用时应正确转交到拆分后的实现。"""
|
||||
assert StringUtils.clear(text="A.B C", replace_word="-", allow_space=True) == "A-B C"
|
||||
assert StringUtils.str_filesize(size=1024 ** 3, pre=2) == "1.0G"
|
||||
assert StringUtils.url_equal(url1="https://www.example.com", url2="example.com") is True
|
||||
assert StringUtils.generate_random_str(randomlength=8)
|
||||
assert StringUtils.to_bool(text="", default_val=True) is True
|
||||
assert StringUtils.str_amount(amount=1234, curr="¥") == "¥1,234"
|
||||
assert StringUtils.get_domain_address(
|
||||
address="example.com:8080", prefix=True
|
||||
) == ("http://example.com", 8080)
|
||||
assert StringUtils.compare_version(
|
||||
v1="1.2.0", compare_type="<", v2="1.3.0"
|
||||
) is True
|
||||
|
||||
|
||||
def test_string_utils_preserves_legacy_method_signatures():
|
||||
"""反射静态方法签名时也应继续看到插件熟悉的旧参数名。"""
|
||||
assert list(inspect.signature(StringUtils.clear).parameters) == [
|
||||
"text",
|
||||
"replace_word",
|
||||
"allow_space",
|
||||
]
|
||||
assert list(inspect.signature(StringUtils.compare_version).parameters) == [
|
||||
"v1",
|
||||
"compare_type",
|
||||
"v2",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
|
||||
def test_string_utils_routes_representative_capabilities():
|
||||
"""容量、站点、媒体、剧集、种子和 DOM 能力应保持历史结果。"""
|
||||
assert StringUtils.num_filesize("10.150 TB") == 11160043021926
|
||||
assert StringUtils.get_url_domain("https://u2.dmhy.org/torrents.php") == "u2.dmhy.org"
|
||||
assert StringUtils.is_media_title_like("The Office S01E01") is True
|
||||
assert StringUtils.format_ep([1, 2, 3, 5]) == "E01-E03、E05"
|
||||
assert StringUtils.is_magnet_link("magnet:?xt=urn:btih:abc") is True
|
||||
assert StringUtils.is_valid_html_element(
|
||||
etree.HTML("<html><body></body></html>")
|
||||
) is True
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from app.modules.indexer import parser as parser_module
|
||||
from app.modules.indexer.parser.torrent_leech import TorrentLeechSiteUserInfo
|
||||
from app.domain.string import StringUtils
|
||||
from app.foundation.size import parse_size
|
||||
|
||||
|
||||
PROFILE_VIEW_HTML = """
|
||||
@@ -93,8 +93,8 @@ def test_torrent_leech_refresh_prefers_topbar_user_and_parses_profile_once(monke
|
||||
|
||||
assert parser.userid == "example_user"
|
||||
assert parser.username == "example_user"
|
||||
assert parser.upload == StringUtils.num_filesize("41.54 GB")
|
||||
assert parser.download == StringUtils.num_filesize("10.16 GB")
|
||||
assert parser.upload == parse_size("41.54 GB")
|
||||
assert parser.download == parse_size("10.16 GB")
|
||||
assert parser.ratio == 4.089
|
||||
assert parser.user_level == "Registered"
|
||||
assert parser.join_at == "2022-09-04 00:00:00"
|
||||
|
||||
Reference in New Issue
Block a user