Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Unity 커넥터의 동작:
| `console` | 콘솔 로그 읽기, 필터링, 지우기 |
| `exec` | Unity 안에서 임의 C# 코드 실행 |
| `test` | EditMode/PlayMode 테스트 실행 |
| `verify` | 컴파일, Play 모드, 콘솔 에러 확인, 정리를 한 번에 실행 |
| `menu` | Unity 메뉴 아이템을 경로로 실행 |
| `reserialize` | Unity 시리얼라이저를 통해 에셋 재직렬화 |
| `screenshot` | Scene/Game 뷰를 PNG로 캡처 |
Expand Down Expand Up @@ -306,6 +307,24 @@ unity-cli test --filter MyTestClass

Unity Test Framework 패키지가 필요합니다. PlayMode 테스트는 도메인 리로드를 일으키며, CLI는 결과 파일을 폴링합니다.

### Verify Project

Common validation flow를 한 번에 실행합니다: refresh/compile, Unity ready 대기, play mode 진입, console error 확인, 그리고 `verify`가 시작한 play mode 정리.

```bash
# 전체 검증 플로우
unity-cli verify

# 스크립트가 이미 최신이면 compile 생략
unity-cli verify --skip-compile

# 수동 확인을 위해 play mode 유지
unity-cli verify --keep-playing

# 더 많은 console error와 user stack frame 확인
unity-cli verify --console-lines 100 --stacktrace user
```

### 도구 목록

```bash
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Before compiling or reloading, the Connector records the state (`compiling`, `re
| `console` | Read, filter, and clear console logs |
| `exec` | Run arbitrary C# code inside Unity |
| `test` | Run EditMode/PlayMode tests |
| `verify` | Compile, enter play mode, inspect console errors, and stop |
| `menu` | Execute any Unity menu item by path |
| `reserialize` | Re-serialize assets through Unity's serializer |
| `screenshot` | Capture scene/game view as PNG |
Expand Down Expand Up @@ -305,6 +306,24 @@ unity-cli test --filter MyTestClass

Requires the Unity Test Framework package. PlayMode tests trigger a domain reload; the CLI polls for results automatically.

### Verify Project

Run the common agent validation flow in one command: refresh and compile scripts, wait until Unity is ready, enter play mode, inspect console errors, then stop play mode if `verify` started it.

```bash
# Full verification flow
unity-cli verify

# Skip compilation when scripts are already fresh
unity-cli verify --skip-compile

# Leave the Editor playing for manual inspection
unity-cli verify --keep-playing

# Inspect more console errors with user stack frames
unity-cli verify --console-lines 100 --stacktrace user
```

### List Tools

```bash
Expand Down
31 changes: 31 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ func Execute() error {
switch category {
case "editor":
resp, err = editorCmd(subArgs, send, resolve)
case "verify":
resp, err = verifyCmd(subArgs, send, resolve)
case "test":
resp, err = testCmd(subArgs, send, resolve)
case "exec":
Expand Down Expand Up @@ -566,6 +568,12 @@ Tests:
test --mode PlayMode Run PlayMode tests
test --filter <name> Filter by namespace, class, or full test name

Verify:
verify Compile, enter play mode, check console errors, stop
verify --skip-compile Skip script compilation
verify --skip-play Check compile and console without entering play mode
verify --keep-playing Leave Unity in play mode after verification

Profiler:
profiler hierarchy Top-level profiler samples (last frame)
profiler hierarchy --depth 5 Recursive drill-down (0=unlimited)
Expand Down Expand Up @@ -771,6 +779,29 @@ Examples:
unity-cli test --mode PlayMode
unity-cli test --filter MyNamespace.MyTests
unity-cli test --mode EditMode --filter MyNamespace.MyTests.SpecificTest
`)
case "verify":
fmt.Print(`Usage: unity-cli verify [options]

Run a common Unity validation flow:
1. Refresh assets and request script compilation
2. Wait until Unity is ready
3. Enter play mode and wait until fully entered
4. Read console errors
5. Stop play mode if verify started it

Options:
--skip-compile Skip refresh and script compilation
--skip-play Do not enter play mode
--keep-playing Leave Unity in play mode after verification
--console-lines <N> Console entries to inspect (default: 50)
--console-type <types> Log types to inspect (default: error)
--stacktrace <mode> none, user, or full (default: none)

Examples:
unity-cli verify
unity-cli verify --skip-compile
unity-cli verify --console-lines 100 --stacktrace user
`)
case "list":
fmt.Print(`Usage: unity-cli list
Expand Down
194 changes: 194 additions & 0 deletions cmd/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package cmd

import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/youngwoocho02/unity-cli/internal/client"
)

type verifySummary struct {
Project string `json:"project,omitempty"`
Compile bool `json:"compile"`
CompileOK bool `json:"compile_ok"`
Play bool `json:"play"`
EnteredPlay bool `json:"entered_play"`
Stopped bool `json:"stopped"`
RuntimeOK bool `json:"runtime_ok"`
ErrorCount int `json:"error_count"`
Errors []string `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
}

