/** * WASI shim 單元測試 * Task 2.2 — Requirements: 3.1, 3.3 */ import { describe, it, expect } from 'vitest'; import { createWasiShim } from '../src/lib/wasi-shim'; // 建立一個最小的 fake WebAssembly.Memory(用 ArrayBuffer 模擬) function makeFakeMemory(size = 65536): WebAssembly.Memory { // 用真實的 WebAssembly.Memory(Vitest 環境支援) return new WebAssembly.Memory({ initial: 1 }); } /** 在 memory 中寫入 iovec 陣列,回傳 iovs 指標 */ function writeIovecs( view: DataView, iovecs: Array<{ buf: number; buf_len: number }>, startPtr: number, ): number { for (let i = 0; i < iovecs.length; i++) { view.setUint32(startPtr + i * 8, iovecs[i].buf, true); view.setUint32(startPtr + i * 8 + 4, iovecs[i].buf_len, true); } return startPtr; } describe('createWasiShim', () => { describe('fd_read(stdin)', () => { it('一次讀取完整 stdin', () => { const input = '{"key":"value"}'; const shim = createWasiShim(input); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); const inputBytes = new TextEncoder().encode(input); // 配置 buffer 區域(offset 100)和 iovec(offset 0) const bufPtr = 100; const iovsPtr = 0; const nreadPtr = 50; writeIovecs(view, [{ buf: bufPtr, buf_len: inputBytes.length }], iovsPtr); const fd_read = (shim.imports.wasi_snapshot_preview1 as Record).fd_read; const result = fd_read(0, iovsPtr, 1, nreadPtr); expect(result).toBe(0); // ESUCCESS const nread = view.getUint32(nreadPtr, true); expect(nread).toBe(inputBytes.length); // 驗證讀取的內容 const readBytes = new Uint8Array(mem.buffer, bufPtr, nread); expect(new TextDecoder().decode(readBytes)).toBe(input); }); it('分多次讀取 stdin', () => { const input = 'hello'; const shim = createWasiShim(input); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); const fd_read = (shim.imports.wasi_snapshot_preview1 as Record).fd_read; // 第一次讀 3 bytes writeIovecs(view, [{ buf: 200, buf_len: 3 }], 0); fd_read(0, 0, 1, 50); expect(view.getUint32(50, true)).toBe(3); expect(new TextDecoder().decode(new Uint8Array(mem.buffer, 200, 3))).toBe('hel'); // 第二次讀剩餘 2 bytes writeIovecs(view, [{ buf: 300, buf_len: 10 }], 0); fd_read(0, 0, 1, 50); expect(view.getUint32(50, true)).toBe(2); expect(new TextDecoder().decode(new Uint8Array(mem.buffer, 300, 2))).toBe('lo'); // 第三次讀:stdin 已耗盡,nread = 0 writeIovecs(view, [{ buf: 400, buf_len: 10 }], 0); fd_read(0, 0, 1, 50); expect(view.getUint32(50, true)).toBe(0); }); it('非 stdin fd 回傳 ENOSYS', () => { const shim = createWasiShim(''); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); writeIovecs(view, [{ buf: 100, buf_len: 10 }], 0); const fd_read = (shim.imports.wasi_snapshot_preview1 as Record).fd_read; expect(fd_read(1, 0, 1, 50)).toBe(76); // ENOSYS expect(fd_read(2, 0, 1, 50)).toBe(76); }); }); describe('fd_write(stdout/stderr)', () => { it('寫入 stdout(fd=1)並可透過 getStdout 讀取', () => { const shim = createWasiShim(''); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); const data = new TextEncoder().encode('{"valid":true}'); const bufPtr = 100; new Uint8Array(mem.buffer).set(data, bufPtr); writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0); const fd_write = (shim.imports.wasi_snapshot_preview1 as Record).fd_write; const result = fd_write(1, 0, 1, 50); expect(result).toBe(0); expect(view.getUint32(50, true)).toBe(data.length); expect(shim.getStdout()).toBe('{"valid":true}'); }); it('寫入 stderr(fd=2)並可透過 getStderr 讀取', () => { const shim = createWasiShim(''); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); const data = new TextEncoder().encode('error message'); const bufPtr = 100; new Uint8Array(mem.buffer).set(data, bufPtr); writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0); const fd_write = (shim.imports.wasi_snapshot_preview1 as Record).fd_write; fd_write(2, 0, 1, 50); expect(shim.getStderr()).toBe('error message'); expect(shim.getStdout()).toBe(''); // stdout 不受影響 }); it('多次寫入 stdout 會合併', () => { const shim = createWasiShim(''); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); const fd_write = (shim.imports.wasi_snapshot_preview1 as Record).fd_write; const write = (text: string, bufPtr: number) => { const data = new TextEncoder().encode(text); new Uint8Array(mem.buffer).set(data, bufPtr); writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0); fd_write(1, 0, 1, 50); }; write('{"valid":', 100); write('true}', 200); expect(shim.getStdout()).toBe('{"valid":true}'); }); it('非 stdout/stderr fd 回傳 ENOSYS', () => { const shim = createWasiShim(''); const mem = makeFakeMemory(); shim.setMemory(mem); const view = new DataView(mem.buffer); writeIovecs(view, [{ buf: 100, buf_len: 5 }], 0); const fd_write = (shim.imports.wasi_snapshot_preview1 as Record).fd_write; expect(fd_write(0, 0, 1, 50)).toBe(76); // stdin 不能寫 expect(fd_write(3, 0, 1, 50)).toBe(76); // 其他 fd }); }); describe('proc_exit', () => { it('proc_exit(0) 拋出 Error(正常結束)', () => { const shim = createWasiShim(''); const proc_exit = (shim.imports.wasi_snapshot_preview1 as Record).proc_exit; expect(() => proc_exit(0)).toThrow('wasm exit: 0'); }); it('proc_exit(1) 拋出 Error(錯誤結束)', () => { const shim = createWasiShim(''); const proc_exit = (shim.imports.wasi_snapshot_preview1 as Record).proc_exit; expect(() => proc_exit(1)).toThrow('wasm exit: 1'); }); }); describe('其餘 syscall 回傳 ENOSYS', () => { it('fd_seek 回傳 ENOSYS', () => { const shim = createWasiShim(''); const fd_seek = (shim.imports.wasi_snapshot_preview1 as Record).fd_seek; expect(fd_seek(1, 0, 0, 0)).toBe(76); }); it('sock_connect 相關 syscall 回傳 ENOSYS', () => { const shim = createWasiShim(''); const imports = shim.imports.wasi_snapshot_preview1 as Record; expect(imports.sock_recv()).toBe(76); expect(imports.sock_send()).toBe(76); expect(imports.sock_shutdown()).toBe(76); }); it('path_open 回傳 ENOSYS', () => { const shim = createWasiShim(''); const imports = shim.imports.wasi_snapshot_preview1 as Record; expect(imports.path_open()).toBe(76); expect(imports.path_create_directory()).toBe(76); }); }); describe('setMemory 未呼叫時', () => { it('fd_write 在 memory 未設定時拋出錯誤', () => { const shim = createWasiShim(''); // 不呼叫 setMemory const fd_write = (shim.imports.wasi_snapshot_preview1 as Record).fd_write; expect(() => fd_write(1, 0, 1, 50)).toThrow('WASI memory not set'); }); }); });