Files
svfs/tests/test_cli_main.py
T
chakcy 06ec7c38bc feat: 统一 svfs 子命令入口,打包进度回调与对象缓存路径
桌面应用(svfs-studio / svfs-client)要用的前置改动:

* 新增 src/sqlite_vfs/cli/main.py:`svfs <子命令>` 统一入口,旧的独立命令
  (vfspack / vfsunpack / …)保留为别名,两套跑同一段 execute(args);各子命令
  因此从 sys.exit() 改成返回退出码,便于被复用;
* FolderPacker.pack_folder(progress=…):每处理一个条目(目录也算)回调一次,
  供 GUI 进度条使用;只有传了回调时才做"只 stat、不读内容"的预扫描,CLI 的
  行为与开销不变;回调抛出的异常一路上抛——GUI 的中断语义就靠它;
* publish.default_cache_dir():对象缓存目录(客户端设置里可改);load_manifest
  对"误把服务器根地址当清单填进来"给出可用插件列表的提示,而不是只甩一个
  format=None。

测试:新增 tests/test_cli_main.py、tests/test_pack_progress.py,test_publish.py 补充。
2026-09-16 17:15:16 +08:00

198 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一入口 ``svfs <子命令>``(sqlite_vfs.cli.main)。
约定:
* 每个子命令与旧的 ``vfs*`` 别名命令跑同一段 ``execute``,输出与退出码一致;
* 退出码:0 成功,1 失败,argparse 用法错误 2;
* 不带子命令 → 打印帮助并以 0 退出(而不是甩 usage 错误)。
端到端走子进程(与 test_cli.py 同一套 run_cli),保证测的就是用户敲的那条命令。
"""
import os
import unittest
import helpers
from sqlite_vfs.core import SQLiteVFS
class DispatcherTest(helpers.VFSTestCase):
def run_svfs(self, *args):
return helpers.run_cli("sqlite_vfs.cli.main", *args)
def setUp(self):
super().setUp()
self.src = helpers.make_tree(
self.path("src"), {"a.txt": b"aaa", "sub/b.txt": b"bbb"}
)
self.db = self.path("app.svfs")
def pack(self, *extra):
result = self.run_svfs("pack", self.src, self.db, "--compress", *extra)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
return result
# ------------------------------------------------------------------
def test_no_subcommand_prints_help_and_exits_zero(self):
result = self.run_svfs()
self.assertEqual(result.returncode, 0)
self.assertIn("svfs pack", result.stdout)
self.assertIn("usage", result.stdout.lower())
def test_help_lists_all_subcommands(self):
result = self.run_svfs("--help")
self.assertEqual(result.returncode, 0)
for name in ("pack", "unpack", "verify", "sign", "info", "migrate", "publish", "fetch"):
self.assertIn(name, result.stdout)
def test_version_flag(self):
result = self.run_svfs("--version")
self.assertEqual(result.returncode, 0)
self.assertIn("svfs", result.stdout)
def test_unknown_command_is_usage_error(self):
result = self.run_svfs("nope")
self.assertEqual(result.returncode, 2)
class SubcommandFlowTest(helpers.VFSTestCase):
"""每个子命令端到端一遍:这条链就是 README 上的主流程。"""
def setUp(self):
super().setUp()
self.src = helpers.make_tree(
self.path("src"),
{"a.txt": b"aaa", "sub/b.txt": b"bbb", "dup.txt": b"aaa"}, # dup 与 a 同内容
)
self.db = self.path("app.svfs")
def run_svfs(self, *args, expect=0):
result = helpers.run_cli("sqlite_vfs.cli.main", *args)
self.assertEqual(
result.returncode, expect, f"svfs {args}\n{result.stdout}\n{result.stderr}"
)
return result
def test_full_lifecycle(self):
self.run_svfs("pack", self.src, self.db, "--compress", "--name", "demo")
# info:统计与去重收益
info = self.run_svfs("info", self.db)
self.assertIn("demo", info.stdout)
self.assertIn("schema v3", info.stdout)
self.assertIn("去重+压缩共省下", info.stdout)
# sign / verify
self.run_svfs("sign", self.db, "--key", "k1", "--key-id", "ci")
verify = self.run_svfs("verify", self.db, "--key", "k1")
self.assertIn("签名: OK", verify.stdout)
wrong = self.run_svfs("verify", self.db, "--key", "别的密钥", expect=1)
self.assertIn("签名: 失败", wrong.stdout)
# unpack + 内容比对
out = self.path("out")
self.run_svfs("unpack", self.db, out)
helpers.assert_tree_equal(
self, helpers.read_tree(self.src), helpers.read_tree(out)
)
# publish → fetch 回环
public = self.path("public")
self.run_svfs("publish", self.db, public)
self.assertTrue(os.path.exists(os.path.join(public, "manifest.json")))
back = self.path("back")
self.run_svfs("fetch", os.path.join(public, "manifest.json"), back)
helpers.assert_tree_equal(
self, helpers.read_tree(self.src), helpers.read_tree(back)
)
def test_info_json(self):
self.run_svfs("pack", self.src, self.db)
import json
result = self.run_svfs("info", self.db, "--json")
data = json.loads(result.stdout)
self.assertEqual(data["name"], "packed_filesystem")
self.assertEqual(data["files"], 3)
self.assertEqual(data["unique_contents"], 2) # a.txt 与 dup.txt 去重
self.assertGreater(data["saved_bytes"], 0)
self.assertEqual(len(data["snapshots"]), 1)
def test_sign_json_and_snapshot_selection(self):
from sqlite_vfs.folder_packer import FolderPacker
self.run_svfs("pack", self.src, self.db)
# 打一版历史出来
vfs = SQLiteVFS(self.db, compress=True)
try:
with helpers.quiet():
FolderPacker(vfs, fs_name="demo").pack_folder(
self.src, incremental=True
)
vfs.create_snapshot("v2", copy_entries=True)
ids = [row["id"] for row in vfs.list_snapshots()]
finally:
vfs.close()
result = self.run_svfs("sign", self.db, "--key", "k", "--snapshot", str(ids[0]), "--json")
import json
data = json.loads(result.stdout)
self.assertEqual(data["snapshot"], ids[0])
self.assertTrue(data["signature"])
# 验签指定同一快照 → OK;最新快照(未签名)→ 失败
self.run_svfs("verify", self.db, "--key", "k", "--snapshot", str(ids[0]))
self.run_svfs("verify", self.db, "--key", "k", expect=1)
def test_pack_snapshot_flag_creates_version(self):
self.run_svfs("pack", self.src, self.db)
result = self.run_svfs("pack", self.src, self.db, "--incremental", "--snapshot", "v2")
self.assertIn("已创建快照 v2", result.stdout)
vfs = SQLiteVFS(self.db, readonly=True)
try:
names = [row["name"] for row in vfs.list_snapshots()]
finally:
vfs.close()
self.assertEqual(len(names), 2)
self.assertIn("v2", names)
def test_error_paths_report_and_exit_1(self):
self.run_svfs("pack", self.path("不存在"), self.db, expect=1)
self.run_svfs("info", self.path("不存在.svfs"), expect=1)
self.run_svfs("sign", self.path("不存在.svfs"), "--key", "k", expect=1)
self.pack_helper()
# 对不是 SVFS 的文件 info/verify/sign 都要干净地失败
garbage = self.path("garbage.db")
with open(garbage, "wb") as handle:
handle.write(b"x" * 4096)
self.run_svfs("info", garbage, expect=1)
self.run_svfs("verify", garbage, expect=1)
self.run_svfs("sign", garbage, "--key", "k", expect=1)
def pack_helper(self):
self.run_svfs("pack", self.src, self.db, "--compress")
def test_legacy_aliases_share_the_same_execute(self):
"""旧命令与新入口是同一段实现:退出码一致,产出的库摘要一致(内容寻址)。"""
fresh_db = self.path("alias.svfs")
new_run = helpers.run_cli("sqlite_vfs.cli.main", "pack", self.src, self.db, "--compress")
legacy_run = helpers.run_cli("sqlite_vfs.cli.packer", self.src, fresh_db, "--compress")
self.assertEqual(new_run.returncode, 0, new_run.stderr)
self.assertEqual(legacy_run.returncode, 0, legacy_run.stderr)
def digest(path):
vfs = SQLiteVFS(path, readonly=True)
try:
return vfs.get_stats()["digest"]
finally:
vfs.close()
self.assertEqual(digest(self.db), digest(fresh_db))
if __name__ == "__main__":
unittest.main()