test(acceptance): cover B113, B88 and the closed pipe, and report more readably

New live cases:

- regression B113: files renamed and moved while sync is paused upload
  without a conflict copy.
- regression B88: pdfs mkdir, rename and rm by local path, the
  mount-root guard, a path outside every mount, and the refusal in a
  mirrored folder.
- pdfs output into a closed pipe exits 0 and prints nothing on stderr.
- open B114 and open B115 check two bugs that are still open. While
  one reproduces, its case ends as "known", which is listed but does
  not fail the run.

The console output names the pdfs version, the storage and the daemon
state of each target, and prints one aligned line per case, with its
details beneath. A failed case points to its docs/BUGS.md entry. The
run ends with a summary table, the slowest cases, the known issues
and the numbered failures. A queue wait now fails at once on a parked
op instead of waiting out its timeout.

docs/BUGS.md records B114, B115 and B116, which the first live run
found, and marks B113 and B88 as verified live.
This commit is contained in:
nvrl committed 2026-10-02 22:16:36 +02:00
1 parent 8cde79b8e3
commit 625776081b
3 files changed
+913 -145

No files matched your search

+90 -4
View File
@@ -12,9 +12,77 @@ Conventions:
---
## B116 — Trashing a folder keeps the queued creates in its subfolders
**Status:** Open.
**Found:** 2026-10-02, by the first live run of the acceptance case "regression B113". That
version wrote `*.part` files, which park their create until a rename instead of minting the file
(B70). It moved one of them, still named `moved.part`, into the subfolder `b113/moved here` and
then hung, waiting for a queue that held a parked op. The run was interrupted, and its cleanup
trashed the test root with `pdfs rm` and deleted it permanently. The parked create stayed queued
and kept its staged bytes. The cleanup reported it as left over (`queued create #2 for
…/b113/moved here/moved.part`).
**Where:** `drop_doomed_ops` in `crates/pdfs-core/src/db/ops.rs`, which
`Core::discard_queued_ops` runs when a folder is trashed.
**Cause.** The ops removed with a folder are those for the folder and for the nodes queued below
it through `pending_op.parent_uid`, plus the revisions of every file the node tree places below
it (B102). A create for a file inside a *real* subfolder is neither: its parent is not a queued
node, and the tree only contributes revisions. Such a create comes from a transient name, or
from a file created offline in a folder that already exists on Drive. A parked one stays queued
for good, holding its bytes. One that is due would run into a trashed parent, and by the code the
drain then re-homes it to the root (`parent folder is gone remotely; creating in the root
instead`), so the file would reappear in My files after its folder was trashed. Only the parked
case has been seen.
**Required fix/test:** take queued creates and mkdirs from the node tree too, as revisions are.
Test: a db test that trashes a folder holding a real subfolder with a queued create below it, and
expects the create and its blob gone.
## B115 — `pdfs ls` shows a file's encrypted size
**Status:** Open.
**Found:** 2026-10-02, while verifying B88 live. `pdfs ls ~/Downloads/pdfs-b88-test` listed the
6-byte `note.txt` as 57 bytes, and listed `tickets.pdf` as 202316 bytes where the mount's `stat`
said 202257. The acceptance case "open B115" then showed it for every file it wrote, in My files
and in an on-demand folder, right after the upload and again after `pdfs refresh`: 6 bytes listed
as 57, 4097 as 4150, and 4 MiB + 7 bytes (4194311) as 4194421. The mount's `stat` had the real
sizes throughout.
**Where:** `node_size` in `crates/pdfs-fuse/src/lib.rs` and `crates/pdfs-core/src/db/nodes.rs`.
**Cause (suspected).** `node_size` reports the node's `claimed_size` and falls back to
`total_size_on_storage` when it has none. That is the size on Drive's storage: encrypted blocks
plus their signatures. A node decoded without a claimed size therefore lists at its encrypted size.
The sizes seen fit that: 51 to 55 bytes more per 4 MiB block.
**Test:** the acceptance case "open B115" compares the sizes `pdfs ls` reports with the written
ones, after the upload and again after `pdfs refresh`. It reports a known issue while they differ.
## B114 — A name `pdfs rm` or `pdfs rename` took away still resolves for up to 30 seconds
**Status:** Open.
**Found:** 2026-10-02, while verifying B88 live. After `pdfs rm ~/Downloads/pdfs-b88-test` the
folder was gone from `ls ~/Downloads`, but `ls -d ~/Downloads/pdfs-b88-test` still found it for
up to 30 seconds. `~/ProtonDrive` behaves the same, so B88's routing did not cause it.
**Where:** `Core::rename` and `Core::delete` in `crates/pdfs-fuse/src/lib.rs`, and the `TTL` the
kernel gets for entries in the same file.
**Cause.** Both change Drive, forget the node and drop the parent's listing, but never tell the
kernel. The kernel cached the name's lookup for `TTL`, 30 seconds, and answers from that cache
until it expires. A rename or unlink through the mount has no such gap, because the kernel made
that change itself. `pdfs move` already sends `inval_entry` for its destination
(`settle_dest` in `crates/pdfs-fuse/src/relocate.rs`); its source was not checked.
**Test:** the acceptance case "open B114" removes one file with `pdfs rm` and renames another with
`pdfs rename`, then checks at once that neither old name resolves. It reports a known issue while
one does. It reproduced in My files and in an on-demand folder.
## B113 — A new file moved before its upload drains lands as a conflict copy of itself
**Status:** Fixed (unverified).
**Status:** Fixed (verified 2026-10-02).
**Found:** 2026-10-02, on the production account. aria2 downloaded two seasons into the on-demand
`~/Downloads`. The episodes were moved into `Videos/anime/…` while their uploads were still
queued. 45 of 48 landed as `(sync-conflict …)` copies next to a 0-byte original, each with
@@ -35,8 +103,18 @@ id.
**Fix.** `WriteHandle::created` takes the minted node's revision id along with its mtime. When a
queued create lands, `adopt_real_uid` rebases the handles still open on it onto the new node,
as `refresh_after_upload` already does after a sealed upload. Both go through
`WriteHandle::rebase_onto`. Unverified: no file has been moved mid-upload against the fixed
daemon yet.
`WriteHandle::rebase_onto`.
**Verified live (2026-10-02)** on the installed daemon, in a scratch folder in `~/Downloads`. A
`pdfs move` advanced an empty file's server mtime from 1790962386 to 1790962408, the change the old
check took for another device's edit. With sync paused, three 16 MiB files were written and moved
into a subfolder while their uploads were queued. After the resume all three logged `pending upload
landed`, with no conflict warning or copy, and they read back at their exact sizes. Each upload was
put off once after the move (`the node is missing locally and was re-read`) and landed on the
retry about 8 seconds later. Meanwhile `pdfs sync queue` showed raw ids instead of the new paths.
Not verified live: a file created offline that is still open when its create lands. Only the unit
test covers that half. The acceptance case "regression B113" replays the online order through the
mount, with a rename and a move, and passed in My files and in an on-demand folder.
## B112 — Folders shared by the same person disappear from Shared with me
@@ -805,7 +883,7 @@ in every version — but that was luck, and the next question of this shape may
## B88 — `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount
**Status:** Partly fixed. B86's fix covers `ls` and `refresh`. `move` takes paths in any location
through `Core::move_between`. `rm`, `rename` and `mkdir` were fixed 2026-10-02 (unverified live).
through `Core::move_between`. `rm`, `rename` and `mkdir` were fixed and verified live 2026-10-02.
`pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions are still open.
**Found:** 2026-08-17, while fixing B86.
**Where:** `crates/pdfs-fuse/src/control.rs`, every handler still calling `rel_to_mount`.
@@ -847,6 +925,14 @@ folder from under its mount. `rm` and `rename` therefore go through `route_node`
path naming a mount's root. Test: `only_a_path_below_a_mount_root_names_a_node`. There is still no
handler-level test, because a `Core` can only be built against a live client.
**Verified live (2026-10-02)** in `~/Downloads`, an on-demand folder. `pdfs mkdir` created a
folder. `pdfs rename` of a file whose upload was still queued reached Drive under the new name, at
6 bytes and with no conflict. `pdfs rm` trashed a file and then the folder. `pdfs rm ~/Downloads`,
`pdfs rename ~/Downloads …` and `pdfs rm ~/ProtonDrive` were refused as the root of a synced
location, and a path outside every mount as not under the mountpoint. `mkdir` and `rm` in
`~/ProtonDrive` still work. Not verified: the refusal in a mirrored folder, because the account has
none. The acceptance case "regression B88" repeats this on every location of a run.
---
## B87 — A revision's block geometry is learned a read too late
+73 -6
View File
@@ -74,6 +74,44 @@ removes its directory on success, failure, or interruption:
scripts/fuse-acceptance.sh --live /mnt/on-demand-testmount
```
### Reading the output
A run opens with what it is about to test: the mode, the `pdfs` binary and its
version, Python and the kernel, the per-case limit, the options and the start
time. Each target then names its root, the storage behind it, the daemon it
talks to and how many cases it runs, followed by one line per case:
```
==> live FUSE /mnt/testmount
-> root /mnt/testmount/pdfs-acceptance-…
-> storage FUSE mount /mnt/testmount (fuse.protondrive)
-> daemon pdfs 2.8.3, user@proton.me, online, 0 queued ops
-> cases 37 of 37
[ 1/37] creation and open flags .................................. ok 0.41s
…
[21/37] throughput floors ........................................ ok 48.20s
write 31 MiB/s, read 54 MiB/s, 4 KiB writes 12.0 MiB/s, metadata 9 ops/s
…
[36/37] open B115: pdfs ls reports a file's real size ............ known 6.12s
B115: pdfs ls shows …
-> reference matches the ordinary filesystem in 412 compared observations
-> result 35 passed, 1 skipped, 1 known issue in 9m41s
```
A case ends as `ok`, `FAIL`, `TIMEOUT`, `skip` when it does not apply to the
target (no daemon, no share at that role), or `known` when an `open B<n>` case
reproduced a bug that is still open (see below). The dimmed lines under a case
are its details: the reason for a skip, measured rates, a hash. A failed
`regression B<n>` case also points to its entry in `docs/BUGS.md`. When a case
prints something itself, such as a service restart, the case line is printed
again once the case ends.
The run ends with a table of every target's counts and time, the report paths,
the slowest cases, the cases over `--budget`, the known issues that still
reproduce, any errors the daemon logged, and each failure with its target. The
last line is `PASS:` or `FAIL:` and the totals. Colour is used only on a
terminal and never with `NO_COLOR` set.
### Edge cases and performance
Besides the syscall contract, the suite covers:
@@ -105,19 +143,20 @@ Besides the syscall contract, the suite covers:
The local reference run is not only a self-test of the runner. Every case
records the facts it established, and a live run is compared against those
recordings, so a case fails when the mount *differs* from an ordinary
filesystem — not only when it raises. The report names the exact key:
filesystem — not only when it raises. The target's `reference` line names the
exact key:
```
[DIVERGENCE] live FUSE /mnt/testmount does not match the reference filesystem:
! creation and open flags.truncated.size: live=6 reference=0
-> reference 1 observation of 412 differs from the ordinary filesystem:
! creation and open flags.truncated.size: live FUSE /mnt/testmount=6 reference=0
```
Facts are recorded in one of two ways, chosen in the test body:
- **compared** — a claim about filesystem semantics. The mount must agree.
- **noted** — context that legitimately differs: inode numbers, block counts,
reported mode bits, and which optional syscalls exist at all. Reported under
`[capabilities]`, never a failure.
reported mode bits, and which optional syscalls exist at all. Listed under
the target's `accepted` line, never a failure.
Because the choice is made at each call site, the set of accepted divergences
is visible in the test that accepts it. There is no separate allowlist to drift
@@ -151,6 +190,24 @@ brought back the provisional sizes the bug reported.
different times before writing, so their empty creates land before, during and after the
bytes arrive, then checks every file after the queue drains.
**B113** (moving a file before its upload makes no conflict) pauses sync with
`pdfs sync pause --for 10m` and writes three files. While their uploads are held,
it renames one, moves one into a folder, and renames and moves the third. It then
resumes sync, waits for the queue, and checks the bytes and names and that no
conflict copy appeared. It skips when sync was already paused. If the run dies
before the resume, the pause ends by itself after ten minutes.
**B88** (`pdfs mkdir`, `rename` and `rm` by local path) creates, renames and
removes a folder and a file by their paths in the mount. Then it checks the
refusals: `pdfs rename` of the mount's root to its own name, and `pdfs rm` of a
file outside every mount. `pdfs rm` of the root itself is tried only on a sync
folder the run created. In a mirrored sync folder all three commands must be
refused instead, and nothing may change.
**pdfs output into a closed pipe** runs `pdfs ls`, `pdfs --json ls` and
`pdfs status` with stdout on a pipe whose reader is already gone. Each must exit
0 with nothing on stderr, as `pdfs ls | head -1` needs.
B74 was found by exactly this mechanism: the B70 case failed, and narrowing it
produced a smaller reproduction that got its own case. The defect is fixed and
live-validated (2026-07-26); the case now guards against its return.
@@ -186,6 +243,15 @@ nothing: the local attribute and content caches will happily return the bytes
that were just written even when nothing reached Drive, which is precisely how
B74 stayed invisible.
### Bugs that are still open
Cases named `open B<n>` check a bug that `docs/BUGS.md` still lists as open.
While the bug reproduces, the case ends as `known`: it is listed at the end of
the run, but does not fail it. So a known bug does not hide new failures, and
the run notices when a fix lands, because the case then passes. Rename it to
`regression B<n>` then. **B114** (a name `pdfs rm` or `rename` took away stops
resolving) and **B115** (`pdfs ls` reports a file's real size) are of this kind.
### Timeouts, reports, and hung mounts
A wedged FUSE operation cannot be interrupted from Python, so each case runs
@@ -200,7 +266,8 @@ scripts/fuse-acceptance.sh --live /mnt/testmount \
```
`--report-json` also writes every recorded observation, which is what to attach
to a bug report. `--budget SECONDS` flags cases that pass but got slower —
to a bug report. A known issue has the status `known` there; JUnit has no such
status, so `--report-junit` reports it as skipped. `--budget SECONDS` flags cases that pass but got slower —
B5 was a 186 ms-per-call regression that a pass/fail suite could not see.
`--list` prints every case and which targets it runs against, and `--fail-fast`
stops at the first failure.
+750 -135
View File
File diff suppressed because it is too large. Load diff