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 } } }