A 階段第二個 milestone、銜接 M9-1 bridge.py、暴露 service layer 給 M9-3 API/WebSocket。 New module `server/internal/firmware/`: - types.go: 123 行(FirmwareVersion / FirmwareProgress / ActiveTaskInfo / UpgradeDriver interface / 8 reason const) - progress.go: 147 行(仿 flash pattern 的 Tracker、Task.cancel 預留 SIGTERM force-cancel godoc) - service.go: 373 行(核心 service:UpgradeFirmware / HasActiveTask / GetActiveTaskInfo / RequestShutdown / WaitForActiveTasks / ListBundledVersions / GetCurrentVersion) - service_test.go: 676 行、13 個 test 含 MultiDeviceParallel Driver layer: - kl720_driver.go: 697 → 1054 行(+357、新 UpgradeFirmware method + tryRouteFirmwareEvent + sendCommandForUpgrade snapshot pattern) - kl720_driver_test.go: 360 行、11 個 test(含 InfoNotBlockedDuringUpgrade / CtxCancelReleasesBridge / StderrEventAfterCtxCancel 100 round stress) 關鍵設計: - flash 與 firmware 模組分離(不 import flash) - UpgradeDriver interface 隔離 driver 細節、DeviceLookup interface 隔離 device manager - 中介 channel pattern(service ↔ driver)方便 service 補欄位(DeviceID / Direction / BeforeVersion) - timeout 雙保險:chip timeout + 30s margin - 8 reason enum 對齊 bridge.py、stage 採 Design 命名 Concurrency race 修復(M9-2 Reviewer round 1 → round 2): - Major 1(mutex deadlock):新 fwUpgradeMu 獨立鎖 + sendCommandForUpgrade snapshot stdin/stdout pattern、避開 d.mu field-level race + 升級期間 Info/Disconnect 不被卡 + timeout 路徑無死鎖 - Major 2(close-channel race):tryRouteFirmwareEvent 持 fwMu 整段、配合 defer setFirmwareProgressCh(nil) 提供 happen-before、絕無 send on closed channel panic Reviewer 兩輪審查: - Round 1: 0 Critical / 2 Major / 5 Minor / 5 Suggestion - Round 2: 0 Critical / 0 Major / 2 Minor / 2 Suggestion(11/12 issue 修到位、Suggestion 4 留 follow-up) M9-1 follow-up 順手清: - m5(test 死碼 _firmware_upgrade_start_ts 殘留兩行)已清 - s5(test 註解 idempotent shape 說明)已加 測試: - go test ./... -race -count=1: 全綠(28s、無 regression) - Python: 36 tests + 22 subtests 全綠(0.31s) - go vet / build: 0 output 下一步:M9-3 API handler + WebSocket progress(CI 建議 `go test -race -count=3` 提升 race 偵測強度) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
677 lines
24 KiB
Go
677 lines
24 KiB
Go
package firmware
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"visiona-local/server/internal/driver"
|
||
)
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// Mocks
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
// fakeDriver 是 UpgradeDriver 的測試實作、可程式化 progress 序列與終態 error。
|
||
type fakeDriver struct {
|
||
info driver.DeviceInfo
|
||
|
||
// scripted progress events:runUpgrade 收到 ctx.Done 之前會依序 push
|
||
events []FirmwareProgress
|
||
// 每個 event 之間的 delay
|
||
delay time.Duration
|
||
// 最終回傳 error
|
||
finalErr error
|
||
|
||
// 觀察:實際被呼叫的 chip / call count
|
||
mu sync.Mutex
|
||
calledChip string
|
||
callCount int
|
||
gotCtxDone bool
|
||
}
|
||
|
||
func (f *fakeDriver) Info() driver.DeviceInfo { return f.info }
|
||
|
||
func (f *fakeDriver) UpgradeFirmware(ctx context.Context, chip string, progressCh chan<- FirmwareProgress) error {
|
||
f.mu.Lock()
|
||
f.calledChip = chip
|
||
f.callCount++
|
||
f.mu.Unlock()
|
||
|
||
for _, ev := range f.events {
|
||
select {
|
||
case <-ctx.Done():
|
||
f.mu.Lock()
|
||
f.gotCtxDone = true
|
||
f.mu.Unlock()
|
||
return ctx.Err()
|
||
case <-time.After(f.delay):
|
||
}
|
||
select {
|
||
case progressCh <- ev:
|
||
case <-ctx.Done():
|
||
f.mu.Lock()
|
||
f.gotCtxDone = true
|
||
f.mu.Unlock()
|
||
return ctx.Err()
|
||
}
|
||
}
|
||
return f.finalErr
|
||
}
|
||
|
||
// fakeLookup 將 deviceID 映射到 fakeDriver。
|
||
type fakeLookup struct {
|
||
drivers map[string]UpgradeDriver
|
||
}
|
||
|
||
func (l *fakeLookup) GetUpgradeDriver(deviceID string) (UpgradeDriver, error) {
|
||
d, ok := l.drivers[deviceID]
|
||
if !ok {
|
||
return nil, errors.New("no such device")
|
||
}
|
||
return d, nil
|
||
}
|
||
|
||
func newServiceWith(drv UpgradeDriver, deviceID string) *Service {
|
||
return NewService(
|
||
&fakeLookup{drivers: map[string]UpgradeDriver{deviceID: drv}},
|
||
FirmwareDir{Root: "/tmp/test-firmware"},
|
||
)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 1. 5-stage 成功路徑:events 都被 forward、driver 端 chip 收到、終態 done
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_SuccessFiveStages(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-1", Name: "KL520-A", FirmwareVer: "KDP"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5, Message: "scan"},
|
||
{Stage: StageLoading, Percent: 20, Message: "loader"},
|
||
{Stage: StageFlashing, Percent: 50, Message: "kdp2"},
|
||
{Stage: StageVerifying, Percent: 90, Message: "verify"},
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
},
|
||
delay: 1 * time.Millisecond,
|
||
}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
taskID, ch, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware error: %v", err)
|
||
}
|
||
if taskID == "" {
|
||
t.Fatalf("expected non-empty taskID")
|
||
}
|
||
|
||
got := drainProgress(t, ch, 5*time.Second)
|
||
|
||
if len(got) != 5 {
|
||
t.Fatalf("expected 5 events, got %d: %+v", len(got), got)
|
||
}
|
||
wantStages := []string{StagePreparing, StageLoading, StageFlashing, StageVerifying, StageDone}
|
||
for i, ev := range got {
|
||
if ev.Stage != wantStages[i] {
|
||
t.Errorf("ev[%d].Stage = %q, want %q", i, ev.Stage, wantStages[i])
|
||
}
|
||
if ev.DeviceID != "dev-1" {
|
||
t.Errorf("ev[%d].DeviceID = %q, want dev-1", i, ev.DeviceID)
|
||
}
|
||
if ev.Direction != DirectionUpgrade {
|
||
t.Errorf("ev[%d].Direction = %q, want upgrade", i, ev.Direction)
|
||
}
|
||
if ev.BeforeVersion != "KDP" {
|
||
t.Errorf("ev[%d].BeforeVersion = %q, want KDP", i, ev.BeforeVersion)
|
||
}
|
||
}
|
||
|
||
// driver 收到 chip
|
||
if drv.calledChip != ChipKL520 {
|
||
t.Errorf("driver chip = %q, want KL520", drv.calledChip)
|
||
}
|
||
if drv.callCount != 1 {
|
||
t.Errorf("driver callCount = %d, want 1", drv.callCount)
|
||
}
|
||
|
||
// task 已 done(tracker 不再 ActiveTasks 列出)
|
||
svc.WaitForActiveTasks(2 * time.Second)
|
||
if svc.HasActiveTask() {
|
||
t.Errorf("HasActiveTask still true after done")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 2. 失敗路徑:各種 reason 都正確 forward
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_FailureReasons(t *testing.T) {
|
||
failureCases := []struct {
|
||
name string
|
||
reason string
|
||
stage string
|
||
}{
|
||
{"scan_not_found", ReasonScanNotFound, StagePreparing},
|
||
{"connect_failed", ReasonConnectFailed, StagePreparing},
|
||
{"loader_write_failed", ReasonLoaderWriteFailed, StageLoading},
|
||
{"upgrade_mid_failed", ReasonUpgradeMidFailed, StageFlashing},
|
||
{"verify_mismatch", ReasonVerifyMismatch, StageVerifying},
|
||
{"verify_not_found", ReasonVerifyNotFound, StageVerifying},
|
||
{"disconnect_during_op", ReasonDisconnectDuringOp, StageFlashing},
|
||
}
|
||
|
||
for _, tc := range failureCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-x", FirmwareVer: "KDP"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{
|
||
Stage: StageError,
|
||
Percent: -1,
|
||
Error: "simulated " + tc.reason,
|
||
Reason: tc.reason,
|
||
RawError: "raw " + tc.reason,
|
||
},
|
||
},
|
||
finalErr: errors.New("bridge failed: " + tc.reason),
|
||
delay: 1 * time.Millisecond,
|
||
}
|
||
svc := newServiceWith(drv, "dev-x")
|
||
_, ch, err := svc.UpgradeFirmware(context.Background(), "dev-x", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware setup error: %v", err)
|
||
}
|
||
got := drainProgress(t, ch, 5*time.Second)
|
||
|
||
// 找到 error event
|
||
var errEv *FirmwareProgress
|
||
for i := range got {
|
||
if got[i].Stage == StageError {
|
||
errEv = &got[i]
|
||
break
|
||
}
|
||
}
|
||
if errEv == nil {
|
||
t.Fatalf("no error event in %+v", got)
|
||
}
|
||
if errEv.Reason != tc.reason {
|
||
t.Errorf("Reason = %q, want %q", errEv.Reason, tc.reason)
|
||
}
|
||
if errEv.Percent != -1 {
|
||
t.Errorf("Percent = %d, want -1", errEv.Percent)
|
||
}
|
||
if errEv.DeviceID != "dev-x" {
|
||
t.Errorf("DeviceID 應被補上、got %q", errEv.DeviceID)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 3. Timeout:service 端外層 context 超時、driver 收到 ctx.Done
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_ContextTimeout(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-slow"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageLoading, Percent: 20},
|
||
},
|
||
delay: 200 * time.Millisecond, // 每個 event 200ms,2 個 events ≈ 400ms
|
||
}
|
||
svc := newServiceWith(drv, "dev-slow")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||
defer cancel()
|
||
|
||
_, ch, err := svc.UpgradeFirmware(ctx, "dev-slow", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware setup error: %v", err)
|
||
}
|
||
got := drainProgress(t, ch, 3*time.Second)
|
||
|
||
// 應該有一個 timeout error event
|
||
var sawTimeout bool
|
||
for _, ev := range got {
|
||
if ev.Stage == StageError && ev.Reason == ReasonTimeout {
|
||
sawTimeout = true
|
||
break
|
||
}
|
||
}
|
||
if !sawTimeout {
|
||
t.Errorf("expected timeout error event in %+v", got)
|
||
}
|
||
|
||
// driver 應該觀察到 ctx.Done
|
||
drv.mu.Lock()
|
||
defer drv.mu.Unlock()
|
||
if !drv.gotCtxDone {
|
||
t.Errorf("driver should have observed ctx.Done")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 4. Mutex:同 device 同時兩個 upgrade → 第二個拒絕(FW_DEVICE_BUSY)
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_DeviceBusy(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-1"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
},
|
||
delay: 100 * time.Millisecond, // 給時間讓第一個還沒做完時第二個就來
|
||
}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
// 第一個 task
|
||
_, ch1, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("first UpgradeFirmware error: %v", err)
|
||
}
|
||
|
||
// 第二個 task 應該被拒絕(device busy)
|
||
_, _, err2 := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err2 == nil {
|
||
t.Fatalf("second UpgradeFirmware should fail with ErrDeviceBusy")
|
||
}
|
||
if !errors.Is(err2, ErrDeviceBusy) {
|
||
t.Errorf("second error = %v, want ErrDeviceBusy", err2)
|
||
}
|
||
|
||
// 等第一個 task 結束、確認 channel 收到 done
|
||
got := drainProgress(t, ch1, 5*time.Second)
|
||
if len(got) == 0 {
|
||
t.Fatalf("first task got no events")
|
||
}
|
||
if got[len(got)-1].Stage != StageDone {
|
||
t.Errorf("last event stage = %q, want done", got[len(got)-1].Stage)
|
||
}
|
||
|
||
// 第一個結束後、可以再 start 一個(同 device)
|
||
svc.WaitForActiveTasks(2 * time.Second)
|
||
svc.CleanupTask("dev-1")
|
||
_, _, err3 := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err3 != nil {
|
||
t.Errorf("third UpgradeFirmware after cleanup should succeed, got: %v", err3)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 5. HasActiveTask / GetActiveTaskInfo
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestHasActiveTask_AndActiveTaskInfo(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-1", Name: "KL720-Z", FirmwareVer: "KDP"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageLoading, Percent: 20},
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
},
|
||
delay: 80 * time.Millisecond,
|
||
}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
if svc.HasActiveTask() {
|
||
t.Fatalf("HasActiveTask should be false initially")
|
||
}
|
||
if len(svc.GetActiveTaskInfo()) != 0 {
|
||
t.Fatalf("GetActiveTaskInfo should be empty initially")
|
||
}
|
||
|
||
_, ch, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL720)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware error: %v", err)
|
||
}
|
||
|
||
// 等 progress 推一個出來、確保 task 已在 active 狀態
|
||
select {
|
||
case <-ch:
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatalf("no progress received")
|
||
}
|
||
|
||
if !svc.HasActiveTask() {
|
||
t.Errorf("HasActiveTask should be true while upgrading")
|
||
}
|
||
infos := svc.GetActiveTaskInfo()
|
||
if len(infos) != 1 {
|
||
t.Fatalf("ActiveTaskInfo len = %d, want 1", len(infos))
|
||
}
|
||
info := infos[0]
|
||
if info.DeviceID != "dev-1" {
|
||
t.Errorf("DeviceID = %q, want dev-1", info.DeviceID)
|
||
}
|
||
if info.DeviceName != "KL720-Z" {
|
||
t.Errorf("DeviceName = %q, want KL720-Z", info.DeviceName)
|
||
}
|
||
if info.Chip != ChipKL720 {
|
||
t.Errorf("Chip = %q, want KL720", info.Chip)
|
||
}
|
||
if info.Direction != DirectionUpgrade {
|
||
t.Errorf("Direction = %q, want upgrade", info.Direction)
|
||
}
|
||
if info.EtaSeconds <= 0 || info.EtaSeconds > 200 {
|
||
t.Errorf("EtaSeconds = %d, want 0 < n <= 200", info.EtaSeconds)
|
||
}
|
||
|
||
// drain rest
|
||
drainProgress(t, ch, 5*time.Second)
|
||
svc.WaitForActiveTasks(2 * time.Second)
|
||
if svc.HasActiveTask() {
|
||
t.Errorf("HasActiveTask should be false after task done")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 6. UnsupportedChip:KL630 / KL730 / 亂值 → ErrUnsupportedChip
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_UnsupportedChip(t *testing.T) {
|
||
drv := &fakeDriver{info: driver.DeviceInfo{ID: "dev-1"}}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
for _, chip := range []string{"KL630", "KL730", "", "kl520", "garbage"} {
|
||
_, _, err := svc.UpgradeFirmware(context.Background(), "dev-1", chip)
|
||
if !errors.Is(err, ErrUnsupportedChip) {
|
||
t.Errorf("chip=%q: err=%v, want ErrUnsupportedChip", chip, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 7. DeviceNotFound
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_DeviceNotFound(t *testing.T) {
|
||
svc := NewService(&fakeLookup{drivers: map[string]UpgradeDriver{}}, FirmwareDir{Root: "/tmp"})
|
||
_, _, err := svc.UpgradeFirmware(context.Background(), "ghost", ChipKL520)
|
||
if !errors.Is(err, ErrDeviceNotFound) {
|
||
t.Errorf("err=%v, want ErrDeviceNotFound", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 8. RequestShutdown:shutdown 後新 task 被拒
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_ShutdownRejection(t *testing.T) {
|
||
drv := &fakeDriver{info: driver.DeviceInfo{ID: "dev-1"}}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
svc.RequestShutdown()
|
||
|
||
_, _, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if !errors.Is(err, ErrDeviceBusy) {
|
||
t.Errorf("err=%v, want ErrDeviceBusy after shutdown", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 9. WaitForActiveTasks:有 active 時 wait 會回 false(timeout)
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestWaitForActiveTasks_TimeoutWhenBusy(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-1"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageDone, Percent: 100},
|
||
},
|
||
delay: 300 * time.Millisecond,
|
||
}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
_, ch, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware error: %v", err)
|
||
}
|
||
|
||
clean := svc.WaitForActiveTasks(50 * time.Millisecond)
|
||
if clean {
|
||
t.Errorf("expected timeout (false), got clean (true)")
|
||
}
|
||
|
||
// drain rest 確保不 leak
|
||
drainProgress(t, ch, 5*time.Second)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 10. ListBundledVersions / GetCurrentVersion 基本行為
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestListBundledVersions(t *testing.T) {
|
||
// 建 temp firmware 結構(Suggestion 3 後、ListBundledVersions 會 os.Stat chipDir)
|
||
tmpRoot := t.TempDir()
|
||
if err := os.MkdirAll(filepath.Join(tmpRoot, ChipKL520), 0o755); err != nil {
|
||
t.Fatalf("setup mkdir: %v", err)
|
||
}
|
||
|
||
svc := NewService(&fakeLookup{drivers: map[string]UpgradeDriver{}}, FirmwareDir{Root: tmpRoot})
|
||
|
||
versions, err := svc.ListBundledVersions(ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("ListBundledVersions error: %v", err)
|
||
}
|
||
if len(versions) != 1 {
|
||
t.Fatalf("versions len = %d, want 1 (A 階段扁平)", len(versions))
|
||
}
|
||
if !versions[0].IsCurrent || !versions[0].IsBundled {
|
||
t.Errorf("expected IsCurrent + IsBundled, got %+v", versions[0])
|
||
}
|
||
|
||
// 不支援 chip
|
||
_, err = svc.ListBundledVersions("KL630")
|
||
if !errors.Is(err, ErrUnsupportedChip) {
|
||
t.Errorf("KL630 error = %v, want ErrUnsupportedChip", err)
|
||
}
|
||
|
||
// 支援的 chip 但 firmware 漏 build(Suggestion 3 行為)
|
||
_, err = svc.ListBundledVersions(ChipKL720)
|
||
if err == nil {
|
||
t.Errorf("expected error when chip dir missing")
|
||
}
|
||
|
||
// 沒設 fwDir
|
||
svc2 := NewService(&fakeLookup{}, FirmwareDir{})
|
||
_, err = svc2.ListBundledVersions(ChipKL520)
|
||
if err == nil {
|
||
t.Errorf("expected error when fwDir.Root is empty")
|
||
}
|
||
}
|
||
|
||
func TestGetCurrentVersion(t *testing.T) {
|
||
drv := &fakeDriver{info: driver.DeviceInfo{ID: "dev-1", FirmwareVer: "v2.2.0"}}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
ver, err := svc.GetCurrentVersion("dev-1")
|
||
if err != nil {
|
||
t.Fatalf("GetCurrentVersion error: %v", err)
|
||
}
|
||
if ver.Version != "v2.2.0" {
|
||
t.Errorf("Version = %q, want v2.2.0", ver.Version)
|
||
}
|
||
|
||
// not found
|
||
_, err = svc.GetCurrentVersion("ghost")
|
||
if !errors.Is(err, ErrDeviceNotFound) {
|
||
t.Errorf("err = %v, want ErrDeviceNotFound", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 11. Multi-device 平行 upgrade(Minor 2 + Suggestion 2)
|
||
//
|
||
// 3 個 device 同時升級、確認:
|
||
// - tracker key 是 deviceID、不同 device 互不影響(不誤匹配)
|
||
// - 每個 task 的 ProgressCh 收到自己的 events、DeviceID 對得到
|
||
// - 3 個 task 都能完成、HasActiveTask 在最後回 false
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_MultiDeviceParallel(t *testing.T) {
|
||
mkDrv := func(id string) *fakeDriver {
|
||
return &fakeDriver{
|
||
info: driver.DeviceInfo{ID: id, Name: id + "-name", FirmwareVer: "KDP"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageFlashing, Percent: 50},
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
},
|
||
delay: 20 * time.Millisecond,
|
||
}
|
||
}
|
||
drvA := mkDrv("dev-A")
|
||
drvB := mkDrv("dev-B")
|
||
drvC := mkDrv("dev-C")
|
||
|
||
svc := NewService(
|
||
&fakeLookup{drivers: map[string]UpgradeDriver{
|
||
"dev-A": drvA,
|
||
"dev-B": drvB,
|
||
"dev-C": drvC,
|
||
}},
|
||
FirmwareDir{Root: "/tmp"},
|
||
)
|
||
|
||
type startResult struct {
|
||
id string
|
||
taskID string
|
||
ch <-chan FirmwareProgress
|
||
err error
|
||
}
|
||
|
||
startCh := make(chan startResult, 3)
|
||
for _, id := range []string{"dev-A", "dev-B", "dev-C"} {
|
||
go func(deviceID string) {
|
||
tid, ch, err := svc.UpgradeFirmware(context.Background(), deviceID, ChipKL520)
|
||
startCh <- startResult{id: deviceID, taskID: tid, ch: ch, err: err}
|
||
}(id)
|
||
}
|
||
|
||
// 收集 3 個 start 結果、組成 deviceID → progressCh map
|
||
results := make(map[string]startResult, 3)
|
||
for i := 0; i < 3; i++ {
|
||
select {
|
||
case r := <-startCh:
|
||
if r.err != nil {
|
||
t.Fatalf("%s start err: %v", r.id, r.err)
|
||
}
|
||
results[r.id] = r
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatalf("timeout waiting for 3 starts; got %d", i)
|
||
}
|
||
}
|
||
|
||
// 各自 drain、驗證 events 的 DeviceID 對得到、且收到 done
|
||
var wg sync.WaitGroup
|
||
for id, r := range results {
|
||
wg.Add(1)
|
||
go func(id string, ch <-chan FirmwareProgress) {
|
||
defer wg.Done()
|
||
evs := drainProgress(t, ch, 5*time.Second)
|
||
if len(evs) == 0 {
|
||
t.Errorf("%s: no events", id)
|
||
return
|
||
}
|
||
// 每個 event 的 DeviceID 必須 = id(不能誤匹配到別的 device)
|
||
for i, ev := range evs {
|
||
if ev.DeviceID != id {
|
||
t.Errorf("%s ev[%d].DeviceID = %q, want %q", id, i, ev.DeviceID, id)
|
||
}
|
||
}
|
||
// 最後 event 必為 done
|
||
if last := evs[len(evs)-1]; last.Stage != StageDone {
|
||
t.Errorf("%s last stage = %q, want done", id, last.Stage)
|
||
}
|
||
}(id, r.ch)
|
||
}
|
||
wg.Wait()
|
||
|
||
// 全部 done 後 HasActiveTask 必為 false
|
||
svc.WaitForActiveTasks(3 * time.Second)
|
||
if svc.HasActiveTask() {
|
||
t.Errorf("HasActiveTask still true after all 3 tasks done")
|
||
}
|
||
|
||
// 每個 driver 都剛好被呼叫一次(沒誤匹配到別的 deviceID)
|
||
for _, d := range []*fakeDriver{drvA, drvB, drvC} {
|
||
d.mu.Lock()
|
||
if d.callCount != 1 {
|
||
t.Errorf("driver(%s) callCount = %d, want 1", d.info.ID, d.callCount)
|
||
}
|
||
if d.calledChip != ChipKL520 {
|
||
t.Errorf("driver(%s) chip = %q, want KL520", d.info.ID, d.calledChip)
|
||
}
|
||
d.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 12. Suggestion 1:StageDone 去重
|
||
//
|
||
// driver 端送雙保險 done(stderr push + sendCommand 補一發)、service 端
|
||
// forward 應只 forward 一次 done 給 caller。
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func TestUpgradeFirmware_DedupeDoneEvent(t *testing.T) {
|
||
drv := &fakeDriver{
|
||
info: driver.DeviceInfo{ID: "dev-1", FirmwareVer: "KDP"},
|
||
events: []FirmwareProgress{
|
||
{Stage: StagePreparing, Percent: 5},
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
// 第二次 done(模擬 stderr + sendCommand 雙保險)
|
||
{Stage: StageDone, Percent: 100, AfterVersion: "2.2.0"},
|
||
},
|
||
delay: 1 * time.Millisecond,
|
||
}
|
||
svc := newServiceWith(drv, "dev-1")
|
||
|
||
_, ch, err := svc.UpgradeFirmware(context.Background(), "dev-1", ChipKL520)
|
||
if err != nil {
|
||
t.Fatalf("UpgradeFirmware error: %v", err)
|
||
}
|
||
got := drainProgress(t, ch, 5*time.Second)
|
||
|
||
// 應該只看到一個 done(去重後)
|
||
doneCount := 0
|
||
for _, ev := range got {
|
||
if ev.Stage == StageDone {
|
||
doneCount++
|
||
}
|
||
}
|
||
if doneCount != 1 {
|
||
t.Errorf("doneCount = %d, want 1 (deduped); events: %+v", doneCount, got)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
// 13. drainProgress helper
|
||
// ──────────────────────────────────────────────────────────────────────
|
||
|
||
func drainProgress(t *testing.T, ch <-chan FirmwareProgress, maxWait time.Duration) []FirmwareProgress {
|
||
t.Helper()
|
||
var out []FirmwareProgress
|
||
timer := time.NewTimer(maxWait)
|
||
defer timer.Stop()
|
||
for {
|
||
select {
|
||
case ev, ok := <-ch:
|
||
if !ok {
|
||
return out
|
||
}
|
||
out = append(out, ev)
|
||
case <-timer.C:
|
||
t.Fatalf("drainProgress timeout after %v, got %d events: %+v", maxWait, len(out), out)
|
||
return out
|
||
}
|
||
}
|
||
}
|