diff --git a/system/configuration/protocol.csv b/system/configuration/protocol.csv index f749122..7d681ea 100644 --- a/system/configuration/protocol.csv +++ b/system/configuration/protocol.csv @@ -102,6 +102,8 @@ # own endpoint (the mouse-listener thread opens /protocol/display like any other # client — threads share no handles), and the input stream that moves the cursor. /system/services/fat, /system/services/volume-manager, open, volume-manager +# exfat reaches the volume manager the same way — the second engine, same lineage. +/system/services/exfat, /system/services/volume-manager, open, volume-manager # The volume manager reaches the device manager to be routed to each storage # provider's block channel, then confines a filesystem to each volume. /system/services/volume-manager, /system/services/init, open, device-manager diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index c707998..2c27cd9 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -227,6 +227,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { usbStorageTest(boot_information); } else if (eql(case, "fat-mount")) { fatMountTest(boot_information); + } else if (eql(case, "exfat-volume")) { + exfatVolumeTest(boot_information); } else if (eql(case, "device-list")) { deviceListTest(boot_information); } else if (eql(case, "pci-scan")) { @@ -3036,6 +3038,32 @@ fn fatMountTest(boot_information: *const BootInformation) void { result(); } +/// The exFAT mount chain (S4): boot the full tree, which brings up the USB storage +/// chain. The harness attaches a SECOND device — a data-only exFAT volume — beside +/// the FAT boot volume, so the volume manager spawns the exfat service for it +/// (content-routed, its id-path /volumes/exfat-). Then spawn exfat-test, +/// which reads the seeded file and mutates through the mount. The reuse of the +/// shared harness by a second engine is proven end to end here. +fn exfatVolumeTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: exfat-volume\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over the initial_ramdisk", false); + result(); + return; + } + const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(ramdisk) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + process.setInitialRamdisk(ramdisk); + const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false; + check("init spawned (boots the tree, incl. the volume manager)", init_ok); + check("exfat-test client spawned", spawnNamed(rd, "exfat-test")); + result(); +} + /// Per-sender range confinement (V2a, docs/volume-manager-plan.md): the fixture /// acquires the block channel, confines ITSELF to a sub-range, and asserts it /// cannot read past that range or widen it. Boots init in REGISTRY-ONLY mode diff --git a/test/qemu_test.py b/test/qemu_test.py index d9b1677..24a346a 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -857,6 +857,23 @@ CASES = [ r"(?=.*fat: mounted /volumes/fat-da7a0001)" r"(?=.*fat: mounted /volumes/fat-da7a0002)", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # S4 second engine: a bare exFAT data device (serial e0fa0001) attached beside + # the FAT boot volume. The volume manager content-routes it to the exFAT + # service (not fat), which mounts it at its id-path /volumes/exfat-e0fa0001; + # the exfat-test client then reads the seeded HELLO.TXT and mutates through the + # mount (mkdir/write/rename/read/remove). Proves the second engine reuses the + # shared harness end to end. Fails against pre-S4 (no exfat binary, csv row, or + # VBR recognizer — the device would go to fat, which rejects the exFAT VBR). + {"name": "exfat-volume", + "build_case": "exfat-volume", + "smp": 4, + "timeout": 150, + "data_volume": {"exfat": True, "serial": "E0FA0001", "size_mib": 48}, + "expect": r"(?s)(?=.*volume 0x0*e0fa0001 -> /system/services/exfat )" + r"(?=.*exfat: mounted /volumes/exfat-e0fa0001)" + r"(?=.*exfat-test: read HELLO.TXT ok)" + r"(?=.*exfat-test: ok)", + "fail": r"exfat-test: FAILED|DANOS-TEST-RESULT: FAIL"}, # Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the # fat-test client, after listing, makes a directory, writes+reads a file inside # it, then removes the file, exercising the whole VFS -> fat mutation path. @@ -1477,6 +1494,10 @@ def run_case(arch, case): gen = [sys.executable, os.path.join(REPO, "tools", "make-partitioned-image.py"), data_img] for part in dv["partitions"]: gen += [part["serial"], str(part.get("size_mib", 40))] + elif dv.get("exfat"): + # One device, a bare exFAT volume — the second engine's medium. + gen = [sys.executable, os.path.join(REPO, "tools", "make-exfat-image.py"), + "--serial", dv["serial"], data_img, str(dv.get("size_mib", 48))] else: # One device, one bare FAT volume. gen = [sys.executable, os.path.join(REPO, "tools", "make-fat-image.py"),