102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
"""验证"插件是可选依赖"这一契约。
|
|||
|
|
|
||
|
|
这个模块在**有/没有** FastAPI 的两种环境下都会运行:
|
||
|
|
它断言的是"核心包永远不依赖第三方框架",以及"缺依赖时的报错要能直接照做"。
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
import helpers
|
||
|
|
|
||
|
|
THIRD_PARTY = ("fastapi", "starlette", "anyio", "pydantic")
|
||
|
|
|
||
|
|
|
||
|
|
class OptionalDependencyTest(unittest.TestCase):
|
||
|
|
def run_snippet(self, code):
|
||
|
|
env = dict(os.environ)
|
||
|
|
env["PYTHONPATH"] = helpers.SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
|
||
|
|
env["PYTHONIOENCODING"] = "utf-8"
|
||
|
|
return subprocess.run(
|
||
|
|
[sys.executable, "-c", code],
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
encoding="utf-8",
|
||
|
|
errors="replace",
|
||
|
|
env=env,
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_third_party_is_installed_or_not(self):
|
||
|
|
"""只用于在输出里说明当前环境状态,不断言。"""
|
||
|
|
result = self.run_snippet(
|
||
|
|
"import importlib.util as u;"
|
||
|
|
"print({m: bool(u.find_spec(m)) for m in %r})" % (THIRD_PARTY,)
|
||
|
|
)
|
||
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
||
|
|
|
||
|
|
def test_importing_core_never_pulls_in_third_party(self):
|
||
|
|
"""import sqlite_vfs 不能顺带把 fastapi/starlette 拖进来。"""
|
||
|
|
result = self.run_snippet(
|
||
|
|
"import sys\n"
|
||
|
|
"import sqlite_vfs, sqlite_vfs.core, sqlite_vfs.contrib\n"
|
||
|
|
"leaked = sorted(\n"
|
||
|
|
" m for m in sys.modules if m.split('.')[0] in %r\n"
|
||
|
|
")\n"
|
||
|
|
"print(leaked)" % (THIRD_PARTY,)
|
||
|
|
)
|
||
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
||
|
|
self.assertEqual(
|
||
|
|
"[]",
|
||
|
|
result.stdout.strip(),
|
||
|
|
"导入 sqlite_vfs 时连带加载了第三方框架模块",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_plugin_module_is_not_imported_by_the_package(self):
|
||
|
|
"""contrib 子包不能自动导入插件模块(否则缺依赖时整包不可用)。"""
|
||
|
|
result = self.run_snippet(
|
||
|
|
"import sys\n"
|
||
|
|
"import sqlite_vfs.contrib\n"
|
||
|
|
"print('sqlite_vfs.contrib.fastapi_static' in sys.modules)"
|
||
|
|
)
|
||
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
||
|
|
self.assertEqual("False", result.stdout.strip())
|
||
|
|
|
||
|
|
def test_missing_fastapi_gives_actionable_error(self):
|
||
|
|
"""缺少可选依赖时,报错信息必须能直接照着做。"""
|
||
|
|
probe = self.run_snippet(
|
||
|
|
"import importlib.util as u; print(bool(u.find_spec('starlette')))"
|
||
|
|
)
|
||
|
|
if probe.stdout.strip() == "True":
|
||
|
|
self.skipTest("当前环境已安装 starlette,无需检查缺失提示")
|
||
|
|
|
||
|
|
result = self.run_snippet("import sqlite_vfs.contrib.fastapi_static")
|
||
|
|
self.assertNotEqual(0, result.returncode)
|
||
|
|
self.assertIn("sqlite-vfs[fastapi]", result.stderr)
|
||
|
|
self.assertIn("pip install", result.stderr)
|
||
|
|
|
||
|
|
def test_missing_database_path_hint(self):
|
||
|
|
"""未生成资源包时的报错要给出下一步命令。"""
|
||
|
|
result = self.run_snippet(
|
||
|
|
"import tempfile, os, sys\n"
|
||
|
|
"sys.path.insert(0, %r)\n"
|
||
|
|
"try:\n"
|
||
|
|
" from sqlite_vfs.contrib.fastapi_static import SVFSStaticFiles\n"
|
||
|
|
"except ImportError as e:\n"
|
||
|
|
" print('SKIP', e); raise SystemExit(0)\n"
|
||
|
|
"try:\n"
|
||
|
|
" SVFSStaticFiles(os.path.join(tempfile.mkdtemp(), 'nope.svfs'))\n"
|
||
|
|
"except RuntimeError as e:\n"
|
||
|
|
" print(e)\n" % helpers.SRC_DIR
|
||
|
|
)
|
||
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
||
|
|
output = result.stdout.strip()
|
||
|
|
if output.startswith("SKIP"):
|
||
|
|
self.skipTest("当前环境没有 FastAPI")
|
||
|
|
self.assertIn("vfspack", output)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|