aboutsummaryrefslogtreecommitdiffhomepage
path: root/worker/swift
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-16 22:49:07 +0900
committernsfisis <nsfisis@gmail.com>2026-02-16 22:49:07 +0900
commitefe05c1444963c046ab91bf54fa51a794bda58c0 (patch)
tree509b48f27d2e888740bea6bfd6f50895705c7472 /worker/swift
parentdb87f85aa7055e597800481b8cc6d006c70bcc88 (diff)
downloadphperkaigi-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.go149
-rw-r--r--worker/swift/handlers_test.go66
-rw-r--r--worker/swift/justfile5
-rw-r--r--worker/swift/models_test.go70
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)
+ }
+ })
+ }
+}