docs: 补 README 能力章节、缺陷报告与两份设计说明
- README:新增「数据能力」四节(内容寻址去重 / 快照与时间旅行 / 完整性校验与签名 / 增量打包)与「发布到对象存储」一节,附实测对比表;重写「要不要用 --compress」 (结论从"别用"反转为"默认就该开")、「迁移旧格式」;修正项目结构与字段表 - BUG_REPORT.md:13 节,含 P0/P1 逐条分析、P2/P3 清单、五批后续追加的记录, 以及过程中被推翻的两处结论(第九节的压缩建议、P3-1 的 uv sync 判断) - docs/encryption-design.md:加密的设计说明——去重与随机 nonce 的冲突、 标准库没有密码学原语、以及现有 freq_blob 可作为每块附加数据的槽位 - docs/evidence-*.txt:各阶段的测试运行证据(修复前 / 修复后 / 双环境)
This commit is contained in:
+722
@@ -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` 可重新生成,
|
||||
删它属于你的决定。
|
||||
@@ -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
|
||||
|
||||
@@ -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` 已有骨架),保证加算法不破坏现有往返。
|
||||
@@ -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
|
||||
@@ -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 "<frozen os>", 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 <sqlite3.Connection object at 0x000002E9E0ACFC40>
|
||||
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 "<frozen runpy>", line 198, in _run_module_as_main\n File "<frozen runpy>", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\packer.py", line 40, in <module>\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 "<frozen runpy>", line 198, in _run_module_as_main\n File "<frozen runpy>", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 77, in <module>\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 "<frozen runpy>", line 198, in _run_module_as_main\n File "<frozen runpy>", line 88, in _run_code\n File "E:\\HuaweiMoveData\\Users\\chakcy\\Desktop\\新建文件夹\\sqlite_vfs\\src\\sqlite_vfs\\cli\\unpacker.py", line 77, in <module>\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 <lambda>\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 <lambda>\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 <lambda>\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
|
||||
@@ -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
|
||||
@@ -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 (<bound method FetchOverHttpTest.test_fetch_from_a_real_http_server of <test_publish.FetchOverHttpTest testMethod=test_fetch_from_a_real_http_server>>)
|
||||
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
|
||||
@@ -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',)]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user