diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-16 22:49:07 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-16 22:49:07 +0900 |
| commit | efe05c1444963c046ab91bf54fa51a794bda58c0 (patch) | |
| tree | 509b48f27d2e888740bea6bfd6f50895705c7472 /worker/swift | |
| parent | db87f85aa7055e597800481b8cc6d006c70bcc88 (diff) | |
| download | phperkaigi-2026-albatross-efe05c1444963c046ab91bf54fa51a794bda58c0.tar.gz phperkaigi-2026-albatross-efe05c1444963c046ab91bf54fa51a794bda58c0.tar.zst phperkaigi-2026-albatross-efe05c1444963c046ab91bf54fa51a794bda58c0.zip | |
test(worker): add unit tests for php and swift workers
Extract testable logic from exec.mjs into lib.mjs (preprocessCode,
createIOCallbacks, buildResult) and add vitest tests. Add Go tests
for models, exec helpers, and handlers in worker/swift. Update
justfiles to include test tasks for local dev and CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'worker/swift')
| -rw-r--r-- | worker/swift/exec_test.go | 149 | ||||
| -rw-r--r-- | worker/swift/handlers_test.go | 66 | ||||
| -rw-r--r-- | worker/swift/justfile | 5 | ||||
| -rw-r--r-- | worker/swift/models_test.go | 70 |
4 files changed, 289 insertions, 1 deletions
diff --git a/worker/swift/exec_test.go b/worker/swift/exec_test.go new file mode 100644 index 0000000..ead2dc6 --- /dev/null +++ b/worker/swift/exec_test.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + "time" +) + +func TestConvertCommandErrorToResultType(t *testing.T) { + tests := []struct { + name string + err error + defaultStatus string + want string + }{ + {"nil error returns success", nil, resultRuntimeError, resultSuccess}, + {"DeadlineExceeded returns timeout", context.DeadlineExceeded, resultRuntimeError, resultTimeout}, + {"other error returns default status", os.ErrNotExist, resultCompileError, resultCompileError}, + {"other error returns runtime_error default", os.ErrPermission, resultRuntimeError, resultRuntimeError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertCommandErrorToResultType(tt.err, tt.defaultStatus) + if got != tt.want { + t.Errorf("convertCommandErrorToResultType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestExecCommandWithTimeout_Success(t *testing.T) { + stdout, stderr, err := execCommandWithTimeout( + context.Background(), + t.TempDir(), + 5*time.Second, + func(ctx context.Context) *exec.Cmd { + return exec.CommandContext(ctx, "echo", "hello") + }, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if stdout != "hello\n" { + t.Errorf("stdout = %q, want %q", stdout, "hello\n") + } + if stderr != "" { + t.Errorf("stderr = %q, want empty", stderr) + } +} + +func TestExecCommandWithTimeout_Failure(t *testing.T) { + _, _, err := execCommandWithTimeout( + context.Background(), + t.TempDir(), + 5*time.Second, + func(ctx context.Context) *exec.Cmd { + return exec.CommandContext(ctx, "false") + }, + ) + if err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestExecCommandWithTimeout_Timeout(t *testing.T) { + _, _, err := execCommandWithTimeout( + context.Background(), + t.TempDir(), + 50*time.Millisecond, + func(ctx context.Context) *exec.Cmd { + return exec.CommandContext(ctx, "sleep", "10") + }, + ) + if err != context.DeadlineExceeded { + t.Errorf("expected DeadlineExceeded, got %v", err) + } +} + +func TestExecCommandWithTimeout_Stderr(t *testing.T) { + _, stderr, _ := execCommandWithTimeout( + context.Background(), + t.TempDir(), + 5*time.Second, + func(ctx context.Context) *exec.Cmd { + return exec.CommandContext(ctx, "sh", "-c", "echo errmsg >&2") + }, + ) + if stderr != "errmsg\n" { + t.Errorf("stderr = %q, want %q", stderr, "errmsg\n") + } +} + +func TestPrepareWorkingDir(t *testing.T) { + dir := filepath.Join(t.TempDir(), "subdir") + res := prepareWorkingDir(dir) + if res.Status != resultSuccess { + t.Fatalf("prepareWorkingDir() status = %q, want %q", res.Status, resultSuccess) + } + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("directory not created: %v", err) + } + if !info.IsDir() { + t.Fatalf("expected directory, got file") + } +} + +func TestPutSwiftSourceFile(t *testing.T) { + t.Run("writes file when Sources/ exists", func(t *testing.T) { + dir := t.TempDir() + sourcesDir := filepath.Join(dir, "Sources") + if err := os.MkdirAll(sourcesDir, 0755); err != nil { + t.Fatalf("failed to create Sources dir: %v", err) + } + res := putSwiftSourceFile(dir, "print(\"hello\")") + if res.Status != resultSuccess { + t.Fatalf("putSwiftSourceFile() status = %q, want %q", res.Status, resultSuccess) + } + content, err := os.ReadFile(filepath.Join(sourcesDir, "main.swift")) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + if string(content) != "print(\"hello\")" { + t.Errorf("file content = %q, want %q", string(content), "print(\"hello\")") + } + }) + + t.Run("returns error when Sources/ does not exist", func(t *testing.T) { + dir := t.TempDir() + res := putSwiftSourceFile(dir, "print(\"hello\")") + if res.Status == resultSuccess { + t.Fatal("expected error status, got success") + } + }) +} + +func TestRemoveWorkingDir(t *testing.T) { + dir := filepath.Join(t.TempDir(), "toremove") + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("failed to create dir: %v", err) + } + removeWorkingDir(dir) + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Errorf("directory still exists after removeWorkingDir") + } +} diff --git a/worker/swift/handlers_test.go b/worker/swift/handlers_test.go new file mode 100644 index 0000000..f9c2e0b --- /dev/null +++ b/worker/swift/handlers_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v4" +) + +func TestNewBadRequestError(t *testing.T) { + httpErr := newBadRequestError(errors.New("test error")) + if httpErr.Code != http.StatusBadRequest { + t.Errorf("status code = %d, want %d", httpErr.Code, http.StatusBadRequest) + } + msg, ok := httpErr.Message.(string) + if !ok { + t.Fatalf("expected string message, got %T", httpErr.Message) + } + if !strings.Contains(msg, "test error") { + t.Errorf("message = %q, want it to contain %q", msg, "test error") + } +} + +func TestHandleExec_InvalidJSON(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/exec", strings.NewReader("not json")) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := handleExec(c) + if err == nil { + t.Fatal("expected error, got nil") + } + httpErr, ok := err.(*echo.HTTPError) + if !ok { + t.Fatalf("expected *echo.HTTPError, got %T", err) + } + if httpErr.Code != http.StatusBadRequest { + t.Errorf("status code = %d, want %d", httpErr.Code, http.StatusBadRequest) + } +} + +func TestHandleExec_ZeroMaxDuration(t *testing.T) { + e := echo.New() + body := `{"code":"print(1)","code_hash":"abc","stdin":"","max_duration_ms":0}` + req := httptest.NewRequest(http.MethodPost, "/exec", strings.NewReader(body)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := handleExec(c) + if err == nil { + t.Fatal("expected error, got nil") + } + httpErr, ok := err.(*echo.HTTPError) + if !ok { + t.Fatalf("expected *echo.HTTPError, got %T", err) + } + if httpErr.Code != http.StatusBadRequest { + t.Errorf("status code = %d, want %d", httpErr.Code, http.StatusBadRequest) + } +} diff --git a/worker/swift/justfile b/worker/swift/justfile index b203597..decaaf0 100644 --- a/worker/swift/justfile +++ b/worker/swift/justfile @@ -2,4 +2,7 @@ check: go build -o /dev/null ./... go tool golangci-lint run -ci: check +test: + go test ./... + +ci: check test diff --git a/worker/swift/models_test.go b/worker/swift/models_test.go new file mode 100644 index 0000000..62d6675 --- /dev/null +++ b/worker/swift/models_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "testing" + "time" +) + +func TestExecRequestData_Validate(t *testing.T) { + tests := []struct { + name string + maxDurationMs int + wantErr error + }{ + {"positive value", 1000, nil}, + {"zero", 0, errInvalidMaxDuration}, + {"negative value", -1, errInvalidMaxDuration}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &execRequestData{MaxDurationMilliseconds: tt.maxDurationMs} + err := req.validate() + if err != tt.wantErr { + t.Errorf("validate() = %v, want %v", err, tt.wantErr) + } + }) + } +} + +func TestExecRequestData_MaxDuration(t *testing.T) { + tests := []struct { + name string + maxDurationMs int + want time.Duration + }{ + {"1000ms", 1000, 1 * time.Second}, + {"500ms", 500, 500 * time.Millisecond}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &execRequestData{MaxDurationMilliseconds: tt.maxDurationMs} + got := req.maxDuration() + if got != tt.want { + t.Errorf("maxDuration() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestExecResponseData_Success(t *testing.T) { + tests := []struct { + name string + status string + want bool + }{ + {"success", resultSuccess, true}, + {"compile_error", resultCompileError, false}, + {"runtime_error", resultRuntimeError, false}, + {"timeout", resultTimeout, false}, + {"internal_error", resultInternalError, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := &execResponseData{Status: tt.status} + got := res.success() + if got != tt.want { + t.Errorf("success() = %v, want %v", got, tt.want) + } + }) + } +} |
