From 1620d3eb5ab92cef199e9a115b535c9608f5f916 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Mon, 14 Sep 2026 00:11:53 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=20README=20=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E7=AB=A0=E8=8A=82=E3=80=81=E7=BC=BA=E9=99=B7=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E4=B8=8E=E4=B8=A4=E4=BB=BD=E8=AE=BE=E8=AE=A1=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README:新增「数据能力」四节(内容寻址去重 / 快照与时间旅行 / 完整性校验与签名 / 增量打包)与「发布到对象存储」一节,附实测对比表;重写「要不要用 --compress」 (结论从"别用"反转为"默认就该开")、「迁移旧格式」;修正项目结构与字段表 - BUG_REPORT.md:13 节,含 P0/P1 逐条分析、P2/P3 清单、五批后续追加的记录, 以及过程中被推翻的两处结论(第九节的压缩建议、P3-1 的 uv sync 判断) - docs/encryption-design.md:加密的设计说明——去重与随机 nonce 的冲突、 标准库没有密码学原语、以及现有 freq_blob 可作为每块附加数据的槽位 - docs/evidence-*.txt:各阶段的测试运行证据(修复前 / 修复后 / 双环境) --- BUG_REPORT.md | 722 ++++++++++++++++++++++++++++++ README.md | 407 +++++++++++++++-- docs/encryption-design.md | 88 ++++ docs/evidence-after.txt | 110 +++++ docs/evidence-before.txt | 829 +++++++++++++++++++++++++++++++++++ docs/evidence-cleanup.txt | 415 ++++++++++++++++++ docs/evidence-publish.txt | 380 ++++++++++++++++ docs/evidence-root-cause.txt | 6 + docs/evidence-storage-v3.txt | 342 +++++++++++++++ 9 files changed, 3263 insertions(+), 36 deletions(-) create mode 100644 BUG_REPORT.md create mode 100644 docs/encryption-design.md create mode 100644 docs/evidence-after.txt create mode 100644 docs/evidence-before.txt create mode 100644 docs/evidence-cleanup.txt create mode 100644 docs/evidence-publish.txt create mode 100644 docs/evidence-root-cause.txt create mode 100644 docs/evidence-storage-v3.txt diff --git a/BUG_REPORT.md b/BUG_REPORT.md new file mode 100644 index 0000000..211c313 --- /dev/null +++ b/BUG_REPORT.md @@ -0,0 +1,722 @@ +# SQLite VFS 缺陷分析与测试报告 + +分析对象:工作区中未提交的哈夫曼压缩集成改动(`M src/sqlite_vfs/core.py`、`?? src/sqlite_vfs/huffman.py`) +分析日期:2026-09-13 +结论:**打包/解包主链路当时 100% 不可用,而且所有故障都被吞掉,命令行依然打印"打包完成"。** + +--- + +## 一、结论与结果 + +| | 修复前 | 修复后 | +|---|---|---| +| 测试套件 | 77 个用例 | 343 个用例(含后续五批追加) | +| 结果 | **42 failures / 9 errors** | **OK** | +| 耗时 | 175.7 s | 14.6 s | +| 打包结果 | 库里 0 个文件,界面报告"N 个文件" | 22 个文件全部入库 | +| 解包结果 | 导出 0 个文件 + 磁盘上生成几百层 `sub/sub/sub/...` 垃圾目录 | `diff -r` 零差异 | +| 失败可见性 | 全部 exit code 0 | 失败 exit code 1 + 中文错误信息 | + +固定的 81 个用例基线见第五节;第九~十三节共追加 262 个用例。 + +最重要的一个发现:**建表用的列名是 `compresse_algo`,而 INSERT 与 `read_file` 用的是 `compress_algo`**。 +这一个拼写不一致就让写入必然失败、读取必然失败,而异常被 `except Exception` 吞掉, +所以用户看到的是"打包完成 / 文件数量: 22",而数据库里其实一条文件记录都没有。 + +修复范围:4 个 P0 + 12 个 P1。P2/P3 按要求只记录不修改(见第六节,其中两条因第九/十节的 +结论而顺带处理)。未提交 git,未改动 `dist/`、`Scripts/`、`resources/`, +仓库自带的 `appcation.svfs` 全程未被修改(md5 前后均为 `cc790c5cffab3155f42f66ed51a71743`)。 + +--- + +## 二、证据 + +### 2.1 修复前的测试结果(`docs/evidence-before.txt`) + +``` +Ran 77 tests in 175.700s + +FAILED (failures=42, errors=9) +``` + +失败信息与根因一一对应,例如: + +``` +FAIL: test_add_file_actually_stores_a_row + AssertionError: False is not true : add_file 没有把文件写进数据库 + +FAIL: test_root_lists_top_level_only + AssertionError: Lists differ: + ['doc', 'docs', 'top.txt'] ← 期望 + != ['doc', 'doc/a.txt', 'doc/sub', 'doc/sub/b.txt', ...] ← 实际返回了整张表 + +FAIL: test_add_file_survives_close_without_further_commit + AssertionError: False is not true : add_file 之后直接 close(),数据丢失——事务没有提交 + +FAIL: test_ignore_timestamps_full_export_does_not_recurse + AssertionError: 'RecursionError' unexpectedly found in output +``` + +耗时 175 秒这个数字本身也是证据:解包时对子目录的无限自我递归在磁盘上创建了几百层 +`sub/sub/sub/...` 目录(本机 `LongPathsEnabled=1`,所以一直递归到 Python 的递归上限才停下)。 + +### 2.2 P0-1 根因的最小复现(`docs/evidence-root-cause.txt`) + +```python +# 修复前的建表语句(注意 compresse_algo 的拼写) +CREATE TABLE files (path TEXT, content BLOB, freq_blob BLOB, + compresse_algo TEXT DEFAULT 'none') +# 而 INSERT / SELECT 用的是 compress_algo +``` + +``` +修复前 INSERT -> OperationalError: table files has no column named compress_algo + → 该异常被 core.py 的 except 吞掉,只打印一行"添加文件失败",库里没有任何记录 +修复前 SELECT -> OperationalError: no such column: compress_algo + → read_file 因此必然失败,所有导出操作都变成"导出文件失败" +修复后 INSERT -> ok, SELECT -> [('none',)] +``` + +### 2.3 修复后的测试结果(`docs/evidence-after.txt`) + +``` +Ran 81 tests in 5.179s + +OK +``` + +### 2.4 真实目录端到端冒烟验证 + +``` +$ python -m sqlite_vfs.cli.packer src /tmp/smoke_p.svfs # 不压缩 +文件数量: 22 总大小: 0.17 MB 耗时: 0.15 秒 +$ python -m sqlite_vfs.cli.packer src /tmp/smoke_c.svfs --compress # 压缩 +文件数量: 22 总大小: 0.17 MB 耗时: 0.39 秒 + +数据库体积: 不压缩 233,472 字节 → 压缩 192,512 字节(省 17.5%) + +$ python -m sqlite_vfs.cli.unpacker /tmp/smoke_c.svfs /tmp/smoke_out --list-root +根目录内容: + DIR: __pycache__ (路径: __pycache__) + DIR: sqlite_vfs (路径: sqlite_vfs) + +$ python -m sqlite_vfs.cli.unpacker /tmp/smoke_c.svfs /tmp/smoke_out +文件系统导出完成! 共导出 22 个文件/目录 + +$ diff -r src /tmp/smoke_out +★ 完全一致,无任何差异 + +$ python -m sqlite_vfs.cli.unpacker /tmp/smoke_c.svfs /tmp/smoke_its --ignore-timestamps +★ diff -r 同样一致(修复前这里必然 RecursionError) + +$ python -m sqlite_vfs.cli.packer src /tmp/smoke_ex.svfs --exclude "__pycache__" "*.pyc" +总大小: 0.05 MB → __pycache__ 被真正剪枝排除 +``` + +各失败路径的退出码与提示(修复前全部是 exit 0,或以 traceback 崩溃): + +| 场景 | 输出 | exit | +|---|---|---| +| 旧格式库 `appcation.svfs` | `错误: appcation.svfs 不是当前版本能够读取的数据库(files 表缺少列: compress_algo, freq_blob)。该库很可能是旧版本创建的;请用旧版本导出内容后重新打包,或换一个新的输出路径。` | 1 | +| 源目录不存在 | `错误: 源文件夹不存在: ./no_such_dir` | 1 | +| 输出目录不存在 | `错误: 输出目录不存在: ...\no_such_dir` | 1 | +| 非 SQLite 文件 | `错误: 无法打开数据库 ...: file is not a database` | 1 | +| 导出不存在的路径 | `错误: 虚拟文件系统中不存在该路径: nope` | 1 | + +--- + +## 三、P0 缺陷(致命:链路完全不可用 / 静默数据丢失,均已修复) + +### P0-1 建表列名与 INSERT / SELECT 列名不一致,打包与解包双向失效 + +- **位置**:原 `core.py:99`(建表 `compresse_algo`)、`:175`(INSERT `compress_algo`)、`:278`(`read_file` 读 `compress_algo`) +- **根因**:同一个字段在两处用了不同拼写。SQLite 不会做任何模糊匹配,列名必须完全一致。 +- **影响**: + - `add_file` 的 INSERT 抛 `OperationalError`,被 `core.py:195` 的 `except Exception` 吞掉,只打印一行"添加文件失败"。**数据库里永远没有任何文件内容。** + - `folder_packer` 不受该异常影响,继续累加自己的计数器,于是 `update_vfs_metadata` 写入的是"走过多少个文件", + 最终打印"文件数量: 22",而库里 0 条记录 —— 用户以为备份成功。 + - `read_file` 取不存在的列,所有导出(`export_file` / `export_file_safe` / `export_directory` / `export_filesystem`)全部失败, + 且失败信息同样是"导出文件失败",看不出真正原因。 +- **修复**:把建表列名改为与代码一致的 `compress_algo`(`core.py:155`)。 + 选择改建表而非改另外两处的原因:INSERT 与 `read_file` 拼写一致且正确;且当前代码根本插不进数据, + 磁盘上不存在任何依赖错误拼写且有内容的库。 +- **状态**:已修复。回归用例:`test_core.SchemaContractTest.test_expected_columns_exist`、 + `test_add_file_actually_stores_a_row`、`test_roundtrip.RoundTripTest.test_every_source_file_is_stored`。 + +### P0-2 `list_directory` 用 `path` 而不是 `parent_path` 过滤,导致导出无限递归 + 数据丢失 + +- **位置**:原 `core.py:210-230` +- **根因**: + - 非根分支写的是 `WHERE path = ?`,返回的是**目录自己那一行**,而不是它的子项; + - 根分支 `WHERE` 条件为空,返回**整张表**(把深层条目也当成顶层条目)。 +- **影响**:`export_directory` 遍历 `list_directory(p)` 拿到的是 `p` 自己, + 于是 `export_directory(p, 更深路径)` 无限自我递归。实测结果是磁盘上生成几百层 + `sub/sub/sub/...` 垃圾目录,**子目录里的文件一个都导不出来**,最后以 `RecursionError` 或 + 路径过长错误收场。这是"解包出来是空的"的直接原因,也是测试耗时 175 秒的原因。 + 顶层文件之所以还能导出一部分,是因为 `_has_root_directory()` 为假、走了 `_export_root_contents` 这条旁路。 +- **修复**: + - `list_directory` 先做路径归一化,根目录用 `parent_path IS NULL OR = '' OR = '/'`,其余用 `WHERE parent_path = ?`(`core.py:323-353`); + - `export_directory` / `_export_root_contents` / `export_filesystem` 补 `ignore_timestamps` 参数并统一走 `_export_one`(`core.py:745`、`:799`、`:852`)。 +- **状态**:已修复。回归用例:`test_core.DirectoryListingTest`(5 个)、 + `test_export_subdirectory_does_not_recurse_forever`、`test_cli.test_list_root_only_shows_top_level`。 + +### P0-3 `--ignore-timestamps` 把 `export_filesystem` 赋值成引用自身的 lambda,必然 RecursionError + +- **位置**:原 `cli/unpacker.py:63-69` +- **根因**: + ```python + unpacker.sqlite_vfs.export_filesystem = ( + lambda export_root: unpacker.sqlite_vfs.export_filesystem(export_root) + ) + ``` + 把实例属性覆盖成 lambda,而 lambda 体内取的还是这个实例属性,于是自己调自己。 + 实例属性一旦赋值还会永久污染该实例。 +- **影响**:`vfsunpack db out --ignore-timestamps`(不带 `--path`)100% 崩溃。 +- **修复**:删掉这段 monkey-patch。改为把 `ignore_timestamps` 作为正式参数贯穿 + `unpack_filesystem` → `export_filesystem` → `export_directory` → `_export_one` → `export_file_safe`, + 该选项这才真正具备"时间戳设置失败也照样导出内容"的语义。 +- **状态**:已修复。回归用例:`test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse`、 + `test_roundtrip.test_roundtrip_with_ignore_timestamps`。 + +### P0-4 `add_file` 缺少 `conn.commit()` + +- **位置**:原 `core.py:147-196`(整个方法内没有任何 commit) +- **根因**:sqlite3 默认是显式事务,INSERT 后不 commit,数据只在连接内的未提交事务里。 +- **影响**:`vfs.add_file(...)` 之后直接 `close()`,`close()` 不提交、直接回滚 → **数据静默丢失**。 + 打包流程里之所以没暴露,纯粹是因为后续 `update_vfs_metadata` 恰好调了一次 commit —— 属于巧合而非设计。 +- **修复**:`add_file` / `add_directory` 写入后立即 `commit()`,异常时 `rollback()`,并返回成功与否(`core.py:235`、`:199`)。 +- **取舍**:这样打包变成"每个文件一次事务提交",大目录树会变慢。正确性优先; + 若要恢复批量提交,应显式开启事务(见第八节建议),而不是依赖"忘了 commit"。 +- **状态**:已修复。回归用例:`test_core.PersistenceTest.test_add_file_survives_close_without_further_commit`。 + +--- + +## 四、P1 缺陷(正确性 / 静默损坏 / 可用性,均已修复) + +### P1-1 `LIKE path || '%'` 前缀误伤,且 `%` `_` 未转义 + +- **位置**:原 `core.py:429-435`(`_remove_directory`)、`:389-403`(`mv_dir` 子树改名) +- **根因**:`LIKE 'doc%'` 会把 `docs`、`doc2` 一并匹配;路径里的 `%` 和 `_` 是 SQL 通配符,未做转义。 +- **影响**:`remove("doc")` 会连 `docs` 一起删掉;`a_b.txt` 的删除会波及 `axb.txt`;`mv_dir` 同理。 + 这是"删一个目录,连带删掉名字相似的兄弟目录"这类最难排查的数据损坏。 +- **修复**:统一改成"精确相等 **或** 以 `src + '/'` 开头",并用 `ESCAPE '\'` 转义 `%` `_` `\` + (新增 `_escape_like`,`core.py:189`;使用点 `core.py:565`、`:612`)。 +- **状态**:已修复。回归用例:`test_remove_directory_does_not_touch_prefix_sibling`、 + `test_remove_file_with_underscore_is_not_a_wildcard`、`test_remove_file_with_percent_is_not_a_wildcard`、 + `test_mv_dir_does_not_touch_prefix_sibling`。 + +### P1-2 `mv_dir` 的第二条 UPDATE 会二次命中刚改名的目录自身 + +- **位置**:原 `core.py:388-403` +- **根因**:第一条 UPDATE 已把 `doc` 改成 `doc_new`,第二条 UPDATE 的条件是 `path LIKE 'doc%'`, + 而 `doc_new` 正好以 `doc` 开头 → 再次被改名,`doc_new` + `SUBSTR('doc_new', 4)` = `doc_new_new`。 +- **影响**:任何"新名字以旧名字开头"的目录移动都会把路径改坏。 +- **修复**:前缀匹配加上 `/` 边界(`path LIKE 'doc/%'`,`core.py:565`),并在入口显式拒绝"移动到自己的子目录" + (`core.py:510`)。 +- **状态**:已修复。回归用例:`test_mv_dir_rename_is_not_applied_twice`、`test_mv_dir_into_itself_raises`。 + +### P1-3 `mv_dir("/", dest)` 根目录分支的三处错误 + +- **位置**:原 `core.py:343-368` +- **根因与影响**: + 1. `parent_path = dest || '/' || parent_path`,对顶层条目(`parent_path` 为空串)得到 `dest/`, + 多了个尾斜杠 → 之后按 `parent_path = 'dest'` 查询不到这些条目; + 2. 两条 UPDATE 分别用 `is_directory = 1` 和 `is_directory = 0` 过滤,逻辑不一致、易漏; + 3. **从不创建 `dest` 目录行** → 移完之后目标目录在库里根本不存在,整个子树变成不可达。 +- **修复**:先 `_insert_directory_row` 建目标目录行(并在 UPDATE 中用 `path != ?` 排除它,避免被二次前缀), + 再用一条 UPDATE 统一处理所有条目,`parent_path` 为空/NULL/`/` 时置为 `dest`、否则前置 `dest/`; + 同时拒绝"把根目录内容移进 `a/b` 这种嵌套目标"这种自引用写法(`core.py:497-576`)。 +- **状态**:已修复。回归用例:`test_mv_dir_root_creates_destination_and_fixes_parent_paths`。 + +### P1-4 唯一约束冲突无处理,事务悬挂 + +- **位置**:原 `mv_file`/`mk_dir`/`add_file`/`add_directory` +- **根因**:`path` 有 UNIQUE 约束,但没有任何一处检查目标是否存在。 +- **影响**:重复 `mk_dir` 抛裸 `IntegrityError`,且事务不 commit / rollback 就留着; + 重复 `add_file` 静默失败,调用方无法感知。 +- **修复**:`mk_dir` / `mv_file` / `mv_dir` 显式检查来源与目标,抛出带中文说明的 + `FileExistsError` / `FileNotFoundError` / `NotADirectoryError`;所有写操作统一 `commit`/`rollback`; + `add_file` 返回布尔值,重复路径返回 `False` 且不影响已有内容和后续操作(`core.py:439-576`)。 +- **状态**:已修复。回归用例:`test_core.MkDirTest`、`AddFileConflictTest`、 + `test_mv_dir_to_existing_destination_raises_and_keeps_source`、`test_mv_dir_to_missing_parent_raises`。 + +### P1-5 异常被吞 + 统计数字造假 + +- **位置**:原 `core.py:141-142`、`:195-196`;`folder_packer.py:64-67` +- **根因**:`add_file`/`add_directory` 捕获所有异常只打印;`pack_folder` 无论成功与否都 + `self.total_files += 1`,最后把"遍历计数"写进元数据。另外 `os.path.getsize` 在 try 之外。 +- **影响**:打包结果自相矛盾(报告 22 个、库里 0 个);源目录里一个坏软链或权限不足的文件 + 会让整个打包抛未捕获异常中断,留下半成品库。 +- **修复**:`pack_folder` 只统计成功项,并在遍历结束后调用 `recount()` + 直接从库里 `SELECT COUNT(*), SUM(file_size)` 取真实数字写回元数据(新增 `core.py:294`); + 文件大小不再由 packer 单独 `getsize`,交给 `add_file` 自己的 try 处理; + 同时打印"已排除 N 个 / 警告: M 个条目未能写入"。 +- **状态**:已修复。回归用例:`test_roundtrip.test_metadata_counts_match_database`、 + `test_cli.test_pack_reports_file_count_matching_database`。 + +### P1-6 `os.makedirs(os.path.dirname(local_path))` 在纯文件名时抛 FileNotFoundError + +- **位置**:原 `core.py:480`、`:671` +- **根因**:`os.path.dirname("out.txt")` 是空串,`makedirs('')` 抛 `FileNotFoundError`。 +- **影响**:`export_file("a.txt", "out.txt")` 这类"导出到当前目录"的合法用法直接失败。 +- **修复**:抽出 `_ensure_parent_dir`,仅在父目录非空时才 `makedirs`(`core.py:658`)。 +- **状态**:已修复。回归用例:`test_core.ExportTest.test_export_file_accepts_bare_filename`。 + +### P1-7 没有 schema 版本校验,旧格式库连接成功后才以"导出文件失败"收场 + +- **位置**:原 `core.py:26`、`:65-113`(只有 `CREATE TABLE IF NOT EXISTS`) +- **根因**:库已存在时 `IF NOT EXISTS` 是空操作,旧表原样保留,代码随后按新列名读写。 +- **影响**:仓库自带的 `appcation.svfs`(旧结构:`content BLOB` + `compressed BOOLEAN`, + 没有 `freq_blob`/`compress_algo`)完全不可用,但打开时不报错, + 只在每个文件导出时打印一行"导出文件失败",用户完全不知道是版本问题。 +- **修复**:新增 `VFSFormatError` 异常(`core.py:12`)与 `_check_schema`(`core.py:60`): + 连接后检查 `files` 表列集合,缺列就抛出明确指出缺哪些列、并说明"该库很可能是旧版本创建的"。 + 按既定选择**只检测报错、不改动用户数据**。顺带修正:校验失败时先关闭连接再抛异常 + (原实现异常路径不关连接,Windows 上会把库文件锁住,连临时目录都删不掉 —— 我在测试清理阶段实际踩到过)。 +- **状态**:已修复。回归用例:`test_core.SchemaCompatibilityTest`(4 个)、 + `test_cli.test_rejects_old_schema_database_cleanly`。 + +### P1-8 所有失败路径都返回 exit code 0 + +- **位置**:原 `cli/packer.py:22-24`、`cli/unpacker.py:29-31`(用 `return` 而非 `sys.exit`); + `FolderUnpacker.unpack_filesystem` 在数据库检查失败时 `return 0` +- **影响**:命令行工具与 CI 完全无法感知失败——脚本里 `vfsunpack` 挂了也继续往下跑。 +- **修复**:校验失败、库格式错误、非 SQLite 文件、导出路径不存在等情况统一 `sys.exit(1)` 并打印中文错误; + `unpack_filesystem` 在数据库检查失败时抛 `RuntimeError` 由 CLI 转成退出码(`cli/packer.py:26-58`、`cli/unpacker.py:33-89`)。 +- **状态**:已修复。回归用例:`test_cli.CliFailureTest`(5 个)。 + +### P1-9 `--exclude` 实为 basename 子串匹配,README 承诺的是 glob + +- **位置**:原 `folder_packer.py:21-27` +- **根因**:`if pattern in filename` —— 子串包含判断。README 示例 `--exclude "*.tmp"` 永远匹配不到任何文件。 + 而且只跳过目录项本身,`os.walk` 仍会走进被"排除"的目录。 +- **修复**:改用 `fnmatch`,同时匹配相对路径与文件名;在 `os.walk` 中原地剪枝 `dirs[:]`, + 被排除的目录整棵不再遍历(`folder_packer.py:31-42`、`:66`)。 +- **状态**:已修复。回归用例:`test_cli.test_exclude_supports_glob_patterns`、 + `test_exclude_prunes_directory_subtree`。 + +### P1-10 `FolderPacker.add_file` 用库内内容覆盖用户的源文件 + +- **位置**:原 `folder_packer.py:95` +- **根因**:`self.sqlite_vfs.export_file_safe(rel_path, abs_path)` —— 参数顺序是 `(vfs 路径, 本地路径)`, + 这里却把"源文件的绝对路径"当成了输出目标,等于把库里的内容写回源文件。 + 紧跟其后的两行还打印了两个互相矛盾的"已保存到"。 +- **影响**:目前 `pack_folder` 没走这条路,属于潜伏地雷,但任何库使用者调用一次就会损坏源文件。 +- **修复**:删掉该调用,`FolderPacker.add_file` 只做写入库这一件事(`folder_packer.py:122`)。 +- **状态**:已修复。 + +### P1-11 `FolderPacker.add_directory` 实参顺序颠倒 + +- **位置**:原 `folder_packer.py:99-101` +- **根因**:调用 `add_directory(rel_path, abs_path)`,而 core 的签名是 `(rel_path, source_folder)` + 且内部执行 `os.path.join(source_folder, rel_path)` → 拼出的路径无效,`os.stat` 必然失败。 +- **修复**:新增 `_source_root(abs_path, rel_path)` 反推源根目录,保证 + `os.path.join(root, rel_path) == abs_path`(`folder_packer.py:12`、`:139`)。 +- **状态**:已修复。 + +### P1-12 输出数据库会被自己打包,导致自包含 + +- **位置**:`folder_packer.py`(原实现完全没有这个判断) +- **根因**:`vfspack . ./out.db` 时 `os.walk` 会走到 `out.db`,而它正是当前正在写入的数据库文件。 +- **影响**:读一个正在被 SQLite 写入的文件,结果不可预期(重复打包时库体积翻倍,或读到损坏页)。 +- **修复**:与 `db_path` 绝对路径相同的文件直接跳过并打印提示(`folder_packer.py:61`、`:89`)。 +- **状态**:已修复(这一条是在写测试用例时新发现的,不在最初列出的清单里)。 + +--- + +## 五、测试套件 + +位置 `tests/`,**纯标准库 `unittest`,零新增依赖**(`unittest.TestCase` 同时可被 pytest 运行)。 +当前 `.venv` 里没有 pytest,dev 依赖组只有 twine,所以不想为了跑测试引入网络依赖。 + +```bash +# 在仓库根目录 +.venv/Scripts/python.exe -m unittest discover -s tests -v +# 或(若安装了 pytest) +pytest tests/ -v +``` + +| 文件 | 用例数 | 覆盖内容 | +|---|---|---| +| `tests/helpers.py` | — | 路径引导、临时目录(含超长路径清理)、目录树构造与差异比对、打包/解包/CLI 封装、原始 SQL 造数据 | +| `tests/test_huffman.py` | 15 | 往返(空/单字节/单符号/双符号/全 256 字节值/不可压缩随机/中文/阈值边界)、频率表长度、魔数与截断校验 | +| `tests/test_core.py` | 52 | schema 契约、事务持久化、读写往返与压缩分支、目录列举语义、移动/删除、唯一约束、导出、旧格式库拒绝 | +| `tests/test_roundtrip.py` | 8 | 端到端打包→解包逐字节比对;含空目录、空文件、中文与空格文件名、`%`/`_` 文件名、`doc`+`docs` 兄弟目录 | +| `tests/test_cli.py` | 34 | 子进程跑四个 CLI:退出码、`--list-root`、`--path`、`--ignore-timestamps`、`--exclude`、快照选项、`vfsverify`、`vfsmigrate` | +| 小计(缺陷修复部分) | **81** | | +| `tests/test_fastapi_static.py` | 51 | 可选插件(见第九节),未安装框架时整类跳过 | +| `tests/test_contrib_optional.py` | 5 | "核心包不依赖第三方框架"的契约,有/无框架两种环境都运行 | +| `tests/test_dedup.py` | 16 | 相同内容合并、跨快照共享、GC 时机、重打不增长 | +| `tests/test_snapshots.py` | 22 | 建/切/删快照、时间旅行、派生复制、按快照导出与统计隔离 | +| `tests/test_integrity.py` | 24 | 摘要稳定性、两级 verify、篡改检测、HMAC 签名与只读验签 | +| `tests/test_migration.py` | 22 | v1/v2 迁移、备份、幂等、去重收益、失败时原库不动 | +| `tests/test_incremental.py` | 21 | 增/改/删/类型互换、不重写未变文件、rehash 抓同秒同大小修改、新快照 | +| `tests/test_publish.py` | 29 | 对象去重导出、清单确定性与校验、真实 HTTP 取回、缓存自愈、损坏检测、统计口径 | +| `tests/test_compression.py` | 12 | 算法分发层:注册表、三种算法往返、未知算法报错、取舍不被退化 | +| `tests/test_core.py` 追加 | +30 | 时间戳存储契约、read_file 边界、blob 原语、add_directory replace、clean、is_file | +| `tests/test_cli.py` 追加 | +1 | `main.py` 冒烟(防止示例脚本腐烂) | +| **合计** | **343** | | + +两个设计取舍值得说明: + +1. **目录语义类用例用原始 SQL 造数据**(`helpers.seed_rows`),只写新旧 schema 共有的列。 + 这样"`list_directory` 语义错误"不会因为"列名拼写错误"而连带失败,失败原因单一、定位快。 +2. **`test_huffman.py` 的作用是排除法**:它在修复前后都是 15/15 通过,说明哈夫曼算法本身没问题, + 所有故障都出在 `core.py` 的集成层。这省掉了"是不是压缩算法写错了"的排查成本。 + +> 说明:`docs/evidence-before.txt` 的红色证据是 77 个用例版本下的结果(sessions 中测试集还在完善)。 +> 定稿为 81 个用例,其中 4 个是修复过程中为新增边界补的 +> (`test_single_symbol_payload_is_not_validated`、`test_mv_dir_to_missing_parent_raises`、 +> `test_mv_dir_into_itself_raises`、`test_stored_blob_of_compressible_file_is_smaller`)。 +> 修复后的 81 个用例全绿。 + +--- + +## 六、P2 / P3:清单(本次未修改;P2-7 与 P3-5 因后续结论已处理,见第九、十节) + +### P2(健壮性 / 兼容性,建议尽快处理) + +| 编号 | 位置 | 问题 | 建议 | +|---|---|---|---| +| P2-1 | `core.py:585` | `get_stats` 硬编码 `WHERE id = 1`;若元数据表里有多行(例如未 `clean()` 就再次 `add_vfs_metadata`),返回的不是最新那行 | 改成 `ORDER BY id DESC LIMIT 1` | +| P2-2 | `core.py:639`、`:644` | `is_dir`/`is_file` 返回 `1` / `None` 而不是布尔值(`file_info and file_info[...]` 直接返回了原值) | 外层加 `bool(...)` | +| P2-3 | `core.py:235`、`:377` | 大文件整个 `f.read()` 进内存后再压缩、再交给 SQLite;解包时也是整块读出来再切片 | 大文件改为分块读写;`read_file` 的 `offset/size` 用 SQL `substr()` 下推到数据库 | +| P2-4 | `core.py:222`、`:275`、`:422` | 时间戳用 `datetime.fromtimestamp` / `datetime.now()` 存的是**本地时间**(README 却写"UTC");且向 sqlite3 传 `datetime` 对象自 Python 3.12 起已 `DeprecationWarning`(`.venv` 的 `sqlite3/dbapi2.py:60` 确认),当前解释器是 3.13。跑测试时该警告已实际出现:`core.py:334 / :495: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12` | 统一存 UTC 并显式 `isoformat()` 成字符串;或注册自定义 adapter。这是剩余 P2 里最该先修的一条——它在 3.12+ 已经可见,且属于"将来某次升级就会坏"的类型 | +| P2-5 | `core.py:70-71` | `_check_schema` 只校验列存在性,不校验列类型,也没有版本号 | 增加 `schema_version` 表或 `PRAGMA user_version`,为将来升级留出通道 | +| P2-6 | `huffman.py:176` | 单符号数据解压走快捷路径,完全不读 payload,**payload 被截断也检测不出来**;整体也没有任何校验和 | 若要防损坏,可加校验和;至少已知这是"无完整性校验"的设计 | +| P2-7 | `huffman.py:115`、`:131` | 每个文件固定带 1024 字节频率表(1.5KB 文件压完反而 120%);`struct.pack('<256I')` 对超过 4GB 的文件会因 uint32 溢出抛 `struct.error` | **已按第九节的结论处理**:默认算法换成 zlib 后,频率表只在使用 `compress_algorithm='huffman'` 时才产生;huffman 现在仅为读旧包而保留。4GB 溢出仍存在于 huffman 路径 | +| P2-8 | `folder_packer.py:56`、`:72` | `os.walk` 不跟随目录符号链接(默认行为,符合预期),但文件型软链会被 `open` 跟随读取,坏软链只在 `add_file` 内部被捕获并计入失败列表 | 明确策略并在文档里说明 | +| P2-9 | `cli/unpacker.py:22` | `--debug` 选项被解析但从未使用 | 实现它或删除 | +| P2-10 | `Scripts/clean_pycache.py` | 无确认地对 `./` 下所有 `__pycache__` 执行 `shutil.rmtree` | 加 `--dry-run` 或范围限制 | + +### P3(工程配置与文档) + +| 编号 | 位置 | 问题 | 建议 | +|---|---|---|---| +| P3-1 | `pyproject.toml` `[tool.uv.workspace]` | `members = ["devmo"]`,但仓库里**没有 `devmo` 目录** | **更正**:本报告早先写的"`uv sync` 会直接失败"是**错的**。实测 `uv sync --dry-run` 退出码 0、正常解析 97 个包——uv 0.11.8 容忍不存在的 workspace 成员。所以这是**死配置(有误导性),不是阻塞项**。教训记在这里:当时只验证了"目录不存在"这个前提,没验证"所以会失败"这个推论 | +| P3-2 | `dist/sqlite_vfs-0.1.0-py3-none-any.whl` | 2025-11 的旧 wheel,里面是**未集成哈夫曼**的 `core.py`,且不含 `huffman.py`。装上它会得到与源码不兼容的行为 | 删除过期的 `dist/` 产物,或在 CI 里重建 | +| P3-3 | 整个包 | `huffman.py` 当前**未被 git 跟踪**(`?? src/sqlite_vfs/huffman.py`),而 `core.py:9` 直接 `from .huffman import ...`。任何人 clone 一份都会 `ImportError`,整包不可用 | 尽快 `git add` | +| P3-4 | `README.md:69-79`、`:139` | 项目结构写成 `src/svfs/`,示例代码也写 `from svfs.core import SQLiteVFS`,实际包名是 `sqlite_vfs` | 改为 `sqlite_vfs` | +| P3-5 | `README.md`「性能说明」 | "使用 zlib 压缩,压缩级别为 6" —— 当时其实是错的(源码已换成哈夫曼),但**说明版本迭代时的真实情况是:zlib 才是更优选择**,详见第九节 | 已修正:现在默认算法确实是 zlib,该行连同阈值、"压不动自动回退"一起重写 | +| P3-6 | `README.md:131-132` | "运行测试 `pytest`" 但当时仓库里没有任何测试 | 已补上 `tests/`(本次),可把命令改为 `python -m unittest discover -s tests` | +| P3-7 | `README.md:39-66` | `--path images/` 这种带尾斜杠的写法现在可用(路径已归一化),`--list-root` 语义也已明确为"只列顶层" | 文档可补充说明 `--path` 支持文件与目录、目录按名导出到 `<导出目录>/<目录名>` | +| P3-8 | `README.md:110-123` | `files` 表字段列表原写 `compressed: 是否压缩`,与实际 schema 不符 | 本次已更新为 `freq_blob` / `compress_algo` | +| P3-9 | 原 `core.py:96` → 现 `core.py:152` | 建表 `permissions INTEGER DEFAULT 0644` 中的 `0644` 在 SQLite 里是**十进制 644**(= 0o1204),不是 0o644 | 本次已顺带改为 `420`(0o644 的十进制)并加注释。之所以顺手修是因为它就在我改的那行隔壁,且当前所有 INSERT 都显式传 `st_mode & 0o777`,所以无行为变化 | +| P3-10 | `core.py:81` | `clean()` 不清理 `sqlite_sequence`(`files` 表用了 AUTOINCREMENT,重复打包 id 会一直涨) | 需要时加 `DELETE FROM sqlite_sequence WHERE name='files'` | +| P3-11 | 包根 | 没有 `__init__.py` 导出(`__init__.py` 为空),`import sqlite_vfs; sqlite_vfs.SQLiteVFS` 会 `AttributeError`;也没有 `__main__.py`,`python -m sqlite_vfs` 不可用 | 在 `__init__.py` 里 re-export 公共 API | +| P3-12 | `core.py:872`、`:875` | `demo_usage()` 里 `SQLiteVFS(...)` 之后又调了一次 `vfs.connect()`(构造函数已经连过),会泄漏一个连接 | 删掉多余的 `connect()` | +| P3-13 | `core.py:863` | `export_filesystem` 打印"共导出 N 个文件/目录",但 N 来自 `export_directory` 的计数,只统计文件、不含目录 | 文案或计数二选一对齐 | + +--- + +## 七、本次变更清单 + +| 文件 | 变更 | +|---|---| +| `src/sqlite_vfs/core.py` | 修复 P0-1~P0-4、P1-1~P1-8;新增 `VFSFormatError`、`_check_schema`、`_normalize`、`_escape_like`、`recount`、`_parent_of`、`_basename_of`、`_require_parent_dir`、`_insert_directory_row`、`_ensure_parent_dir`、`_apply_timestamps`、`_apply_permissions`、`_export_one`;`export_file`/`export_file_safe` 合并到 `_export_file`(删掉约 90 行重复代码)。后续为可选插件追加(见第九节):`readonly` / `check_same_thread` 两个构造参数、`_readonly_uri`、`@_writable` 写保护 | +| `src/sqlite_vfs/folder_packer.py` | 修复 P1-5、P1-9、P1-10、P1-11、P1-12;`--exclude` 改 glob + 目录剪枝;统计改为从库里 `recount()` | +| `src/sqlite_vfs/folder_unpacker.py` | `unpack_filesystem`/`unpack_directory` 支持并透传 `ignore_timestamps`;数据库检查失败改为抛异常(供 CLI 转退出码) | +| `src/sqlite_vfs/cli/packer.py` | 修复 P1-8;新增源目录/输出目录校验、覆盖警告、数据库异常处理 | +| `src/sqlite_vfs/cli/unpacker.py` | 修复 P0-3、P1-8;删除自引用 lambda;`--path` 不存在时明确报错 | +| `src/sqlite_vfs/contrib/`(新增,见第九节) | 可选插件子包:`__init__.py` + `fastapi_static.py` | +| `pyproject.toml` | 新增 `[project.optional-dependencies] fastapi`,把 FastAPI/Starlette 声明为可选依赖而不是固定依赖 | +| `README.md` | 更新 `files` 表字段列表(P3-8)、新增"可选集成:FastAPI 静态文件服务"章节;其余陈旧内容见第六节 | +| `tests/`(新增 12 个文件) | 270 个用例(核心回归 81 + 插件集成 51 + 可选性契约 5 + 去重/快照/完整性/迁移/增量 133,见第十一节) | +| `docs/`(新增 3 个文件) | `evidence-before.txt`、`evidence-after.txt`、`evidence-root-cause.txt` | + +未改动:`huffman.py`(算法本身经 15 个用例验证无误)、`appcation.svfs`、`dist/`、`Scripts/`、`resources/`、`uv.lock`。 +未执行 git commit。 + +--- + +## 八、后续建议 + +1. **先提交再改**:`huffman.py` 目前未被 git 跟踪,而 `core.py` 已经 import 它。 + 现在的仓库状态是"clone 下来直接 ImportError"。建议优先提交这两个文件。 +2. **把测试纳入流程**:README 声称用 pytest 但仓库没有测试,现在有了。建议在 CI 里跑 + `python -m unittest discover -s tests`,上面 12 个 P1 里有 8 个是"静默失败"类型, + 只有自动化测试能拦住。 +3. **批量提交与正确性的取舍**:`add_file` 现在每写一个文件提交一次。如果需要打包大目录树时的吞吐, + 正确的做法是在 `pack_folder` 里显式包一个事务,并在结束时统一 commit + rollback, + 而不是回到"忘了 commit、靠别人顺手提交"的状态。 +4. **不要吞异常**:`except Exception: print(...)` 是这次最严重问题的放大器—— + 它把一个"必然失败"变成了"看起来成功"。建议至少把失败条目累计起来,在收尾时以非 0 退出码反馈 + (`pack_folder` 现在会返回成功与否,CLI 也会 `sys.exit(1)`)。 +5. **`permissions` 与跨平台**:Windows 上 `st_mode & 0o777` 是 Python 合成出来的值, + 导出到 Linux 后权限可能与预期不符;`os.chmod` 在 Windows 也只对只读位有效。 + 若这个工具要跨平台搬文件,建议在文档里说明这一点。 + +--- + +## 九、后续追加(一):FastAPI 静态文件服务(可选插件) + +在缺陷修复之外,追加了一个把 `.svfs` 当作静态文件目录服务出去的可选插件。 +它**不改变**上面的结论,但确实动到了 `core.py`,因此在此登记。 + +| 文件 | 说明 | +|---|---| +| `src/sqlite_vfs/contrib/__init__.py` | 可选插件子包说明;刻意不导入任何插件模块 | +| `src/sqlite_vfs/contrib/fastapi_static.py` | `SVFSStaticFiles`:复用 `StaticFiles` 的路由决策,替换字节来源 | +| `pyproject.toml` | 新增 `[project.optional-dependencies] fastapi` | +| `src/sqlite_vfs/core.py` | 追加两个**向后兼容**的构造参数 `readonly`(默认 False)、`check_same_thread`(默认 True),新增只读 URI 构造函数与 `@_writable` 写保护装饰器。默认值不变,原有调用方行为不受影响 | +| `tests/test_fastapi_static.py` | 46 个用例:真实 ASGI 栈(FastAPI + TestClient)跑 HTTP 语义 | +| `tests/test_contrib_optional.py` | 5 个用例:断言"核心包不依赖第三方框架"这一契约,在有无 FastAPI 的环境下都会运行 | + +关于"可选依赖"这一约束,用测试锁死而不是靠约定: + +* `import sqlite_vfs` / `core` / `contrib` 后,`sys.modules` 里不能出现 + `fastapi` / `starlette` / `anyio` / `pydantic`(子进程断言); +* `contrib` 子包不能自动导入插件模块(否则缺依赖时整包不可用); +* 缺少 FastAPI 时导入插件必须给出可照做的报错(含 `pip install "sqlite-vfs[fastapi]"`)。 + +验证结果: + +``` +项目 venv(未安装 FastAPI) : Ran 132 tests OK (skipped=47) # 插件用例整类跳过 +临时 venv(已安装 FastAPI) : Ran 132 tests OK (skipped=1) # 只剩"缺少依赖提示"那一条被跳过 +``` + +性能实测(733 KB 前端产物,Python 3.13,单线程串行)——这条比缺陷本身更影响选型, +已写进 README: + +| | 数据库体积 | 打包耗时 | 服务 600KB 资源 | 顺序吞吐 | +|---|---|---|---|---| +| 不压缩 | 768 KB | 0.06 s | **7.5 ms** | 255 req/s | +| `--compress`(当时是哈夫曼) | **368 KB**(50%) | 0.80 s | **529 ms** | 30 req/s | + +> 注意:下面这个"不压缩更合适"的结论是**针对哈夫曼**得出的, +> 不是针对压缩本身。第十节把默认算法换回 zlib 之后,结论完全反转。 + +结论是:**静态服务场景不压缩更合适**。哈夫曼解码是纯 Python 实现,大资源冷启动约慢 70 倍; +`304` 路径不受影响(不读内容也不解压,实测 3.4 ms),所以配合浏览器缓存时体验良好。 +另外 `Range` 请求虽然语义正确,但也是"整份解压后再切片",成本与整份读取相同 +(`read_file` 用 SQL `substr()` 下推可优化,见 P2-3)。 + +--- + +## 十、后续追加(二):把默认压缩算法换回 zlib + +第九节那句"不压缩更合适"是**针对哈夫曼说的**,不是针对压缩本身。查 git 历史发现: +已提交版本用的本来就是 `zlib.compress(content, level=6)`,未提交的哈夫曼改造把它替换掉了, +README 里那句"使用 zlib 压缩,压缩级别为 6"因此变成了陈旧的错误说明(P3-5)。 +用标准库 zlib 重新对拍之后,结论完全反转——**zlib 是纯赢,不是取舍**: + +| 方案 | 数据库体积 | 打包耗时 | 服务 600KB 资源 | 304 | 顺序 100 次请求 | +|---|---|---|---|---|---| +| 不压缩 | 768 KB | 152 ms | 6.3 ms | 5.7 ms | 497 ms | +| **`--compress`(zlib,新默认)** | **88 KB** | 121 ms | **6.9 ms** | 5.7 ms | **431 ms** | +| `--compress --compress-algo huffman` | 368 KB | 885 ms | 587 ms | 4.3 ms | 1411 ms | + +纯编解码层面对拍(同一份语料): + +| 语料 | huffman | zlib level=6 | +|---|---|---| +| 真实代码文本 71.8 KB | 50.0 K(70%)· 解压 109 ms | **18.9 K(26%)· 0.6 ms** | +| 高度重复文本 640 KB | 331.1 K(52%)· 解压 772 ms | **1.9 K(0.3%)· 1.2 ms** | +| 近似构建产物 630 KB | 531.2 K(84%)· 解压 1176 ms | **309.3 K(49%)· 2.1 ms** | +| 小文件 1.5 KB | 1.8 K(120%,反而变大) | **0.05 K(3%)** | + +哈夫曼只有熵编码、没有 LZ77 滑动窗口,所以吃不到"重复"这个语料里最主要的冗余; +它慢在两个数量级上,原因是纯 Python 位操作(0.5~0.8 MB/s vs zlib 的 300~600 MB/s)。 + +改动内容: + +| 文件 | 说明 | +|---|---| +| `src/sqlite_vfs/compression.py`(新增) | 把算法选择收敛到一处:`compress_with` / `decompress_with` / `stored_size`,`core.py` 只按 `compress_algo` 分发 | +| `src/sqlite_vfs/core.py` | 新增 `compress_algorithm` 参数(默认 `'zlib'`)、`COMPRESS_MIN_SIZE`(zlib 64 字节 / huffman 1025 字节,保持旧行为),读写分支改为调用 `compression`;构造时校验算法名 | +| `src/sqlite_vfs/cli/packer.py` | 新增 `--compress-algo {zlib,huffman}`,默认 zlib | +| `README.md` | 重写「要不要用 --compress」(三方案实测表 + 结论反转)、修正 P3-5 与「注意事项 2」(时间戳其实是本地时间,不是 UTC)、新增「旧格式迁移」配方 | +| `tests/` | 新增/改写 17 个用例:zlib 与 huffman 各自往返、zlib 体积必须小于 huffman、构造期与读库时的未知算法报错、阈值行为(zlib 64 字节 / huffman 1025 字节)、CLI `--compress-algo` 端到端、huffman 资源包在插件下仍可服务(含 Range 与 304) | + +**huffman 保留不删的理由是向后兼容**:任何用当前工作区版本打过的包,`compress_algo='huffman'`, +删掉解码器会让那些库再也读不出来。新包不建议再用它。 + +顺带把 P1-7 从"旧库只能报错"提升为"旧库可迁移":旧格式的压缩数据本来就是 zlib, +所以迁移只需补两列 + 把 `compressed` 映射成算法名,**数据一个字节都不用动**。 +这条配方有回归用例 `test_legacy_pack_is_readable_after_documented_migration` 兜底, +不是只写在文档里的空话。 + +--- + +## 十一、后续追加(三):存储层改造(去重 / 快照 / 完整性 / 增量) + +这次是**格式变更**,不只是修缺陷,所以单独登记。 + +### 做了什么 + +| 能力 | 实现 | +|---|---| +| 内容寻址与去重 | 内容从 `files` 搬到 `blobs(hash PRIMARY KEY)`,`hash = sha256(原始字节)`;相同内容只存一份,跨路径、跨快照共享 | +| 快照 / 时间旅行 | `files` 加 `snapshot_id`,唯一约束从 `path` 改为 `UNIQUE(snapshot_id, path)`;新增 `snapshots` 表(含每个版本的统计与摘要) | +| 防篡改 | 每文件 sha256 + 快照级摘要(对按 path 排序的全部条目做 sha256)+ `signatures` 表存 HMAC-SHA256 签名;校验可在只读连接上完成 | +| 增量打包 | `pack_folder(incremental=True)` 按 mtime+size(或 `rehash=True` 时按 sha256)比对,只写变化项,删除磁盘上已消失的路径并回收 blob | +| 迁移 | `migration.migrate()` + `vfsmigrate`:v1/v2 → v3,先备份、在临时文件上重建、原子替换;也可 `--output` 输出到新文件而不动原库 | + +### 有意做出的破坏性变更 + +1. **格式变更(v2 → v3)无法用 `ALTER TABLE` 完成**,因为 `UNIQUE(snapshot_id, path)` + 改不了约束,必须重建表。按之前"不静默改用户数据"的原则,这里给的是显式迁移工具, + 而不是打开时自动升级。旧库打开会报错并直接给出迁移命令。 +2. **`get_file_info()` 不再返回 `content` / `freq_blob` / `compress_algo`**,改为返回 + `content_hash`,并新增 `get_blob_info()` 做内省。 + 这么改的收益是实打实的:盘点时发现 `exists()` / `is_dir()` / `list_directory()` + 都在用 `SELECT *` 把整个 BLOB 拖进内存,拆表顺手把这个也修了。 + 代价是 16 处测试断言要改(已全部改完)。 + +### 过程中发现并修掉的问题 + +除了计划内的改动,实施时又踩到几个真问题: + +| # | 问题 | 影响 | +|---|---|---| +| 1 | `get_stats()` 在把 total_* 从 `filesystem_metadata` 挪进 `snapshots` 后忘了透出 | 统计恒为 0——测试直接抓到 | +| 2 | 默认快照解析成"最早"而不是"最新" | 只读会话验签落在错误的快照上,验签必然失败 | +| 3 | 增量打包对已存在的路径直接 `add_file` | 撞 `UNIQUE(snapshot_id, path)`,被 `except` 吞成"未能写入",**文件更新静默丢失**;已加 `replace=True` | +| 4 | 增量不处理"文件变目录 / 目录变文件" | 类型不一致的陈旧条目会残留 | +| 5 | `SQLiteVFS(snapshot_id=999)` 不校验存在性 | `vfsunpack --snapshot 999` 静默导出 0 个文件、退出码 0(正是本报告一直在批的那类失败) | +| 6 | `verify()` 对"没有记录摘要"的快照判为通过 | 摘要被抹掉就能掩盖篡改;改为保守的判失败 | +| 7 | 只读迁移在 Windows 上 `os.replace` 失败 | 只读文件无法被替换;补了 `--output` 模式,让只读介质上的库也能迁移 | + +### 验证 + +``` +项目 venv(未安装 FastAPI): Ran 270 tests OK (skipped=52) +临时 venv(已安装 FastAPI): Ran 270 tests OK (skipped=1) +``` + +用例分布:核心回归 81 + 可选插件 51 + 可选性契约 5 + 去重 16 + 快照 22 + 完整性 24 + +迁移 22 + 增量 21 + CLI 扩展 28(部分重叠计数,合计 270)。 + +端到端冒烟(真实 CLI): + +* 打包含两个内容相同的文件 → `files` 3 行、`blobs` **2 行**(相同内容合并) +* 旧格式库 → 报错并给出 `vfsmigrate` 命令,退出码 1 +* `vfsmigrate` → 生成 `.bak`、摘要落库、解包退出码 0 且内容一致 +* `vfsverify --check-content` → 通过;绕过 API 改一个 `permissions` → 摘要不一致、退出码 1 +* `--snapshot v2` 增量 → 旧版本原样保留,新版本"新增 0 / 更新 1 / 未变 1 / 删除 1" + +### 新增的命令行工具 + +| 命令 | 用途 | +|---|---| +| `vfspack` | 新增 `--incremental` / `--rehash` / `--snapshot NAME` | +| `vfsunpack` | 新增 `--snapshot ID` / `--list-snapshots` | +| `vfsverify` | 列快照、打印摘要、`--check-content` 权威校验、`--sign` / `--key` 签名验签、`--json` 输出 | +| `vfsmigrate` | v1/v2 → v3,支持原地(带 `.bak`)或 `--output` 输出到新文件 | + +--- + +## 十二、后续追加(四):按 hash 发布到对象存储 + +起因是查"把 .svfs 放对象存储、按 HTTP Range 按页读"这条路时发现的坑:CPython 的 `sqlite3` +**没有注册自定义 VFS 的 API**,要按页远程读必须引入 APSW 之类的编译扩展;实测那条路也不便宜 +(不做预读时读 1 MB 要 265 次往返、约 13 秒 @50ms RTT)。 + +换个思路就绕开了整个问题:库里的内容本来就按 sha256 寻址,那就把每个 blob 导出成 +一个以 hash 命名的对象,再把目录结构导出成一份小清单。 + +| 文件 | 内容 | +|---|---| +| `src/sqlite_vfs/publish.py`(新增) | `export_objects()` / `build_manifest()` 与 `ObjectFetcher`(取回 + 逐对象校验 + 缓存自愈) | +| `src/sqlite_vfs/mime.py`(新增) | 把 Content-Type 覆盖表从插件里抽出来共用,避免发布工具误引入 fastapi 依赖 | +| `src/sqlite_vfs/core.py` | 追加内容寻址读原语 `all_entries()` / `list_blobs()` / `read_blob()`;移除未使用的 `iter_blob_contents()` | +| `src/sqlite_vfs/cli/publish.py`、`cli/fetch.py`(新增) | `vfspublish` / `vfsfetch` 两个入口 | +| `tests/test_publish.py`(新增) | 29 个用例,含真实 HTTP 服务器、损坏检测、缓存自愈、硬链接、统计口径 | + +实测对比(1108 KB 包、61 文件、真实 HTTP 服务器): + +| 操作 | 按 hash 取对象 | 远程 VFS 按页读 | +|---|---|---| +| 列目录 | 1 次请求 / 13 KB(gzip 0.34 KB) | 5 次请求 / 20 KB | +| 取 0.8 KB 文件 | 1 次请求 / 0.8 KB | 9 次请求 / 36 KB | +| 取 1 MB 文件 | 1 次请求 / 1 MB | 265 次请求 / 1060 KB | + +过程中的三个发现: + +1. **对象缓存是用户级的**(`~/.cache/svfs-objects`),这让我的 CLI 测试一开始不隔离, + 出现"下载 0 个对象"的假失败——其实是跨测试的内容寻址去重生效了。测试已改为指定临时缓存。 + 这条本身说明了该设计在跨项目复用上的效果,但也提醒:默认用户级缓存的工具, + 测试必须显式隔离。 +2. **硬链接模式不能还原时间戳/权限**。硬链接与缓存对象共用 inode,`utime`/`chmod` + 会连带改到缓存,破坏"缓存对象不可变"的前提;现在 `--link` 跳过这两步并写进文档。 +3. **构造期校验失败会泄漏连接**(core 的真缺陷,不是测试问题)。`SQLiteVFS(db, snapshot_id=999)` + 在 `__init__` 里校验快照并抛 `FileNotFoundError`,但那时 `connect()` 已经成功, + 连接没人释放——Windows 上库文件从此删不掉(就是测试临时目录残留暴露出来的)。 + 已按 `connect()` 里同样的方式补上 `close()`,并加了回归用例。 + +另外把 MIME 覆盖表从 `contrib/fastapi_static.py` 抽到 `mime.py`:原来的位置会让发布工具 +为了一个 Content-Type 表而依赖 fastapi(可选依赖),这属于"顺手踩到的耦合", +抽出来两边共用一份,也避免了两处漂移。 + +签名这条线也顺下来了:清单里带快照摘要,所以可以用 `vfsverify --sign` 签完摘要后把清单一起发布—— +客户端既逐个校验对象的 sha256,也能用公开摘要核对整棵树的版本。 + +--- + +## 十三、后续追加(五):清理 P2-4、补齐直接单测、定下算法扩展点 + +按"会不会咬人"的顺序做收尾,这一节的四件事都是纯收益。 + +### 1. P2-4 清掉了:1240 条弃用警告 → 0 + +向 sqlite3 传 `datetime` 对象自 Python 3.12 起弃用(将来会移除),实测**一次测试跑 +产生 1240 条 `DeprecationWarning`**。修法是显式转换: + +```python +def _db_time(value): + """必须显式转换:sqlite3 的默认 datetime adapter 已弃用。""" + return None if value is None else value.isoformat(sep=" ") +``` + +关键点是**盘上格式一个字节都没变**——当年 adapter 的实现就是 `val.isoformat(" ")`, +所以现有库、旧包、以及依赖 `modified_time` 的快照摘要全都不受影响。两条用例钉住: +一条用 `simplefilter("error", DeprecationWarning)` 把所有写入路径跑一遍(再出现就直接失败), +一条断言落库文本仍然匹配 `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$`。 + +### 2. 补直接单测:这些 API 之前只有间接覆盖 + +盘点发现 66 个公开 API 里,多数只通过 packer/CLI/publish 路径被间接覆盖过。 +端到端测试价值更高,但**重构核心方法时没有安全网**(比如把 `read_file` 改成分块读取, +现有用例不一定抓得住细微语义变化)。补了 30 个直接用例,覆盖: + +* `read_file` 的 offset/size 边界(0、越界、等于 EOF、超出剩余)+ 压缩内容下的边界; +* `list_blobs` / `read_blob` / `all_entries` / `get_blob_info` 四个内容寻址原语; +* `add_directory` 的返回值与 `replace` 语义、失败不留半条记录; +* `clean()` 真的清空全部五张表;`exists/is_dir/is_file` 三者一致(`is_file` 之前 + 在 src 里**没有任何调用方**,属于死 API,现在至少被钉住了)。 + +顺手加固了两个契约边界: + +* `read_file(offset=-3)` 之前会走 Python 切片语义**从末尾取**——对调用方是陷阱,现在报错; +* `get_blob_info(目录)` 之前返回一堆 `None` 字段,现在返回 `None`。 + +### 3. 算法层定成明确的扩展点(配合"自定义算法 + 后续加密") + +`compression.py` 的 docstring 现在写清了契约与扩展步骤(往 `ALGORITHMS` 加名字、 +两个分支、`COMPRESS_MIN_SIZE`、补往返用例),并点出一个对加密有用的既成事实: +**`blobs.freq_blob` 本质是通用的"每块附加数据"槽**,加密的 nonce + 认证标签可以直接 +放这里,不必改 schema。新增 `tests/test_compression.py`(12 个用例)把契约钉住, +其中一条专门断言"zlib 对重复文本的体积优势必须在一个数量级以上"—— +防止默认算法被悄悄改回哈夫曼。 + +### 4. 加密的设计说明:两件事必须先定 + +写成 `docs/encryption-design.md`,因为它们在写代码前就要决定,事后发现要返工: + +* **去重与随机 nonce 冲突**。`blobs.hash` 是 `sha256(明文)` 且为主键,去重全靠它。 + 确定性(收敛)加密能保留去重与稳定的对象名,代价是确认攻击;每块随机 nonce 更安全, + 但对象名不能再是明文 hash,`vfsfetch` 的"名字即 sha256"校验和 CDN 缓存语义都要跟着改。 +* **标准库没有密码学原语**。所以"尽可能高效"指向 `cryptography` 的 AES-GCM(AES-NI, + 1–5 GB/s)而不是自己写——纯 Python AES 慢三个数量级,自己写密码学本身也是风险。 + 另外三条纪律:先压缩再加密、必须做认证、密钥不要进库。 + +### 扫尾 + +`pyproject.toml` 里 `[tool.uv.workspace] members = ["devmo"]` 是死配置(uv 容忍它, +所以不是阻塞项,已删并留注释);`requires-python` 从拍出来的 `>=3.7` 改成按实际 API +推导的 `>=3.9`(`usedforsecurity=` 与 `randbytes` 各要 3.9),并注明这是代码审查结论、 +未在 3.9 上实测;`main.py` 从"引用早已不存在的路径"改成自给自足的端到端演示 +(打包→校验→快照→发布→取回→逐字节比对),并加了冒烟测试——**上一版就是没人跑它才烂掉的**。 +`dist/` 里 2025-11 的过期 wheel 我没动:那是本地未跟踪的产物,`uv build` 可重新生成, +删它属于你的决定。 diff --git a/README.md b/README.md index 4cd52fe..81ac540 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,18 @@ ## 特性 - 高效存储: 使用 SQLite 数据库存储文件和目录结构 -- 压缩支持: 可选的文件压缩功能,节省存储空间 +- 压缩支持: 可选压缩,默认 zlib(标准库 C 实现),压不动的内容自动回退为原样存储 +- 内容寻址去重: 相同内容只存一份,多个路径/多个版本共享同一份数据 +- 快照与版本历史: 一个库里存同一棵树的历史版本,可以按版本导出、按版本校验 +- 完整性校验: 每个文件带 sha256,快照级摘要 + HMAC-SHA256 签名,支持只读校验 +- 增量打包: 只写入变化的条目,未变的文件不重写、不重压、不重复存储 +- 可发布到对象存储: 导出成「按 sha256 命名的对象 + 一份小清单」,普通静态托管 / CDN 即可分发, + 客户端按清单取回并逐个校验(不需要任何编译扩展) - 完整元数据: 保留文件权限、创建时间、修改时间等元数据 - 目录结构保持: 完整的目录层级结构保持 -- 简单易用: 提供命令行工具,支持打包和解包操作 +- 简单易用: 四个命令行工具(打包 / 解包 / 校验 / 迁移),零第三方依赖 - 跨平台: 支持 Windows、Linux、macOS +- 可选集成: FastAPI 静态文件服务作为 extras 提供,核心不依赖任何第三方框架 ## 安装 ### 从源码安装 @@ -40,14 +47,26 @@ uv sync # 基本用法 vfspack <源文件夹路径> <输出数据库路径> -# 启用压缩 +# 启用压缩(默认 zlib,推荐) vfspack ./my_folder ./output.db --compress +# 指定压缩算法(zlib 默认 / huffman 仅用于兼容早期版本打的包) +vfspack ./my_folder ./output.db --compress --compress-algo huffman + # 指定文件系统名称 vfspack ./my_folder ./output.db --name "MyFileSystem" -# 排除特定文件模式 +# 排除特定文件模式(glob 语义,作用于相对路径与文件名) vfspack ./my_folder ./output.db --exclude "*.tmp" "*.log" + +# 增量打包:只写变化的条目(默认按 mtime+size 判定) +vfspack ./my_folder ./output.db --compress --incremental + +# 增量 + 按 sha256 逐文件比对(更可靠,代价是要把文件都读一遍) +vfspack ./my_folder ./output.db --compress --incremental --rehash + +# 打成新快照:保留历史版本,只写入相对上一版的变化 +vfspack ./my_folder ./output.db --snapshot v2 ``` ### 解包文件系统 ```bash @@ -63,20 +82,309 @@ vfsunpack ./output.db ./output --path "images/" # 忽略时间戳错误(仅导出文件内容) vfsunpack ./output.db ./output --ignore-timestamps + +# 列出所有快照 +vfsunpack ./output.db ./output --list-snapshots + +# 导出指定快照(默认取最新快照) +vfsunpack ./output.db ./output --snapshot 1 ``` +### 校验完整性 +```bash +# 元数据级校验:摘要是否被改动、blob 是否缺失、大小是否一致 +vfsverify ./output.db + +# 权威校验:逐文件重算 sha256(代价是读一遍全部内容) +vfsverify ./output.db --check-content + +# 列出快照与各自摘要 +vfsverify ./output.db --list-snapshots + +# 签名与验签(HMAC-SHA256,对称密钥) +vfsverify ./output.db --sign --key my-secret --key-id release-1 +vfsverify ./output.db --key my-secret --key-id release-1 + +# 输出 JSON,便于接到 CI 或外部签名工具 +vfsverify ./output.db --check-content --json +``` + +## 可选集成:FastAPI 静态文件服务 + +把 `.svfs` 直接当作 FastAPI / Starlette 的静态文件目录来用, +适合"把前端构建产物打包成一个文件分发"的场景(单文件部署、资源包随镜像走)。 + +FastAPI 属于**可选依赖**:核心功能只用标准库,`import sqlite_vfs` 不会导入任何第三方框架。 + +```bash +pip install "sqlite-vfs[fastapi]" + +# 打包前端构建产物(不需要 --compress 时见下方性能说明) +vfspack ./dist static.svfs +``` + +```python +from fastapi import FastAPI +from sqlite_vfs.contrib.fastapi_static import SVFSStaticFiles + +app = FastAPI() +statics = SVFSStaticFiles("static.svfs") # 默认 html=True, spa=True, readonly=True +app.mount("/", statics, name="static") + +@app.on_event("shutdown") +def _close(): + statics.close() +``` + +行为说明: + +- **SPA 回退**:找不到且不像静态资源(末段路径无扩展名、且客户端接受 HTML)时返回 `index.html`, + 前端路由(`/settings`、`/user/42`)可直接刷新。需要严格 404 就传 `spa=False`。 +- **目录索引**:`/sub` 会 307 重定向到 `/sub/`,然后返回 `sub/index.html`; + 与 `StaticFiles` 一致,索引文件名固定为 `index.html`。传 `html=False` 关闭。 +- **缓存**:每个响应带 `ETag` 与 `Last-Modified`,命中 `If-None-Match` / `If-Modified-Since` + 时返回 `304`(此时**不会**读取文件内容)。需要强缓存可传 `max_age=3600`。 +- **Range**:支持单段 `Range`(视频/断点续传),返回 `206`;多段请求按 RFC 允许的方式退回整份内容。 + 越界返回 `416`,语法错误则忽略该头。 +- **只读打开**:默认以 `mode=ro` 打开数据库,因此资源包放在只读挂载/容器镜像层里也能用, + 且服务过程永远不会改动资源包(有测试用 md5 校验这一点)。 +- **安全**:路径只在数据库内做精确匹配,不做磁盘路径拼接,`..` 之类无法越出资源包。 + +### 要不要用 `--compress`? + +**要,默认就该开。** 用默认的 zlib 算法时,压缩几乎不花钱(实测 733 KB 前端产物, +Python 3.13,`TestClient` 单线程串行): + +| 方案 | 数据库体积 | 打包耗时 | 服务 600KB 资源 | 304 条件请求 | 顺序 100 次请求 | +|---|---|---|---|---|---| +| 不压缩 | 768 KB | 152 ms | 6.3 ms | 5.7 ms | 497 ms | +| **`--compress`(zlib,默认)** | **88 KB** | 121 ms | **6.9 ms** | 5.7 ms | **431 ms** | +| `--compress --compress-algo huffman` | 368 KB | 885 ms | 587 ms | 4.3 ms | 1411 ms | + +zlib 把体积压到 1/8.7,而**读写延迟基本不变**——解压是 C 实现的 300~600 MB/s, +630 KB 只要 2 ms,比省下来的 I/O 还划算;打包反而更快,因为要写的字节更少。 + +`huffman` 是纯 Python 实现的历史算法(没有 LZ77,压不动"重复"),现在只为读得懂 +早期版本打的包而保留:同一份数据它体积是 zlib 的 4 倍,解压慢 85 倍。新包不要用它。 + +- `304` 路径不需要读内容也不需要解压,配合浏览器缓存时几乎零成本; +- 小于 64 字节的文件不压缩(压了反而更大),库内会记成 `none`。 + +## 迁移旧格式 + +格式演进: + +| schema | 内容存放 | 说明 | +|---|---|---| +| v1 | `files.content` + `files.compressed`(布尔) | 最早的格式,压缩用 zlib | +| v2 | `files.content` + `freq_blob` + `compress_algo` | 引入可选压缩算法 | +| **v3** | `blobs`(内容寻址)+ `files.content_hash` | 当前格式:去重、快照、完整性校验 | + +当前版本打开 v1 / v2 的库会**直接报错并给出迁移命令**,不会静默失败: + +``` +错误: old.db 是旧格式的虚拟文件系统,当前版本(schema 3)需要 blobs + snapshots 结构。 +请先迁移并保留备份:vfsmigrate "old.db" +``` + +```bash +vfsmigrate ./old.db # 原地迁移,先复制一份 ./old.db.bak +vfsmigrate ./old.db --output ./new.db # 输出到新文件,原库一个字节都不动 +``` + +迁移会读旧库、在旁边的临时文件上重建、校验通过后原子替换,所以中途失败不会留下半成品。 +迁移过程顺带按 sha256 去重:历史库里同一份内容存了多次会被合并(`vfsmigrate` 会报告省下多少字节)。 +迁移完可以用 `vfsverify --check-content` 复核每一个文件的内容。 + +## 数据能力 + +### 内容寻址与去重 + +文件内容不放在 `files` 表里,而是存进 `blobs` 表,主键是 `sha256(原始字节)`, +所以**相同内容天然只存一份**: + +```python +vfs.add_file("copy1/logo.png", "copy1/logo.png") +vfs.add_file("copy2/logo.png", "copy2/logo.png") +# blobs 表里只有一行,两个路径的 content_hash 相同 +``` + +去重是按内容算的,与路径无关,所以这些场景都省空间:同一个文件出现在多个目录、 +多个快照共享没变过的文件、重新打包一棵没变的树(内容命中的 blob 连压缩都跳过)。 + +代价是**引用计数语义**:删掉一个路径不会立刻删掉内容,只有没有任何快照再引用它时 +才会被回收(`gc_blobs()`,在 `remove()` / `clear_snapshot()` / `delete_snapshot()` +里自动执行)。 + +### 快照与版本历史 + +一个库里可以装同一棵树的历史版本。`files` 表的唯一约束是 +`UNIQUE(snapshot_id, path)`,所以同一路径在不同快照里各有一行: + +```python +vfs = SQLiteVFS("history.svfs") +vfs.add_vfs_metadata("my-app") + +# 第一版 +vfs.create_snapshot("v1") # 或直接写入,会自动建默认快照 +vfs.add_file("dist/app.js", "app.js") +vfs.recount() + +# 第二版:从 v1 派生(只复制元数据行,内容 blob 共享,所以几乎不占空间) +vfs.create_snapshot("v2", parent_id=1, copy_entries=True) +vfs.remove("app.js") +vfs.add_file("dist/app-v2.js", "app.js") +vfs.recount() + +vfs.use_snapshot(1) # 切回第一版 +vfs.read_file("app.js") # 拿到的是第一版的内容 + +for snapshot in vfs.list_snapshots(): + print(snapshot["id"], snapshot["name"], snapshot["digest"]) +``` + +配合增量打包就是一个完整的版本流程: + +```bash +vfspack ./dist app.svfs --compress # 第一版 +vfspack ./dist app.svfs --compress --snapshot v2 # 新版本,只写入变化的部分 +vfsunpack app.svfs ./restore --snapshot 1 # 按版本导出 +``` + +### 完整性校验与签名 + +每个文件都带 `sha256`,每个快照都有一份摘要(对"按 path 排序后的全部条目"做 sha256, +字段包括路径、类型、大小、时间戳、权限、内容哈希): + +```python +report = vfs.verify() # 元数据级:便宜 +report["ok"], report["errors"] + +report = vfs.verify(check_content=True) # 权威级:逐文件重算 sha256 +report["content_checked"] + +vfs.sign("my-secret", key_id="release-1") # HMAC-SHA256 +vfs.verify_signature("my-secret")["ok"] +``` + +**两个层级必须分清**: + +* `verify()` 只看元数据。同大小的内容被替换时它**发现不了**(因为 `file_size` 没变)。 +* `verify(check_content=True)` 会重算每个文件的 sha256,能发现任何内容改动,代价是读一遍全部数据。 + +`vfsverify` 的退出码可以直接用于 CI:校验失败或验签失败都返回非 0。 + +签名用标准库的 HMAC-SHA256,是**对称**的——验证方必须持有同一个密钥。 +需要非对称信任链(谁都能验、只有持有私钥的人能签)时,用 `--json` 把摘要交给 +GPG / cosign 之类的外部工具签名。 + +另外要注意摘要的语义:**走 API 的写入会在 `recount()` 时刷新摘要**,所以摘要防的是 +"绕过 API 的改动"。要当防篡改凭证用,请在打包完成后立刻签名或把摘要记到库外。 + +### 增量打包 + +```bash +vfspack ./dist app.svfs --compress --incremental +``` + +只写入变化的条目,磁盘上已消失的路径会被删除,并汇报四个计数: + +``` +增量结果: 新增 1,更新 1,未变 1,删除 1 +``` + +变更判定默认用 `mtime + size`:不读文件内容,所以很快,代价是**理论上会漏掉 +"同一秒内、同样大小"的修改**(这类场景请用 `--rehash`,它按 sha256 逐个文件比对)。 + +未变的文件不会被重写、重压或重新存储;内容相同的新路径会直接复用已有 blob。 + +### 发布到对象存储(按 hash 取对象) + +`.svfs` 是一个文件,但**不必**把它当"远程文件系统"用。更简单也更快的一条路: +既然库里的内容本来就按 sha256 寻址,就把每个 blob 直接导出成一个以 hash 命名的对象, +再把目录结构导出成一份小清单。 + +```bash +# 导出成「对象 + 清单」 +vfspublish ./app.svfs ./public + +# 同步到对象存储 / 静态托管(任选其一) +aws s3 sync ./public s3://my-bucket/app --cache-control "public, max-age=31536000, immutable" +rclone copy ./public remote:bucket/app + +# 客户端取回(对象默认从清单同级目录取,也可以指向 CDN) +vfsfetch https://cdn.example.com/app/manifest.json ./dist +vfsfetch https://cdn.example.com/app/manifest.json ./dist --path sub/ --workers 16 +``` + +产出结构: + +```text +public/ +├── blobs/cd/cd8f...e21 # 对象名 = 内容的 sha256,内容是原始(解压后)字节 +├── blobs/7a/7a41...09c +└── manifest.json # 路径 → {hash, size, mime, mtime, mode},含空目录 +``` + +这条路带来的性质: + +* **可端到端校验**:对象名就是内容的 sha256,客户端逐个核对,CDN 或中间人替换不掉内容 + (把某个对象改成同样长度的别的内容,取回时会被拒绝——有测试覆盖)。 +* **缓存可以永久**:内容不可变,对象 URL 天然适合 `max-age=31536000, immutable`。 +* **去重跨路径、跨版本生效**:相同内容只有一个对象,多个版本共享没变过的文件。 +* **不需要编译扩展**:普通静态托管 / CDN 就能用。 +* **清单可复现**:里面不放"生成时间"(用快照的创建时间),所以同一快照导出两次得到完全一样的清单, + 可以 diff、可以签名。 + +实测对比(1108 KB 的包、61 个文件、真实 HTTP 服务器): + +| 操作 | 按 hash 取对象 | 远程 VFS 按页读(APSW) | +|---|---|---| +| 列目录 | **1 次请求 / 13 KB**(清单,gzip 后 0.34 KB) | 5 次请求 / 20 KB | +| 取一个 0.8 KB 文件 | **1 次请求 / 0.8 KB**(精确等于文件大小) | 9 次请求 / 36 KB | +| 取一个 1 MB 文件 | 1 次请求 / 1 MB | 265 次请求 / 1060 KB(不做预读,约 13 秒 @50ms RTT) | + +对"发布一个资源包"这个场景,对象方案在请求数和字节数上都好一个数量级,而且不引入编译依赖。 + +**远程 VFS(按页读 SQLite)真正的用武之地是另一种需求**:一个很大、结构复杂的数据库, +客户端只随机查其中很少一部分。那种情况下"按页取"才有意义,代价是你得自己实现预读与缓存 +(实测不做预读时读 1 MB 要 265 次往返;预读 256 KB 后降到 5 次,字节数几乎不变)。 +另外 CPython 的 `sqlite3` 没有注册自定义 VFS 的 API,走这条路必须引入 APSW 之类的编译扩展。 + +已知边界: + +* 清单大小与**文件数**成正比(每文件约 150~200 字节;gzip 后约 5 字节/文件, + 任何 CDN 都会自动对 JSON 压缩)。10 万文件的清单 gzip 后约 0.5 MB——一次性成本,但值得知道。 +* `vfsfetch` 取的是**整个文件**。如果你要"取一个 2 GB 文件的中间一段", + 那属于远程 VFS 的领域,不是这条路的。 +* 清单是公开信息(路径、大小、时间戳)。要发布敏感内容,请把对象目录放在需要鉴权的位置。 +* `--link` 用硬链接落盘省空间,但**不还原时间戳与权限**:硬链接与缓存对象共用 inode, + `utime`/`chmod` 会连带改到缓存,破坏"缓存对象不可变"的前提。 + ## 项目结构 ```text -svfs/ -├── pyproject.toml # 项目配置和依赖 -├── src/svfs/ +sqlite_vfs/ +├── pyproject.toml # 项目配置、可选依赖(extras)与命令入口 +├── src/sqlite_vfs/ │ ├── __init__.py -│ ├── core.py # 核心 SQLiteVFS 类 -│ ├── folder_packer.py # 文件夹打包逻辑 -│ ├── folder_unpacker.py # 文件夹解包逻辑 +│ ├── core.py # 核心 SQLiteVFS 类(存储层 / 快照 / 完整性) +│ ├── compression.py # 压缩算法分发(zlib / huffman / none) +│ ├── huffman.py # 哈夫曼实现(仅为读早期版本打的包而保留) +│ ├── migration.py # v1 / v2 → v3 格式迁移 +│ ├── mime.py # Content-Type 覆盖表(插件与发布工具共用) +│ ├── publish.py # 按 hash 发布对象 + 清单,以及取回客户端 +│ ├── folder_packer.py # 打包逻辑(全量 / 增量 / 新快照) +│ ├── folder_unpacker.py # 解包逻辑(支持指定快照) +│ ├── contrib/ +│ │ ├── __init__.py +│ │ └── fastapi_static.py # 可选:FastAPI 静态文件服务插件 │ └── cli/ -│ ├── __init__.py -│ ├── packer.py # 打包命令行工具 -│ └── unpacker.py # 解包命令行工具 +│ ├── packer.py # vfspack +│ ├── unpacker.py # vfsunpack +│ ├── verify.py # vfsverify(校验 / 签名 / 列快照) +│ ├── migrate.py # vfsmigrate +│ ├── publish.py # vfspublish(导出对象 + 清单) +│ └── fetch.py # vfsfetch(按清单取回并校验) +├── tests/ # 单元与集成测试(纯标准库 unittest) └── README.md ``` ## 核心组件 @@ -84,42 +392,62 @@ svfs/ 主要功能类,提供以下功能: - 创建和管理 SQLite 虚拟文件系统数据库 -- 添加/删除文件和目录 -- 读取文件内容 -- 导出文件到本地文件系统 +- 添加/删除/移动文件和目录,读写文件内容 +- 内容寻址存储与去重(`get_blob_info` 可查看某个文件对应哪份 blob) +- 快照管理:`create_snapshot` / `use_snapshot` / `list_snapshots` / `delete_snapshot` +- 完整性:`compute_digest` / `verify` / `sign` / `verify_signature` +- 导出文件或整个目录树到本地文件系统 - 压缩和解压缩文件内容 +- 可以只读打开(`readonly=True`),适合把资源包放在只读介质上服务 ### FolderPacker 类 文件夹打包工具: - 遍历本地文件夹结构 - 将文件和目录信息存储到 SQLite 数据库 -- 支持文件排除模式 +- 支持文件排除模式(glob 语义,按目录剪枝) +- 支持全量与增量打包,以及"打成新快照"的版本流程 ### FolderUnpacker 类 文件夹解包工具: -- 从 SQLite 数据库恢复文件系统 +- 从 SQLite 数据库恢复文件系统(可指定快照) - 验证数据库完整性 - 支持部分导出和完整导出 ## 数据库结构 ### filesystem_metadata 表 -存储文件系统元数据,包括名称、创建时间、文件总数和总大小。 +整包信息:名称、创建时间、格式版本(`schema_version`)。 + +### snapshots 表 +每个版本的记录:名称、创建时间、父快照(`parent_id`)、文件数、总大小、摘要(`digest`)。 + +### blobs 表 +内容寻址存储,**相同内容只存一份**: + +- hash: `sha256(原始字节)` 的十六进制,主键 +- size: 原始(未压缩)字节数 +- stored_size: 实际占用字节数(含 huffman 频率表) +- content: 压缩后的数据(未压缩时就是原始字节) +- freq_blob: 哈夫曼频率表(1024 字节 = 256 个 uint32 小端),仅 huffman 算法有值 +- compress_algo: 压缩算法,'none' / 'zlib'(默认)/ 'huffman' + +### signatures 表 +快照摘要的签名记录:snapshot_id、algorithm(hmac-sha256)、key_id、signature、signed_time。 ### files 表 -存储所有文件和目录信息: +存储所有目录项(含目录本身),唯一约束是 `(snapshot_id, path)`: -- path: 文件/目录路径(唯一) -- name: 文件名 -- parent_path: 父目录路径 +- snapshot_id: 属于哪个快照 +- path / name / parent_path: 路径与层级 - is_directory: 是否为目录 -- file_size: 文件大小 -- created_time: 创建时间 -- modified_time: 修改时间 +- file_size: 原始文件大小 +- created_time / modified_time: 时间戳 - permissions: 文件权限 -- content: 文件内容(BLOB) -- compressed: 是否压缩 +- content_hash: 指向 `blobs.hash`;目录为 NULL + +文件内容不在这个表里——这样的拆分让"同一份内容被多个路径/多个快照引用"成为可能, +也让 `get_file_info` / `exists` / `list_directory` 这些元数据查询不再把 BLOB 读进内存。 ## 开发 ### 设置开发环境 @@ -135,11 +463,10 @@ uv build ``` ### 代码示例 ```python -from svfs.core import SQLiteVFS +from sqlite_vfs.core import SQLiteVFS -# 创建文件系统 +# 创建文件系统(open 时已自动连接,不必再调 connect()) vfs = SQLiteVFS("my_files.db", compress=True) -vfs.connect() # 获取文件系统统计信息 stats = vfs.get_stats() @@ -153,17 +480,25 @@ for item in vfs.list_directory("/"): vfs.close() ``` ## 性能说明 -- 压缩功能默认对大于 1KB 的文件启用 -- 使用 zlib 压缩,压缩级别为 6(平衡压缩比和速度) +- 压缩默认使用 zlib(标准库,C 实现),压缩级别为 6(平衡压缩比和速度) +- 大于等于 64 字节的文件才尝试压缩;压不动的(随机数据)自动回退为原样存储 +- 每个响应命中 `ETag` / `Last-Modified` 条件请求时返回 304,不需要读内容也不需要解压 - 文件索引优化,支持快速路径查找 -- 批量操作减少数据库事务开销 +- 各算法的实测对比见上文「要不要用 `--compress`?」 +- `verify()` 是元数据级、很便宜;`verify(check_content=True)` 要读一遍全部内容 + +> 读取大文件时,`read_file` 会整块读入内存再切片,因此超大媒体文件(几百 MB 级以上) +> 不适合放进资源包;`Range` 请求虽然语义正确,但当前实现是"整份解压后再切片", +> 成本与整份读取相同。 ## 注意事项 1. 文件路径统一使用正斜杠(/)存储,与操作系统无关 -2. 时间戳使用 UTC 时间存储 +2. 时间戳按**本机本地时间**存储(`datetime.fromtimestamp`),不是 UTC;跨时区搬运后时间含义会变 3. 文件权限使用 Unix 权限位表示 -4. 数据库文件可以在不同平台间迁移 +4. 数据库文件可以在不同平台间迁移(当前格式为 schema 3,旧库用 `vfsmigrate` 升级) 5. 大文件处理建议启用压缩以减少数据库大小 +6. 内容是**按 sha256 去重共享**的:删掉一个路径后,只有再无任何快照引用时内容才会被回收 +7. 快照摘要会被走 API 的写入刷新,所以它防的是"绕过 API 的改动";当防篡改凭证用请配合签名 ## 许可证 MIT License diff --git a/docs/encryption-design.md b/docs/encryption-design.md new file mode 100644 index 0000000..465a026 --- /dev/null +++ b/docs/encryption-design.md @@ -0,0 +1,88 @@ +# 加密设计说明(写代码之前必须先定的两件事) + +这份说明是给"后面考虑加加密"准备的。它不实现任何东西,只把两个**必须先决定**的 +约束摆在前面——因为它们都会反过来改数据结构,等到写完再发现就要返工。 + +--- + +## 一、去重与随机 nonce 是冲突的 + +这是最要紧的一条。当前设计里 `blobs.hash` 是 `sha256(原始内容)` 并且是主键, +"相同内容只存一份"完全建立在这上面。加密会直接冲击它: + +| 方案 | nonce 怎么来 | 去重 | 代价 | +|---|---|---|---| +| **A. 确定性(收敛)加密** | nonce 由 `HMAC(key, 明文hash)` 派生 | **保留** | 密文相同 ⇒ 泄露"这两份内容一样"(其实去重本来就泄露);可被确认攻击:拿到明文猜测的人能判断库里有没有这份内容 | +| **B. 每块随机 nonce** | `os.urandom` | **包内保留,跨包丢失** | 同一份内容在不同包里密文不同 → 对象名(当前是明文 hash)不再能当 URL;`vfsfetch` 的"名字即 sha256"校验失效;同一个 CDN 路径可能返回不同字节 | +| **C. 按文件加密** | 每个文件一份 | **完全丢失** | 相同的两个文件各存一份密文,体积翻倍 | +| **D. 只加密容器/对象,不动 blob** | 不适用 | 保留 | 服务方(插件/CDN 之外的那一层)拿不到明文;本地 `.svfs` 仍是明文 | + +### 我的建议 + +**先明确你要防谁**,答案不同、方案不同: + +* **防"存储方/传输方看到内容"**(S3、CDN、对象存储被拖库)→ 选 **D**:发布前把对象加密 + (或直接用带 SSE 的桶 + TLS),本地 `.svfs` 保持明文。代价最低,完全不动现有结构, + 而且**不影响去重与对象名**。这是我最推荐的起点。 +* **防"本地库被拿走也读不出"**(笔记本丢了、镜像层被人翻)→ 需要 A 或 B: + * 如果你同时要"跨包去重 + 对象名稳定"(也就是现在的发布能力),只能选 **A**, + 并接受确认攻击这个已知代价。这在"内容多半是公开前端资源"的场景里通常可以接受。 + * 如果你只要"包内去重",**B** 更安全,但发布能力要跟着改: + 对象名不能再用明文 hash,`manifest.json` 里得同时记录明文 hash 与密文 hash, + `vfsfetch` 改为校验"解密后再算 hash"(那就意味着客户端必须持有密钥)。 + +一句话:**A 与现在的发布/去重能力兼容,B 与它不兼容。这个岔路口必须先选。** + +--- + +## 二、标准库没有密码学原语,所以"高效"指向一个依赖 + +Python 标准库只有 `hashlib` / `hmac` / `secrets` / `os.urandom`——**没有任何分组密码**。 +所以: + +| 选项 | 性能(量级) | 评价 | +|---|---|---| +| `cryptography`(pyca)的 AES-256-GCM | **1~5 GB/s**(AES-NI 硬件加速) | 推荐。单一依赖、AEAD 一次过、有认证 | +| 同库的 ChaCha20-Poly1305 | 1~3 GB/s(纯软件即很快) | 没有 AES-NI 的机器上更稳 | +| 纯 Python 实现 AES | **~1 MB/s** | 不可用(慢三个数量级),且自己写密码学实现本身就是风险 | +| 用 HMAC-SHA256 / SHA-256 计数器模式自建流密码 + HMAC 认证 | 约 0.5~1.5 GB/s | 构造上是成立的(PRF 派生 keystream + encrypt-then-MAC),但**必须**自己保证"同一密钥下计数器永不重复",且要正确实现认证——省一个依赖,多一份责任 | + +**"尽可能高效"的实际含义**:用 AEAD(AES-GCM 或 ChaCha20-Poly1305), +而不是"自己写一个更快的"。自己写在 Python 里赢不了硬件指令。 + +顺带三条实现纪律: + +1. **先压缩再加密**。压缩靠的是内容里的冗余,密文是随机-looking 的,压不动。 + 顺序反了等于白压。 +2. **认证必须做**(AEAD 或 encrypt-then-MAC)。只加密不认证,攻击者可以翻动密文比特, + 密文解出来就是"看起来合法但已被改动"的数据——而我们整个完整性体系(sha256 + 摘要 + + HMAC 签名)就是为了不让这种事发生。 +3. **密钥不要进库**。库里的 `signatures` 表存的是 HMAC 签名,那是防篡改用的, + 不是密钥保管。密钥从环境变量/密钥管理服务来。 + +--- + +## 三、现有结构对加密友好的地方 + +好消息是**几乎不用改 schema**: + +* `blobs.freq_blob`(BLOB)本质是一个**通用的"每块附加数据"槽**。它现在是哈夫曼频率表, + 加密的 nonce + 认证标签可以放这里,不需要加列。 +* `blobs.compress_algo`(TEXT)现在存 `'none' / 'zlib' / 'huffman'`。加一档 `'aes-gcm'` + 就走通了——`compression.py` 是唯一的算法分发点(见该模块 docstring 的契约说明)。 +* 如果"先压后加"需要同时表达两层,建议**加一列 `encrypt_algo`** 而不是把算法名拼成 + `'zlib+aes-gcm'` 这种字符串:拼字符串会让 `decompress_with` 的分发逻辑变脆, + 而且查询"这个包里加密了吗"要靠 LIKE。加列的成本是一次 v4 迁移(迁移机制已经在了, + `migration.py` + `vfsmigrate`)。 + +--- + +## 四、建议的推进顺序 + +1. **先只做 D**(发布层的信封加密):不动 blob、不动去重、不动对象名。 + 验证一下"发布到不可信存储"这个需求是不是就满足了——大概率是。 +2. 如果确实需要"静态加密整个库",再做 A(确定性 nonce),并接受确认攻击; + 这时要同步改:`manifest.json` 增加密文字段、`vfsfetch` 的校验时机、 + 以及"未授权方拿不到密钥就什么也读不出"的密钥分发方案。 +3. 无论走哪条,先给 `compression.py` 加一层算法链的**契约测试** + (`tests/test_compression.py` 已有骨架),保证加算法不破坏现有往返。 diff --git a/docs/evidence-after.txt b/docs/evidence-after.txt new file mode 100644 index 0000000..7f284be --- /dev/null +++ b/docs/evidence-after.txt @@ -0,0 +1,110 @@ +test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) ... ok +test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) ... ok +test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) ... ok +test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) ... ok +test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) ... ok +test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) ... ok +test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, ... ok +test_pack_reports_file_count_matching_database (test_cli.CliPackTest.test_pack_reports_file_count_matching_database) ... ok +test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) ... ok +test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) ... ok +test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 ... ok +test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) ... ok +test_list_root_only_shows_top_level (test_cli.CliUnpackTest.test_list_root_only_shows_top_level) +P0-2: 根目录列表不能把整张表铺平。 ... ok +test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) ... ok +test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) ... ok +test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 ... ok +test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:264: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) ... ok +test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) ... ok +test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) ... ok +test_paths_are_returned_as_dicts_with_datetime (test_core.DirectoryListingTest.test_paths_are_returned_as_dicts_with_datetime) ... ok +test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) ... ok +test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) ... ok +test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) ... ok +test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 ... ok +test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) ... ok +test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) ... ok +test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 ... ok +test_below_threshold_is_not_compressed (test_core.FileContentTest.test_below_threshold_is_not_compressed) ... ok +test_compressible_file_uses_huffman (test_core.FileContentTest.test_compressible_file_uses_huffman) ... ok +test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) ... ok +test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) ... ok +test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) ... ok +test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) ... ok +test_read_file_rejects_directory_and_missing_path (test_core.FileContentTest.test_read_file_rejects_directory_and_missing_path) ... ok +test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) ... ok +test_mk_dir_creates_directory (test_core.MkDirTest.test_mk_dir_creates_directory) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:425: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_mk_dir_existing_raises_cleanly (test_core.MkDirTest.test_mk_dir_existing_raises_cleanly) ... ok +test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) ... ok +test_mv_dir_into_existing_parent_directory (test_core.MoveAndRemoveTest.test_mv_dir_into_existing_parent_directory) ... ok +test_mv_dir_into_itself_raises (test_core.MoveAndRemoveTest.test_mv_dir_into_itself_raises) ... ok +test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 ... ok +test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, ... ok +test_mv_dir_to_existing_destination_raises_and_keeps_source (test_core.MoveAndRemoveTest.test_mv_dir_to_existing_destination_raises_and_keeps_source) ... ok +test_mv_dir_to_missing_parent_raises (test_core.MoveAndRemoveTest.test_mv_dir_to_missing_parent_raises) +目标的父目录不存在时要明确报错,而不是造出一个不可达的目录。 ... ok +test_mv_dir_updates_children_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_updates_children_parent_paths) ... ok +test_mv_file_updates_name_and_parent (test_core.MoveAndRemoveTest.test_mv_file_updates_name_and_parent) ... ok +test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 ... ok +test_remove_file_with_percent_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_percent_is_not_a_wildcard) ... ok +test_remove_file_with_underscore_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_underscore_is_not_a_wildcard) +文件名里的 _ 不能被当成 LIKE 的单字符通配符。 ... ok +test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) ... ok +test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) ... ok +test_fresh_database_is_accepted (test_core.SchemaCompatibilityTest.test_fresh_database_is_accepted) ... ok +test_non_sqlite_file_is_rejected (test_core.SchemaCompatibilityTest.test_non_sqlite_file_is_rejected) ... ok +test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) ... ok +test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) ... ok +test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 ... ok +test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) ... ok +test_bad_magic_rejected (test_huffman.HeaderValidationTest.test_bad_magic_rejected) ... ok +test_single_symbol_payload_is_not_validated (test_huffman.HeaderValidationTest.test_single_symbol_payload_is_not_validated) +记录既有行为(非缺陷):单符号数据解压时直接按原长度展开,完全不读 payload, ... ok +test_too_short_blob_rejected (test_huffman.HeaderValidationTest.test_too_short_blob_rejected) ... ok +test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) ... ok +test_wrong_freq_blob_size_rejected (test_huffman.HeaderValidationTest.test_wrong_freq_blob_size_rejected) ... ok +test_all_256_byte_values (test_huffman.RoundTripTest.test_all_256_byte_values) ... ok +test_binary_with_nulls_and_high_bytes (test_huffman.RoundTripTest.test_binary_with_nulls_and_high_bytes) ... ok +test_empty (test_huffman.RoundTripTest.test_empty) ... ok +test_incompressible_random (test_huffman.RoundTripTest.test_incompressible_random) ... ok +test_just_above_core_threshold (test_huffman.RoundTripTest.test_just_above_core_threshold) ... ok +test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) ... ok +test_single_byte (test_huffman.RoundTripTest.test_single_byte) ... ok +test_single_symbol_repeated (test_huffman.RoundTripTest.test_single_symbol_repeated) ... ok +test_two_symbols (test_huffman.RoundTripTest.test_two_symbols) ... ok +test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) ... ok +test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) +用一个足够大的重复文本,让压缩效果超过 SQLite 页粒度(4KB)的噪声。 ... ok +test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:214: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 ... ok +test_packed_database_contains_no_duplicate_paths (test_roundtrip.RoundTripTest.test_packed_database_contains_no_duplicate_paths) ... ok +test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) ... ok +test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) ... ok +test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) ... ok +test_stored_blob_of_compressible_file_is_smaller (test_roundtrip.RoundTripTest.test_stored_blob_of_compressible_file_is_smaller) +压缩不只是"标记了一下":库里真正存的字节必须变少。 ... ok + +---------------------------------------------------------------------- +Ran 81 tests in 6.741s + +OK +EXIT=0 diff --git a/docs/evidence-before.txt b/docs/evidence-before.txt new file mode 100644 index 0000000..510ca74 --- /dev/null +++ b/docs/evidence-before.txt @@ -0,0 +1,829 @@ +test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) ... FAIL +test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) ... FAIL +test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) ... FAIL +test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) ... FAIL +test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) ... FAIL +test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) ... FAIL +test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, ... FAIL +test_pack_reports_file_count_matching_database (test_cli.CliPackTest.test_pack_reports_file_count_matching_database) ... ok +test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) ... FAIL +test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) ... FAIL +test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 ... FAIL +test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) ... FAIL +test_list_root_only_shows_top_level (test_cli.CliUnpackTest.test_list_root_only_shows_top_level) +P0-2: 根目录列表不能把整张表铺平。 ... ok +test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) ... ERROR +test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) ... FAIL +test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 ... FAIL +test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) ... FAIL +test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) ... FAIL +test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) ... FAIL +test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) ... FAIL +test_paths_are_returned_as_dicts_with_datetime (test_core.DirectoryListingTest.test_paths_are_returned_as_dicts_with_datetime) ... ok +test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) ... FAIL +test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) ... FAIL +test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) ... Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 483, in export_file + content = self.read_file(vfs_path) + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 278, in read_file + algo = file_info["compress_algo"] or "none" + ~~~~~~~~~^^^^^^^^^^^^^^^^^ +KeyError: 'compress_algo' +FAIL +test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 ... Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 480, in export_file + os.makedirs(os.path.dirname(local_path), exist_ok=True) + ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 228, in makedirs +FileNotFoundError: [WinError 3] 系统找不到指定的路径。: '' +FAIL +test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) ... Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 483, in export_file + content = self.read_file(vfs_path) + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 278, in read_file + algo = file_info["compress_algo"] or "none" + ~~~~~~~~~^^^^^^^^^^^^^^^^^ +KeyError: 'compress_algo' +FAIL +test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) ... Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 483, in export_file + content = self.read_file(vfs_path) + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 278, in read_file + algo = file_info["compress_algo"] or "none" + ~~~~~~~~~^^^^^^^^^^^^^^^^^ +KeyError: 'compress_algo' +FAIL +test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 ... FAIL +test_below_threshold_is_not_compressed (test_core.FileContentTest.test_below_threshold_is_not_compressed) ... ERROR +test_compressible_file_uses_huffman (test_core.FileContentTest.test_compressible_file_uses_huffman) ... ERROR +test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) ... ERROR +test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) ... ERROR +test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) ... ERROR +test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) ... ERROR +test_read_file_rejects_directory_and_missing_path (test_core.FileContentTest.test_read_file_rejects_directory_and_missing_path) ... ok +test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) ... ERROR +test_mk_dir_creates_directory (test_core.MkDirTest.test_mk_dir_creates_directory) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:319: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_mk_dir_existing_raises_cleanly (test_core.MkDirTest.test_mk_dir_existing_raises_cleanly) ... ok +test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) ... FAIL +test_mv_dir_into_new_parent_directory (test_core.MoveAndRemoveTest.test_mv_dir_into_new_parent_directory) ... ok +test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 ... FAIL +test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, ... FAIL +test_mv_dir_to_existing_destination_raises_and_keeps_source (test_core.MoveAndRemoveTest.test_mv_dir_to_existing_destination_raises_and_keeps_source) ... ok +test_mv_dir_updates_children_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_updates_children_parent_paths) ... ok +test_mv_file_updates_name_and_parent (test_core.MoveAndRemoveTest.test_mv_file_updates_name_and_parent) ... ok +test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 ... FAIL +test_remove_file_with_percent_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_percent_is_not_a_wildcard) ... ok +test_remove_file_with_underscore_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_underscore_is_not_a_wildcard) +文件名里的 _ 不能被当成 LIKE 的单字符通配符。 ... ok +test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) ... FAIL +test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) ... FAIL +test_fresh_database_is_accepted (test_core.SchemaCompatibilityTest.test_fresh_database_is_accepted) ... ok +test_non_sqlite_file_is_rejected (test_core.SchemaCompatibilityTest.test_non_sqlite_file_is_rejected) ... ok +test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) ... FAIL +test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) ... FAIL +test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 ... FAIL +test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) ... FAIL +test_bad_magic_rejected (test_huffman.HeaderValidationTest.test_bad_magic_rejected) ... ok +test_too_short_blob_rejected (test_huffman.HeaderValidationTest.test_too_short_blob_rejected) ... ok +test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) ... FAIL +test_wrong_freq_blob_size_rejected (test_huffman.HeaderValidationTest.test_wrong_freq_blob_size_rejected) ... ok +test_all_256_byte_values (test_huffman.RoundTripTest.test_all_256_byte_values) ... ok +test_binary_with_nulls_and_high_bytes (test_huffman.RoundTripTest.test_binary_with_nulls_and_high_bytes) ... ok +test_empty (test_huffman.RoundTripTest.test_empty) ... ok +test_incompressible_random (test_huffman.RoundTripTest.test_incompressible_random) ... ok +test_just_above_core_threshold (test_huffman.RoundTripTest.test_just_above_core_threshold) ... ok +test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) ... FAIL +test_single_byte (test_huffman.RoundTripTest.test_single_byte) ... ok +test_single_symbol_repeated (test_huffman.RoundTripTest.test_single_symbol_repeated) ... ok +test_two_symbols (test_huffman.RoundTripTest.test_two_symbols) ... ok +test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) ... ERROR +test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:124: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +FAIL +test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 ... FAIL +test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 ... FAIL +test_packed_database_contains_no_duplicate_paths (test_roundtrip.RoundTripTest.test_packed_database_contains_no_duplicate_paths) ... ok +test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:242: ResourceWarning: unclosed database in + parsed_row["created_time"].replace("Z", "+00:00") +ResourceWarning: Enable tracemalloc to get the object allocation traceback +FAIL +test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) ... FAIL +test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) ... FAIL + +====================================================================== +ERROR: test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 184, in test_path_exports_single_file + with open(os.path.join(out, "top.txt"), "rb") as f: + ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\chakcy\\AppData\\Local\\Temp\\svfs_test_kelch2hu\\out\\top.txt' + +====================================================================== +ERROR: test_below_threshold_is_not_compressed (test_core.FileContentTest.test_below_threshold_is_not_compressed) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 116, in test_below_threshold_is_not_compressed + vfs, content = self._write_and_read(data, compress=True) + ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_compressible_file_uses_huffman (test_core.FileContentTest.test_compressible_file_uses_huffman) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 93, in test_compressible_file_uses_huffman + vfs, content = self._write_and_read(data, compress=True) + ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 88, in test_empty_file_roundtrip + _, content = self._write_and_read(b"") + ~~~~~~~~~~~~~~~~~~~~^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 122, in test_file_size_recorded_matches_content + vfs, _ = self._write_and_read(data) + ~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 106, in test_incompressible_file_falls_back_to_none + vfs, content = self._write_and_read(data, compress=True) + ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 126, in test_read_file_offset_and_size + vfs, _ = self._write_and_read(b"0123456789") + ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 83, in test_small_file_roundtrip + vfs, content = self._write_and_read(b"hello sqlite vfs") + ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 80, in _write_and_read + return vfs, vfs.read_file(rel_path) + ~~~~~~~~~~~~~^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py", line 273, in read_file + raise FileNotFoundError(f"文件不存在: {path}") +FileNotFoundError: 文件不存在: data.bin + +====================================================================== +ERROR: test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_huffman.py", line 78, in test_utf8_chinese + self.assert_roundtrip("中文内容,测试哈夫曼编码是否无损。\n" * 200, "中文") + ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_huffman.py", line 24, in assert_roundtrip + freq_blob, compressed_blob = compress_bytes(data) + ~~~~~~~~~~~~~~^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\huffman.py", line 128, in compress_bytes + freq[b] += 1 + ~~~~^^^ +TypeError: list indices must be integers or slices, not str + +====================================================================== +FAIL: test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 49, in test_pack_missing_source_returns_nonzero + self.assertNotEqual(0, result.returncode, "源目录不存在却返回了成功") + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 0 == 0 : 源目录不存在却返回了成功 + +====================================================================== +FAIL: test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 61, in test_pack_to_missing_output_directory_reports_clean_error + self.assertNotIn("Traceback", helpers.cli_output(result)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'Traceback' unexpectedly found in 'Traceback (most recent call last):\n File "", line 198, in _run_module_as_main\n File "", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\packer.py", line 40, in \n pack()\n ~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\packer.py", line 28, in pack\n sqlite_vfs=SQLiteVFS(args.output_db, args.compress),\n ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\core.py", line 20, in __init__\n self.connect()\n ~~~~~~~~~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\core.py", line 24, in connect\n self.conn = sqlite3.connect(self.db_path)\n ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^\nsqlite3.OperationalError: unable to open database file\n' + +====================================================================== +FAIL: test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 54, in test_unpack_missing_db_returns_nonzero + self.assertNotEqual(0, result.returncode, "数据库不存在却返回了成功") + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 0 == 0 : 数据库不存在却返回了成功 + +====================================================================== +FAIL: test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 73, in test_unpack_missing_path_returns_nonzero + self.assertNotEqual(0, result.returncode, "导出不存在的路径却返回了成功") + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 0 == 0 : 导出不存在的路径却返回了成功 + +====================================================================== +FAIL: test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 68, in test_unpack_non_database_file_reports_clean_error + self.assertNotIn("Traceback", helpers.cli_output(result)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'Traceback' unexpectedly found in 'Traceback (most recent call last):\n File "", line 198, in _run_module_as_main\n File "", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 77, in \n unpack()\n ~~~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 34, in unpack\n unpacker = FolderUnpacker(sqlite_vfs=SQLiteVFS(args.db_file))\n ~~~~~~~~~^^^^^^^^^^^^^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\core.py", line 20, in __init__\n self.connect()\n ~~~~~~~~~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\core.py", line 26, in connect\n self._create_tables()\n ~~~~~~~~~~~~~~~~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\core.py", line 72, in _create_tables\n cursor.execute(\n ~~~~~~~~~~~~~~^\n """\n ^^^\n ...<7 lines>...\n """\n ^^^\n )\n ^\nsqlite3.DatabaseError: file is not a database\n' + +====================================================================== +FAIL: test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 123, in test_exclude_prunes_directory_subtree + self.assertEqual({"keep.txt"}, set(helpers.stored_files(vfs))) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Items in the first set but not the second: +'keep.txt' + +====================================================================== +FAIL: test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 106, in test_exclude_supports_glob_patterns + self.assertEqual( + ~~~~~~~~~~~~~~~~^ + {"keep.txt", "catalog.txt", "sub/keep2.txt"}, set(helpers.stored_files(vfs)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: Items in the first set but not the second: +'catalog.txt' +'sub/keep2.txt' +'keep.txt' + +====================================================================== +FAIL: test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 86, in test_pack_stores_every_file + self.assertEqual(expected, set(helpers.stored_files(vfs))) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Items in the first set but not the second: +'sub/inner.txt' +'top.txt' +'sub/deep/leaf.bin' + +====================================================================== +FAIL: test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 152, in test_full_roundtrip_via_cli + helpers.assert_tree_equal(self, expected, helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 3 项,例如: ['sub/deep/leaf.bin', 'sub/in[1427 chars]ir']" +Diff is 1482 characters long. Set self.maxDiff to None to see it. + +====================================================================== +FAIL: test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 194, in test_ignore_timestamps_full_export_does_not_recurse + self.assertNotIn("RecursionError", output) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'RecursionError' unexpectedly found in '数据库状态: 数据库正常,共有 6 个文件/目录,根目录下有 3 个项目\nTraceback (most recent call last):\n File "", line 198, in _run_module_as_main\n File "", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 77, in \n unpack()\n ~~~~~~^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 70, in unpack\n unpacker.unpack_filesystem(args.export_folder)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\folder_unpacker.py", line 60, in unpack_filesystem\n return self.sqlite_vfs.export_filesystem(export_folder)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 66, in \n lambda export_root: unpacker.sqlite_vfs.export_filesystem(\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^\n export_root\n ^^^^^^^^^^^\n )\n ^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 66, in \n lambda export_root: unpacker.sqlite_vfs.export_filesystem(\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^\n export_root\n ^^^^^^^^^^^\n )\n ^\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 66, in \n lambda export_root: unpacker.sqlite_vfs.export_filesystem(\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^\n export_root\n ^^^^^^^^^^^\n )\n ^\n [Previous line repeated 992 more times]\nRecursionError: maximum recursion depth exceeded\n' + +====================================================================== +FAIL: test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 215, in test_ignore_timestamps_with_path_subdirectory + helpers.assert_tree_equal(self, expected, helpers.read_tree(os.path.join(out, "doc"))) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'][536 chars]oc']" +- 无差异 ++ 缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'] ++ 多出 991 项,例如: ['doc', 'doc/doc', 'doc/doc/doc', 'doc/doc/doc/doc', 'doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc'] + + +====================================================================== +FAIL: test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 177, in test_path_exports_subdirectory_with_contents + helpers.assert_tree_equal(self, expected, helpers.read_tree(os.path.join(out, "doc"))) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'][536 chars]oc']" +- 无差异 ++ 缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'] ++ 多出 991 项,例如: ['doc', 'doc/doc', 'doc/doc/doc', 'doc/doc/doc/doc', 'doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc'] + + +====================================================================== +FAIL: test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_cli.py", line 222, in test_rejects_old_schema_database_cleanly + self.assertNotEqual(0, result.returncode) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 0 == 0 + +====================================================================== +FAIL: test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 368, in test_duplicate_add_file_is_reported_and_keeps_first_content + self.assertTrue(first, "首次写入应当成功") + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: None is not true : 首次写入应当成功 + +====================================================================== +FAIL: test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 182, in test_directories_come_before_files + self.assertEqual([1, 0], [row["is_directory"] for row in rows]) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: [1, 0] != [1] + +First list contains 1 additional elements. +First extra element 1: +0 + +- [1, 0] ++ [1] + +====================================================================== +FAIL: test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 186, in test_leaf_directory_is_empty + self.assertEqual([], vfs.list_directory("docs")) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: [] != [{'id': 2, 'path': 'docs', 'name': 'docs',[177 chars]ne'}] + +Second list contains 1 additional elements. +First extra element 0: +{'id': 2, 'path': 'docs', 'name': 'docs', 'parent_path': '', 'is_directory': 1, 'file_size': 0, 'created_time': None, 'modified_time': None, 'permissions': 644, 'content': None, 'freq_blob': None, 'compresse_algo': 'none'} + +- [] ++ [{'compresse_algo': 'none', ++ 'content': None, ++ 'created_time': None, ++ 'file_size': 0, ++ 'freq_blob': None, ++ 'id': 2, ++ 'is_directory': 1, ++ 'modified_time': None, ++ 'name': 'docs', ++ 'parent_path': '', ++ 'path': 'docs', ++ 'permissions': 644}] + +====================================================================== +FAIL: test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 177, in test_nested_directory_lists_its_children + self.assertEqual(["doc/sub/b.txt"], paths) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: ['doc/sub/b.txt'] != ['doc/sub'] + +First differing element 0: +'doc/sub/b.txt' +'doc/sub' + +- ['doc/sub/b.txt'] +? ------ + ++ ['doc/sub'] + +====================================================================== +FAIL: test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 167, in test_root_lists_top_level_only + self.assertEqual(["doc", "docs", "top.txt"], paths) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: ['doc', 'docs', 'top.txt'] != ['doc', 'doc/a.txt', 'doc/sub', 'doc/sub/b.txt', 'doc[23 chars]txt'] + +First differing element 1: +'docs' +'doc/a.txt' + +Second list contains 4 additional elements. +First extra element 3: +'doc/sub/b.txt' + +- ['doc', 'docs', 'top.txt'] ++ ['doc', ++ 'doc/a.txt', ++ 'doc/sub', ++ 'doc/sub/b.txt', ++ 'docs', ++ 'docs/c.txt', ++ 'top.txt'] + +====================================================================== +FAIL: test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 172, in test_subdirectory_lists_its_children + self.assertEqual(["doc/a.txt", "doc/sub"], paths) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: ['doc/a.txt', 'doc/sub'] != ['doc'] + +First differing element 0: +'doc/a.txt' +'doc' + +First list contains 1 additional elements. +First extra element 1: +'doc/sub' + +- ['doc/a.txt', 'doc/sub'] ++ ['doc'] + +====================================================================== +FAIL: test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 432, in test_export_directory_full_tree + helpers.assert_tree_equal(self, expected, helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 4 项,例如: ['doc/a.txt', 'doc/sub', 'doc[620 chars]oc']" +Diff is 675 characters long. Set self.maxDiff to None to see it. + +====================================================================== +FAIL: test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 413, in test_export_file_accepts_bare_filename + self.assertTrue(ok, "导出到当前目录下的裸文件名应当成功") + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: False is not true : 导出到当前目录下的裸文件名应当成功 + +====================================================================== +FAIL: test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 401, in test_export_file_writes_exact_bytes + self.assertTrue(vfs.export_file("top.txt", target)) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: False is not true + +====================================================================== +FAIL: test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 457, in test_export_filesystem_matches_tree + helpers.assert_tree_equal(self, expected, helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 4 项,例如: ['doc/a.txt', 'doc/sub', 'doc[620 chars]oc']" +Diff is 675 characters long. Set self.maxDiff to None to see it. + +====================================================================== +FAIL: test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 442, in test_export_subdirectory_does_not_recurse_forever + helpers.assert_tree_equal(self, expected, helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'][536 chars]oc']" +- 无差异 ++ 缺失 3 项,例如: ['a.txt', 'sub', 'sub/b.txt'] ++ 多出 980 项,例如: ['doc', 'doc/doc', 'doc/doc/doc', 'doc/doc/doc/doc', 'doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc', 'doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc/doc'] + + +====================================================================== +FAIL: test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 277, in test_mv_dir_does_not_touch_prefix_sibling + self.assertIn("docs", helpers.all_paths(vfs)) + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'docs' not found in {'doc_new/sub/b.txt', 'top.txt', 'doc_new/a.txt', 'doc_new/sub', 'doc_news/c.txt', 'doc_new_new', 'doc_news'} + +====================================================================== +FAIL: test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 261, in test_mv_dir_rename_is_not_applied_twice + self.assertEqual( + ~~~~~~~~~~~~~~~~^ + { + ^ + ...<8 lines>... + helpers.all_paths(vfs), + ^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: Items in the first set but not the second: +'docs/c.txt' +'docs' +'doc_new' +Items in the second set but not the first: +'doc_new_new' +'doc_news/c.txt' +'doc_news' + +====================================================================== +FAIL: test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 303, in test_mv_dir_root_creates_destination_and_fixes_parent_paths + self.assertIn("archive", parents, "目标目录 archive 没有对应的目录行") + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'archive' not found in {'archive/doc': 'archive/', 'archive/doc/a.txt': 'archive/doc', 'archive/doc/sub': 'archive/doc', 'archive/doc/sub/b.txt': 'archive/doc/sub', 'archive/docs': 'archive/', 'archive/docs/c.txt': 'archive/docs', 'archive/top.txt': 'archive'} : 目标目录 archive 没有对应的目录行 + +====================================================================== +FAIL: test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 224, in test_remove_directory_does_not_touch_prefix_sibling + self.assertEqual( + ~~~~~~~~~~~~~~~~^ + {"docs", "docs/c.txt", "top.txt"}, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + paths, + ^^^^^^ + "删除 doc 误伤了 docs", + ^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: Items in the first set but not the second: +'docs/c.txt' +'docs' : 删除 doc 误伤了 docs + +====================================================================== +FAIL: test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 253, in test_remove_missing_path_reports_error + with self.assertRaises(FileNotFoundError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^ +AssertionError: FileNotFoundError not raised + +====================================================================== +FAIL: test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 65, in test_add_file_survives_close_without_further_commit + self.assertTrue( + ~~~~~~~~~~~~~~~^ + reopened.exists("x.bin"), + ^^^^^^^^^^^^^^^^^^^^^^^^^ + "add_file 之后直接 close(),数据丢失——事务没有提交", + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: False is not true : add_file 之后直接 close(),数据丢失——事务没有提交 + +====================================================================== +FAIL: test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 480, in test_repository_sample_db_is_rejected + self.fail("core 没有定义 VFSFormatError 一类的异常") + ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: core 没有定义 VFSFormatError 一类的异常 + +====================================================================== +FAIL: test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 466, in test_synthesized_old_schema_db_is_rejected + self.fail( + ~~~~~~~~~^ + "core 没有定义 VFSFormatError 一类的异常," + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "调用方无法区分「旧格式库」与「正常库」,只能在导出阶段看到一堆导出失败" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: core 没有定义 VFSFormatError 一类的异常,调用方无法区分「旧格式库」与「正常库」,只能在导出阶段看到一堆导出失败 + +====================================================================== +FAIL: test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 47, in test_add_file_actually_stores_a_row + self.assertTrue(vfs.exists("input.bin"), "add_file 没有把文件写进数据库") + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: False is not true : add_file 没有把文件写进数据库 + +====================================================================== +FAIL: test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_core.py", line 32, in test_expected_columns_exist + self.assertIn(name, columns, f"files 表缺少列 {name}") + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'compress_algo' not found in {'content', 'permissions', 'id', 'modified_time', 'compresse_algo', 'is_directory', 'file_size', 'freq_blob', 'created_time', 'name', 'parent_path', 'path'} : files 表缺少列 compress_algo + +====================================================================== +FAIL: test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_huffman.py", line 112, in test_truncated_payload_rejected + with self.assertRaises(ValueError) as ctx: + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_huffman.py", line 71, in test_repetitive_text_compresses + self.assertLess( + ~~~~~~~~~~~~~~~^ + len(compressed_blob), + ^^^^^^^^^^^^^^^^^^^^^ + len(data) // 2, + ^^^^^^^^^^^^^^^ + "高度重复的文本应当被显著压缩", + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: 50263 not less than 45000 : 高度重复的文本应当被显著压缩 + +====================================================================== +FAIL: test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 112, in test_compression_reduces_database_size + self.assertLess( + ~~~~~~~~~~~~~~~^ + compressed_size, + ^^^^^^^^^^^^^^^^ + plain_size, + ^^^^^^^^^^^ + "源目录里有大量重复文本,启用压缩后数据库应当变小", + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: 32768 not less than 32768 : 源目录里有大量重复文本,启用压缩后数据库应当变小 + +====================================================================== +FAIL: test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 77, in test_every_source_file_is_stored + self.assertEqual( + ~~~~~~~~~~~~~~~~^ + "无差异", + ^^^^^^^^^ + helpers.diff_trees({p: None for p in expected}, {p: None for p in stored}), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: '无差异' != "缺失 12 项,例如: ['below_threshold.bin', 'com[184 chars]xt']" +- 无差异 ++ 缺失 12 项,例如: ['below_threshold.bin', 'compressible.txt', 'doc/a.txt', 'docs/b.txt', 'empty.txt', 'one_byte.bin', 'random.bin', 'sub/deep/deeper/nested.txt', 'sub/percent%name_.txt', 'sub/space name.txt', 'sub/中文名.txt', 'top.txt'] + + +====================================================================== +FAIL: test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 94, in test_metadata_counts_match_database + self.assertEqual(source_count, stored_count, "入库文件数 ≠ 源文件数") + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 12 != 0 : 入库文件数 ≠ 源文件数 + +====================================================================== +FAIL: test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 63, in test_roundtrip_with_compression + self._assert_lossless(src, db) + ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 55, in _assert_lossless + helpers.assert_tree_equal(self, expected_tree(build_spec()), helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 12 项,例如: ['below_threshold.bin', 'com[725 chars]oc']" +Diff is 780 characters long. Set self.maxDiff to None to see it. + +====================================================================== +FAIL: test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 67, in test_roundtrip_with_ignore_timestamps + self._assert_lossless(src, db, ignore_timestamps=True) + ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 55, in _assert_lossless + helpers.assert_tree_equal(self, expected_tree(build_spec()), helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 12 项,例如: ['below_threshold.bin', 'com[725 chars]oc']" +Diff is 780 characters long. Set self.maxDiff to None to see it. + +====================================================================== +FAIL: test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 59, in test_roundtrip_without_compression + self._assert_lossless(src, db) + ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\test_roundtrip.py", line 55, in _assert_lossless + helpers.assert_tree_equal(self, expected_tree(build_spec()), helpers.read_tree(out)) + ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\tests\helpers.py", line 136, in assert_tree_equal + case.assertEqual("无差异", diff_trees(expected, actual)) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '无差异' != "缺失 12 项,例如: ['below_threshold.bin', 'com[725 chars]oc']" +Diff is 780 characters long. Set self.maxDiff to None to see it. + +---------------------------------------------------------------------- +Ran 77 tests in 175.700s + +FAILED (failures=42, errors=9) +EXIT=1 diff --git a/docs/evidence-cleanup.txt b/docs/evidence-cleanup.txt new file mode 100644 index 0000000..be72a3d --- /dev/null +++ b/docs/evidence-cleanup.txt @@ -0,0 +1,415 @@ +test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) ... ok +test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) ... ok +test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) ... ok +test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) ... ok +test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) ... ok +test_migrates_in_place_with_backup (test_cli.CliMigrateTest.test_migrates_in_place_with_backup) ... ok +test_non_vfs_file_is_rejected (test_cli.CliMigrateTest.test_non_vfs_file_is_rejected) ... ok +test_output_mode_leaves_the_original_alone (test_cli.CliMigrateTest.test_output_mode_leaves_the_original_alone) ... ok +test_reports_when_nothing_to_do (test_cli.CliMigrateTest.test_reports_when_nothing_to_do) ... ok +test_compress_algo_huffman_is_still_selectable (test_cli.CliPackTest.test_compress_algo_huffman_is_still_selectable) +早期算法仍然可选,且打出来的包能被正常读回。 ... ok +test_default_compression_algorithm_is_zlib (test_cli.CliPackTest.test_default_compression_algorithm_is_zlib) ... ok +test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) ... ok +test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, ... ok +test_pack_reports_file_count_matching_database (test_cli.CliPackTest.test_pack_reports_file_count_matching_database) ... ok +test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) ... ok +test_list_snapshots_option (test_cli.CliUnpackSnapshotTest.test_list_snapshots_option) ... ok +test_unknown_snapshot_is_rejected_cleanly (test_cli.CliUnpackSnapshotTest.test_unknown_snapshot_is_rejected_cleanly) ... ok +test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) ... ok +test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 ... ok +test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) ... ok +test_list_root_only_shows_top_level (test_cli.CliUnpackTest.test_list_root_only_shows_top_level) +P0-2: 根目录列表不能把整张表铺平。 ... ok +test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) ... ok +test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) ... ok +test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 ... ok +test_content_tampering_needs_check_content (test_cli.CliVerifyTest.test_content_tampering_needs_check_content) ... ok +test_json_output (test_cli.CliVerifyTest.test_json_output) ... ok +test_list_snapshots (test_cli.CliVerifyTest.test_list_snapshots) ... ok +test_missing_db_exits_nonzero (test_cli.CliVerifyTest.test_missing_db_exits_nonzero) ... ok +test_sign_then_verify (test_cli.CliVerifyTest.test_sign_then_verify) ... ok +test_snapshot_selection (test_cli.CliVerifyTest.test_snapshot_selection) ... ok +test_verify_does_not_modify_the_pack (test_cli.CliVerifyTest.test_verify_does_not_modify_the_pack) ... ok +test_verify_exits_nonzero_on_metadata_tampering (test_cli.CliVerifyTest.test_verify_exits_nonzero_on_metadata_tampering) ... ok +test_verify_reports_ok (test_cli.CliVerifyTest.test_verify_reports_ok) ... ok +test_verify_with_content_check (test_cli.CliVerifyTest.test_verify_with_content_check) ... ok +test_demo_runs_and_reports_success (test_cli.DemoScriptTest.test_demo_runs_and_reports_success) ... ok +test_default_algorithm_is_zlib (test_compression.AlgorithmRegistryTest.test_default_algorithm_is_zlib) ... ok +test_every_registered_algorithm_roundtrips (test_compression.AlgorithmRegistryTest.test_every_registered_algorithm_roundtrips) ... ok +test_registry_contains_the_three_builtins (test_compression.AlgorithmRegistryTest.test_registry_contains_the_three_builtins) ... ok +test_unknown_algorithm_is_rejected_on_both_sides (test_compression.AlgorithmRegistryTest.test_unknown_algorithm_is_rejected_on_both_sides) ... ok +test_huffman_is_not_used_for_small_files (test_compression.AlgorithmTradeoffTest.test_huffman_is_not_used_for_small_files) +huffman 每个文件要带 1024 字节频率表,小文件必然负收益。 ... ok +test_zlib_beats_huffman_by_a_lot_on_repetitive_text (test_compression.AlgorithmTradeoffTest.test_zlib_beats_huffman_by_a_lot_on_repetitive_text) ... ok +test_huffman_carries_a_fixed_size_frequency_table (test_compression.CompressContractTest.test_huffman_carries_a_fixed_size_frequency_table) ... ok +test_huffman_without_frequency_table_is_rejected (test_compression.CompressContractTest.test_huffman_without_frequency_table_is_rejected) ... ok +test_none_returns_the_input_unchanged (test_compression.CompressContractTest.test_none_returns_the_input_unchanged) ... ok +test_stored_size_counts_the_frequency_table (test_compression.CompressContractTest.test_stored_size_counts_the_frequency_table) ... ok +test_stored_size_ignores_an_empty_frequency_table (test_compression.CompressContractTest.test_stored_size_ignores_an_empty_frequency_table) ... ok +test_zlib_produces_a_real_zlib_stream (test_compression.CompressContractTest.test_zlib_produces_a_real_zlib_stream) ... ok +test_importing_core_never_pulls_in_third_party (test_contrib_optional.OptionalDependencyTest.test_importing_core_never_pulls_in_third_party) +import sqlite_vfs 不能顺带把 fastapi/starlette 拖进来。 ... ok +test_missing_database_path_hint (test_contrib_optional.OptionalDependencyTest.test_missing_database_path_hint) +未生成资源包时的报错要给出下一步命令。 ... skipped '当前环境没有 FastAPI' +test_missing_fastapi_gives_actionable_error (test_contrib_optional.OptionalDependencyTest.test_missing_fastapi_gives_actionable_error) +缺少可选依赖时,报错信息必须能直接照着做。 ... ok +test_plugin_module_is_not_imported_by_the_package (test_contrib_optional.OptionalDependencyTest.test_plugin_module_is_not_imported_by_the_package) +contrib 子包不能自动导入插件模块(否则缺依赖时整包不可用)。 ... ok +test_third_party_is_installed_or_not (test_contrib_optional.OptionalDependencyTest.test_third_party_is_installed_or_not) +只用于在输出里说明当前环境状态,不断言。 ... ok +test_connection_survives_a_failed_insert (test_core.AddDirectoryContractTest.test_connection_survives_a_failed_insert) ... ok +test_duplicate_without_replace_fails_cleanly (test_core.AddDirectoryContractTest.test_duplicate_without_replace_fails_cleanly) ... ok +test_replace_updates_in_place (test_core.AddDirectoryContractTest.test_replace_updates_in_place) ... ok +test_returns_false_for_a_missing_source_directory (test_core.AddDirectoryContractTest.test_returns_false_for_a_missing_source_directory) ... ok +test_returns_true_and_creates_the_row (test_core.AddDirectoryContractTest.test_returns_true_and_creates_the_row) ... ok +test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) ... ok +test_all_entries_covers_the_whole_tree (test_core.BlobPrimitiveTest.test_all_entries_covers_the_whole_tree) ... ok +test_all_entries_is_snapshot_scoped (test_core.BlobPrimitiveTest.test_all_entries_is_snapshot_scoped) ... ok +test_get_blob_info_returns_none_for_a_directory (test_core.BlobPrimitiveTest.test_get_blob_info_returns_none_for_a_directory) ... ok +test_get_blob_info_returns_none_for_a_missing_path (test_core.BlobPrimitiveTest.test_get_blob_info_returns_none_for_a_missing_path) ... ok +test_list_blobs_reports_sizes_without_content (test_core.BlobPrimitiveTest.test_list_blobs_reports_sizes_without_content) ... ok +test_list_blobs_returns_one_row_per_unique_content (test_core.BlobPrimitiveTest.test_list_blobs_returns_one_row_per_unique_content) ... ok +test_read_blob_detects_a_size_mismatch_without_asking (test_core.BlobPrimitiveTest.test_read_blob_detects_a_size_mismatch_without_asking) +大小对不上是便宜的结构性检查,不需要调用方显式要求。 ... ok +test_read_blob_detects_tampering_when_asked (test_core.BlobPrimitiveTest.test_read_blob_detects_tampering_when_asked) ... ok +test_read_blob_returns_original_bytes (test_core.BlobPrimitiveTest.test_read_blob_returns_original_bytes) ... ok +test_read_blob_unknown_hash_raises (test_core.BlobPrimitiveTest.test_read_blob_unknown_hash_raises) ... ok +test_clean_wipes_every_table (test_core.CleanTest.test_clean_wipes_every_table) ... ok +test_pack_after_clean_starts_fresh (test_core.CleanTest.test_pack_after_clean_starts_fresh) ... ok +test_compression_disabled_stores_raw (test_core.CompressionAlgorithmTest.test_compression_disabled_stores_raw) ... ok +test_default_algorithm_is_zlib (test_core.CompressionAlgorithmTest.test_default_algorithm_is_zlib) ... ok +test_each_algorithm_roundtrips (test_core.CompressionAlgorithmTest.test_each_algorithm_roundtrips) ... ok +test_early_huffman_packs_are_still_readable (test_core.CompressionAlgorithmTest.test_early_huffman_packs_are_still_readable) +早期版本用 huffman 打的包必须还能读——这是保留该算法唯一的理由。 ... ok +test_unknown_algorithm_in_database_raises_on_read (test_core.CompressionAlgorithmTest.test_unknown_algorithm_in_database_raises_on_read) +库里的算法名本版本不认识时要明确报错,而不是返回坏数据。 ... ok +test_unknown_algorithm_rejected_at_construction (test_core.CompressionAlgorithmTest.test_unknown_algorithm_rejected_at_construction) ... ok +test_zlib_compresses_better_than_huffman (test_core.CompressionAlgorithmTest.test_zlib_compresses_better_than_huffman) +同一份文本,zlib 的结果必须明显小于 huffman(LZ77 能吃重复)。 ... ok +test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) ... ok +test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) ... ok +test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) ... ok +test_paths_are_returned_as_dicts_with_datetime (test_core.DirectoryListingTest.test_paths_are_returned_as_dicts_with_datetime) ... ok +test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) ... ok +test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) ... ok +test_all_three_agree_with_the_entry_type (test_core.ExistsHelpersTest.test_all_three_agree_with_the_entry_type) ... ok +test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) ... ok +test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 ... ok +test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) ... ok +test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) ... ok +test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 ... ok +test_compressible_file_uses_zlib (test_core.FileContentTest.test_compressible_file_uses_zlib) ... ok +test_compression_applies_below_the_old_1kb_bar (test_core.FileContentTest.test_compression_applies_below_the_old_1kb_bar) +zlib 包头只有几字节,小文件也值得压(旧的哈夫曼阈值是 1KB)。 ... ok +test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) ... ok +test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) ... ok +test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) ... ok +test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) ... ok +test_read_file_rejects_directory_and_missing_path (test_core.FileContentTest.test_read_file_rejects_directory_and_missing_path) ... ok +test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) ... ok +test_tiny_file_is_not_compressed (test_core.FileContentTest.test_tiny_file_is_not_compressed) ... ok +test_mk_dir_creates_directory (test_core.MkDirTest.test_mk_dir_creates_directory) ... ok +test_mk_dir_existing_raises_cleanly (test_core.MkDirTest.test_mk_dir_existing_raises_cleanly) ... ok +test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) ... ok +test_mv_dir_into_existing_parent_directory (test_core.MoveAndRemoveTest.test_mv_dir_into_existing_parent_directory) ... ok +test_mv_dir_into_itself_raises (test_core.MoveAndRemoveTest.test_mv_dir_into_itself_raises) ... ok +test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 ... ok +test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, ... ok +test_mv_dir_to_existing_destination_raises_and_keeps_source (test_core.MoveAndRemoveTest.test_mv_dir_to_existing_destination_raises_and_keeps_source) ... ok +test_mv_dir_to_missing_parent_raises (test_core.MoveAndRemoveTest.test_mv_dir_to_missing_parent_raises) +目标的父目录不存在时要明确报错,而不是造出一个不可达的目录。 ... ok +test_mv_dir_updates_children_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_updates_children_parent_paths) ... ok +test_mv_file_updates_name_and_parent (test_core.MoveAndRemoveTest.test_mv_file_updates_name_and_parent) ... ok +test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 ... ok +test_remove_file_with_percent_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_percent_is_not_a_wildcard) ... ok +test_remove_file_with_underscore_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_underscore_is_not_a_wildcard) +文件名里的 _ 不能被当成 LIKE 的单字符通配符。 ... ok +test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) ... ok +test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) ... ok +test_boundary_semantics_hold_for_compressed_content (test_core.ReadFileBoundaryTest.test_boundary_semantics_hold_for_compressed_content) +压缩路径下 offset/size 作用的是**解压后**的字节。 ... ok +test_full_read (test_core.ReadFileBoundaryTest.test_full_read) ... ok +test_negative_offset_is_rejected (test_core.ReadFileBoundaryTest.test_negative_offset_is_rejected) +负数会走 Python 切片语义(从末尾数),对调用方是陷阱,直接拒绝。 ... ok +test_negative_size_is_rejected (test_core.ReadFileBoundaryTest.test_negative_size_is_rejected) ... ok +test_offset_at_eof_returns_empty (test_core.ReadFileBoundaryTest.test_offset_at_eof_returns_empty) ... ok +test_offset_beyond_eof_returns_empty (test_core.ReadFileBoundaryTest.test_offset_beyond_eof_returns_empty) ... ok +test_offset_in_the_middle (test_core.ReadFileBoundaryTest.test_offset_in_the_middle) ... ok +test_size_larger_than_remaining_clamps (test_core.ReadFileBoundaryTest.test_size_larger_than_remaining_clamps) ... ok +test_zero_size (test_core.ReadFileBoundaryTest.test_zero_size) ... ok +test_fresh_database_is_accepted (test_core.SchemaCompatibilityTest.test_fresh_database_is_accepted) ... ok +test_legacy_pack_is_readable_after_migration (test_core.SchemaCompatibilityTest.test_legacy_pack_is_readable_after_migration) +迁移工具必须真的能读懂最早那版格式(compressed 布尔 + zlib)。 ... ok +test_non_sqlite_file_is_rejected (test_core.SchemaCompatibilityTest.test_non_sqlite_file_is_rejected) ... ok +test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) ... ok +test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) ... ok +test_v2_schema_db_is_rejected_with_migration_hint (test_core.SchemaCompatibilityTest.test_v2_schema_db_is_rejected_with_migration_hint) ... ok +test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 ... ok +test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) +v3:目录项与内容分家,files 只留元数据 + content_hash。 ... ok +test_no_deprecation_warning_from_any_write_path (test_core.TimestampStorageTest.test_no_deprecation_warning_from_any_write_path) ... ok +test_stored_text_keeps_the_historical_format (test_core.TimestampStorageTest.test_stored_text_keeps_the_historical_format) ... ok +test_stored_text_parses_back_to_the_file_mtime (test_core.TimestampStorageTest.test_stored_text_parses_back_to_the_file_mtime) ... ok +test_clear_snapshot_keeps_other_snapshot_content (test_dedup.BlobGarbageCollectionTest.test_clear_snapshot_keeps_other_snapshot_content) ... ok +test_deleting_snapshot_allows_gc (test_dedup.BlobGarbageCollectionTest.test_deleting_snapshot_allows_gc) +只被某个快照引用的内容,在删掉那个快照后应当被回收。 ... ok +test_gc_keeps_blobs_referenced_by_another_snapshot (test_dedup.BlobGarbageCollectionTest.test_gc_keeps_blobs_referenced_by_another_snapshot) +多快照共用内容时,删掉一个版本不能影响另一个版本。 ... ok +test_gc_returns_deleted_count (test_dedup.BlobGarbageCollectionTest.test_gc_returns_deleted_count) ... ok +test_remove_directory_collects_blobs (test_dedup.BlobGarbageCollectionTest.test_remove_directory_collects_blobs) ... ok +test_remove_file_collects_unreferenced_blob (test_dedup.BlobGarbageCollectionTest.test_remove_file_collects_unreferenced_blob) ... ok +test_shared_blob_survives_partial_removal (test_dedup.BlobGarbageCollectionTest.test_shared_blob_survives_partial_removal) ... ok +test_blob_records_original_and_stored_size (test_dedup.DedupTest.test_blob_records_original_and_stored_size) ... ok +test_dedup_skips_recompression (test_dedup.DedupTest.test_dedup_skips_recompression) +第二次写入相同内容时不该再压一遍——这是增量打包能变快的原因。 ... ok +test_distinct_content_gets_distinct_blobs (test_dedup.DedupTest.test_distinct_content_gets_distinct_blobs) ... ok +test_empty_files_share_one_blob (test_dedup.DedupTest.test_empty_files_share_one_blob) ... ok +test_identical_content_in_different_directories_shares_blob (test_dedup.DedupTest.test_identical_content_in_different_directories_shares_blob) ... ok +test_identical_files_share_one_blob (test_dedup.DedupTest.test_identical_files_share_one_blob) ... ok +test_repacking_identical_tree_does_not_grow_storage (test_dedup.DedupTest.test_repacking_identical_tree_does_not_grow_storage) ... ok +test_replacing_with_identical_content_does_not_add_blob (test_dedup.DedupTest.test_replacing_with_identical_content_does_not_add_blob) ... ok +test_rewrite_different_content_keeps_only_new_blob (test_dedup.DedupTest.test_rewrite_different_content_keeps_only_new_blob) ... ok +test_content_length_matches_body (test_fastapi_static.BasicServingTest.test_content_length_matches_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_compressed_file_correctly (test_fastapi_static.BasicServingTest.test_serves_compressed_file_correctly) +大文件在库里是哈夫曼压缩存储的,取出来必须还原成原始字节。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_empty_file (test_fastapi_static.BasicServingTest.test_serves_empty_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_nested_file (test_fastapi_static.BasicServingTest.test_serves_nested_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_top_level_file (test_fastapi_static.BasicServingTest.test_serves_top_level_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_large_asset_survives_compressed_roundtrip (test_fastapi_static.CompressionAlgorithmServingTest.test_large_asset_survives_compressed_roundtrip) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_pack_uses_zlib_by_default (test_fastapi_static.CompressionAlgorithmServingTest.test_pack_uses_zlib_by_default) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_and_last_modified_present (test_fastapi_static.ConditionalRequestTest.test_etag_and_last_modified_present) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_is_stable_across_instances (test_fastapi_static.ConditionalRequestTest.test_etag_is_stable_across_instances) +同一个资源包重新打开,ETag 不能变,否则缓存永远失效。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_modified_since_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_modified_since_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_none_match_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_with_weak_prefix (test_fastapi_static.ConditionalRequestTest.test_if_none_match_with_weak_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_max_age_sets_cache_control (test_fastapi_static.ConditionalRequestTest.test_max_age_sets_cache_control) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_stale_if_none_match_returns_full_body (test_fastapi_static.ConditionalRequestTest.test_stale_if_none_match_returns_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_pack_raises_runtime_error_with_hint (test_fastapi_static.ConstructionTest.test_missing_pack_raises_runtime_error_with_hint) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_non_database_file_is_rejected (test_fastapi_static.ConstructionTest.test_non_database_file_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_old_format_pack_is_rejected_clearly (test_fastapi_static.ConstructionTest.test_old_format_pack_is_rejected_clearly) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repr_and_close (test_fastapi_static.ConstructionTest.test_repr_and_close) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unknown_extension_falls_back_to_octet_stream (test_fastapi_static.ContentTypeTest.test_unknown_extension_falls_back_to_octet_stream) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_web_types_are_not_platform_dependent (test_fastapi_static.ContentTypeTest.test_web_types_are_not_platform_dependent) +Windows 注册表会把 .mjs 判成 text/plain(浏览器会拒绝执行模块), ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_has_no_body_but_real_length (test_fastapi_static.HeadAndRangeTest.test_head_has_no_body_but_real_length) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_with_range (test_fastapi_static.HeadAndRangeTest.test_head_with_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_match_uses_range (test_fastapi_static.HeadAndRangeTest.test_if_range_match_uses_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_mismatch_ignores_range (test_fastapi_static.HeadAndRangeTest.test_if_range_mismatch_ignores_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_malformed_range_is_ignored (test_fastapi_static.HeadAndRangeTest.test_malformed_range_is_ignored) +语法错误的 Range 按 RFC 应当被忽略,返回整份内容。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_multi_range_falls_back_to_full_body (test_fastapi_static.HeadAndRangeTest.test_multi_range_falls_back_to_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_on_compressed_content_is_correct (test_fastapi_static.HeadAndRangeTest.test_range_on_compressed_content_is_correct) +库里存的是压缩数据,Range 必须作用在解压后的内容上。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_returns_206_with_slice (test_fastapi_static.HeadAndRangeTest.test_range_returns_206_with_slice) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_with_open_end (test_fastapi_static.HeadAndRangeTest.test_range_with_open_end) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_suffix_range (test_fastapi_static.HeadAndRangeTest.test_suffix_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unsatisfiable_range_returns_416 (test_fastapi_static.HeadAndRangeTest.test_unsatisfiable_range_returns_416) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_conditional_request_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_conditional_request_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_range_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_huffman_compressed_asset (test_fastapi_static.HuffmanPackServingTest.test_serves_huffman_compressed_asset) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_redirects_to_trailing_slash (test_fastapi_static.IndexAndSpaTest.test_directory_redirects_to_trailing_slash) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_with_slash_serves_its_index (test_fastapi_static.IndexAndSpaTest.test_directory_with_slash_serves_its_index) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_file_returns_404 (test_fastapi_static.IndexAndSpaTest.test_missing_file_returns_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_json_request (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_json_request) +XHR/接口请求(Accept 不含 text/html)不该被回退成 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_missing_asset (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_missing_asset) +缺静态资源必须是 404,否则前端拿到一份 HTML 会报诡异的解析错误。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_path_traversal_cannot_escape_the_pack (test_fastapi_static.IndexAndSpaTest.test_path_traversal_cannot_escape_the_pack) +资源包不是磁盘目录,.. 之类无法越出数据库。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_post_is_rejected (test_fastapi_static.IndexAndSpaTest.test_post_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repeated_requests_do_not_modify_pack (test_fastapi_static.IndexAndSpaTest.test_repeated_requests_do_not_modify_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_serves_index_html (test_fastapi_static.IndexAndSpaTest.test_root_serves_index_html) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_deep_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_deep_route) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_frontend_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_frontend_route) +浏览器导航到前端路由 /settings 时应当拿到 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_304_does_not_read_content (test_fastapi_static.InterruptedReadTest.test_304_does_not_read_content) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_outside_prefix_is_404 (test_fastapi_static.MountPrefixTest.test_outside_prefix_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_under_prefix (test_fastapi_static.MountPrefixTest.test_serves_under_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_plain_file_still_served (test_fastapi_static.StrictModeTest.test_plain_file_still_served) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_is_404 (test_fastapi_static.StrictModeTest.test_root_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_route_is_404 (test_fastapi_static.StrictModeTest.test_route_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_bad_magic_rejected (test_huffman.HeaderValidationTest.test_bad_magic_rejected) ... ok +test_single_symbol_payload_is_not_validated (test_huffman.HeaderValidationTest.test_single_symbol_payload_is_not_validated) +记录既有行为(非缺陷):单符号数据解压时直接按原长度展开,完全不读 payload, ... ok +test_too_short_blob_rejected (test_huffman.HeaderValidationTest.test_too_short_blob_rejected) ... ok +test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) ... ok +test_wrong_freq_blob_size_rejected (test_huffman.HeaderValidationTest.test_wrong_freq_blob_size_rejected) ... ok +test_all_256_byte_values (test_huffman.RoundTripTest.test_all_256_byte_values) ... ok +test_binary_with_nulls_and_high_bytes (test_huffman.RoundTripTest.test_binary_with_nulls_and_high_bytes) ... ok +test_empty (test_huffman.RoundTripTest.test_empty) ... ok +test_incompressible_random (test_huffman.RoundTripTest.test_incompressible_random) ... ok +test_just_above_core_threshold (test_huffman.RoundTripTest.test_just_above_core_threshold) ... ok +test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) ... ok +test_single_byte (test_huffman.RoundTripTest.test_single_byte) ... ok +test_single_symbol_repeated (test_huffman.RoundTripTest.test_single_symbol_repeated) ... ok +test_two_symbols (test_huffman.RoundTripTest.test_two_symbols) ... ok +test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) ... ok +test_deleted_directory_is_removed (test_incremental.ChangeDetectionTest.test_deleted_directory_is_removed) ... ok +test_deleted_file_is_removed (test_incremental.ChangeDetectionTest.test_deleted_file_is_removed) ... ok +test_file_replaced_by_directory (test_incremental.ChangeDetectionTest.test_file_replaced_by_directory) +同一路径从文件变成目录时必须换掉类型,而不是留着旧条目。 ... ok +test_mixed_changes_in_one_run (test_incremental.ChangeDetectionTest.test_mixed_changes_in_one_run) ... ok +test_modified_file_is_updated (test_incremental.ChangeDetectionTest.test_modified_file_is_updated) ... ok +test_new_directory_is_created (test_incremental.ChangeDetectionTest.test_new_directory_is_created) ... ok +test_new_file_is_added (test_incremental.ChangeDetectionTest.test_new_file_is_added) ... ok +test_result_still_verifies (test_incremental.ChangeDetectionTest.test_result_still_verifies) ... ok +test_unchanged_tree_writes_nothing (test_incremental.ChangeDetectionTest.test_unchanged_tree_writes_nothing) ... ok +test_cli_incremental (test_incremental.IncrementalCliTest.test_cli_incremental) ... ok +test_cli_snapshot_creates_a_new_version (test_incremental.IncrementalCliTest.test_cli_snapshot_creates_a_new_version) ... ok +test_first_incremental_on_an_empty_database (test_incremental.IncrementalSnapshotTest.test_first_incremental_on_an_empty_database) ... ok +test_incremental_does_not_touch_other_snapshots (test_incremental.IncrementalSnapshotTest.test_incremental_does_not_touch_other_snapshots) ... ok +test_incremental_into_a_new_snapshot_keeps_history (test_incremental.IncrementalSnapshotTest.test_incremental_into_a_new_snapshot_keeps_history) +派生新快照后再增量:旧版本原样保留,新版本只有变化的部分被重写。 ... ok +test_dedup_reuses_blob_for_identical_new_file (test_incremental.NoRewriteTest.test_dedup_reuses_blob_for_identical_new_file) +新路径但内容与已有文件相同 → 直接复用,不新增 blob。 ... ok +test_rehash_skips_files_whose_content_is_identical (test_incremental.NoRewriteTest.test_rehash_skips_files_whose_content_is_identical) +mtime 变了但内容没变:rehash 模式应当判为未变,不做任何写入。 ... ok +test_replacing_content_frees_the_old_blob (test_incremental.NoRewriteTest.test_replacing_content_frees_the_old_blob) ... ok +test_unchanged_file_keeps_its_blob_hash (test_incremental.NoRewriteTest.test_unchanged_file_keeps_its_blob_hash) ... ok +test_unchanged_files_do_not_add_blobs (test_incremental.NoRewriteTest.test_unchanged_files_do_not_add_blobs) ... ok +test_mtime_only_change_is_detected_without_rehash (test_incremental.RehashDetectionTest.test_mtime_only_change_is_detected_without_rehash) +同大小 + 同秒修改:默认模式可能漏掉,这正是 rehash 存在的理由。 ... ok +test_rehash_catches_same_size_same_mtime_edit (test_incremental.RehashDetectionTest.test_rehash_catches_same_size_same_mtime_edit) ... ok +test_digest_changes_when_a_file_is_added (test_integrity.DigestTest.test_digest_changes_when_a_file_is_added) ... ok +test_digest_changes_when_content_changes (test_integrity.DigestTest.test_digest_changes_when_content_changes) ... ok +test_digest_changes_when_metadata_changes (test_integrity.DigestTest.test_digest_changes_when_metadata_changes) +路径/大小/时间戳/权限也参与摘要,不只是内容。 ... ok +test_digest_ignores_insertion_order (test_integrity.DigestTest.test_digest_ignores_insertion_order) +摘要必须只取决于内容与元数据,与插入顺序无关。 ... ok +test_digest_is_recorded_and_stable_across_reopen (test_integrity.DigestTest.test_digest_is_recorded_and_stable_across_reopen) ... ok +test_digest_matches_manual_recomputation (test_integrity.DigestTest.test_digest_matches_manual_recomputation) ... ok +test_digest_can_be_exported_for_external_signing (test_integrity.SignatureTest.test_digest_can_be_exported_for_external_signing) +非对称签名要交给外部工具,所以摘要必须能干净地取出来。 ... ok +test_sign_and_verify (test_integrity.SignatureTest.test_sign_and_verify) ... ok +test_sign_requires_write_access (test_integrity.SignatureTest.test_sign_requires_write_access) ... ok +test_signature_verifies_on_a_readonly_connection (test_integrity.SignatureTest.test_signature_verifies_on_a_readonly_connection) +只读方必须能在不改动库的前提下完成验证。 ... ok +test_tampering_after_signing_fails (test_integrity.SignatureTest.test_tampering_after_signing_fails) ... ok +test_verify_signature_by_key_id (test_integrity.SignatureTest.test_verify_signature_by_key_id) ... ok +test_verify_without_a_signature_is_reported (test_integrity.SignatureTest.test_verify_without_a_signature_is_reported) ... ok +test_wrong_key_fails (test_integrity.SignatureTest.test_wrong_key_fails) ... ok +test_content_tamper_is_caught_by_check_content (test_integrity.VerifyTest.test_content_tamper_is_caught_by_check_content) +同大小改掉内容:元数据级校验看不出来,权威校验必须抓到。 ... ok +test_metadata_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_metadata_tamper_is_caught_by_digest) ... ok +test_missing_blob_is_caught (test_integrity.VerifyTest.test_missing_blob_is_caught) ... ok +test_path_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_path_tamper_is_caught_by_digest) ... ok +test_read_file_with_verify_hash_raises_on_tamper (test_integrity.VerifyTest.test_read_file_with_verify_hash_raises_on_tamper) ... ok +test_size_mismatch_is_caught (test_integrity.VerifyTest.test_size_mismatch_is_caught) ... ok +test_verify_can_target_an_older_snapshot (test_integrity.VerifyTest.test_verify_can_target_an_older_snapshot) +摘要防的是"绕过 API 的改动";走 API 的写入会刷新摘要,不算篡改。 ... ok +test_verify_passes_on_a_fresh_pack (test_integrity.VerifyTest.test_verify_passes_on_a_fresh_pack) ... ok +test_verify_reports_empty_database (test_integrity.VerifyTest.test_verify_reports_empty_database) ... ok +test_verify_with_content_check_passes (test_integrity.VerifyTest.test_verify_with_content_check_passes) ... ok +test_detects_each_format (test_migration.DetectVersionTest.test_detects_each_format) ... ok +test_detects_non_vfs_database (test_migration.DetectVersionTest.test_detects_non_vfs_database) ... ok +test_missing_file_raises (test_migration.DetectVersionTest.test_missing_file_raises) ... ok +test_migrated_database_unpacks_via_cli (test_migration.MigrateCliTest.test_migrated_database_unpacks_via_cli) ... ok +test_v1_bool_flag_is_mapped_to_zlib (test_migration.MigrateFromV1Test.test_v1_bool_flag_is_mapped_to_zlib) ... ok +test_backup_can_be_disabled (test_migration.MigrateFromV2Test.test_backup_can_be_disabled) ... ok +test_backup_can_be_forced_with_output (test_migration.MigrateFromV2Test.test_backup_can_be_forced_with_output) ... ok +test_backup_is_created_and_original_untouched (test_migration.MigrateFromV2Test.test_backup_is_created_and_original_untouched) ... ok +test_content_survives_byte_for_byte (test_migration.MigrateFromV2Test.test_content_survives_byte_for_byte) ... ok +test_custom_snapshot_name (test_migration.MigrateFromV2Test.test_custom_snapshot_name) ... ok +test_identical_content_is_deduplicated_during_migration (test_migration.MigrateFromV2Test.test_identical_content_is_deduplicated_during_migration) ... ok +test_metadata_is_carried_over (test_migration.MigrateFromV2Test.test_metadata_is_carried_over) ... ok +test_migrate_to_a_new_file_leaves_the_original_alone (test_migration.MigrateFromV2Test.test_migrate_to_a_new_file_leaves_the_original_alone) +原库在只读介质上时不能原地替换,所以支持输出到新路径。 ... ok +test_migrated_pack_verifies_and_signs (test_migration.MigrateFromV2Test.test_migrated_pack_verifies_and_signs) ... ok +test_migration_of_a_readonly_file_into_a_new_file (test_migration.MigrateFromV2Test.test_migration_of_a_readonly_file_into_a_new_file) ... ok +test_refuses_to_overwrite_an_existing_output (test_migration.MigrateFromV2Test.test_refuses_to_overwrite_an_existing_output) ... ok +test_second_migration_is_a_no_op (test_migration.MigrateFromV2Test.test_second_migration_is_a_no_op) ... ok +test_snapshot_name_defaults_to_the_pack_name (test_migration.MigrateFromV2Test.test_snapshot_name_defaults_to_the_pack_name) ... ok +test_structure_is_preserved (test_migration.MigrateFromV2Test.test_structure_is_preserved) ... ok +test_missing_file_raises (test_migration.MigrateInvalidInputTest.test_missing_file_raises) ... ok +test_non_vfs_database_is_rejected (test_migration.MigrateInvalidInputTest.test_non_vfs_database_is_rejected) ... ok +test_original_survives_a_failure (test_migration.MigrateInvalidInputTest.test_original_survives_a_failure) +迁移失败时原库必须原样保留(中间态写在临时文件里)。 ... ok +test_manifest_contents (test_publish.ExportTest.test_manifest_contents) ... ok +test_manifest_is_deterministic_for_the_same_snapshot (test_publish.ExportTest.test_manifest_is_deterministic_for_the_same_snapshot) +清单里不放"生成时间",所以同一快照导出两次得到完全一样的清单(可 diff、可签名)。 ... ok +test_manifest_of_a_nonexistent_snapshot_is_rejected (test_publish.ExportTest.test_manifest_of_a_nonexistent_snapshot_is_rejected) ... ok +test_object_content_is_the_original_bytes (test_publish.ExportTest.test_object_content_is_the_original_bytes) +库里内容是压缩的,导出必须是原始字节——消费方不该需要我们的解压代码。 ... ok +test_object_name_is_its_hash (test_publish.ExportTest.test_object_name_is_its_hash) ... ok +test_objects_are_deduplicated (test_publish.ExportTest.test_objects_are_deduplicated) ... ok +test_publish_a_specific_snapshot (test_publish.ExportTest.test_publish_a_specific_snapshot) ... ok +test_publish_is_read_only_on_the_pack (test_publish.ExportTest.test_publish_is_read_only_on_the_pack) ... ok +test_refuses_non_empty_output_without_overwrite (test_publish.ExportTest.test_refuses_non_empty_output_without_overwrite) ... ok +test_savings_are_reported_in_separate_units (test_publish.ExportTest.test_savings_are_reported_in_separate_units) +去重与压缩必须分开报:把"压缩省下的"算进"去重省下的"会误导选型。 ... ok +test_shard_zero_puts_objects_flat (test_publish.ExportTest.test_shard_zero_puts_objects_flat) ... ok +test_cache_is_reused_and_verified (test_publish.FetchLocalTest.test_cache_is_reused_and_verified) ... ok +test_corrupted_cache_entry_is_healed (test_publish.FetchLocalTest.test_corrupted_cache_entry_is_healed) ... ok +test_link_mode_hardlinks_into_the_cache (test_publish.FetchLocalTest.test_link_mode_hardlinks_into_the_cache) ... ok +test_mtime_and_mode_are_preserved (test_publish.FetchLocalTest.test_mtime_and_mode_are_preserved) ... ok +test_partial_fetch_by_file_and_by_prefix (test_publish.FetchLocalTest.test_partial_fetch_by_file_and_by_prefix) ... ok +test_partial_fetch_writes_only_selected (test_publish.FetchLocalTest.test_partial_fetch_writes_only_selected) ... ok +test_roundtrip_restores_the_tree (test_publish.FetchLocalTest.test_roundtrip_restores_the_tree) ... ok +test_bad_manifest_format_is_rejected (test_publish.FetchOverHttpTest.test_bad_manifest_format_is_rejected) ... ok +test_fetch_from_a_real_http_server (test_publish.FetchOverHttpTest.test_fetch_from_a_real_http_server) ... ok +test_manifest_from_url_mismatch_is_reported (test_publish.FetchOverHttpTest.test_manifest_from_url_mismatch_is_reported) ... ok +test_missing_object_is_reported_clearly (test_publish.FetchOverHttpTest.test_missing_object_is_reported_clearly) ... ok +test_tampered_object_over_http_is_rejected (test_publish.FetchOverHttpTest.test_tampered_object_over_http_is_rejected) ... ok +test_fetch_json_output (test_publish.PublishCliTest.test_fetch_json_output) ... ok +test_fetch_missing_path_exits_nonzero (test_publish.PublishCliTest.test_fetch_missing_path_exits_nonzero) ... ok +test_manifest_only_mode (test_publish.PublishCliTest.test_manifest_only_mode) ... ok +test_publish_and_fetch_via_cli (test_publish.PublishCliTest.test_publish_and_fetch_via_cli) ... ok +test_publish_missing_db_exits_nonzero (test_publish.PublishCliTest.test_publish_missing_db_exits_nonzero) ... ok +test_publish_to_nonempty_dir_requires_overwrite (test_publish.PublishCliTest.test_publish_to_nonempty_dir_requires_overwrite) ... ok +test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) +用一个足够大的重复文本,让压缩效果超过 SQLite 页粒度(4KB)的噪声。 ... ok +test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 ... ok +test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 ... ok +test_packed_database_contains_no_duplicate_paths (test_roundtrip.RoundTripTest.test_packed_database_contains_no_duplicate_paths) +唯一性现在是"每个快照内"的:UNIQUE(snapshot_id, path)。 ... ok +test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) ... ok +test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) ... ok +test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) ... ok +test_stored_blob_of_compressible_file_is_smaller (test_roundtrip.RoundTripTest.test_stored_blob_of_compressible_file_is_smaller) +压缩不只是"标记了一下":库里真正存的字节必须变少。 ... ok +test_add_vfs_metadata_names_the_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_add_vfs_metadata_names_the_default_snapshot) ... ok +test_constructor_releases_the_connection_when_snapshot_is_invalid (test_snapshots.SnapshotLifecycleTest.test_constructor_releases_the_connection_when_snapshot_is_invalid) +构造期抛异常也要释放连接,否则 Windows 上库文件被锁住删不掉。 ... ok +test_create_snapshot_becomes_active_and_is_listed (test_snapshots.SnapshotLifecycleTest.test_create_snapshot_becomes_active_and_is_listed) ... ok +test_delete_snapshot_removes_entries_and_signature_rows (test_snapshots.SnapshotLifecycleTest.test_delete_snapshot_removes_entries_and_signature_rows) ... ok +test_first_write_creates_a_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_first_write_creates_a_default_snapshot) ... ok +test_readonly_never_creates_a_snapshot (test_snapshots.SnapshotLifecycleTest.test_readonly_never_creates_a_snapshot) +只读打开不能因为"读一下"就写库——否则只读挂载会失败。 ... ok +test_reopen_defaults_to_the_latest_snapshot (test_snapshots.SnapshotLifecycleTest.test_reopen_defaults_to_the_latest_snapshot) ... ok +test_use_missing_snapshot_raises (test_snapshots.SnapshotLifecycleTest.test_use_missing_snapshot_raises) ... ok +test_use_snapshot_switches_the_view (test_snapshots.SnapshotLifecycleTest.test_use_snapshot_switches_the_view) ... ok +test_blobs_are_shared_between_snapshots (test_snapshots.TimeTravelTest.test_blobs_are_shared_between_snapshots) ... ok +test_copy_entries_forks_from_the_parent (test_snapshots.TimeTravelTest.test_copy_entries_forks_from_the_parent) ... ok +test_counts_are_per_snapshot (test_snapshots.TimeTravelTest.test_counts_are_per_snapshot) ... ok +test_each_snapshot_has_its_own_digest (test_snapshots.TimeTravelTest.test_each_snapshot_has_its_own_digest) ... ok +test_export_respects_the_active_snapshot (test_snapshots.TimeTravelTest.test_export_respects_the_active_snapshot) ... ok +test_list_directory_is_snapshot_scoped (test_snapshots.TimeTravelTest.test_list_directory_is_snapshot_scoped) ... ok +test_mkdir_in_one_snapshot_does_not_leak (test_snapshots.TimeTravelTest.test_mkdir_in_one_snapshot_does_not_leak) ... ok +test_new_files_are_invisible_in_the_older_snapshot (test_snapshots.TimeTravelTest.test_new_files_are_invisible_in_the_older_snapshot) ... ok +test_remove_in_one_snapshot_does_not_affect_the_other (test_snapshots.TimeTravelTest.test_remove_in_one_snapshot_does_not_affect_the_other) ... ok +test_same_path_has_different_content_per_snapshot (test_snapshots.TimeTravelTest.test_same_path_has_different_content_per_snapshot) ... ok +test_snapshot_id_can_be_chosen_at_construction (test_snapshots.TimeTravelTest.test_snapshot_id_can_be_chosen_at_construction) ... ok +test_unpacker_check_database_counts_only_the_active_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_check_database_counts_only_the_active_snapshot) ... ok +test_unpacker_export_follows_the_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_export_follows_the_snapshot) ... ok +test_unpacker_list_root_is_snapshot_scoped (test_snapshots.UnpackerSnapshotTest.test_unpacker_list_root_is_snapshot_scoped) ... ok + +---------------------------------------------------------------------- +Ran 343 tests in 22.328s + +OK (skipped=52) + + [HTTP 实测] 请求 4 次(清单 1 + 对象 3),下载 256.0 KB(清单 1.1 KB),耗时 29ms +EXIT=0 diff --git a/docs/evidence-publish.txt b/docs/evidence-publish.txt new file mode 100644 index 0000000..a6065fe --- /dev/null +++ b/docs/evidence-publish.txt @@ -0,0 +1,380 @@ +test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) ... ok +test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) ... ok +test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) ... ok +test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:426: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) ... ok +test_migrates_in_place_with_backup (test_cli.CliMigrateTest.test_migrates_in_place_with_backup) ... ok +test_non_vfs_file_is_rejected (test_cli.CliMigrateTest.test_non_vfs_file_is_rejected) ... ok +test_output_mode_leaves_the_original_alone (test_cli.CliMigrateTest.test_output_mode_leaves_the_original_alone) ... ok +test_reports_when_nothing_to_do (test_cli.CliMigrateTest.test_reports_when_nothing_to_do) ... ok +test_compress_algo_huffman_is_still_selectable (test_cli.CliPackTest.test_compress_algo_huffman_is_still_selectable) +早期算法仍然可选,且打出来的包能被正常读回。 ... ok +test_default_compression_algorithm_is_zlib (test_cli.CliPackTest.test_default_compression_algorithm_is_zlib) ... ok +test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) ... ok +test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, ... ok +test_pack_reports_file_count_matching_database (test_cli.CliPackTest.test_pack_reports_file_count_matching_database) ... ok +test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) ... ok +test_list_snapshots_option (test_cli.CliUnpackSnapshotTest.test_list_snapshots_option) ... ok +test_unknown_snapshot_is_rejected_cleanly (test_cli.CliUnpackSnapshotTest.test_unknown_snapshot_is_rejected_cleanly) ... ok +test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) ... ok +test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 ... ok +test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) ... ok +test_list_root_only_shows_top_level (test_cli.CliUnpackTest.test_list_root_only_shows_top_level) +P0-2: 根目录列表不能把整张表铺平。 ... ok +test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) ... ok +test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) ... ok +test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 ... ok +test_content_tampering_needs_check_content (test_cli.CliVerifyTest.test_content_tampering_needs_check_content) ... ok +test_json_output (test_cli.CliVerifyTest.test_json_output) ... ok +test_list_snapshots (test_cli.CliVerifyTest.test_list_snapshots) ... ok +test_missing_db_exits_nonzero (test_cli.CliVerifyTest.test_missing_db_exits_nonzero) ... ok +test_sign_then_verify (test_cli.CliVerifyTest.test_sign_then_verify) ... ok +test_snapshot_selection (test_cli.CliVerifyTest.test_snapshot_selection) ... ok +test_verify_does_not_modify_the_pack (test_cli.CliVerifyTest.test_verify_does_not_modify_the_pack) ... ok +test_verify_exits_nonzero_on_metadata_tampering (test_cli.CliVerifyTest.test_verify_exits_nonzero_on_metadata_tampering) ... ok +test_verify_reports_ok (test_cli.CliVerifyTest.test_verify_reports_ok) ... ok +test_verify_with_content_check (test_cli.CliVerifyTest.test_verify_with_content_check) ... ok +test_importing_core_never_pulls_in_third_party (test_contrib_optional.OptionalDependencyTest.test_importing_core_never_pulls_in_third_party) +import sqlite_vfs 不能顺带把 fastapi/starlette 拖进来。 ... ok +test_missing_database_path_hint (test_contrib_optional.OptionalDependencyTest.test_missing_database_path_hint) +未生成资源包时的报错要给出下一步命令。 ... skipped '当前环境没有 FastAPI' +test_missing_fastapi_gives_actionable_error (test_contrib_optional.OptionalDependencyTest.test_missing_fastapi_gives_actionable_error) +缺少可选依赖时,报错信息必须能直接照着做。 ... ok +test_plugin_module_is_not_imported_by_the_package (test_contrib_optional.OptionalDependencyTest.test_plugin_module_is_not_imported_by_the_package) +contrib 子包不能自动导入插件模块(否则缺依赖时整包不可用)。 ... ok +test_third_party_is_installed_or_not (test_contrib_optional.OptionalDependencyTest.test_third_party_is_installed_or_not) +只用于在输出里说明当前环境状态,不断言。 ... ok +test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:746: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_compression_disabled_stores_raw (test_core.CompressionAlgorithmTest.test_compression_disabled_stores_raw) ... ok +test_default_algorithm_is_zlib (test_core.CompressionAlgorithmTest.test_default_algorithm_is_zlib) ... ok +test_each_algorithm_roundtrips (test_core.CompressionAlgorithmTest.test_each_algorithm_roundtrips) ... ok +test_early_huffman_packs_are_still_readable (test_core.CompressionAlgorithmTest.test_early_huffman_packs_are_still_readable) +早期版本用 huffman 打的包必须还能读——这是保留该算法唯一的理由。 ... ok +test_unknown_algorithm_in_database_raises_on_read (test_core.CompressionAlgorithmTest.test_unknown_algorithm_in_database_raises_on_read) +库里的算法名本版本不认识时要明确报错,而不是返回坏数据。 ... ok +test_unknown_algorithm_rejected_at_construction (test_core.CompressionAlgorithmTest.test_unknown_algorithm_rejected_at_construction) ... ok +test_zlib_compresses_better_than_huffman (test_core.CompressionAlgorithmTest.test_zlib_compresses_better_than_huffman) +同一份文本,zlib 的结果必须明显小于 huffman(LZ77 能吃重复)。 ... ok +test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) ... ok +test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) ... ok +test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) ... ok +test_paths_are_returned_as_dicts_with_datetime (test_core.DirectoryListingTest.test_paths_are_returned_as_dicts_with_datetime) ... ok +test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) ... ok +test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) ... ok +test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) ... ok +test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 ... ok +test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) ... ok +test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) ... ok +test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 ... ok +test_compressible_file_uses_zlib (test_core.FileContentTest.test_compressible_file_uses_zlib) ... ok +test_compression_applies_below_the_old_1kb_bar (test_core.FileContentTest.test_compression_applies_below_the_old_1kb_bar) +zlib 包头只有几字节,小文件也值得压(旧的哈夫曼阈值是 1KB)。 ... ok +test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) ... ok +test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) ... ok +test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) ... ok +test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) ... ok +test_read_file_rejects_directory_and_missing_path (test_core.FileContentTest.test_read_file_rejects_directory_and_missing_path) ... ok +test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) ... ok +test_tiny_file_is_not_compressed (test_core.FileContentTest.test_tiny_file_is_not_compressed) ... ok +test_mk_dir_creates_directory (test_core.MkDirTest.test_mk_dir_creates_directory) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:1329: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_mk_dir_existing_raises_cleanly (test_core.MkDirTest.test_mk_dir_existing_raises_cleanly) ... ok +test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) ... ok +test_mv_dir_into_existing_parent_directory (test_core.MoveAndRemoveTest.test_mv_dir_into_existing_parent_directory) ... ok +test_mv_dir_into_itself_raises (test_core.MoveAndRemoveTest.test_mv_dir_into_itself_raises) ... ok +test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 ... ok +test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, ... ok +test_mv_dir_to_existing_destination_raises_and_keeps_source (test_core.MoveAndRemoveTest.test_mv_dir_to_existing_destination_raises_and_keeps_source) ... ok +test_mv_dir_to_missing_parent_raises (test_core.MoveAndRemoveTest.test_mv_dir_to_missing_parent_raises) +目标的父目录不存在时要明确报错,而不是造出一个不可达的目录。 ... ok +test_mv_dir_updates_children_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_updates_children_parent_paths) ... ok +test_mv_file_updates_name_and_parent (test_core.MoveAndRemoveTest.test_mv_file_updates_name_and_parent) ... ok +test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 ... ok +test_remove_file_with_percent_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_percent_is_not_a_wildcard) ... ok +test_remove_file_with_underscore_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_underscore_is_not_a_wildcard) +文件名里的 _ 不能被当成 LIKE 的单字符通配符。 ... ok +test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) ... ok +test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) ... ok +test_fresh_database_is_accepted (test_core.SchemaCompatibilityTest.test_fresh_database_is_accepted) ... ok +test_legacy_pack_is_readable_after_migration (test_core.SchemaCompatibilityTest.test_legacy_pack_is_readable_after_migration) +迁移工具必须真的能读懂最早那版格式(compressed 布尔 + zlib)。 ... ok +test_non_sqlite_file_is_rejected (test_core.SchemaCompatibilityTest.test_non_sqlite_file_is_rejected) ... ok +test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) ... ok +test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) ... ok +test_v2_schema_db_is_rejected_with_migration_hint (test_core.SchemaCompatibilityTest.test_v2_schema_db_is_rejected_with_migration_hint) ... ok +test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 ... ok +test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) +v3:目录项与内容分家,files 只留元数据 + content_hash。 ... ok +test_clear_snapshot_keeps_other_snapshot_content (test_dedup.BlobGarbageCollectionTest.test_clear_snapshot_keeps_other_snapshot_content) ... ok +test_deleting_snapshot_allows_gc (test_dedup.BlobGarbageCollectionTest.test_deleting_snapshot_allows_gc) +只被某个快照引用的内容,在删掉那个快照后应当被回收。 ... ok +test_gc_keeps_blobs_referenced_by_another_snapshot (test_dedup.BlobGarbageCollectionTest.test_gc_keeps_blobs_referenced_by_another_snapshot) +多快照共用内容时,删掉一个版本不能影响另一个版本。 ... ok +test_gc_returns_deleted_count (test_dedup.BlobGarbageCollectionTest.test_gc_returns_deleted_count) ... ok +test_remove_directory_collects_blobs (test_dedup.BlobGarbageCollectionTest.test_remove_directory_collects_blobs) ... ok +test_remove_file_collects_unreferenced_blob (test_dedup.BlobGarbageCollectionTest.test_remove_file_collects_unreferenced_blob) ... ok +test_shared_blob_survives_partial_removal (test_dedup.BlobGarbageCollectionTest.test_shared_blob_survives_partial_removal) ... ok +test_blob_records_original_and_stored_size (test_dedup.DedupTest.test_blob_records_original_and_stored_size) ... ok +test_dedup_skips_recompression (test_dedup.DedupTest.test_dedup_skips_recompression) +第二次写入相同内容时不该再压一遍——这是增量打包能变快的原因。 ... ok +test_distinct_content_gets_distinct_blobs (test_dedup.DedupTest.test_distinct_content_gets_distinct_blobs) ... ok +test_empty_files_share_one_blob (test_dedup.DedupTest.test_empty_files_share_one_blob) ... ok +test_identical_content_in_different_directories_shares_blob (test_dedup.DedupTest.test_identical_content_in_different_directories_shares_blob) ... ok +test_identical_files_share_one_blob (test_dedup.DedupTest.test_identical_files_share_one_blob) ... ok +test_repacking_identical_tree_does_not_grow_storage (test_dedup.DedupTest.test_repacking_identical_tree_does_not_grow_storage) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:656: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_replacing_with_identical_content_does_not_add_blob (test_dedup.DedupTest.test_replacing_with_identical_content_does_not_add_blob) ... ok +test_rewrite_different_content_keeps_only_new_blob (test_dedup.DedupTest.test_rewrite_different_content_keeps_only_new_blob) ... ok +test_content_length_matches_body (test_fastapi_static.BasicServingTest.test_content_length_matches_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_compressed_file_correctly (test_fastapi_static.BasicServingTest.test_serves_compressed_file_correctly) +大文件在库里是哈夫曼压缩存储的,取出来必须还原成原始字节。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_empty_file (test_fastapi_static.BasicServingTest.test_serves_empty_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_nested_file (test_fastapi_static.BasicServingTest.test_serves_nested_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_top_level_file (test_fastapi_static.BasicServingTest.test_serves_top_level_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_large_asset_survives_compressed_roundtrip (test_fastapi_static.CompressionAlgorithmServingTest.test_large_asset_survives_compressed_roundtrip) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_pack_uses_zlib_by_default (test_fastapi_static.CompressionAlgorithmServingTest.test_pack_uses_zlib_by_default) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_and_last_modified_present (test_fastapi_static.ConditionalRequestTest.test_etag_and_last_modified_present) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_is_stable_across_instances (test_fastapi_static.ConditionalRequestTest.test_etag_is_stable_across_instances) +同一个资源包重新打开,ETag 不能变,否则缓存永远失效。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_modified_since_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_modified_since_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_none_match_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_with_weak_prefix (test_fastapi_static.ConditionalRequestTest.test_if_none_match_with_weak_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_max_age_sets_cache_control (test_fastapi_static.ConditionalRequestTest.test_max_age_sets_cache_control) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_stale_if_none_match_returns_full_body (test_fastapi_static.ConditionalRequestTest.test_stale_if_none_match_returns_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_pack_raises_runtime_error_with_hint (test_fastapi_static.ConstructionTest.test_missing_pack_raises_runtime_error_with_hint) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_non_database_file_is_rejected (test_fastapi_static.ConstructionTest.test_non_database_file_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_old_format_pack_is_rejected_clearly (test_fastapi_static.ConstructionTest.test_old_format_pack_is_rejected_clearly) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repr_and_close (test_fastapi_static.ConstructionTest.test_repr_and_close) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unknown_extension_falls_back_to_octet_stream (test_fastapi_static.ContentTypeTest.test_unknown_extension_falls_back_to_octet_stream) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_web_types_are_not_platform_dependent (test_fastapi_static.ContentTypeTest.test_web_types_are_not_platform_dependent) +Windows 注册表会把 .mjs 判成 text/plain(浏览器会拒绝执行模块), ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_has_no_body_but_real_length (test_fastapi_static.HeadAndRangeTest.test_head_has_no_body_but_real_length) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_with_range (test_fastapi_static.HeadAndRangeTest.test_head_with_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_match_uses_range (test_fastapi_static.HeadAndRangeTest.test_if_range_match_uses_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_mismatch_ignores_range (test_fastapi_static.HeadAndRangeTest.test_if_range_mismatch_ignores_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_malformed_range_is_ignored (test_fastapi_static.HeadAndRangeTest.test_malformed_range_is_ignored) +语法错误的 Range 按 RFC 应当被忽略,返回整份内容。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_multi_range_falls_back_to_full_body (test_fastapi_static.HeadAndRangeTest.test_multi_range_falls_back_to_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_on_compressed_content_is_correct (test_fastapi_static.HeadAndRangeTest.test_range_on_compressed_content_is_correct) +库里存的是压缩数据,Range 必须作用在解压后的内容上。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_returns_206_with_slice (test_fastapi_static.HeadAndRangeTest.test_range_returns_206_with_slice) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_with_open_end (test_fastapi_static.HeadAndRangeTest.test_range_with_open_end) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_suffix_range (test_fastapi_static.HeadAndRangeTest.test_suffix_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unsatisfiable_range_returns_416 (test_fastapi_static.HeadAndRangeTest.test_unsatisfiable_range_returns_416) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_conditional_request_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_conditional_request_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_range_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_huffman_compressed_asset (test_fastapi_static.HuffmanPackServingTest.test_serves_huffman_compressed_asset) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_redirects_to_trailing_slash (test_fastapi_static.IndexAndSpaTest.test_directory_redirects_to_trailing_slash) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_with_slash_serves_its_index (test_fastapi_static.IndexAndSpaTest.test_directory_with_slash_serves_its_index) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_file_returns_404 (test_fastapi_static.IndexAndSpaTest.test_missing_file_returns_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_json_request (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_json_request) +XHR/接口请求(Accept 不含 text/html)不该被回退成 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_missing_asset (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_missing_asset) +缺静态资源必须是 404,否则前端拿到一份 HTML 会报诡异的解析错误。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_path_traversal_cannot_escape_the_pack (test_fastapi_static.IndexAndSpaTest.test_path_traversal_cannot_escape_the_pack) +资源包不是磁盘目录,.. 之类无法越出数据库。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_post_is_rejected (test_fastapi_static.IndexAndSpaTest.test_post_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repeated_requests_do_not_modify_pack (test_fastapi_static.IndexAndSpaTest.test_repeated_requests_do_not_modify_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_serves_index_html (test_fastapi_static.IndexAndSpaTest.test_root_serves_index_html) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_deep_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_deep_route) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_frontend_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_frontend_route) +浏览器导航到前端路由 /settings 时应当拿到 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_304_does_not_read_content (test_fastapi_static.InterruptedReadTest.test_304_does_not_read_content) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_outside_prefix_is_404 (test_fastapi_static.MountPrefixTest.test_outside_prefix_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_under_prefix (test_fastapi_static.MountPrefixTest.test_serves_under_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_plain_file_still_served (test_fastapi_static.StrictModeTest.test_plain_file_still_served) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_is_404 (test_fastapi_static.StrictModeTest.test_root_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_route_is_404 (test_fastapi_static.StrictModeTest.test_route_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_bad_magic_rejected (test_huffman.HeaderValidationTest.test_bad_magic_rejected) ... ok +test_single_symbol_payload_is_not_validated (test_huffman.HeaderValidationTest.test_single_symbol_payload_is_not_validated) +记录既有行为(非缺陷):单符号数据解压时直接按原长度展开,完全不读 payload, ... ok +test_too_short_blob_rejected (test_huffman.HeaderValidationTest.test_too_short_blob_rejected) ... ok +test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) ... ok +test_wrong_freq_blob_size_rejected (test_huffman.HeaderValidationTest.test_wrong_freq_blob_size_rejected) ... ok +test_all_256_byte_values (test_huffman.RoundTripTest.test_all_256_byte_values) ... ok +test_binary_with_nulls_and_high_bytes (test_huffman.RoundTripTest.test_binary_with_nulls_and_high_bytes) ... ok +test_empty (test_huffman.RoundTripTest.test_empty) ... ok +test_incompressible_random (test_huffman.RoundTripTest.test_incompressible_random) ... ok +test_just_above_core_threshold (test_huffman.RoundTripTest.test_just_above_core_threshold) ... ok +test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) ... ok +test_single_byte (test_huffman.RoundTripTest.test_single_byte) ... ok +test_single_symbol_repeated (test_huffman.RoundTripTest.test_single_symbol_repeated) ... ok +test_two_symbols (test_huffman.RoundTripTest.test_two_symbols) ... ok +test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) ... ok +test_deleted_directory_is_removed (test_incremental.ChangeDetectionTest.test_deleted_directory_is_removed) ... ok +test_deleted_file_is_removed (test_incremental.ChangeDetectionTest.test_deleted_file_is_removed) ... ok +test_file_replaced_by_directory (test_incremental.ChangeDetectionTest.test_file_replaced_by_directory) +同一路径从文件变成目录时必须换掉类型,而不是留着旧条目。 ... ok +test_mixed_changes_in_one_run (test_incremental.ChangeDetectionTest.test_mixed_changes_in_one_run) ... ok +test_modified_file_is_updated (test_incremental.ChangeDetectionTest.test_modified_file_is_updated) ... ok +test_new_directory_is_created (test_incremental.ChangeDetectionTest.test_new_directory_is_created) ... ok +test_new_file_is_added (test_incremental.ChangeDetectionTest.test_new_file_is_added) ... ok +test_result_still_verifies (test_incremental.ChangeDetectionTest.test_result_still_verifies) ... ok +test_unchanged_tree_writes_nothing (test_incremental.ChangeDetectionTest.test_unchanged_tree_writes_nothing) ... ok +test_cli_incremental (test_incremental.IncrementalCliTest.test_cli_incremental) ... ok +test_cli_snapshot_creates_a_new_version (test_incremental.IncrementalCliTest.test_cli_snapshot_creates_a_new_version) ... ok +test_first_incremental_on_an_empty_database (test_incremental.IncrementalSnapshotTest.test_first_incremental_on_an_empty_database) ... ok +test_incremental_does_not_touch_other_snapshots (test_incremental.IncrementalSnapshotTest.test_incremental_does_not_touch_other_snapshots) ... ok +test_incremental_into_a_new_snapshot_keeps_history (test_incremental.IncrementalSnapshotTest.test_incremental_into_a_new_snapshot_keeps_history) +派生新快照后再增量:旧版本原样保留,新版本只有变化的部分被重写。 ... ok +test_dedup_reuses_blob_for_identical_new_file (test_incremental.NoRewriteTest.test_dedup_reuses_blob_for_identical_new_file) +新路径但内容与已有文件相同 → 直接复用,不新增 blob。 ... ok +test_rehash_skips_files_whose_content_is_identical (test_incremental.NoRewriteTest.test_rehash_skips_files_whose_content_is_identical) +mtime 变了但内容没变:rehash 模式应当判为未变,不做任何写入。 ... ok +test_replacing_content_frees_the_old_blob (test_incremental.NoRewriteTest.test_replacing_content_frees_the_old_blob) ... ok +test_unchanged_file_keeps_its_blob_hash (test_incremental.NoRewriteTest.test_unchanged_file_keeps_its_blob_hash) ... ok +test_unchanged_files_do_not_add_blobs (test_incremental.NoRewriteTest.test_unchanged_files_do_not_add_blobs) ... ok +test_mtime_only_change_is_detected_without_rehash (test_incremental.RehashDetectionTest.test_mtime_only_change_is_detected_without_rehash) +同大小 + 同秒修改:默认模式可能漏掉,这正是 rehash 存在的理由。 ... ok +test_rehash_catches_same_size_same_mtime_edit (test_incremental.RehashDetectionTest.test_rehash_catches_same_size_same_mtime_edit) ... ok +test_digest_changes_when_a_file_is_added (test_integrity.DigestTest.test_digest_changes_when_a_file_is_added) ... ok +test_digest_changes_when_content_changes (test_integrity.DigestTest.test_digest_changes_when_content_changes) ... ok +test_digest_changes_when_metadata_changes (test_integrity.DigestTest.test_digest_changes_when_metadata_changes) +路径/大小/时间戳/权限也参与摘要,不只是内容。 ... ok +test_digest_ignores_insertion_order (test_integrity.DigestTest.test_digest_ignores_insertion_order) +摘要必须只取决于内容与元数据,与插入顺序无关。 ... ok +test_digest_is_recorded_and_stable_across_reopen (test_integrity.DigestTest.test_digest_is_recorded_and_stable_across_reopen) ... ok +test_digest_matches_manual_recomputation (test_integrity.DigestTest.test_digest_matches_manual_recomputation) ... ok +test_digest_can_be_exported_for_external_signing (test_integrity.SignatureTest.test_digest_can_be_exported_for_external_signing) +非对称签名要交给外部工具,所以摘要必须能干净地取出来。 ... ok +test_sign_and_verify (test_integrity.SignatureTest.test_sign_and_verify) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:1231: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + self.conn.execute( +ok +test_sign_requires_write_access (test_integrity.SignatureTest.test_sign_requires_write_access) ... ok +test_signature_verifies_on_a_readonly_connection (test_integrity.SignatureTest.test_signature_verifies_on_a_readonly_connection) +只读方必须能在不改动库的前提下完成验证。 ... ok +test_tampering_after_signing_fails (test_integrity.SignatureTest.test_tampering_after_signing_fails) ... ok +test_verify_signature_by_key_id (test_integrity.SignatureTest.test_verify_signature_by_key_id) ... ok +test_verify_without_a_signature_is_reported (test_integrity.SignatureTest.test_verify_without_a_signature_is_reported) ... ok +test_wrong_key_fails (test_integrity.SignatureTest.test_wrong_key_fails) ... ok +test_content_tamper_is_caught_by_check_content (test_integrity.VerifyTest.test_content_tamper_is_caught_by_check_content) +同大小改掉内容:元数据级校验看不出来,权威校验必须抓到。 ... ok +test_metadata_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_metadata_tamper_is_caught_by_digest) ... ok +test_missing_blob_is_caught (test_integrity.VerifyTest.test_missing_blob_is_caught) ... ok +test_path_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_path_tamper_is_caught_by_digest) ... ok +test_read_file_with_verify_hash_raises_on_tamper (test_integrity.VerifyTest.test_read_file_with_verify_hash_raises_on_tamper) ... ok +test_size_mismatch_is_caught (test_integrity.VerifyTest.test_size_mismatch_is_caught) ... ok +test_verify_can_target_an_older_snapshot (test_integrity.VerifyTest.test_verify_can_target_an_older_snapshot) +摘要防的是"绕过 API 的改动";走 API 的写入会刷新摘要,不算篡改。 ... ok +test_verify_passes_on_a_fresh_pack (test_integrity.VerifyTest.test_verify_passes_on_a_fresh_pack) ... ok +test_verify_reports_empty_database (test_integrity.VerifyTest.test_verify_reports_empty_database) ... ok +test_verify_with_content_check_passes (test_integrity.VerifyTest.test_verify_with_content_check_passes) ... ok +test_detects_each_format (test_migration.DetectVersionTest.test_detects_each_format) ... ok +test_detects_non_vfs_database (test_migration.DetectVersionTest.test_detects_non_vfs_database) ... ok +test_missing_file_raises (test_migration.DetectVersionTest.test_missing_file_raises) ... ok +test_migrated_database_unpacks_via_cli (test_migration.MigrateCliTest.test_migrated_database_unpacks_via_cli) ... ok +test_v1_bool_flag_is_mapped_to_zlib (test_migration.MigrateFromV1Test.test_v1_bool_flag_is_mapped_to_zlib) ... ok +test_backup_can_be_disabled (test_migration.MigrateFromV2Test.test_backup_can_be_disabled) ... ok +test_backup_can_be_forced_with_output (test_migration.MigrateFromV2Test.test_backup_can_be_forced_with_output) ... ok +test_backup_is_created_and_original_untouched (test_migration.MigrateFromV2Test.test_backup_is_created_and_original_untouched) ... ok +test_content_survives_byte_for_byte (test_migration.MigrateFromV2Test.test_content_survives_byte_for_byte) ... ok +test_custom_snapshot_name (test_migration.MigrateFromV2Test.test_custom_snapshot_name) ... ok +test_identical_content_is_deduplicated_during_migration (test_migration.MigrateFromV2Test.test_identical_content_is_deduplicated_during_migration) ... ok +test_metadata_is_carried_over (test_migration.MigrateFromV2Test.test_metadata_is_carried_over) ... ok +test_migrate_to_a_new_file_leaves_the_original_alone (test_migration.MigrateFromV2Test.test_migrate_to_a_new_file_leaves_the_original_alone) +原库在只读介质上时不能原地替换,所以支持输出到新路径。 ... ok +test_migrated_pack_verifies_and_signs (test_migration.MigrateFromV2Test.test_migrated_pack_verifies_and_signs) ... ok +test_migration_of_a_readonly_file_into_a_new_file (test_migration.MigrateFromV2Test.test_migration_of_a_readonly_file_into_a_new_file) ... ok +test_refuses_to_overwrite_an_existing_output (test_migration.MigrateFromV2Test.test_refuses_to_overwrite_an_existing_output) ... ok +test_second_migration_is_a_no_op (test_migration.MigrateFromV2Test.test_second_migration_is_a_no_op) ... ok +test_snapshot_name_defaults_to_the_pack_name (test_migration.MigrateFromV2Test.test_snapshot_name_defaults_to_the_pack_name) ... ok +test_structure_is_preserved (test_migration.MigrateFromV2Test.test_structure_is_preserved) ... ok +test_missing_file_raises (test_migration.MigrateInvalidInputTest.test_missing_file_raises) ... ok +test_non_vfs_database_is_rejected (test_migration.MigrateInvalidInputTest.test_non_vfs_database_is_rejected) ... ok +test_original_survives_a_failure (test_migration.MigrateInvalidInputTest.test_original_survives_a_failure) +迁移失败时原库必须原样保留(中间态写在临时文件里)。 ... ok +test_manifest_contents (test_publish.ExportTest.test_manifest_contents) ... ok +test_manifest_is_deterministic_for_the_same_snapshot (test_publish.ExportTest.test_manifest_is_deterministic_for_the_same_snapshot) +清单里不放"生成时间",所以同一快照导出两次得到完全一样的清单(可 diff、可签名)。 ... ok +test_manifest_of_a_nonexistent_snapshot_is_rejected (test_publish.ExportTest.test_manifest_of_a_nonexistent_snapshot_is_rejected) ... ok +test_object_content_is_the_original_bytes (test_publish.ExportTest.test_object_content_is_the_original_bytes) +库里内容是压缩的,导出必须是原始字节——消费方不该需要我们的解压代码。 ... ok +test_object_name_is_its_hash (test_publish.ExportTest.test_object_name_is_its_hash) ... ok +test_objects_are_deduplicated (test_publish.ExportTest.test_objects_are_deduplicated) ... ok +test_publish_a_specific_snapshot (test_publish.ExportTest.test_publish_a_specific_snapshot) ... ok +test_publish_is_read_only_on_the_pack (test_publish.ExportTest.test_publish_is_read_only_on_the_pack) ... ok +test_refuses_non_empty_output_without_overwrite (test_publish.ExportTest.test_refuses_non_empty_output_without_overwrite) ... ok +test_savings_are_reported_in_separate_units (test_publish.ExportTest.test_savings_are_reported_in_separate_units) +去重与压缩必须分开报:把"压缩省下的"算进"去重省下的"会误导选型。 ... ok +test_shard_zero_puts_objects_flat (test_publish.ExportTest.test_shard_zero_puts_objects_flat) ... ok +test_cache_is_reused_and_verified (test_publish.FetchLocalTest.test_cache_is_reused_and_verified) ... ok +test_corrupted_cache_entry_is_healed (test_publish.FetchLocalTest.test_corrupted_cache_entry_is_healed) ... ok +test_link_mode_hardlinks_into_the_cache (test_publish.FetchLocalTest.test_link_mode_hardlinks_into_the_cache) ... ok +test_mtime_and_mode_are_preserved (test_publish.FetchLocalTest.test_mtime_and_mode_are_preserved) ... ok +test_partial_fetch_by_file_and_by_prefix (test_publish.FetchLocalTest.test_partial_fetch_by_file_and_by_prefix) ... ok +test_partial_fetch_writes_only_selected (test_publish.FetchLocalTest.test_partial_fetch_writes_only_selected) ... ok +test_roundtrip_restores_the_tree (test_publish.FetchLocalTest.test_roundtrip_restores_the_tree) ... ok +test_bad_manifest_format_is_rejected (test_publish.FetchOverHttpTest.test_bad_manifest_format_is_rejected) ... ok +test_fetch_from_a_real_http_server (test_publish.FetchOverHttpTest.test_fetch_from_a_real_http_server) ... C:\Users\chakcy\AppData\Roaming\uv\python\cpython-3.13-windows-x86_64-none\Lib\unittest\case.py:707: DeprecationWarning: It is deprecated to return a value that is not None from a test case (>) + return self.run(*args, **kwds) +ok +test_manifest_from_url_mismatch_is_reported (test_publish.FetchOverHttpTest.test_manifest_from_url_mismatch_is_reported) ... ok +test_missing_object_is_reported_clearly (test_publish.FetchOverHttpTest.test_missing_object_is_reported_clearly) ... ok +test_tampered_object_over_http_is_rejected (test_publish.FetchOverHttpTest.test_tampered_object_over_http_is_rejected) ... ok +test_fetch_json_output (test_publish.PublishCliTest.test_fetch_json_output) ... ok +test_fetch_missing_path_exits_nonzero (test_publish.PublishCliTest.test_fetch_missing_path_exits_nonzero) ... ok +test_manifest_only_mode (test_publish.PublishCliTest.test_manifest_only_mode) ... ok +test_publish_and_fetch_via_cli (test_publish.PublishCliTest.test_publish_and_fetch_via_cli) ... ok +test_publish_missing_db_exits_nonzero (test_publish.PublishCliTest.test_publish_missing_db_exits_nonzero) ... ok +test_publish_to_nonempty_dir_requires_overwrite (test_publish.PublishCliTest.test_publish_to_nonempty_dir_requires_overwrite) ... ok +test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) +用一个足够大的重复文本,让压缩效果超过 SQLite 页粒度(4KB)的噪声。 ... ok +test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 ... ok +test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 ... ok +test_packed_database_contains_no_duplicate_paths (test_roundtrip.RoundTripTest.test_packed_database_contains_no_duplicate_paths) +唯一性现在是"每个快照内"的:UNIQUE(snapshot_id, path)。 ... ok +test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) ... ok +test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) ... ok +test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) ... ok +test_stored_blob_of_compressible_file_is_smaller (test_roundtrip.RoundTripTest.test_stored_blob_of_compressible_file_is_smaller) +压缩不只是"标记了一下":库里真正存的字节必须变少。 ... ok +test_add_vfs_metadata_names_the_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_add_vfs_metadata_names_the_default_snapshot) ... ok +test_constructor_releases_the_connection_when_snapshot_is_invalid (test_snapshots.SnapshotLifecycleTest.test_constructor_releases_the_connection_when_snapshot_is_invalid) +构造期抛异常也要释放连接,否则 Windows 上库文件被锁住删不掉。 ... ok +test_create_snapshot_becomes_active_and_is_listed (test_snapshots.SnapshotLifecycleTest.test_create_snapshot_becomes_active_and_is_listed) ... ok +test_delete_snapshot_removes_entries_and_signature_rows (test_snapshots.SnapshotLifecycleTest.test_delete_snapshot_removes_entries_and_signature_rows) ... ok +test_first_write_creates_a_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_first_write_creates_a_default_snapshot) ... ok +test_readonly_never_creates_a_snapshot (test_snapshots.SnapshotLifecycleTest.test_readonly_never_creates_a_snapshot) +只读打开不能因为"读一下"就写库——否则只读挂载会失败。 ... ok +test_reopen_defaults_to_the_latest_snapshot (test_snapshots.SnapshotLifecycleTest.test_reopen_defaults_to_the_latest_snapshot) ... ok +test_use_missing_snapshot_raises (test_snapshots.SnapshotLifecycleTest.test_use_missing_snapshot_raises) ... ok +test_use_snapshot_switches_the_view (test_snapshots.SnapshotLifecycleTest.test_use_snapshot_switches_the_view) ... ok +test_blobs_are_shared_between_snapshots (test_snapshots.TimeTravelTest.test_blobs_are_shared_between_snapshots) ... ok +test_copy_entries_forks_from_the_parent (test_snapshots.TimeTravelTest.test_copy_entries_forks_from_the_parent) ... ok +test_counts_are_per_snapshot (test_snapshots.TimeTravelTest.test_counts_are_per_snapshot) ... ok +test_each_snapshot_has_its_own_digest (test_snapshots.TimeTravelTest.test_each_snapshot_has_its_own_digest) ... ok +test_export_respects_the_active_snapshot (test_snapshots.TimeTravelTest.test_export_respects_the_active_snapshot) ... ok +test_list_directory_is_snapshot_scoped (test_snapshots.TimeTravelTest.test_list_directory_is_snapshot_scoped) ... ok +test_mkdir_in_one_snapshot_does_not_leak (test_snapshots.TimeTravelTest.test_mkdir_in_one_snapshot_does_not_leak) ... ok +test_new_files_are_invisible_in_the_older_snapshot (test_snapshots.TimeTravelTest.test_new_files_are_invisible_in_the_older_snapshot) ... ok +test_remove_in_one_snapshot_does_not_affect_the_other (test_snapshots.TimeTravelTest.test_remove_in_one_snapshot_does_not_affect_the_other) ... ok +test_same_path_has_different_content_per_snapshot (test_snapshots.TimeTravelTest.test_same_path_has_different_content_per_snapshot) ... ok +test_snapshot_id_can_be_chosen_at_construction (test_snapshots.TimeTravelTest.test_snapshot_id_can_be_chosen_at_construction) ... ok +test_unpacker_check_database_counts_only_the_active_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_check_database_counts_only_the_active_snapshot) ... ok +test_unpacker_export_follows_the_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_export_follows_the_snapshot) ... ok +test_unpacker_list_root_is_snapshot_scoped (test_snapshots.UnpackerSnapshotTest.test_unpacker_list_root_is_snapshot_scoped) ... ok + +---------------------------------------------------------------------- +Ran 300 tests in 21.571s + +OK (skipped=52) + + [HTTP 实测] 请求 4 次(清单 1 + 对象 3),下载 256.0 KB(清单 1.1 KB),耗时 17ms +EXIT=0 diff --git a/docs/evidence-root-cause.txt b/docs/evidence-root-cause.txt new file mode 100644 index 0000000..36d58ea --- /dev/null +++ b/docs/evidence-root-cause.txt @@ -0,0 +1,6 @@ +=== 复现 P0-1 根因:建表列名与 INSERT 列名不一致 === + 修复前 INSERT -> OperationalError: table files has no column named compress_algo + → 该异常被 core.py 的 except 吞掉,只打印一行"添加文件失败",库里没有任何记录 + 修复前 SELECT -> OperationalError: no such column: compress_algo + → read_file 因此必然失败,所有导出操作都变成"导出文件失败" + 修复后 INSERT -> ok, SELECT -> [('none',)] diff --git a/docs/evidence-storage-v3.txt b/docs/evidence-storage-v3.txt new file mode 100644 index 0000000..4314bd3 --- /dev/null +++ b/docs/evidence-storage-v3.txt @@ -0,0 +1,342 @@ +test_pack_missing_source_returns_nonzero (test_cli.CliFailureTest.test_pack_missing_source_returns_nonzero) ... ok +test_pack_to_missing_output_directory_reports_clean_error (test_cli.CliFailureTest.test_pack_to_missing_output_directory_reports_clean_error) ... ok +test_unpack_missing_db_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_db_returns_nonzero) ... ok +test_unpack_missing_path_returns_nonzero (test_cli.CliFailureTest.test_unpack_missing_path_returns_nonzero) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:420: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_unpack_non_database_file_reports_clean_error (test_cli.CliFailureTest.test_unpack_non_database_file_reports_clean_error) ... ok +test_migrates_in_place_with_backup (test_cli.CliMigrateTest.test_migrates_in_place_with_backup) ... ok +test_non_vfs_file_is_rejected (test_cli.CliMigrateTest.test_non_vfs_file_is_rejected) ... ok +test_output_mode_leaves_the_original_alone (test_cli.CliMigrateTest.test_output_mode_leaves_the_original_alone) ... ok +test_reports_when_nothing_to_do (test_cli.CliMigrateTest.test_reports_when_nothing_to_do) ... ok +test_compress_algo_huffman_is_still_selectable (test_cli.CliPackTest.test_compress_algo_huffman_is_still_selectable) +早期算法仍然可选,且打出来的包能被正常读回。 ... ok +test_default_compression_algorithm_is_zlib (test_cli.CliPackTest.test_default_compression_algorithm_is_zlib) ... ok +test_exclude_prunes_directory_subtree (test_cli.CliPackTest.test_exclude_prunes_directory_subtree) ... ok +test_exclude_supports_glob_patterns (test_cli.CliPackTest.test_exclude_supports_glob_patterns) +P1-9: README 承诺 glob 语义,`*.log` 必须真的排除 .log 文件, ... ok +test_pack_reports_file_count_matching_database (test_cli.CliPackTest.test_pack_reports_file_count_matching_database) ... ok +test_pack_stores_every_file (test_cli.CliPackTest.test_pack_stores_every_file) ... ok +test_list_snapshots_option (test_cli.CliUnpackSnapshotTest.test_list_snapshots_option) ... ok +test_unknown_snapshot_is_rejected_cleanly (test_cli.CliUnpackSnapshotTest.test_unknown_snapshot_is_rejected_cleanly) ... ok +test_full_roundtrip_via_cli (test_cli.CliUnpackTest.test_full_roundtrip_via_cli) ... ok +test_ignore_timestamps_full_export_does_not_recurse (test_cli.CliUnpackTest.test_ignore_timestamps_full_export_does_not_recurse) +P0-3: --ignore-timestamps 曾把 export_filesystem 赋成引用自身的 lambda。 ... ok +test_ignore_timestamps_with_path_subdirectory (test_cli.CliUnpackTest.test_ignore_timestamps_with_path_subdirectory) ... ok +test_list_root_only_shows_top_level (test_cli.CliUnpackTest.test_list_root_only_shows_top_level) +P0-2: 根目录列表不能把整张表铺平。 ... ok +test_path_exports_single_file (test_cli.CliUnpackTest.test_path_exports_single_file) ... ok +test_path_exports_subdirectory_with_contents (test_cli.CliUnpackTest.test_path_exports_subdirectory_with_contents) ... ok +test_rejects_old_schema_database_cleanly (test_cli.CliUnpackTest.test_rejects_old_schema_database_cleanly) +P1-7: 旧格式库要给出明确提示,退出码非 0,且不能是 traceback。 ... ok +test_content_tampering_needs_check_content (test_cli.CliVerifyTest.test_content_tampering_needs_check_content) ... ok +test_json_output (test_cli.CliVerifyTest.test_json_output) ... ok +test_list_snapshots (test_cli.CliVerifyTest.test_list_snapshots) ... ok +test_missing_db_exits_nonzero (test_cli.CliVerifyTest.test_missing_db_exits_nonzero) ... ok +test_sign_then_verify (test_cli.CliVerifyTest.test_sign_then_verify) ... ok +test_snapshot_selection (test_cli.CliVerifyTest.test_snapshot_selection) ... ok +test_verify_does_not_modify_the_pack (test_cli.CliVerifyTest.test_verify_does_not_modify_the_pack) ... ok +test_verify_exits_nonzero_on_metadata_tampering (test_cli.CliVerifyTest.test_verify_exits_nonzero_on_metadata_tampering) ... ok +test_verify_reports_ok (test_cli.CliVerifyTest.test_verify_reports_ok) ... ok +test_verify_with_content_check (test_cli.CliVerifyTest.test_verify_with_content_check) ... ok +test_importing_core_never_pulls_in_third_party (test_contrib_optional.OptionalDependencyTest.test_importing_core_never_pulls_in_third_party) +import sqlite_vfs 不能顺带把 fastapi/starlette 拖进来。 ... ok +test_missing_database_path_hint (test_contrib_optional.OptionalDependencyTest.test_missing_database_path_hint) +未生成资源包时的报错要给出下一步命令。 ... skipped '当前环境没有 FastAPI' +test_missing_fastapi_gives_actionable_error (test_contrib_optional.OptionalDependencyTest.test_missing_fastapi_gives_actionable_error) +缺少可选依赖时,报错信息必须能直接照着做。 ... ok +test_plugin_module_is_not_imported_by_the_package (test_contrib_optional.OptionalDependencyTest.test_plugin_module_is_not_imported_by_the_package) +contrib 子包不能自动导入插件模块(否则缺依赖时整包不可用)。 ... ok +test_third_party_is_installed_or_not (test_contrib_optional.OptionalDependencyTest.test_third_party_is_installed_or_not) +只用于在输出里说明当前环境状态,不断言。 ... ok +test_duplicate_add_file_is_reported_and_keeps_first_content (test_core.AddFileConflictTest.test_duplicate_add_file_is_reported_and_keeps_first_content) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:740: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_compression_disabled_stores_raw (test_core.CompressionAlgorithmTest.test_compression_disabled_stores_raw) ... ok +test_default_algorithm_is_zlib (test_core.CompressionAlgorithmTest.test_default_algorithm_is_zlib) ... ok +test_each_algorithm_roundtrips (test_core.CompressionAlgorithmTest.test_each_algorithm_roundtrips) ... ok +test_early_huffman_packs_are_still_readable (test_core.CompressionAlgorithmTest.test_early_huffman_packs_are_still_readable) +早期版本用 huffman 打的包必须还能读——这是保留该算法唯一的理由。 ... ok +test_unknown_algorithm_in_database_raises_on_read (test_core.CompressionAlgorithmTest.test_unknown_algorithm_in_database_raises_on_read) +库里的算法名本版本不认识时要明确报错,而不是返回坏数据。 ... ok +test_unknown_algorithm_rejected_at_construction (test_core.CompressionAlgorithmTest.test_unknown_algorithm_rejected_at_construction) ... ok +test_zlib_compresses_better_than_huffman (test_core.CompressionAlgorithmTest.test_zlib_compresses_better_than_huffman) +同一份文本,zlib 的结果必须明显小于 huffman(LZ77 能吃重复)。 ... ok +test_directories_come_before_files (test_core.DirectoryListingTest.test_directories_come_before_files) ... ok +test_leaf_directory_is_empty (test_core.DirectoryListingTest.test_leaf_directory_is_empty) ... ok +test_nested_directory_lists_its_children (test_core.DirectoryListingTest.test_nested_directory_lists_its_children) ... ok +test_paths_are_returned_as_dicts_with_datetime (test_core.DirectoryListingTest.test_paths_are_returned_as_dicts_with_datetime) ... ok +test_root_lists_top_level_only (test_core.DirectoryListingTest.test_root_lists_top_level_only) ... ok +test_subdirectory_lists_its_children (test_core.DirectoryListingTest.test_subdirectory_lists_its_children) ... ok +test_export_directory_full_tree (test_core.ExportTest.test_export_directory_full_tree) ... ok +test_export_file_accepts_bare_filename (test_core.ExportTest.test_export_file_accepts_bare_filename) +P1-6: local_path 没有目录部分时 dirname 为 '',不能抛 FileNotFoundError。 ... ok +test_export_file_writes_exact_bytes (test_core.ExportTest.test_export_file_writes_exact_bytes) ... ok +test_export_filesystem_matches_tree (test_core.ExportTest.test_export_filesystem_matches_tree) ... ok +test_export_subdirectory_does_not_recurse_forever (test_core.ExportTest.test_export_subdirectory_does_not_recurse_forever) +P0-2: list_directory 按 path 过滤时,导出 doc 会无限自我递归。 ... ok +test_compressible_file_uses_zlib (test_core.FileContentTest.test_compressible_file_uses_zlib) ... ok +test_compression_applies_below_the_old_1kb_bar (test_core.FileContentTest.test_compression_applies_below_the_old_1kb_bar) +zlib 包头只有几字节,小文件也值得压(旧的哈夫曼阈值是 1KB)。 ... ok +test_empty_file_roundtrip (test_core.FileContentTest.test_empty_file_roundtrip) ... ok +test_file_size_recorded_matches_content (test_core.FileContentTest.test_file_size_recorded_matches_content) ... ok +test_incompressible_file_falls_back_to_none (test_core.FileContentTest.test_incompressible_file_falls_back_to_none) ... ok +test_read_file_offset_and_size (test_core.FileContentTest.test_read_file_offset_and_size) ... ok +test_read_file_rejects_directory_and_missing_path (test_core.FileContentTest.test_read_file_rejects_directory_and_missing_path) ... ok +test_small_file_roundtrip (test_core.FileContentTest.test_small_file_roundtrip) ... ok +test_tiny_file_is_not_compressed (test_core.FileContentTest.test_tiny_file_is_not_compressed) ... ok +test_mk_dir_creates_directory (test_core.MkDirTest.test_mk_dir_creates_directory) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:1262: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_mk_dir_existing_raises_cleanly (test_core.MkDirTest.test_mk_dir_existing_raises_cleanly) ... ok +test_mv_dir_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_mv_dir_does_not_touch_prefix_sibling) ... ok +test_mv_dir_into_existing_parent_directory (test_core.MoveAndRemoveTest.test_mv_dir_into_existing_parent_directory) ... ok +test_mv_dir_into_itself_raises (test_core.MoveAndRemoveTest.test_mv_dir_into_itself_raises) ... ok +test_mv_dir_rename_is_not_applied_twice (test_core.MoveAndRemoveTest.test_mv_dir_rename_is_not_applied_twice) +P1-2: mv_dir("doc","doc_new") 的第二条 UPDATE 会再次命中已改名的目录自身。 ... ok +test_mv_dir_root_creates_destination_and_fixes_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_root_creates_destination_and_fixes_parent_paths) +P1-3: 把根目录整体移进 archive 时,archive 目录行必须存在, ... ok +test_mv_dir_to_existing_destination_raises_and_keeps_source (test_core.MoveAndRemoveTest.test_mv_dir_to_existing_destination_raises_and_keeps_source) ... ok +test_mv_dir_to_missing_parent_raises (test_core.MoveAndRemoveTest.test_mv_dir_to_missing_parent_raises) +目标的父目录不存在时要明确报错,而不是造出一个不可达的目录。 ... ok +test_mv_dir_updates_children_parent_paths (test_core.MoveAndRemoveTest.test_mv_dir_updates_children_parent_paths) ... ok +test_mv_file_updates_name_and_parent (test_core.MoveAndRemoveTest.test_mv_file_updates_name_and_parent) ... ok +test_remove_directory_does_not_touch_prefix_sibling (test_core.MoveAndRemoveTest.test_remove_directory_does_not_touch_prefix_sibling) +删除 doc 不能连带删掉 docs(LIKE 'doc%' 会把 docs 一起匹配上)。 ... ok +test_remove_file_with_percent_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_percent_is_not_a_wildcard) ... ok +test_remove_file_with_underscore_is_not_a_wildcard (test_core.MoveAndRemoveTest.test_remove_file_with_underscore_is_not_a_wildcard) +文件名里的 _ 不能被当成 LIKE 的单字符通配符。 ... ok +test_remove_missing_path_reports_error (test_core.MoveAndRemoveTest.test_remove_missing_path_reports_error) ... ok +test_add_file_survives_close_without_further_commit (test_core.PersistenceTest.test_add_file_survives_close_without_further_commit) ... ok +test_fresh_database_is_accepted (test_core.SchemaCompatibilityTest.test_fresh_database_is_accepted) ... ok +test_legacy_pack_is_readable_after_migration (test_core.SchemaCompatibilityTest.test_legacy_pack_is_readable_after_migration) +迁移工具必须真的能读懂最早那版格式(compressed 布尔 + zlib)。 ... ok +test_non_sqlite_file_is_rejected (test_core.SchemaCompatibilityTest.test_non_sqlite_file_is_rejected) ... ok +test_repository_sample_db_is_rejected (test_core.SchemaCompatibilityTest.test_repository_sample_db_is_rejected) ... ok +test_synthesized_old_schema_db_is_rejected (test_core.SchemaCompatibilityTest.test_synthesized_old_schema_db_is_rejected) ... ok +test_v2_schema_db_is_rejected_with_migration_hint (test_core.SchemaCompatibilityTest.test_v2_schema_db_is_rejected_with_migration_hint) ... ok +test_add_file_actually_stores_a_row (test_core.SchemaContractTest.test_add_file_actually_stores_a_row) +add_file 之后库里必须真的有这条记录。 ... ok +test_expected_columns_exist (test_core.SchemaContractTest.test_expected_columns_exist) +v3:目录项与内容分家,files 只留元数据 + content_hash。 ... ok +test_clear_snapshot_keeps_other_snapshot_content (test_dedup.BlobGarbageCollectionTest.test_clear_snapshot_keeps_other_snapshot_content) ... ok +test_deleting_snapshot_allows_gc (test_dedup.BlobGarbageCollectionTest.test_deleting_snapshot_allows_gc) +只被某个快照引用的内容,在删掉那个快照后应当被回收。 ... ok +test_gc_keeps_blobs_referenced_by_another_snapshot (test_dedup.BlobGarbageCollectionTest.test_gc_keeps_blobs_referenced_by_another_snapshot) +多快照共用内容时,删掉一个版本不能影响另一个版本。 ... ok +test_gc_returns_deleted_count (test_dedup.BlobGarbageCollectionTest.test_gc_returns_deleted_count) ... ok +test_remove_directory_collects_blobs (test_dedup.BlobGarbageCollectionTest.test_remove_directory_collects_blobs) ... ok +test_remove_file_collects_unreferenced_blob (test_dedup.BlobGarbageCollectionTest.test_remove_file_collects_unreferenced_blob) ... ok +test_shared_blob_survives_partial_removal (test_dedup.BlobGarbageCollectionTest.test_shared_blob_survives_partial_removal) ... ok +test_blob_records_original_and_stored_size (test_dedup.DedupTest.test_blob_records_original_and_stored_size) ... ok +test_dedup_skips_recompression (test_dedup.DedupTest.test_dedup_skips_recompression) +第二次写入相同内容时不该再压一遍——这是增量打包能变快的原因。 ... ok +test_distinct_content_gets_distinct_blobs (test_dedup.DedupTest.test_distinct_content_gets_distinct_blobs) ... ok +test_empty_files_share_one_blob (test_dedup.DedupTest.test_empty_files_share_one_blob) ... ok +test_identical_content_in_different_directories_shares_blob (test_dedup.DedupTest.test_identical_content_in_different_directories_shares_blob) ... ok +test_identical_files_share_one_blob (test_dedup.DedupTest.test_identical_files_share_one_blob) ... ok +test_repacking_identical_tree_does_not_grow_storage (test_dedup.DedupTest.test_repacking_identical_tree_does_not_grow_storage) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:650: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + cursor.execute( +ok +test_replacing_with_identical_content_does_not_add_blob (test_dedup.DedupTest.test_replacing_with_identical_content_does_not_add_blob) ... ok +test_rewrite_different_content_keeps_only_new_blob (test_dedup.DedupTest.test_rewrite_different_content_keeps_only_new_blob) ... ok +test_content_length_matches_body (test_fastapi_static.BasicServingTest.test_content_length_matches_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_compressed_file_correctly (test_fastapi_static.BasicServingTest.test_serves_compressed_file_correctly) +大文件在库里是哈夫曼压缩存储的,取出来必须还原成原始字节。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_empty_file (test_fastapi_static.BasicServingTest.test_serves_empty_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_nested_file (test_fastapi_static.BasicServingTest.test_serves_nested_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_top_level_file (test_fastapi_static.BasicServingTest.test_serves_top_level_file) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_large_asset_survives_compressed_roundtrip (test_fastapi_static.CompressionAlgorithmServingTest.test_large_asset_survives_compressed_roundtrip) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_pack_uses_zlib_by_default (test_fastapi_static.CompressionAlgorithmServingTest.test_pack_uses_zlib_by_default) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_and_last_modified_present (test_fastapi_static.ConditionalRequestTest.test_etag_and_last_modified_present) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_etag_is_stable_across_instances (test_fastapi_static.ConditionalRequestTest.test_etag_is_stable_across_instances) +同一个资源包重新打开,ETag 不能变,否则缓存永远失效。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_modified_since_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_modified_since_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_returns_304 (test_fastapi_static.ConditionalRequestTest.test_if_none_match_returns_304) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_none_match_with_weak_prefix (test_fastapi_static.ConditionalRequestTest.test_if_none_match_with_weak_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_max_age_sets_cache_control (test_fastapi_static.ConditionalRequestTest.test_max_age_sets_cache_control) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_stale_if_none_match_returns_full_body (test_fastapi_static.ConditionalRequestTest.test_stale_if_none_match_returns_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_pack_raises_runtime_error_with_hint (test_fastapi_static.ConstructionTest.test_missing_pack_raises_runtime_error_with_hint) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_non_database_file_is_rejected (test_fastapi_static.ConstructionTest.test_non_database_file_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_old_format_pack_is_rejected_clearly (test_fastapi_static.ConstructionTest.test_old_format_pack_is_rejected_clearly) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repr_and_close (test_fastapi_static.ConstructionTest.test_repr_and_close) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unknown_extension_falls_back_to_octet_stream (test_fastapi_static.ContentTypeTest.test_unknown_extension_falls_back_to_octet_stream) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_web_types_are_not_platform_dependent (test_fastapi_static.ContentTypeTest.test_web_types_are_not_platform_dependent) +Windows 注册表会把 .mjs 判成 text/plain(浏览器会拒绝执行模块), ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_has_no_body_but_real_length (test_fastapi_static.HeadAndRangeTest.test_head_has_no_body_but_real_length) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_head_with_range (test_fastapi_static.HeadAndRangeTest.test_head_with_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_match_uses_range (test_fastapi_static.HeadAndRangeTest.test_if_range_match_uses_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_if_range_mismatch_ignores_range (test_fastapi_static.HeadAndRangeTest.test_if_range_mismatch_ignores_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_malformed_range_is_ignored (test_fastapi_static.HeadAndRangeTest.test_malformed_range_is_ignored) +语法错误的 Range 按 RFC 应当被忽略,返回整份内容。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_multi_range_falls_back_to_full_body (test_fastapi_static.HeadAndRangeTest.test_multi_range_falls_back_to_full_body) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_on_compressed_content_is_correct (test_fastapi_static.HeadAndRangeTest.test_range_on_compressed_content_is_correct) +库里存的是压缩数据,Range 必须作用在解压后的内容上。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_returns_206_with_slice (test_fastapi_static.HeadAndRangeTest.test_range_returns_206_with_slice) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_with_open_end (test_fastapi_static.HeadAndRangeTest.test_range_with_open_end) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_suffix_range (test_fastapi_static.HeadAndRangeTest.test_suffix_range) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_unsatisfiable_range_returns_416 (test_fastapi_static.HeadAndRangeTest.test_unsatisfiable_range_returns_416) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_conditional_request_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_conditional_request_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_range_works_on_huffman_pack (test_fastapi_static.HuffmanPackServingTest.test_range_works_on_huffman_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_huffman_compressed_asset (test_fastapi_static.HuffmanPackServingTest.test_serves_huffman_compressed_asset) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_redirects_to_trailing_slash (test_fastapi_static.IndexAndSpaTest.test_directory_redirects_to_trailing_slash) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_directory_with_slash_serves_its_index (test_fastapi_static.IndexAndSpaTest.test_directory_with_slash_serves_its_index) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_missing_file_returns_404 (test_fastapi_static.IndexAndSpaTest.test_missing_file_returns_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_json_request (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_json_request) +XHR/接口请求(Accept 不含 text/html)不该被回退成 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_no_fallback_for_missing_asset (test_fastapi_static.IndexAndSpaTest.test_no_fallback_for_missing_asset) +缺静态资源必须是 404,否则前端拿到一份 HTML 会报诡异的解析错误。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_path_traversal_cannot_escape_the_pack (test_fastapi_static.IndexAndSpaTest.test_path_traversal_cannot_escape_the_pack) +资源包不是磁盘目录,.. 之类无法越出数据库。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_post_is_rejected (test_fastapi_static.IndexAndSpaTest.test_post_is_rejected) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_repeated_requests_do_not_modify_pack (test_fastapi_static.IndexAndSpaTest.test_repeated_requests_do_not_modify_pack) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_serves_index_html (test_fastapi_static.IndexAndSpaTest.test_root_serves_index_html) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_deep_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_deep_route) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_spa_fallback_for_frontend_route (test_fastapi_static.IndexAndSpaTest.test_spa_fallback_for_frontend_route) +浏览器导航到前端路由 /settings 时应当拿到 index.html。 ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_304_does_not_read_content (test_fastapi_static.InterruptedReadTest.test_304_does_not_read_content) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_outside_prefix_is_404 (test_fastapi_static.MountPrefixTest.test_outside_prefix_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_serves_under_prefix (test_fastapi_static.MountPrefixTest.test_serves_under_prefix) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_plain_file_still_served (test_fastapi_static.StrictModeTest.test_plain_file_still_served) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_root_is_404 (test_fastapi_static.StrictModeTest.test_root_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_route_is_404 (test_fastapi_static.StrictModeTest.test_route_is_404) ... skipped "需要 fastapi/starlette/httpx: No module named 'fastapi'" +test_bad_magic_rejected (test_huffman.HeaderValidationTest.test_bad_magic_rejected) ... ok +test_single_symbol_payload_is_not_validated (test_huffman.HeaderValidationTest.test_single_symbol_payload_is_not_validated) +记录既有行为(非缺陷):单符号数据解压时直接按原长度展开,完全不读 payload, ... ok +test_too_short_blob_rejected (test_huffman.HeaderValidationTest.test_too_short_blob_rejected) ... ok +test_truncated_payload_rejected (test_huffman.HeaderValidationTest.test_truncated_payload_rejected) ... ok +test_wrong_freq_blob_size_rejected (test_huffman.HeaderValidationTest.test_wrong_freq_blob_size_rejected) ... ok +test_all_256_byte_values (test_huffman.RoundTripTest.test_all_256_byte_values) ... ok +test_binary_with_nulls_and_high_bytes (test_huffman.RoundTripTest.test_binary_with_nulls_and_high_bytes) ... ok +test_empty (test_huffman.RoundTripTest.test_empty) ... ok +test_incompressible_random (test_huffman.RoundTripTest.test_incompressible_random) ... ok +test_just_above_core_threshold (test_huffman.RoundTripTest.test_just_above_core_threshold) ... ok +test_repetitive_text_compresses (test_huffman.RoundTripTest.test_repetitive_text_compresses) ... ok +test_single_byte (test_huffman.RoundTripTest.test_single_byte) ... ok +test_single_symbol_repeated (test_huffman.RoundTripTest.test_single_symbol_repeated) ... ok +test_two_symbols (test_huffman.RoundTripTest.test_two_symbols) ... ok +test_utf8_chinese (test_huffman.RoundTripTest.test_utf8_chinese) ... ok +test_deleted_directory_is_removed (test_incremental.ChangeDetectionTest.test_deleted_directory_is_removed) ... ok +test_deleted_file_is_removed (test_incremental.ChangeDetectionTest.test_deleted_file_is_removed) ... ok +test_file_replaced_by_directory (test_incremental.ChangeDetectionTest.test_file_replaced_by_directory) +同一路径从文件变成目录时必须换掉类型,而不是留着旧条目。 ... ok +test_mixed_changes_in_one_run (test_incremental.ChangeDetectionTest.test_mixed_changes_in_one_run) ... ok +test_modified_file_is_updated (test_incremental.ChangeDetectionTest.test_modified_file_is_updated) ... ok +test_new_directory_is_created (test_incremental.ChangeDetectionTest.test_new_directory_is_created) ... ok +test_new_file_is_added (test_incremental.ChangeDetectionTest.test_new_file_is_added) ... ok +test_result_still_verifies (test_incremental.ChangeDetectionTest.test_result_still_verifies) ... ok +test_unchanged_tree_writes_nothing (test_incremental.ChangeDetectionTest.test_unchanged_tree_writes_nothing) ... ok +test_cli_incremental (test_incremental.IncrementalCliTest.test_cli_incremental) ... ok +test_cli_snapshot_creates_a_new_version (test_incremental.IncrementalCliTest.test_cli_snapshot_creates_a_new_version) ... ok +test_first_incremental_on_an_empty_database (test_incremental.IncrementalSnapshotTest.test_first_incremental_on_an_empty_database) ... ok +test_incremental_does_not_touch_other_snapshots (test_incremental.IncrementalSnapshotTest.test_incremental_does_not_touch_other_snapshots) ... ok +test_incremental_into_a_new_snapshot_keeps_history (test_incremental.IncrementalSnapshotTest.test_incremental_into_a_new_snapshot_keeps_history) +派生新快照后再增量:旧版本原样保留,新版本只有变化的部分被重写。 ... ok +test_dedup_reuses_blob_for_identical_new_file (test_incremental.NoRewriteTest.test_dedup_reuses_blob_for_identical_new_file) +新路径但内容与已有文件相同 → 直接复用,不新增 blob。 ... ok +test_rehash_skips_files_whose_content_is_identical (test_incremental.NoRewriteTest.test_rehash_skips_files_whose_content_is_identical) +mtime 变了但内容没变:rehash 模式应当判为未变,不做任何写入。 ... ok +test_replacing_content_frees_the_old_blob (test_incremental.NoRewriteTest.test_replacing_content_frees_the_old_blob) ... ok +test_unchanged_file_keeps_its_blob_hash (test_incremental.NoRewriteTest.test_unchanged_file_keeps_its_blob_hash) ... ok +test_unchanged_files_do_not_add_blobs (test_incremental.NoRewriteTest.test_unchanged_files_do_not_add_blobs) ... ok +test_mtime_only_change_is_detected_without_rehash (test_incremental.RehashDetectionTest.test_mtime_only_change_is_detected_without_rehash) +同大小 + 同秒修改:默认模式可能漏掉,这正是 rehash 存在的理由。 ... ok +test_rehash_catches_same_size_same_mtime_edit (test_incremental.RehashDetectionTest.test_rehash_catches_same_size_same_mtime_edit) ... ok +test_digest_changes_when_a_file_is_added (test_integrity.DigestTest.test_digest_changes_when_a_file_is_added) ... ok +test_digest_changes_when_content_changes (test_integrity.DigestTest.test_digest_changes_when_content_changes) ... ok +test_digest_changes_when_metadata_changes (test_integrity.DigestTest.test_digest_changes_when_metadata_changes) +路径/大小/时间戳/权限也参与摘要,不只是内容。 ... ok +test_digest_ignores_insertion_order (test_integrity.DigestTest.test_digest_ignores_insertion_order) +摘要必须只取决于内容与元数据,与插入顺序无关。 ... ok +test_digest_is_recorded_and_stable_across_reopen (test_integrity.DigestTest.test_digest_is_recorded_and_stable_across_reopen) ... ok +test_digest_matches_manual_recomputation (test_integrity.DigestTest.test_digest_matches_manual_recomputation) ... ok +test_digest_can_be_exported_for_external_signing (test_integrity.SignatureTest.test_digest_can_be_exported_for_external_signing) +非对称签名要交给外部工具,所以摘要必须能干净地取出来。 ... ok +test_sign_and_verify (test_integrity.SignatureTest.test_sign_and_verify) ... E:\HuaweiMoveData\Users\chakcy\Desktop\新建文件夹\sqlite_vfs\src\sqlite_vfs\core.py:1164: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes + self.conn.execute( +ok +test_sign_requires_write_access (test_integrity.SignatureTest.test_sign_requires_write_access) ... ok +test_signature_verifies_on_a_readonly_connection (test_integrity.SignatureTest.test_signature_verifies_on_a_readonly_connection) +只读方必须能在不改动库的前提下完成验证。 ... ok +test_tampering_after_signing_fails (test_integrity.SignatureTest.test_tampering_after_signing_fails) ... ok +test_verify_signature_by_key_id (test_integrity.SignatureTest.test_verify_signature_by_key_id) ... ok +test_verify_without_a_signature_is_reported (test_integrity.SignatureTest.test_verify_without_a_signature_is_reported) ... ok +test_wrong_key_fails (test_integrity.SignatureTest.test_wrong_key_fails) ... ok +test_content_tamper_is_caught_by_check_content (test_integrity.VerifyTest.test_content_tamper_is_caught_by_check_content) +同大小改掉内容:元数据级校验看不出来,权威校验必须抓到。 ... ok +test_metadata_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_metadata_tamper_is_caught_by_digest) ... ok +test_missing_blob_is_caught (test_integrity.VerifyTest.test_missing_blob_is_caught) ... ok +test_path_tamper_is_caught_by_digest (test_integrity.VerifyTest.test_path_tamper_is_caught_by_digest) ... ok +test_read_file_with_verify_hash_raises_on_tamper (test_integrity.VerifyTest.test_read_file_with_verify_hash_raises_on_tamper) ... ok +test_size_mismatch_is_caught (test_integrity.VerifyTest.test_size_mismatch_is_caught) ... ok +test_verify_can_target_an_older_snapshot (test_integrity.VerifyTest.test_verify_can_target_an_older_snapshot) +摘要防的是"绕过 API 的改动";走 API 的写入会刷新摘要,不算篡改。 ... ok +test_verify_passes_on_a_fresh_pack (test_integrity.VerifyTest.test_verify_passes_on_a_fresh_pack) ... ok +test_verify_reports_empty_database (test_integrity.VerifyTest.test_verify_reports_empty_database) ... ok +test_verify_with_content_check_passes (test_integrity.VerifyTest.test_verify_with_content_check_passes) ... ok +test_detects_each_format (test_migration.DetectVersionTest.test_detects_each_format) ... ok +test_detects_non_vfs_database (test_migration.DetectVersionTest.test_detects_non_vfs_database) ... ok +test_missing_file_raises (test_migration.DetectVersionTest.test_missing_file_raises) ... ok +test_migrated_database_unpacks_via_cli (test_migration.MigrateCliTest.test_migrated_database_unpacks_via_cli) ... ok +test_v1_bool_flag_is_mapped_to_zlib (test_migration.MigrateFromV1Test.test_v1_bool_flag_is_mapped_to_zlib) ... ok +test_backup_can_be_disabled (test_migration.MigrateFromV2Test.test_backup_can_be_disabled) ... ok +test_backup_can_be_forced_with_output (test_migration.MigrateFromV2Test.test_backup_can_be_forced_with_output) ... ok +test_backup_is_created_and_original_untouched (test_migration.MigrateFromV2Test.test_backup_is_created_and_original_untouched) ... ok +test_content_survives_byte_for_byte (test_migration.MigrateFromV2Test.test_content_survives_byte_for_byte) ... ok +test_custom_snapshot_name (test_migration.MigrateFromV2Test.test_custom_snapshot_name) ... ok +test_identical_content_is_deduplicated_during_migration (test_migration.MigrateFromV2Test.test_identical_content_is_deduplicated_during_migration) ... ok +test_metadata_is_carried_over (test_migration.MigrateFromV2Test.test_metadata_is_carried_over) ... ok +test_migrate_to_a_new_file_leaves_the_original_alone (test_migration.MigrateFromV2Test.test_migrate_to_a_new_file_leaves_the_original_alone) +原库在只读介质上时不能原地替换,所以支持输出到新路径。 ... ok +test_migrated_pack_verifies_and_signs (test_migration.MigrateFromV2Test.test_migrated_pack_verifies_and_signs) ... ok +test_migration_of_a_readonly_file_into_a_new_file (test_migration.MigrateFromV2Test.test_migration_of_a_readonly_file_into_a_new_file) ... ok +test_refuses_to_overwrite_an_existing_output (test_migration.MigrateFromV2Test.test_refuses_to_overwrite_an_existing_output) ... ok +test_second_migration_is_a_no_op (test_migration.MigrateFromV2Test.test_second_migration_is_a_no_op) ... ok +test_snapshot_name_defaults_to_the_pack_name (test_migration.MigrateFromV2Test.test_snapshot_name_defaults_to_the_pack_name) ... ok +test_structure_is_preserved (test_migration.MigrateFromV2Test.test_structure_is_preserved) ... ok +test_missing_file_raises (test_migration.MigrateInvalidInputTest.test_missing_file_raises) ... ok +test_non_vfs_database_is_rejected (test_migration.MigrateInvalidInputTest.test_non_vfs_database_is_rejected) ... ok +test_original_survives_a_failure (test_migration.MigrateInvalidInputTest.test_original_survives_a_failure) +迁移失败时原库必须原样保留(中间态写在临时文件里)。 ... ok +test_compression_reduces_database_size (test_roundtrip.RoundTripTest.test_compression_reduces_database_size) +用一个足够大的重复文本,让压缩效果超过 SQLite 页粒度(4KB)的噪声。 ... ok +test_every_source_file_is_stored (test_roundtrip.RoundTripTest.test_every_source_file_is_stored) +P0-1:打包后库里必须有和源目录一一对应的文件记录。 ... ok +test_metadata_counts_match_database (test_roundtrip.RoundTripTest.test_metadata_counts_match_database) +P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。 ... ok +test_packed_database_contains_no_duplicate_paths (test_roundtrip.RoundTripTest.test_packed_database_contains_no_duplicate_paths) +唯一性现在是"每个快照内"的:UNIQUE(snapshot_id, path)。 ... ok +test_roundtrip_with_compression (test_roundtrip.RoundTripTest.test_roundtrip_with_compression) ... ok +test_roundtrip_with_ignore_timestamps (test_roundtrip.RoundTripTest.test_roundtrip_with_ignore_timestamps) ... ok +test_roundtrip_without_compression (test_roundtrip.RoundTripTest.test_roundtrip_without_compression) ... ok +test_stored_blob_of_compressible_file_is_smaller (test_roundtrip.RoundTripTest.test_stored_blob_of_compressible_file_is_smaller) +压缩不只是"标记了一下":库里真正存的字节必须变少。 ... ok +test_add_vfs_metadata_names_the_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_add_vfs_metadata_names_the_default_snapshot) ... ok +test_create_snapshot_becomes_active_and_is_listed (test_snapshots.SnapshotLifecycleTest.test_create_snapshot_becomes_active_and_is_listed) ... ok +test_delete_snapshot_removes_entries_and_signature_rows (test_snapshots.SnapshotLifecycleTest.test_delete_snapshot_removes_entries_and_signature_rows) ... ok +test_first_write_creates_a_default_snapshot (test_snapshots.SnapshotLifecycleTest.test_first_write_creates_a_default_snapshot) ... ok +test_readonly_never_creates_a_snapshot (test_snapshots.SnapshotLifecycleTest.test_readonly_never_creates_a_snapshot) +只读打开不能因为"读一下"就写库——否则只读挂载会失败。 ... ok +test_reopen_defaults_to_the_latest_snapshot (test_snapshots.SnapshotLifecycleTest.test_reopen_defaults_to_the_latest_snapshot) ... ok +test_use_missing_snapshot_raises (test_snapshots.SnapshotLifecycleTest.test_use_missing_snapshot_raises) ... ok +test_use_snapshot_switches_the_view (test_snapshots.SnapshotLifecycleTest.test_use_snapshot_switches_the_view) ... ok +test_blobs_are_shared_between_snapshots (test_snapshots.TimeTravelTest.test_blobs_are_shared_between_snapshots) ... ok +test_copy_entries_forks_from_the_parent (test_snapshots.TimeTravelTest.test_copy_entries_forks_from_the_parent) ... ok +test_counts_are_per_snapshot (test_snapshots.TimeTravelTest.test_counts_are_per_snapshot) ... ok +test_each_snapshot_has_its_own_digest (test_snapshots.TimeTravelTest.test_each_snapshot_has_its_own_digest) ... ok +test_export_respects_the_active_snapshot (test_snapshots.TimeTravelTest.test_export_respects_the_active_snapshot) ... ok +test_list_directory_is_snapshot_scoped (test_snapshots.TimeTravelTest.test_list_directory_is_snapshot_scoped) ... ok +test_mkdir_in_one_snapshot_does_not_leak (test_snapshots.TimeTravelTest.test_mkdir_in_one_snapshot_does_not_leak) ... ok +test_new_files_are_invisible_in_the_older_snapshot (test_snapshots.TimeTravelTest.test_new_files_are_invisible_in_the_older_snapshot) ... ok +test_remove_in_one_snapshot_does_not_affect_the_other (test_snapshots.TimeTravelTest.test_remove_in_one_snapshot_does_not_affect_the_other) ... ok +test_same_path_has_different_content_per_snapshot (test_snapshots.TimeTravelTest.test_same_path_has_different_content_per_snapshot) ... ok +test_snapshot_id_can_be_chosen_at_construction (test_snapshots.TimeTravelTest.test_snapshot_id_can_be_chosen_at_construction) ... ok +test_unpacker_check_database_counts_only_the_active_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_check_database_counts_only_the_active_snapshot) ... ok +test_unpacker_export_follows_the_snapshot (test_snapshots.UnpackerSnapshotTest.test_unpacker_export_follows_the_snapshot) ... ok +test_unpacker_list_root_is_snapshot_scoped (test_snapshots.UnpackerSnapshotTest.test_unpacker_list_root_is_snapshot_scoped) ... ok + +---------------------------------------------------------------------- +Ran 270 tests in 14.648s + +OK (skipped=52) +EXIT=0