Files

213 lines
8.1 KiB
Python
Raw Permalink Normal View History

"""内容寻址与去重。
核心承诺:**相同内容只存一份**。这里用 blob 行数与 stored_size 直接验证,
而不是只看读回来的字节——字节对不代表没多存。
"""
import os
import unittest
import helpers
from sqlite_vfs.core import SQLiteVFS
class DedupTest(helpers.VFSTestCase):
def _add(self, vfs, name, data):
src = helpers.write_file(self.path(f"src_{name.replace('/', '_')}"), data)
with helpers.quiet():
self.assertTrue(vfs.add_file(src, name), f"写入失败: {name}")
def _vfs(self, **kwargs):
vfs = SQLiteVFS(":memory:", **kwargs)
self.addCleanup(vfs.close)
return vfs
def test_identical_files_share_one_blob(self):
vfs = self._vfs()
self._add(vfs, "a.txt", b"same content")
self._add(vfs, "b.txt", b"same content")
self._add(vfs, "c.txt", b"same content")
self.assertEqual(3, helpers.count_files(vfs))
self.assertEqual(1, helpers.blob_count(vfs), "相同内容应当只占一行 blob")
def test_identical_content_in_different_directories_shares_blob(self):
vfs = self._vfs()
self._add(vfs, "doc/logo.png", b"PNGDATA" * 100)
self._add(vfs, "assets/logo.png", b"PNGDATA" * 100)
self.assertEqual(1, helpers.blob_count(vfs))
def test_distinct_content_gets_distinct_blobs(self):
vfs = self._vfs()
self._add(vfs, "a.txt", b"aaa")
self._add(vfs, "b.txt", b"bbb")
self.assertEqual(2, helpers.blob_count(vfs))
def test_empty_files_share_one_blob(self):
vfs = self._vfs()
self._add(vfs, "e1.txt", b"")
self._add(vfs, "e2.txt", b"")
self.assertEqual(1, helpers.blob_count(vfs))
self.assertEqual(b"", vfs.read_file("e1.txt"))
def test_replacing_with_identical_content_does_not_add_blob(self):
vfs = self._vfs()
self._add(vfs, "a.txt", b"payload")
vfs.remove("a.txt")
self._add(vfs, "a.txt", b"payload")
self.assertEqual(1, helpers.blob_count(vfs), "重新写入相同内容不该新增 blob")
def test_rewrite_different_content_keeps_only_new_blob(self):
vfs = self._vfs()
self._add(vfs, "a.txt", b"first")
vfs.remove("a.txt")
self._add(vfs, "a.txt", b"second")
self.assertEqual(1, helpers.blob_count(vfs), "旧内容已经没人引用,应当被回收")
def test_blob_records_original_and_stored_size(self):
vfs = self._vfs(compress=True)
data = b"compressible text " * 200
self._add(vfs, "big.txt", data)
blob = vfs.get_blob_info("big.txt")
self.assertEqual(len(data), blob["size"], "size 记的是原始大小")
self.assertLess(blob["stored_size"], len(data), "stored_size 记的是实际占用")
self.assertEqual("zlib", blob["compress_algo"])
def test_dedup_skips_recompression(self):
"""第二次写入相同内容时不该再压一遍——这是增量打包能变快的原因。"""
vfs = self._vfs(compress=True)
data = b"same bytes " * 500
src = helpers.write_file(self.path("in.bin"), data)
with helpers.quiet():
vfs.add_file(src, "one.bin")
first = vfs.get_blob_info("one.bin")
vfs.add_file(src, "two.bin")
second = vfs.get_blob_info("two.bin")
self.assertEqual(first["hash"], second["hash"])
self.assertEqual(1, helpers.blob_count(vfs))
# 两个路径指向同一份压缩数据,占用没有翻倍
self.assertEqual(first["stored_size"], second["stored_size"])
def test_repacking_identical_tree_does_not_grow_storage(self):
src = helpers.make_tree(
self.path("tree"),
{"a.txt": b"hello", "b.txt": b"hello", "sub/c.txt": b"world"},
)
db = self.path("p.svfs")
helpers.pack(src, db, compress=True)
first_size = os.path.getsize(db)
vfs = SQLiteVFS(db)
self.assertEqual(2, helpers.blob_count(vfs), "hello 与 world 各一份")
vfs.close()
helpers.pack(src, db, compress=True)
vfs = SQLiteVFS(db)
self.addCleanup(vfs.close)
self.assertEqual(2, helpers.blob_count(vfs))
self.assertEqual(3, helpers.count_files(vfs))
self.assertLess(
os.path.getsize(db),
first_size * 1.5,
"重打同一棵树不该让库明显变大",
)
class BlobGarbageCollectionTest(helpers.VFSTestCase):
def _vfs(self, **kwargs):
vfs = SQLiteVFS(":memory:", **kwargs)
self.addCleanup(vfs.close)
helpers.seed_rows(
vfs,
[
{"path": "keep.txt", "content": b"keep"},
{"path": "shared.txt", "content": b"shared"},
{"path": "also_shared.txt", "content": b"shared"},
{"path": "temp", "is_directory": True},
{"path": "temp/gone.txt", "content": b"gone"},
],
)
return vfs
def test_remove_file_collects_unreferenced_blob(self):
vfs = self._vfs()
self.assertEqual(3, helpers.blob_count(vfs))
vfs.remove("temp/gone.txt")
self.assertEqual(2, helpers.blob_count(vfs), "'gone' 已无人引用,应被回收")
def test_remove_directory_collects_blobs(self):
vfs = self._vfs()
vfs.remove("temp")
self.assertEqual(2, helpers.blob_count(vfs))
def test_shared_blob_survives_partial_removal(self):
vfs = self._vfs()
vfs.remove("shared.txt")
self.assertEqual(
3, helpers.blob_count(vfs), "'shared' 还有另一个路径在用,不能回收"
)
self.assertEqual(b"shared", vfs.read_file("also_shared.txt"))
def test_gc_keeps_blobs_referenced_by_another_snapshot(self):
"""多快照共用内容时,删掉一个版本不能影响另一个版本。"""
vfs = self._vfs()
second = vfs.create_snapshot("v2", copy_entries=True)
# 在 v2 里删掉一部分,v1 仍然引用着这些内容
vfs.remove("temp/gone.txt")
vfs.remove("shared.txt")
self.assertEqual(
3, helpers.blob_count(vfs), "v1 还在引用,blob 不该被回收"
)
vfs.use_snapshot(1)
self.assertEqual(b"gone", vfs.read_file("temp/gone.txt"))
self.assertEqual(b"shared", vfs.read_file("shared.txt"))
self.assertEqual(second, 2)
def test_deleting_snapshot_allows_gc(self):
"""只被某个快照引用的内容,在删掉那个快照后应当被回收。"""
vfs = self._vfs()
vfs.create_snapshot("v2") # 空快照,不与 v1 共享内容
src = helpers.write_file(self.path("v2only.bin"), b"v2-only-content")
with helpers.quiet():
vfs.add_file(src, "only-in-v2.txt")
self.assertEqual(4, helpers.blob_count(vfs))
vfs.delete_snapshot(2)
self.assertEqual(
3, helpers.blob_count(vfs), "只被 v2 引用的内容应当被回收"
)
self.assertFalse(vfs.exists("only-in-v2.txt"))
# v1 完全不受影响
self.assertEqual(b"shared", vfs.read_file("shared.txt"))
self.assertEqual(b"gone", vfs.read_file("temp/gone.txt"))
def test_gc_returns_deleted_count(self):
vfs = self._vfs()
# 直接造一个没人引用的 blob
vfs.conn.execute(
"INSERT INTO blobs (hash, size, stored_size, content) "
"VALUES ('orphan', 1, 1, X'00')"
)
vfs.conn.commit()
self.assertEqual(1, vfs.gc_blobs(), "应当回收到孤立的 blob")
self.assertEqual(0, vfs.gc_blobs(), "再跑一次应当没有可回收的")
def test_clear_snapshot_keeps_other_snapshot_content(self):
vfs = self._vfs()
vfs.create_snapshot("v2", copy_entries=True)
vfs.clear_snapshot()
self.assertEqual(0, helpers.count_files(vfs), "当前快照已清空")
self.assertEqual(3, helpers.blob_count(vfs), "v1 还在引用")
vfs.use_snapshot(1)
self.assertEqual(5, len(helpers.all_paths(vfs)))
self.assertEqual(b"keep", vfs.read_file("keep.txt"))
if __name__ == "__main__":
unittest.main()