func verifyCmd(args []string, send sendFn, resolve instanceResolver) (*client.CommandResponse, error) {
flags := parseSubFlags(args)

summary := verifySummary{
Compile: !hasFlag(flags, "skip-compile"),
CompileOK: true,
Play: !hasFlag(flags, "skip-play"),
RuntimeOK: true,
}

lines := parsePositiveIntFlag(flags, "console-lines", 50)
consoleType := flagValue(flags, "console-type", "error")
stacktrace := flagValue(flags, "stacktrace", "none")
keepPlaying := hasFlag(flags, "keep-playing")

initial, err := resolve()
if err != nil {
return nil, err
}
summary.Project = initial.ProjectPath
initialPlaying := isPlayState(initial.State)
shouldStop := false

if summary.Compile {
resp, err := send("refresh_unity", map[string]interface{}{"compile": "request"})
if err != nil {
return nil, err
}
if !resp.Success {
summary.CompileOK = false
summary.Messages = append(summary.Messages, resp.Message)
return verifyResponse(summary), nil
}

if waitForReady(resolve) {
summary.CompileOK = false
summary.Messages = append(summary.Messages, "Compilation finished with errors.")
return verifyResponse(summary), nil
}
}

if summary.Play {
resp, err := send("manage_editor", map[string]interface{}{
"action": "play",
"wait_for_completion": true,
})
if err != nil {
return nil, err
}
if !resp.Success {
summary.Messages = append(summary.Messages, resp.Message)
return verifyResponse(summary), nil
}
summary.EnteredPlay = true
summary.Messages = append(summary.Messages, resp.Message)
shouldStop = !initialPlaying && !keepPlaying
}

consoleResp, err := send("console", map[string]interface{}{
"type": consoleType,
"lines": lines,
"stacktrace": stacktrace,
})
if err != nil {
return nil, err
}
if !consoleResp.Success {
summary.RuntimeOK = false
summary.Messages = append(summary.Messages, consoleResp.Message)
stopVerifyPlayMode(send, shouldStop, &summary)
return verifyResponse(summary), nil
}

entries, err := consoleEntries(consoleResp)
if err != nil {
return nil, err
}
summary.Errors = entries
summary.ErrorCount = len(entries)
summary.RuntimeOK = summary.ErrorCount == 0

if summary.RuntimeOK && summary.CompileOK {
summary.Messages = append(summary.Messages, "Verification passed.")
}

stopVerifyPlayMode(send, shouldStop, &summary)
return verifyResponse(summary), nil
}

func stopVerifyPlayMode(send sendFn, shouldStop bool, summary *verifySummary) {
if !shouldStop {
return
}
stopResp, stopErr := send("manage_editor", map[string]interface{}{
"action": "stop",
"wait_for_completion": true,
})
if stopErr != nil {
summary.Messages = append(summary.Messages, "Failed to stop play mode: "+stopErr.Error())
return
}
if stopResp == nil {
summary.Messages = append(summary.Messages, "Failed to stop play mode: empty response")
return
}
if !stopResp.Success {
summary.Messages = append(summary.Messages, "Failed to stop play mode: "+stopResp.Message)
return
}
summary.Stopped = true
}

func verifyResponse(summary verifySummary) *client.CommandResponse {
success := summary.CompileOK && summary.RuntimeOK
message := "Verify passed."
if !success {
message = "Verify failed."
}
data, _ := json.Marshal(summary)
return &client.CommandResponse{
Success: success,
Message: message,
Data: data,
}
}

func consoleEntries(resp *client.CommandResponse) ([]string, error) {
if resp == nil || len(resp.Data) == 0 || string(resp.Data) == "null" {
return nil, nil
}
var entries []string
if err := json.Unmarshal(resp.Data, &entries); err != nil {
return nil, fmt.Errorf("failed to parse console entries: %w", err)
}
return entries, nil
}

func hasFlag(flags map[string]string, name string) bool {
_, ok := flags[name]
return ok
}

func flagValue(flags map[string]string, name, fallback string) string {
value := strings.TrimSpace(flags[name])
if value == "" {
return fallback
}
return value
}

func parsePositiveIntFlag(flags map[string]string, name string, fallback int) int {
value := strings.TrimSpace(flags[name])
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}

func isPlayState(state string) bool {
switch strings.ToLower(strings.TrimSpace(state)) {
case "playing", "paused", "entering_playmode":
return true
default:
return false
}
}
Loading