起始状态:主链路 100% 不可用,而且失败被静默吞掉——打包后库里 0 个文件、 界面仍打印"文件数量: N";解包导出 0 个文件并在磁盘上生成几百层 sub/sub/sub 垃圾目录。 缺陷修复(4 个 P0 + 12 个 P1,逐条见 BUG_REPORT.md): - 建表列名 compresse_algo 与 INSERT/read_file 用的 compress_algo 不一致 - list_directory 按 path 而非 parent_path 过滤,导致导出无限自递归 - CLI --ignore-timestamps 把 export_filesystem 赋成引用自身的 lambda - add_file 缺少 commit(直接调用 API 时静默丢数据) - LIKE 前缀误伤兄弟目录(删 doc 连带删 docs);mv_dir 二次改名 - 失败路径一律返回 exit code 0;--exclude 是子串匹配而非 glob - FolderPacker.add_file 反向调用 export_file_safe,会用库内内容覆盖源文件 存储层(schema v3,格式变更,提供 vfsmigrate 迁移): - 内容移入 blobs(hash PRIMARY KEY):相同内容只存一份,跨路径/跨快照共享 - files 的唯一约束改为 UNIQUE(snapshot_id, path):一个库存多个版本 - 完整性:每文件 sha256 + 快照级摘要 + HMAC-SHA256 签名,只读连接即可验证 - 增量打包:按 mtime+size 或 sha256 只写变化项,未变文件不重写不重压 - get_file_info 不再返回 content(元数据查询不该搬 BLOB) 新增能力: - 可选 FastAPI 静态文件插件(extras,核心仍零第三方依赖) - 内容寻址发布到对象存储 + 逐对象校验的取回客户端 - 压缩默认改回 zlib:实测比自研哈夫曼体积小 1/3~1/170、快数百倍 其它: - P2-4:时间戳显式转字符串,清掉 1240 条 DeprecationWarning(盘上格式不变) - read_file 负数 offset/size 改为报错(原先会走切片语义从末尾取) - requires-python 从 >=3.7 改为按实际 API 推导的 >=3.9 测试:0 → 343 个用例,纯标准库 unittest,有/无 FastAPI 两种环境都跑通。 证据见 docs/evidence-*.txt。
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()
|