198 lines
7.5 KiB
Python
198 lines
7.5 KiB
Python
"""统一入口 ``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()
|