forked from mikael-lovqvist/fa2json
Compare commits
3 Commits
main
...
f8aed56faf
| Author | SHA1 | Date | |
|---|---|---|---|
| f8aed56faf | |||
| c7c46dc15d | |||
| 850d2ceb75 |
82
test/Manual experiment.md
Normal file
82
test/Manual experiment.md
Normal file
@@ -0,0 +1,82 @@
|
||||
## Manual experiment
|
||||
|
||||
> [!NOTE]
|
||||
> This manual experiment shows how we can do the testing (teardown not included). Note that we don't need the `losetup`-stuff, we know where everything is.
|
||||
|
||||
### Compile
|
||||
```sh
|
||||
gcc fs-watcher.c json-writer.c -o fa2json
|
||||
```
|
||||
### Create image file
|
||||
```sh
|
||||
mktemp /tmp/fa2json-test-XXXXXX.img
|
||||
```
|
||||
|
||||
```text
|
||||
/tmp/fa2json-test-UrwpOb.img
|
||||
```
|
||||
|
||||
```sh
|
||||
truncate -s 10M /tmp/fa2json-test-UrwpOb.img
|
||||
```
|
||||
|
||||
```sh
|
||||
mkfs.ext4 /tmp/fa2json-test-UrwpOb.img
|
||||
```
|
||||
|
||||
```text
|
||||
mke2fs 1.47.3 (8-Jul-2025)
|
||||
Discarding device blocks: done
|
||||
Creating filesystem with 10240 1k blocks and 2560 inodes
|
||||
Filesystem UUID: 035c508e-dec0-4a21-a4d1-1efb6fa72415
|
||||
Superblock backups stored on blocks:
|
||||
8193
|
||||
|
||||
Allocating group tables: done
|
||||
Writing inode tables: done
|
||||
Creating journal (1024 blocks): done
|
||||
Writing superblocks and filesystem accounting information: done
|
||||
```
|
||||
|
||||
|
||||
### Create mount point
|
||||
|
||||
```sh
|
||||
mktemp -d /tmp/fa2json-mnt-XXXXXX
|
||||
```
|
||||
|
||||
```text
|
||||
/tmp/fa2json-mnt-ts2Dik
|
||||
```
|
||||
|
||||
### Mount loop device
|
||||
```sh
|
||||
sudo mount /tmp/fa2json-test-UrwpOb.img /tmp/fa2json-mnt-ts2Dik/
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> In a different terminal I now ran - but we could do this after `chown` or possibly `chown` + `sync`?
|
||||
> ```sh
|
||||
> fa2json /tmp/fa2json-mnt-ts2Dik
|
||||
> ```
|
||||
|
||||
### Let current user own file system
|
||||
```sh
|
||||
sudo chown $(id -u) /tmp/fa2json-mnt-ts2Dik/
|
||||
```
|
||||
|
||||
#### `fa2json` output
|
||||
```json
|
||||
{"ts": [1772658052, 704412412, 386988, 865842867], "name": "/tmp/fa2json-mnt-ts2Dik/.", "mask": 1073741828}
|
||||
```
|
||||
|
||||
### Touch marker
|
||||
```sh
|
||||
touch /tmp/fa2json-mnt-ts2Dik/MARKER
|
||||
```
|
||||
|
||||
#### `fa2json` output
|
||||
```json
|
||||
{"ts": [1772658064, 151070715, 387000, 312501190], "name": "/tmp/fa2json-mnt-ts2Dik/MARKER", "mask": 256}
|
||||
{"ts": [1772658064, 151099105, 387000, 312529600], "name": "/tmp/fa2json-mnt-ts2Dik/MARKER", "mask": 12}
|
||||
```
|
||||
123
test/PLAN.md
Normal file
123
test/PLAN.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 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 <img> <mntdir>` (no `losetup` needed — `mount` accepts image files directly)
|
||||
6. `sudo chown $(id -u) <mntdir>` to hand ownership to the current user
|
||||
7. `sync` to flush before fa2json starts listening
|
||||
8. `sudo` spawn `fa2json <mountpoint>` 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 <mountpoint>`
|
||||
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 <mountpoint>/.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)`
|
||||
Reference in New Issue
Block a user