# 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 /tmp/fa2json-test-XXXXXX.img`) 2. `truncate -s 10M` the image (sparse file, no need for `dd`) 3. `mkfs.ext4` the image 4. Create a temporary mount directory (`mktemp -d /tmp/fa2json-mnt-XXXXXX`) 5. `sudo mount ` (no `losetup` needed — `mount` accepts image files directly) 6. `sudo chown $(id -u) ` to hand ownership to the current user 7. `sync` to flush before fa2json starts listening 8. `sudo` spawn `fa2json ` as a child process (needs `CAP_SYS_ADMIN`) 9. Attach a `readline` interface to its stdout; parse each line as JSON and push into an event buffer Steps 6 and 7 ensure the `chown` event never enters the fa2json stream, and all subsequent FS operations run unprivileged. --- ## Teardown Runs unconditionally in a `finally` block: 1. Kill the `fa2json` child process 2. `sudo umount ` 3. `rm` the image file 4. `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)`