package executor_test import ( "context" "os" "path/filepath" "strings" "testing" "time" "tangled.sh/calabro.io/watsm/executor" ) func loadTestdata(t *testing.T, name string) []byte { t.Helper() data, err := os.ReadFile(filepath.Join("testdata", name)) if err != nil { t.Fatalf("reading testdata/%s: %v", name, err) } return data } // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- func TestValidate(t *testing.T) { t.Parallel() tests := []struct { name string input []byte maxSize int64 wantErr string }{ { name: "valid wasm header", input: append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 100)...), maxSize: 1024, }, { name: "too small", input: []byte{0x00, 0x61, 0x73, 0x6d}, maxSize: 1024, wantErr: "too small", }, { name: "bad magic", input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, maxSize: 1024, wantErr: "magic number", }, { name: "too large", input: append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 2000)...), maxSize: 1024, wantErr: "too large", }, { name: "empty", input: []byte{}, maxSize: 1024, wantErr: "too small", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() err := executor.Validate(tt.input, tt.maxSize) if tt.wantErr == "" { if err != nil { t.Fatalf("unexpected error: %v", err) } return } if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error %q does not contain %q", err.Error(), tt.wantErr) } }) } } // --------------------------------------------------------------------------- // Basic execution // --------------------------------------------------------------------------- func TestExecuteHelloWorld(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "hello.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if got := strings.TrimSpace(result.Stdout); got != "hello world" { t.Errorf("stdout: got %q, want %q", got, "hello world") } if result.Stderr != "" { t.Errorf("stderr: got %q, want empty", result.Stderr) } if result.DurationMs < 0 { t.Errorf("duration should be non-negative: %d", result.DurationMs) } } func TestExecuteStderr(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "stderr.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if got := strings.TrimSpace(result.Stdout); got != "out" { t.Errorf("stdout: got %q, want %q", got, "out") } if got := strings.TrimSpace(result.Stderr); got != "err" { t.Errorf("stderr: got %q, want %q", got, "err") } } func TestExecuteExitError(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "exit_err.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusError { t.Errorf("status: got %q, want %q", result.Status, executor.StatusError) } if result.ExitCode != 42 { t.Errorf("exit code: got %d, want 42", result.ExitCode) } } // --------------------------------------------------------------------------- // Resource limits // --------------------------------------------------------------------------- func TestExecuteTimeout(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "spin.wasm") cfg := executor.DefaultConfig() cfg.MaxExecTime = 500 * time.Millisecond start := time.Now() result, err := executor.Execute(context.Background(), cfg, wasm) elapsed := time.Since(start) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusTimeout { t.Errorf("status: got %q, want %q", result.Status, executor.StatusTimeout) } // Should complete near the timeout, not hang. // Allow generous slack for race detector overhead. if elapsed > 30*time.Second { t.Errorf("took too long: %v (expected near 500ms)", elapsed) } } func TestExecuteContextCancellation(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "spin.wasm") cfg := executor.DefaultConfig() cfg.MaxExecTime = 10 * time.Second // long timeout — we'll cancel early ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(300 * time.Millisecond) cancel() }() start := time.Now() result, err := executor.Execute(ctx, cfg, wasm) elapsed := time.Since(start) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusTimeout { t.Errorf("status: got %q, want %q", result.Status, executor.StatusTimeout) } // Allow generous slack for race detector overhead. if elapsed > 30*time.Second { t.Errorf("took too long: %v (expected near 300ms)", elapsed) } } func TestExecuteOutputTruncation(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "bigout.wasm") cfg := executor.DefaultConfig() cfg.MaxOutputBytes = 100 result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if len(result.Stdout) > 100 { t.Errorf("stdout not truncated: got %d bytes, max 100", len(result.Stdout)) } if len(result.Stdout) < 100 { t.Errorf("stdout shorter than expected: got %d bytes, want 100", len(result.Stdout)) } } func TestExecuteMemoryLimit(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "allocmem.wasm") cfg := executor.DefaultConfig() cfg.MaxMemoryMB = 16 // 16 MB — module tries to allocate 64 MB result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } // Module should fail to allocate — it will either get an error status // or crash. Either way it must NOT succeed. if strings.Contains(result.Stdout, "ALLOC_OK") { t.Error("module was able to allocate 64 MB despite 16 MB limit") } } func TestExecuteInvalidWasm(t *testing.T) { t.Parallel() cfg := executor.DefaultConfig() _, err := executor.Execute(context.Background(), cfg, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) if err == nil { t.Fatal("expected error for invalid wasm") } } func TestExecuteOversizedWasm(t *testing.T) { t.Parallel() cfg := executor.DefaultConfig() cfg.MaxWasmSize = 100 wasm := append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 200)...) _, err := executor.Execute(context.Background(), cfg, wasm) if err == nil { t.Fatal("expected error for oversized wasm") } if !strings.Contains(err.Error(), "too large") { t.Errorf("error %q does not mention size", err.Error()) } } // --------------------------------------------------------------------------- // Sandbox isolation // --------------------------------------------------------------------------- func TestSandboxNoFilesystemAccess(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "readfile.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if strings.Contains(result.Stdout, "LEAKED") { t.Error("module was able to read /etc/passwd — filesystem not sandboxed") } if !strings.Contains(result.Stdout, "BLOCKED") { t.Errorf("expected BLOCKED in stdout, got %q", result.Stdout) } } func TestSandboxNoEnvironmentVariables(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "readenv.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if strings.Contains(result.Stdout, "ENV_LEAKED") { t.Errorf("module was able to read env vars — sandbox leaking: %s", result.Stdout) } if !strings.Contains(result.Stdout, "ENV_EMPTY") { t.Errorf("expected ENV_EMPTY in stdout, got %q", result.Stdout) } } func TestSandboxNoArgs(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "readargs.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } if strings.Contains(result.Stdout, "ARGS_LEAKED") { t.Errorf("module was able to read os.Args — sandbox leaking: %s", result.Stdout) } if !strings.Contains(result.Stdout, "ARGS_EMPTY") { t.Errorf("expected ARGS_EMPTY in stdout, got %q", result.Stdout) } } func TestSandboxFakeClock(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "readtime.wasm") cfg := executor.DefaultConfig() result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) } // wazero's fake walltime starts at 2022-01-01T00:00:00Z. // The module prints YEAR:. It must NOT see the real year. if strings.Contains(result.Stdout, "YEAR:2026") { t.Error("module sees real system time — clock not sandboxed") } if !strings.Contains(result.Stdout, "YEAR:2022") { t.Errorf("expected YEAR:2022 (fake clock), got %q", result.Stdout) } } // --------------------------------------------------------------------------- // Isolation between executions // --------------------------------------------------------------------------- func TestExecutionsAreIsolated(t *testing.T) { t.Parallel() wasm := loadTestdata(t, "hello.wasm") cfg := executor.DefaultConfig() // Run the same module twice. Each should get a fresh runtime. r1, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("first execute: %v", err) } r2, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("second execute: %v", err) } // Both should succeed identically — no state leakage. if r1.Status != executor.StatusSuccess || r2.Status != executor.StatusSuccess { t.Errorf("status: r1=%q r2=%q", r1.Status, r2.Status) } if strings.TrimSpace(r1.Stdout) != strings.TrimSpace(r2.Stdout) { t.Errorf("different stdout across runs: %q vs %q", r1.Stdout, r2.Stdout) } } // --------------------------------------------------------------------------- // Config defaults // --------------------------------------------------------------------------- func TestDefaultConfig(t *testing.T) { t.Parallel() cfg := executor.DefaultConfig() if cfg.MaxExecTime != 5*time.Second { t.Errorf("MaxExecTime: got %v, want 5s", cfg.MaxExecTime) } if cfg.MaxCompileTime != 10*time.Second { t.Errorf("MaxCompileTime: got %v, want 10s", cfg.MaxCompileTime) } if cfg.MaxMemoryMB != 16 { t.Errorf("MaxMemoryMB: got %d, want 16", cfg.MaxMemoryMB) } if cfg.MaxWasmSize != 10*1024*1024 { t.Errorf("MaxWasmSize: got %d, want 10MB", cfg.MaxWasmSize) } if cfg.MaxOutputBytes != 10_000 { t.Errorf("MaxOutputBytes: got %d, want 10000", cfg.MaxOutputBytes) } }