package camera import ( "os/exec" "strings" "testing" "time" ) // 這些測試用 shell 腳本假扮 ffmpeg,驗證 WaitForFirstFrame 的三種結束路徑, // 不依賴真實攝影機(CI / 無 camera 環境也能跑)。 // TestWaitForFirstFrame_EarlyExit 模擬 ffmpeg 啟動即失敗(如 avfoundation 權限被拒): // 進程 fork 成功但隨即 exit、不產出任何 frame。WaitForFirstFrame 應回錯誤, // 且錯誤訊息帶上 stderr 尾端(診斷用)。 func TestWaitForFirstFrame_EarlyExit(t *testing.T) { // 寫一行錯誤到 stderr 後立刻 exit 1,stdout 沒有任何 JPEG。 cmd := exec.Command("sh", "-c", "echo 'avfoundation: not authorized to capture video' 1>&2; exit 1") cam, err := newFFmpegCameraFromCmd(cmd) if err != nil { t.Fatalf("newFFmpegCameraFromCmd failed: %v", err) } defer cam.Close() if err := cam.WaitForFirstFrame(3 * time.Second); err == nil { t.Fatal("expected error when ffmpeg exits before producing a frame, got nil") } else if !strings.Contains(err.Error(), "not authorized") { t.Errorf("expected stderr tail in error, got: %v", err) } } // TestWaitForFirstFrame_Timeout 模擬 ffmpeg 啟動後長時間不產出 frame(進程還活著, // 但 avfoundation 卡住 / 沒資料)。WaitForFirstFrame 應在 timeout 後回逾時錯誤。 func TestWaitForFirstFrame_Timeout(t *testing.T) { // 進程存活 10s、不輸出任何 JPEG 到 stdout。 cmd := exec.Command("sh", "-c", "sleep 10") cam, err := newFFmpegCameraFromCmd(cmd) if err != nil { t.Fatalf("newFFmpegCameraFromCmd failed: %v", err) } defer cam.Close() start := time.Now() if err := cam.WaitForFirstFrame(300 * time.Millisecond); err == nil { t.Fatal("expected timeout error when no frame is produced, got nil") } else if !strings.Contains(err.Error(), "timed out") { t.Errorf("expected timeout error, got: %v", err) } if elapsed := time.Since(start); elapsed > 3*time.Second { t.Errorf("WaitForFirstFrame took too long (%s), expected ~timeout", elapsed) } } // TestWaitForFirstFrame_Success 模擬 ffmpeg 正常產出一張最小 JPEG(SOI+EOI): // WaitForFirstFrame 應回 nil,且 ReadFrame 拿得到該 frame。 func TestWaitForFirstFrame_Success(t *testing.T) { // printf 出最小合法 JPEG 標記:FF D8 ... FF D9,然後 sleep 保持進程存活。 cmd := exec.Command("sh", "-c", `printf '\xFF\xD8\x00\xFF\xD9'; sleep 2`) cam, err := newFFmpegCameraFromCmd(cmd) if err != nil { t.Fatalf("newFFmpegCameraFromCmd failed: %v", err) } defer cam.Close() if err := cam.WaitForFirstFrame(3 * time.Second); err != nil { t.Fatalf("expected success when a frame is produced, got: %v", err) } frame, err := cam.ReadFrame() if err != nil { t.Fatalf("ReadFrame after first frame failed: %v", err) } if len(frame) < 4 || frame[0] != 0xFF || frame[1] != 0xD8 { t.Errorf("expected a JPEG frame starting with FFD8, got % x", frame) } } // TestRingBuffer_KeepsTail 驗證 ringBuffer 只保留尾端、不無界成長。 func TestRingBuffer_KeepsTail(t *testing.T) { rb := newRingBuffer(8) if _, err := rb.Write([]byte("0123456789ABCDEF")); err != nil { t.Fatalf("write failed: %v", err) } got := rb.String() if got != "9ABCDEF" && got != "89ABCDEF" { // TrimSpace 不影響此輸入;預期保留最後 8 bytes "9ABCDEF" 前含 '8' t.Logf("tail = %q", got) } if len(got) > 8 { t.Errorf("ringBuffer exceeded max: len=%d content=%q", len(got), got) } }