package kneron import ( "bufio" "context" "io" "sync" "testing" "time" "visiona-local/server/internal/driver" "visiona-local/server/internal/firmware" ) // testCtx 回傳 test 用的 ctx with 5 秒 timeout,加自動 cleanup。 func testCtx(t *testing.T) context.Context { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) t.Cleanup(cancel) return ctx } // shortTimeout 回傳一個 500ms 的 timer channel、供 test 偵測非阻塞行為。 func shortTimeout() <-chan time.Time { return time.After(500 * time.Millisecond) } // TestTryRouteFirmwareEvent_ValidProgress:valid firmware_progress JSON line // 會被 unmarshal 並寫到 fwProgressCh。 func TestTryRouteFirmwareEvent_ValidProgress(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 4) d.setFirmwareProgressCh(ch) line := `{"event":"firmware_progress","percent":50,"stage":"flashing","message":"writing KDP2","elapsed_ms":1500,"eta_ms":3000}` if !d.tryRouteFirmwareEvent(line) { t.Fatalf("expected tryRouteFirmwareEvent=true") } select { case ev := <-ch: if ev.Stage != firmware.StageFlashing { t.Errorf("Stage = %q, want flashing", ev.Stage) } if ev.Percent != 50 { t.Errorf("Percent = %d, want 50", ev.Percent) } if ev.Message != "writing KDP2" { t.Errorf("Message = %q", ev.Message) } if ev.ElapsedMs != 1500 { t.Errorf("ElapsedMs = %d, want 1500", ev.ElapsedMs) } if ev.EtaMs != 3000 { t.Errorf("EtaMs = %d, want 3000", ev.EtaMs) } default: t.Fatalf("no event on channel") } } // TestTryRouteFirmwareEvent_ErrorEvent:error event 帶 reason / raw_error / // before_version 都要保留。 func TestTryRouteFirmwareEvent_ErrorEvent(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 4) d.setFirmwareProgressCh(ch) line := `{"event":"firmware_progress","percent":-1,"stage":"error","message":"loader missing","elapsed_ms":800,"eta_ms":0,"error":"loader missing","reason":"loader_write_failed","raw_error":"_FwError loader","before_version":"KDP"}` if !d.tryRouteFirmwareEvent(line) { t.Fatalf("expected tryRouteFirmwareEvent=true") } ev := <-ch if ev.Stage != firmware.StageError { t.Errorf("Stage = %q, want error", ev.Stage) } if ev.Reason != firmware.ReasonLoaderWriteFailed { t.Errorf("Reason = %q, want loader_write_failed", ev.Reason) } if ev.RawError != "_FwError loader" { t.Errorf("RawError = %q", ev.RawError) } if ev.BeforeVersion != "KDP" { t.Errorf("BeforeVersion = %q, want KDP", ev.BeforeVersion) } } // TestTryRouteFirmwareEvent_NoChannel:channel 未註冊時 return false、 // caller 應 fall back 到 broadcaster。 func TestTryRouteFirmwareEvent_NoChannel(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") // 不註冊 channel if d.tryRouteFirmwareEvent(`{"event":"firmware_progress","stage":"done","percent":100}`) { t.Errorf("expected false when no channel registered") } } // TestTryRouteFirmwareEvent_NonFirmwareEvent:非 firmware_progress event 不 route。 func TestTryRouteFirmwareEvent_NonFirmwareEvent(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 4) d.setFirmwareProgressCh(ch) // shutdown_rejected event 不 route if d.tryRouteFirmwareEvent(`{"event":"shutdown_rejected","reason":"firmware_upgrade_in_progress"}`) { t.Errorf("expected false for non-firmware event") } // 亂 JSON if d.tryRouteFirmwareEvent(`{not json}`) { t.Errorf("expected false for malformed JSON") } // 完全不是 JSON if d.tryRouteFirmwareEvent(`[kneron_bridge] regular log line`) { t.Errorf("expected false for plain log line") } } // TestTryRouteFirmwareEvent_ChannelFull:channel 滿 → 不 block、route 仍回 true。 func TestTryRouteFirmwareEvent_ChannelFull(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 1) // 先塞滿 ch <- firmware.FirmwareProgress{Stage: "x"} d.setFirmwareProgressCh(ch) line := `{"event":"firmware_progress","stage":"flashing","percent":50}` // 應該回 true(route 函式視為已嘗試處理)、但不 block test done := make(chan bool, 1) go func() { ok := d.tryRouteFirmwareEvent(line) done <- ok }() // 不該卡住 select { case ok := <-done: if !ok { t.Errorf("expected true (event was a firmware_progress event)") } case <-shortTimeout(): t.Fatalf("tryRouteFirmwareEvent should not block when channel is full") } } // TestSetFirmwareProgressCh_Unregister:設 nil 後恢復原狀。 func TestSetFirmwareProgressCh_Unregister(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 1) d.setFirmwareProgressCh(ch) d.setFirmwareProgressCh(nil) if d.tryRouteFirmwareEvent(`{"event":"firmware_progress","stage":"done","percent":100}`) { t.Errorf("expected false after unregister") } } // TestUpgradeFirmware_UnsupportedChip:A 階段限 KL520 / KL720。 func TestUpgradeFirmware_UnsupportedChip(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test", Type: "KL720"}, "/dev/null") // 強制 pythonReady=true、避開 "bridge not running" 早退(測 chip check) d.pythonReady = true defer func() { d.pythonReady = false }() ch := make(chan firmware.FirmwareProgress, 4) err := d.UpgradeFirmware(testCtx(t), "KL630", ch) if err == nil { t.Fatalf("expected error for unsupported chip") } } // TestUpgradeFirmware_NoPythonBridge:未 Connect 時 UpgradeFirmware 應拒絕。 func TestUpgradeFirmware_NoPythonBridge(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "test", Type: "KL720"}, "/dev/null") // 預設 pythonReady=false ch := make(chan firmware.FirmwareProgress, 4) err := d.UpgradeFirmware(testCtx(t), "KL720", ch) if err == nil { t.Fatalf("expected error when python bridge not running") } } // ────────────────────────────────────────────────────────────────────── // 以下測試使用 in-memory pipes 模擬 bridge stdin / stdout、不真正起 Python。 // // setupFakeBridge 把 driver 連到一對 io.Pipe、回 (stdinReader, stdoutWriter, close)。 // 測試代碼可從 stdinReader 讀 driver 寫出的 command JSON、用 stdoutWriter // 模擬 bridge 回應或保持不寫(模擬卡住)。 // ────────────────────────────────────────────────────────────────────── type fakeBridge struct { stdinR *io.PipeReader // driver 寫入端的讀方(test 端) stdoutW *io.PipeWriter // driver 讀取端的寫方(test 端) } func setupFakeBridge(t *testing.T, d *KneronDriver) *fakeBridge { t.Helper() stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() d.stdin = stdinW d.stdout = bufio.NewScanner(stdoutR) d.stdout.Buffer(make([]byte, 0, 64*1024), 1024*1024) d.pythonReady = true t.Cleanup(func() { _ = stdinR.Close() _ = stdinW.Close() _ = stdoutR.Close() _ = stdoutW.Close() d.pythonReady = false }) return &fakeBridge{stdinR: stdinR, stdoutW: stdoutW} } // TestUpgradeFirmware_InfoNotBlockedDuringUpgrade(Major 1 驗證) // // 升級期間 sendCommand goroutine 持 fwUpgradeMu 卡住等 stdout 回應、不可 // 阻塞 d.Info() 等 d.mu 操作。 func TestUpgradeFirmware_InfoNotBlockedDuringUpgrade(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "dev-x", Type: "KL720", Port: "USB"}, "/dev/null") setupFakeBridge(t, d) progressCh := make(chan firmware.FirmwareProgress, 16) // UpgradeFirmware 在 goroutine 跑、sendCommand 會卡在 stdout.Scan 等 bridge 回應 upgradeCtx, cancelUpgrade := context.WithCancel(context.Background()) t.Cleanup(cancelUpgrade) upgradeDone := make(chan error, 1) go func() { upgradeDone <- d.UpgradeFirmware(upgradeCtx, "KL720", progressCh) }() // 等一下讓 sendCommand goroutine 進入 stdout.Scan blocking time.Sleep(50 * time.Millisecond) // Info 必須能立即回(不被 sendCommand 持鎖卡住) infoDone := make(chan driver.DeviceInfo, 1) go func() { infoDone <- d.Info() }() select { case info := <-infoDone: if info.ID != "dev-x" { t.Errorf("Info().ID = %q, want dev-x", info.ID) } case <-time.After(500 * time.Millisecond): t.Fatalf("Info() blocked during firmware upgrade — Major 1 deadlock not fixed") } // IsConnected 也不該被卡(即使 d.connected=false、它走 d.mu) icDone := make(chan bool, 1) go func() { icDone <- d.IsConnected() }() select { case <-icDone: case <-time.After(500 * time.Millisecond): t.Fatalf("IsConnected() blocked during firmware upgrade") } // 收尾:cancel ctx、讓 UpgradeFirmware 走 timeout 路徑(stopPython 殺 bridge) cancelUpgrade() select { case <-upgradeDone: case <-time.After(2 * time.Second): t.Fatalf("UpgradeFirmware did not return after ctx cancel") } } // TestUpgradeFirmware_CtxCancelReleasesBridge(Minor 2 驗證) // // ctx cancel 後 UpgradeFirmware 應在合理時間內 return;不能因 sendCommand // goroutine 持鎖造成 d.mu.Lock(stopPython) deadlock。 func TestUpgradeFirmware_CtxCancelReleasesBridge(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "dev-x", Type: "KL520", Port: "USB"}, "/dev/null") setupFakeBridge(t, d) progressCh := make(chan firmware.FirmwareProgress, 16) ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) go func() { errCh <- d.UpgradeFirmware(ctx, "KL520", progressCh) }() // 等 sendCommand 進 stdout.Scan time.Sleep(50 * time.Millisecond) // cancel ctx、UpgradeFirmware 應走 ctx.Done branch、stopPython 殺 bridge、return cancel() select { case err := <-errCh: if err == nil { t.Errorf("expected ctx-cancel error, got nil") } case <-time.After(2 * time.Second): t.Fatalf("UpgradeFirmware did not return within 2s after ctx cancel (deadlock?)") } // timeout event 應該被推到 progressCh select { case ev := <-progressCh: if ev.Stage != firmware.StageError { t.Errorf("first event.Stage = %q, want error", ev.Stage) } if ev.Reason != firmware.ReasonTimeout { t.Errorf("first event.Reason = %q, want timeout", ev.Reason) } case <-time.After(500 * time.Millisecond): t.Fatalf("no timeout event pushed after ctx cancel") } } // TestUpgradeFirmware_StderrEventAfterCtxCancel(Major 2 驗證) // // 模擬 stderr goroutine 在 service 已 close intermediate channel 後仍嘗試 // route event;fwMu + setFirmwareProgressCh(nil) 應保證不會 send on closed // channel panic。 func TestUpgradeFirmware_StderrEventAfterCtxCancel(t *testing.T) { d := NewKneronDriver(driver.DeviceInfo{ID: "dev-x", Type: "KL520"}, "/dev/null") ch := make(chan firmware.FirmwareProgress, 4) d.setFirmwareProgressCh(ch) // 模擬 service 端流程:unregister → close channel // 為了驗證 race window、用很多 goroutine 同時做 tryRouteFirmwareEvent var wg sync.WaitGroup const N = 100 // 一組 goroutine 不斷試 route(模擬 stderr 上來的 inflight events) stopRoute := make(chan struct{}) for i := 0; i < 8; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-stopRoute: return default: _ = d.tryRouteFirmwareEvent(`{"event":"firmware_progress","stage":"flashing","percent":50}`) } } }() } // 主流程:unregister → close for i := 0; i < N; i++ { // 暫停一下讓 route goroutine 抓到 ch time.Sleep(10 * time.Microsecond) d.setFirmwareProgressCh(nil) // 必須 happen-before close close(ch) // 換新 ch、再來一輪 ch = make(chan firmware.FirmwareProgress, 4) d.setFirmwareProgressCh(ch) } // 收工 close(stopRoute) wg.Wait() // 沒 panic 就算過 }