Files
svfs/tests/test_pack_progress.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

146 lines
5.2 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.
"""打包进度回调(pack_folder 的 progress 参数)。
约定:
* 每处理一个条目(目录也算)回调一次,``done`` 从 1 递增到 ``total``;
* ``total`` 是完整遍历一遍得到的条目数,为了拿到它多做一次"只 stat"的预扫描,
因此**只有传了回调时**才算,不传回调的路径(CLI)开销一点不变;
* 回调抛出的异常会一路向上抛——GUI 的中断按钮就靠这个语义。
"""
import unittest
import helpers
from sqlite_vfs.core import SQLiteVFS
from sqlite_vfs.folder_packer import FolderPacker
class PackProgressTest(helpers.VFSTestCase):
def setUp(self):
super().setUp()
self.src = helpers.make_tree(
self.path("src"),
{
"a.txt": b"aaa",
"b.txt": b"bbb",
"sub/c.txt": b"ccc",
"sub/deep/d.txt": b"ddd",
"empty_dir/.keep": b"",
},
)
# 条目数 = 5 个文件(含 empty_dir/.keep)+ 3 个目录(sub / sub/deep / empty_dir)
self.expected_entries = 8
def collect(self, incremental=False):
calls = []
def progress(done, total, rel_path):
calls.append((done, total, rel_path))
vfs = SQLiteVFS(self.path("prog.svfs"), compress=True)
try:
packer = FolderPacker(vfs, fs_name="prog")
with helpers.quiet():
ok = packer.pack_folder(self.src, incremental=incremental, progress=progress)
finally:
vfs.close()
return ok, calls
def test_full_progress_is_one_call_per_entry(self):
ok, calls = self.collect()
self.assertTrue(ok)
self.assertEqual(len(calls), self.expected_entries)
self.assertEqual([done for done, _total, _path in calls], list(range(1, len(calls) + 1)))
def test_total_matches_entry_count(self):
_ok, calls = self.collect()
totals = {total for _done, total, _path in calls}
self.assertEqual(totals, {self.expected_entries})
def test_progress_paths_are_relative_and_normalized(self):
_ok, calls = self.collect()
paths = [path.replace("\\", "/") for _done, _total, path in calls]
self.assertEqual(sorted(paths), sorted(["a.txt", "b.txt", "sub", "sub/c.txt",
"sub/deep", "sub/deep/d.txt",
"empty_dir", "empty_dir/.keep"]))
def test_incremental_progress_counts_all_entries(self):
ok, calls = self.collect(incremental=True)
self.assertTrue(ok)
self.assertEqual(len(calls), self.expected_entries)
self.assertEqual({total for _done, total, _path in calls}, {self.expected_entries})
def test_result_is_same_with_and_without_progress(self):
"""带回调不该改变打包结果:条目集合必须一模一样。"""
db_with = self.path("with.svfs")
db_without = self.path("without.svfs")
vfs = SQLiteVFS(db_with, compress=True)
try:
with helpers.quiet():
FolderPacker(vfs, fs_name="prog").pack_folder(
self.src, progress=lambda *_args: None
)
finally:
vfs.close()
helpers.pack(self.src, db_without, compress=True)
def snapshot(db_path):
vfs = SQLiteVFS(db_path, readonly=True)
try:
return [
(row["path"], row["is_directory"], row["file_size"], row["content_hash"])
for row in vfs.all_entries()
], sorted(row["hash"] for row in vfs.list_blobs())
finally:
vfs.close()
self.assertEqual(snapshot(db_with), snapshot(db_without))
def test_no_precount_when_progress_is_absent(self):
"""不传回调就不该多做那次预扫描(CLI 的开销必须保持原样)。"""
vfs = SQLiteVFS(self.path("nopre.svfs"), compress=True)
try:
packer = FolderPacker(vfs, fs_name="prog")
original = packer._count_entries
called = []
def spy(source_folder):
called.append(source_folder)
return original(source_folder)
packer._count_entries = spy
with helpers.quiet():
self.assertTrue(packer.pack_folder(self.src))
self.assertEqual(called, [])
finally:
vfs.close()
def test_exception_from_callback_propagates(self):
"""回调抛异常要能中断打包——GUI 的"中断"就是靠这个语义实现的。"""
class Stop(Exception):
pass
seen = []
def progress(done, total, rel_path):
seen.append(rel_path)
if done >= 2:
raise Stop("用户中断")
vfs = SQLiteVFS(self.path("cancel.svfs"), compress=True)
try:
packer = FolderPacker(vfs, fs_name="prog")
with helpers.quiet():
with self.assertRaises(Stop):
packer.pack_folder(self.src, progress=progress)
self.assertEqual(len(seen), 2)
finally:
vfs.close()
if __name__ == "__main__":
unittest.main()