package camera import ( "strings" "testing" ) // TestBuildCaptureArgsForOS 驗證三平台各自用正確的 ffmpeg input device(indev): // macOS→avfoundation、Windows→dshow、Linux→v4l2。這是 ADR-020 的核心修復: // 原本 Linux 會誤落 default 分支用 avfoundation → 必壞。 func TestBuildCaptureArgsForOS(t *testing.T) { const ( width = 640 height = 480 fps = 30 index = 0 ) tests := []struct { name string goos string cameraName string // wantInputFlag 是預期的 "-f " 值 wantIndev string // wantInputArg 是 "-i" 後面的值 wantInputArg string }{ { name: "macOS uses avfoundation with index:none", goos: "darwin", wantIndev: "avfoundation", wantInputArg: "0:none", }, { name: "Windows uses dshow with video=name", goos: "windows", cameraName: "Integrated Camera", wantIndev: "dshow", wantInputArg: "video=Integrated Camera", }, { name: "Linux uses v4l2 with /dev/video path", goos: "linux", wantIndev: "v4l2", wantInputArg: "/dev/video0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { args := buildCaptureArgsForOS(tt.goos, index, tt.cameraName, width, height, fps) gotIndev := valueAfterFlag(args, "-f") // 第一個 -f 是 input format if gotIndev != tt.wantIndev { t.Errorf("indev = %q, want %q\nargs: %v", gotIndev, tt.wantIndev, args) } gotInput := valueAfterFlag(args, "-i") if gotInput != tt.wantInputArg { t.Errorf("input = %q, want %q\nargs: %v", gotInput, tt.wantInputArg, args) } // 三平台後段皆須為 image2pipe / mjpeg,MJPEG pipe 架構共用。 if !containsSeq(args, "-f", "image2pipe") { t.Errorf("missing image2pipe output, args: %v", args) } if !containsSeq(args, "-vcodec", "mjpeg") { t.Errorf("missing mjpeg vcodec, args: %v", args) } if args[len(args)-1] != "-" { t.Errorf("last arg should be stdout '-', got %q", args[len(args)-1]) } }) } } // TestBuildCaptureArgsForOS_LinuxIndexToDevicePath 驗證 Linux 的 cameraIndex 正確 // 對應到 /dev/video 節點路徑。 func TestBuildCaptureArgsForOS_LinuxIndexToDevicePath(t *testing.T) { for _, idx := range []int{0, 1, 2, 10} { args := buildCaptureArgsForOS("linux", idx, "", 640, 480, 30) want := "/dev/video" + itoa(idx) if got := valueAfterFlag(args, "-i"); got != want { t.Errorf("index %d → input %q, want %q", idx, got, want) } } } // TestBuildCaptureArgsForOS_LinuxNotAVFoundation 是 ADR-020 的回歸鎖:Linux 絕不能 // 用 avfoundation(macOS 專用)。若未來有人把 Linux case 拿掉、讓它落回 default, // 這個測試會抓到。 func TestBuildCaptureArgsForOS_LinuxNotAVFoundation(t *testing.T) { args := buildCaptureArgsForOS("linux", 0, "", 640, 480, 30) joined := strings.Join(args, " ") if strings.Contains(joined, "avfoundation") { t.Fatalf("Linux must NOT use avfoundation (macOS-only), args: %v", args) } if !strings.Contains(joined, "v4l2") { t.Fatalf("Linux must use v4l2, args: %v", args) } } // TestParseV4L2Devices 驗證 /dev/video* 路徑清單解析成 CameraInfo,且依 index 數值 // 排序(video10 排在 video2 之後、而非字典序)。 func TestParseV4L2Devices(t *testing.T) { paths := []string{ "/dev/video10", "/dev/video2", "/dev/video0", "/dev/video-not-a-number", // 應被略過 "/dev/videoX", // 應被略過 } got := parseV4L2Devices(paths) wantIndexes := []int{0, 2, 10} if len(got) != len(wantIndexes) { t.Fatalf("got %d devices, want %d: %+v", len(got), len(wantIndexes), got) } for i, want := range wantIndexes { if got[i].Index != want { t.Errorf("device[%d].Index = %d, want %d", i, got[i].Index, want) } if got[i].Name != "/dev/video"+itoa(want) { t.Errorf("device[%d].Name = %q, want /dev/video%d", i, got[i].Name, want) } } } // TestParseV4L2Devices_Empty 驗證無裝置時回 nil/空。 func TestParseV4L2Devices_Empty(t *testing.T) { if got := parseV4L2Devices(nil); len(got) != 0 { t.Errorf("expected no devices, got %+v", got) } } // TestV4L2Index 驗證從路徑取 index。 func TestV4L2Index(t *testing.T) { cases := map[string]int{ "/dev/video0": 0, "/dev/video12": 12, "/dev/video": -1, "/dev/videoab": -1, "videoX": -1, } for path, want := range cases { if got := v4l2Index(path); got != want { t.Errorf("v4l2Index(%q) = %d, want %d", path, got, want) } } } // --- helpers --- // valueAfterFlag 回傳 args 中第一個等於 flag 的元素的下一個值。 func valueAfterFlag(args []string, flag string) string { for i := 0; i < len(args)-1; i++ { if args[i] == flag { return args[i+1] } } return "" } // containsSeq 判斷 args 是否包含連續的 a b 兩個元素。 func containsSeq(args []string, a, b string) bool { for i := 0; i < len(args)-1; i++ { if args[i] == a && args[i+1] == b { return true } } return false } func itoa(n int) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var buf []byte for n > 0 { buf = append([]byte{byte('0' + n%10)}, buf...) n /= 10 } if neg { buf = append([]byte{'-'}, buf...) } return string(buf) }