@@ -187,6 +187,26 @@ RGB readBMPPixel(const fs::path& bmpPath, int x, int y) {
187187 return pixel;
188188}
189189
190+ // Helper to create a solid color YUV I420 image
191+ void createSolidColorI420 (const fs::path& path, int width, int height, uint8_t y, uint8_t u, uint8_t v) {
192+ std::ofstream file (path, std::ios::binary);
193+ if (!file.is_open ()) {
194+ throw std::runtime_error (" Failed to create YUV file" );
195+ }
196+
197+ // Y plane
198+ std::vector<uint8_t > yPlane (width * height, y);
199+ file.write (reinterpret_cast <const char *>(yPlane.data ()), yPlane.size ());
200+
201+ // U plane
202+ std::vector<uint8_t > uPlane ((width / 2 ) * (height / 2 ), u);
203+ file.write (reinterpret_cast <const char *>(uPlane.data ()), uPlane.size ());
204+
205+ // V plane
206+ std::vector<uint8_t > vPlane ((width / 2 ) * (height / 2 ), v);
207+ file.write (reinterpret_cast <const char *>(vPlane.data ()), vPlane.size ());
208+ }
209+
190210// Test fixture for device-independent tests
191211class CCAPCLITest : public ::testing::Test {
192212protected:
@@ -264,6 +284,57 @@ TEST_F(CCAPCLITest, NoArgumentsShowsHelp) {
264284// EXPECT_NE(result.exitCode, 0);
265285// }
266286
287+ TEST_F (CCAPCLITest, VerboseOption) {
288+ // Test that verbose flag is accepted
289+ auto result = runCLI (" --verbose --help" );
290+ EXPECT_EQ (result.exitCode , 0 );
291+ EXPECT_THAT (result.output , ::testing::HasSubstr (" Usage:" ));
292+ }
293+
294+ TEST_F (CCAPCLITest, InvalidYUVConversion_MissingDimensions) {
295+ // Create a dummy YUV file
296+ fs::path yuvPath = testOutputDir / " test.yuv" ;
297+ createSolidColorNV12 (yuvPath, 64 , 64 , 128 , 128 , 128 );
298+
299+ // Try to convert without specifying dimensions (should fail)
300+ fs::path outputPath = testOutputDir / " output.bmp" ;
301+ std::string cmd = " --convert " + yuvPath.string () +
302+ " --yuv-format nv12" +
303+ " --convert-output " + outputPath.string ();
304+
305+ auto result = runCLI (cmd);
306+ // Should fail or handle gracefully
307+ EXPECT_NE (result.exitCode , 0 ) << " Should fail without YUV dimensions" ;
308+ }
309+
310+ TEST_F (CCAPCLITest, InvalidYUVConversion_MissingFormat) {
311+ // Create a dummy YUV file
312+ fs::path yuvPath = testOutputDir / " test.yuv" ;
313+ createSolidColorNV12 (yuvPath, 64 , 64 , 128 , 128 , 128 );
314+
315+ // Try to convert without specifying format (should fail)
316+ fs::path outputPath = testOutputDir / " output.bmp" ;
317+ std::string cmd = " --convert " + yuvPath.string () +
318+ " --yuv-width 64 --yuv-height 64" +
319+ " --convert-output " + outputPath.string ();
320+
321+ auto result = runCLI (cmd);
322+ // Should fail or handle gracefully
323+ EXPECT_NE (result.exitCode , 0 ) << " Should fail without YUV format" ;
324+ }
325+
326+ TEST_F (CCAPCLITest, InvalidYUVConversion_NonExistentFile) {
327+ // Try to convert a non-existent file
328+ fs::path yuvPath = testOutputDir / " nonexistent.yuv" ;
329+ fs::path outputPath = testOutputDir / " output.bmp" ;
330+ std::string cmd = " --convert " + yuvPath.string () +
331+ " --yuv-format nv12 --yuv-width 64 --yuv-height 64" +
332+ " --convert-output " + outputPath.string ();
333+
334+ auto result = runCLI (cmd);
335+ EXPECT_NE (result.exitCode , 0 ) << " Should fail for non-existent input file" ;
336+ }
337+
267338// ============================================================================
268339// Device-Dependent Tests (requires camera)
269340// ============================================================================
@@ -371,6 +442,105 @@ TEST_F(CCAPCLIDeviceTest, CaptureWithInternalFormat) {
371442 ASSERT_GT (imageCount, 0 ) << " No image files created" ;
372443}
373444
445+ TEST_F (CCAPCLIDeviceTest, CaptureWithOutputFormat) {
446+ std::string outputDir = testOutputDir.string ();
447+ auto result = runCLI (" -d 0 -c 1 --format rgb24 -o " + outputDir);
448+
449+ // Camera exists, capture MUST succeed
450+ ASSERT_EQ (result.exitCode , 0 ) << " Capture command failed: " << result.output ;
451+
452+ // Verify image file was created
453+ int imageCount = 0 ;
454+ for (const auto & entry : fs::directory_iterator (testOutputDir)) {
455+ if (entry.path ().extension () == " .bmp" ) {
456+ imageCount++;
457+ }
458+ }
459+
460+ ASSERT_EQ (imageCount, 1 ) << " Expected 1 image file, found " << imageCount;
461+ }
462+
463+ TEST_F (CCAPCLIDeviceTest, CaptureWithFPS) {
464+ std::string outputDir = testOutputDir.string ();
465+ // Test with different FPS (30 fps)
466+ auto result = runCLI (" -d 0 -f 30 -c 1 -o " + outputDir);
467+
468+ ASSERT_EQ (result.exitCode , 0 ) << " Capture command failed: " << result.output ;
469+
470+ // Verify image file was created
471+ int imageCount = 0 ;
472+ for (const auto & entry : fs::directory_iterator (testOutputDir)) {
473+ if (entry.path ().extension () == " .bmp" ) {
474+ imageCount++;
475+ }
476+ }
477+
478+ ASSERT_EQ (imageCount, 1 ) << " Expected 1 image file, found " << imageCount;
479+ }
480+
481+ TEST_F (CCAPCLIDeviceTest, CaptureWithTimeout) {
482+ std::string outputDir = testOutputDir.string ();
483+ // Test with short timeout (should still succeed for 1 frame)
484+ auto result = runCLI (" -d 0 -t 3000 -c 1 -o " + outputDir);
485+
486+ ASSERT_EQ (result.exitCode , 0 ) << " Capture command failed: " << result.output ;
487+
488+ // Verify image file was created
489+ int imageCount = 0 ;
490+ for (const auto & entry : fs::directory_iterator (testOutputDir)) {
491+ if (entry.path ().extension () == " .bmp" ) {
492+ imageCount++;
493+ }
494+ }
495+
496+ ASSERT_EQ (imageCount, 1 ) << " Expected 1 image file, found " << imageCount;
497+ }
498+
499+ TEST_F (CCAPCLIDeviceTest, CaptureInvalidDevice) {
500+ std::string outputDir = testOutputDir.string ();
501+ // Try to capture from device index 999 (should fail or fallback to default)
502+ auto result = runCLI (" -d 999 -c 1 -o " + outputDir);
503+
504+ // Some implementations may fallback to default device instead of failing
505+ // So we just check that it doesn't crash
506+ // If it succeeds, verify output exists
507+ if (result.exitCode == 0 ) {
508+ // Fallback to default device, verify output
509+ int imageCount = 0 ;
510+ for (const auto & entry : fs::directory_iterator (testOutputDir)) {
511+ if (entry.path ().extension () == " .bmp" ) {
512+ imageCount++;
513+ }
514+ }
515+ EXPECT_GT (imageCount, 0 ) << " If command succeeds, should create images" ;
516+ }
517+ // If it fails, that's also acceptable behavior
518+ }
519+
520+ TEST_F (CCAPCLIDeviceTest, CaptureWithVerbose) {
521+ std::string outputDir = testOutputDir.string ();
522+ auto result = runCLI (" --verbose -d 0 -c 1 -o " + outputDir);
523+
524+ ASSERT_EQ (result.exitCode , 0 ) << " Capture command failed: " << result.output ;
525+
526+ // Verify image file was created
527+ int imageCount = 0 ;
528+ for (const auto & entry : fs::directory_iterator (testOutputDir)) {
529+ if (entry.path ().extension () == " .bmp" ) {
530+ imageCount++;
531+ }
532+ }
533+
534+ ASSERT_EQ (imageCount, 1 ) << " Expected 1 image file, found " << imageCount;
535+ }
536+
537+ TEST_F (CCAPCLIDeviceTest, ShowDeviceInfoAll) {
538+ // Test showing info for all devices (-1 means all)
539+ auto result = runCLI (" --device-info" );
540+ EXPECT_EQ (result.exitCode , 0 );
541+ EXPECT_THAT (result.output , ::testing::HasSubstr (" Device" ));
542+ }
543+
374544// ============================================================================
375545// Format Conversion Tests
376546// ============================================================================
@@ -466,3 +636,45 @@ TEST_F(CCAPCLITest, ConvertNV12ToImage_White) {
466636 EXPECT_GT (pixel.g , 240 ) << " Green channel too low: " << (int )pixel.g ;
467637 EXPECT_GT (pixel.b , 240 ) << " Blue channel too low: " << (int )pixel.b ;
468638}
639+
640+ TEST_F (CCAPCLITest, ConvertI420ToImage_Red) {
641+ // Create a solid red I420 image (64x64)
642+ fs::path yuvPath = testOutputDir / " test_i420_red.yuv" ;
643+ createSolidColorI420 (yuvPath, 64 , 64 , 76 , 84 , 255 );
644+
645+ fs::path outputPath = testOutputDir / " output_i420_red.bmp" ;
646+ std::string cmd = " --convert " + yuvPath.string () +
647+ " --yuv-format i420 --yuv-width 64 --yuv-height 64" +
648+ " --convert-output " + outputPath.string ();
649+
650+ auto result = runCLI (cmd);
651+ ASSERT_EQ (result.exitCode , 0 ) << " Convert command failed: " << result.output ;
652+ ASSERT_TRUE (fs::exists (outputPath)) << " Output BMP file not created" ;
653+
654+ // Read and verify pixel color (should be close to red)
655+ RGB pixel = readBMPPixel (outputPath, 32 , 32 );
656+ EXPECT_GT (pixel.r , 200 ) << " Red channel too low: " << (int )pixel.r ;
657+ EXPECT_LT (pixel.g , 100 ) << " Green channel too high: " << (int )pixel.g ;
658+ EXPECT_LT (pixel.b , 100 ) << " Blue channel too high: " << (int )pixel.b ;
659+ }
660+
661+ TEST_F (CCAPCLITest, ConvertI420ToImage_Green) {
662+ // Create a solid green I420 image (64x64)
663+ fs::path yuvPath = testOutputDir / " test_i420_green.yuv" ;
664+ createSolidColorI420 (yuvPath, 64 , 64 , 149 , 43 , 21 );
665+
666+ fs::path outputPath = testOutputDir / " output_i420_green.bmp" ;
667+ std::string cmd = " --convert " + yuvPath.string () +
668+ " --yuv-format i420 --yuv-width 64 --yuv-height 64" +
669+ " --convert-output " + outputPath.string ();
670+
671+ auto result = runCLI (cmd);
672+ ASSERT_EQ (result.exitCode , 0 ) << " Convert command failed: " << result.output ;
673+ ASSERT_TRUE (fs::exists (outputPath)) << " Output BMP file not created" ;
674+
675+ // Read and verify pixel color (should be close to green)
676+ RGB pixel = readBMPPixel (outputPath, 32 , 32 );
677+ EXPECT_LT (pixel.r , 100 ) << " Red channel too high: " << (int )pixel.r ;
678+ EXPECT_GT (pixel.g , 200 ) << " Green channel too low: " << (int )pixel.g ;
679+ EXPECT_LT (pixel.b , 100 ) << " Blue channel too high: " << (int )pixel.b ;
680+ }
0 commit comments