From 850d2ceb75ea04998d2b49b1bbcfb4a59e62b975 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Wed, 4 Mar 2026 19:49:24 +0000 Subject: [PATCH] Add test plan document --- test/PLAN.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 test/PLAN.md diff --git a/test/PLAN.md b/test/PLAN.md new file mode 100644 index 0000000..4fdbf9d --- /dev/null +++ b/test/PLAN.md @@ -0,0 +1,119 @@ +# fa2json Test Plan + +## Overview + +A Node.js test runner (`test/test.mjs`) that exercises `fa2json` against a +temporary ext4 filesystem on a loop device. The test produces a single +pass/fail result and cleans up after itself unconditionally. + +Requires root (`fanotify` FID reporting and `mount` both need `CAP_SYS_ADMIN`). + +--- + +## Files + +| File | Purpose | +|---|---| +| `test/test.mjs` | Test runner | +| `Makefile` | `make test` target calls `sudo node test/test.mjs` | + +--- + +## Setup + +1. Create a temporary image file (`mktemp`) +2. `dd` 10M of zeros into it +3. `mkfs.ext4` the image +4. `losetup --find --show` to attach it as a loop device +5. `mount` the loop device to a temporary directory (`mktemp -d`) +6. Spawn `fa2json ` as a child process +7. Attach a `readline` interface to its stdout; parse each line as JSON and + push into an event buffer + +--- + +## Teardown + +Runs unconditionally in a `finally` block: + +1. Kill the `fa2json` child process +2. `umount ` +3. `losetup -d ` +4. `rm` the image file +5. `rmdir` the mount directory + +--- + +## Event Collection and the Marker Pattern + +`fa2json` runs continuously for the entire test. To associate events with +specific operations, a marker file is used as a synchronisation barrier: + +1. Perform a filesystem operation +2. Immediately `touch /.marker_N` (where N is a counter) +3. Wait until the event stream contains a CREATE event for `.marker_N` +4. Collect all events since the previous marker — this batch belongs to the + current operation +5. Assert on the batch, then advance the counter + +If a marker event never arrives the test hangs, which indicates a failure at +the fa2json level itself. + +--- + +## Path Handling + +`fa2json` emits full paths including the mount prefix +(e.g. `/tmp/fa2json-mnt-XXXXX/dir_a/file.txt`). The runner strips this prefix +so assertions work against a virtual root: + +``` +/tmp/fa2json-mnt-XXXXX/dir_a/file.txt → /dir_a/file.txt +``` + +--- + +## Fanotify Mask Constants + +Relevant flags (bitwise, check with `mask & FLAG`): + +| Constant | Value | Meaning | +|---|---|---| +| `FAN_ATTRIB` | `0x4` | Metadata/attribute change | +| `FAN_CLOSE_WRITE` | `0x8` | File closed after writing | +| `FAN_CREATE` | `0x100` | File or directory created | +| `FAN_DELETE` | `0x200` | File or directory deleted | +| `FAN_RENAME` | `0x10000000` | Rename (has `old` and `new` fields) | +| `FAN_ONDIR` | `0x40000000` | Event subject is a directory | + +--- + +## Operations and Expected Events + +Each row is one `doOp()` call. Events are matched by presence (not exact list) +— extra events from ext4 internals are ignored. + +| Operation | Expected event(s) | +|---|---| +| `mkdir /dir_a` | CREATE \| ONDIR, name `/dir_a` | +| `touch /file_a.txt` | CREATE, name `/file_a.txt` | +| `echo "content" >> /file_a.txt` | CLOSE_WRITE, name `/file_a.txt` | +| `mkdir /dir_b` | CREATE \| ONDIR, name `/dir_b` | +| `touch /dir_b/nested.txt` | CREATE, name `/dir_b/nested.txt` | +| `mv /file_a.txt /file_b.txt` | RENAME, old `/file_a.txt`, new `/file_b.txt` | +| `mv /dir_b /dir_a/dir_b_moved` | RENAME \| ONDIR, old `/dir_b`, new `/dir_a/dir_b_moved` | +| `chmod 600 /file_b.txt` | ATTRIB, name `/file_b.txt` | +| `touch -m /file_b.txt` | ATTRIB, name `/file_b.txt` | +| `chmod 755 /dir_a` | ATTRIB \| ONDIR, name `/dir_a` | +| `rm /file_b.txt` | DELETE, name `/file_b.txt` | +| `rm /dir_a/dir_b_moved/nested.txt` | DELETE, name `/dir_a/dir_b_moved/nested.txt` | +| `rmdir /dir_a/dir_b_moved` | DELETE \| ONDIR, name `/dir_a/dir_b_moved` | +| `rmdir /dir_a` | DELETE \| ONDIR, name `/dir_a` | + +--- + +## Pass / Fail + +- All assertions pass → print summary, `process.exit(0)` +- Any assertion throws → print the failing operation, the expected event, and + the actual batch received, then `process.exit(1)`