/* * Oak Video Editor - Viewer/Preview Subsystem Smoke Tests * Copyright (C) 2025 Olive CE Team * * Comprehensive smoke tests for the viewer and preview display subsystem including: * - ViewerPlaybackTimer timing calculations * - ViewerQueue frame management * - ViewerSafeMarginInfo safety margin calculations * - PreviewAutoCacher cache management */ #include #include #include // Viewer headers #include "widget/viewer/viewerplaybacktimer.h" #include "widget/viewer/viewerqueue.h" #include "widget/viewer/viewersafemargininfo.h" #include "render/previewautocacher.h" #include "render/audioplaybackcache.h" #include "olive/core/util/rational.h" using namespace olive; using namespace olive::core; namespace olive { namespace viewer { namespace test { // ============================================================================ // Smoke Test: ViewerPlaybackTimer // ============================================================================ TEST(ViewerSmokeTimer, DefaultConstruction) { ViewerPlaybackTimer timer; // Timer should be in a valid but not-started state // After Start() is called, it should return valid timestamps } TEST(ViewerSmokeTimer, BasicTiming) { ViewerPlaybackTimer timer; // Start at timestamp 0, 1x speed, 24fps (timebase = 1/24) timer.Start(0, 1, 1.0 / 24.0); // Immediately get timestamp (should be close to 0) int64_t ts = timer.GetTimestampNow(); EXPECT_GE(ts, 0); // Wait a bit and check timestamp has increased QThread::msleep(50); // 50ms int64_t ts2 = timer.GetTimestampNow(); // At 24fps, 50ms should be approximately 1 frame (or slightly more) // Allow for some timing variance EXPECT_GE(ts2, ts); } TEST(ViewerSmokeTimer, PlaybackSpeedForward) { ViewerPlaybackTimer timer; // Start at timestamp 100, 2x speed, 30fps timer.Start(100, 2, 1.0 / 30.0); int64_t ts1 = timer.GetTimestampNow(); QThread::msleep(50); int64_t ts2 = timer.GetTimestampNow(); // At 2x speed, time should advance twice as fast EXPECT_GT(ts2, ts1); } TEST(ViewerSmokeTimer, PlaybackSpeedReverse) { ViewerPlaybackTimer timer; // Start at timestamp 1000, -1x speed (reverse), 24fps timer.Start(1000, -1, 1.0 / 24.0); int64_t ts1 = timer.GetTimestampNow(); QThread::msleep(50); int64_t ts2 = timer.GetTimestampNow(); // In reverse, timestamp should decrease EXPECT_LT(ts2, ts1); } TEST(ViewerSmokeTimer, DifferentTimebases) { ViewerPlaybackTimer timer; // Test with 24fps timer.Start(0, 1, 1.0 / 24.0); QThread::msleep(100); int64_t ts24 = timer.GetTimestampNow(); // Test with 60fps timer.Start(0, 1, 1.0 / 60.0); QThread::msleep(100); int64_t ts60 = timer.GetTimestampNow(); // At same real time, 60fps should have more frames than 24fps EXPECT_GT(ts60, ts24); } TEST(ViewerSmokeTimer, ZeroSpeed) { ViewerPlaybackTimer timer; // Start with 0 speed (paused) timer.Start(500, 0, 1.0 / 24.0); int64_t ts1 = timer.GetTimestampNow(); QThread::msleep(50); int64_t ts2 = timer.GetTimestampNow(); // With 0 speed, timestamp should not change EXPECT_EQ(ts1, ts2); } // ============================================================================ // Smoke Test: ViewerQueue // ============================================================================ TEST(ViewerSmokeQueue, DefaultConstruction) { ViewerQueue queue; EXPECT_TRUE(queue.empty()); } TEST(ViewerSmokeQueue, AppendForwardPlayback) { ViewerQueue queue; // Append frames for forward playback ViewerPlaybackFrame frame1{rational(0), QVariant()}; ViewerPlaybackFrame frame2{rational(1, 24), QVariant()}; ViewerPlaybackFrame frame3{rational(2, 24), QVariant()}; queue.AppendTimewise(frame1, 1); // speed = 1 (forward) queue.AppendTimewise(frame2, 1); queue.AppendTimewise(frame3, 1); EXPECT_EQ(queue.size(), 3); // Verify order (should be chronological for forward playback) auto it = queue.begin(); EXPECT_EQ(it->timestamp, rational(0)); ++it; EXPECT_EQ(it->timestamp, rational(1, 24)); ++it; EXPECT_EQ(it->timestamp, rational(2, 24)); } TEST(ViewerSmokeQueue, AppendReversePlayback) { ViewerQueue queue; // Append frames for reverse playback ViewerPlaybackFrame frame1{rational(2, 24), QVariant()}; ViewerPlaybackFrame frame2{rational(1, 24), QVariant()}; ViewerPlaybackFrame frame3{rational(0), QVariant()}; queue.AppendTimewise(frame1, -1); // speed = -1 (reverse) queue.AppendTimewise(frame2, -1); queue.AppendTimewise(frame3, -1); EXPECT_EQ(queue.size(), 3); // Verify order (should be reverse chronological for reverse playback) auto it = queue.begin(); EXPECT_EQ(it->timestamp, rational(2, 24)); ++it; EXPECT_EQ(it->timestamp, rational(1, 24)); ++it; EXPECT_EQ(it->timestamp, rational(0)); } TEST(ViewerSmokeQueue, InsertOutOfOrder) { ViewerQueue queue; // Insert frames out of order for forward playback ViewerPlaybackFrame frame1{rational(0), QVariant()}; ViewerPlaybackFrame frame2{rational(2, 24), QVariant()}; ViewerPlaybackFrame frame3{rational(1, 24), QVariant()}; // Middle frame queue.AppendTimewise(frame1, 1); queue.AppendTimewise(frame2, 1); queue.AppendTimewise(frame3, 1); // Should insert in middle EXPECT_EQ(queue.size(), 3); // Verify correct order auto it = queue.begin(); EXPECT_EQ(it->timestamp, rational(0)); ++it; EXPECT_EQ(it->timestamp, rational(1, 24)); ++it; EXPECT_EQ(it->timestamp, rational(2, 24)); } TEST(ViewerSmokeQueue, PurgeBefore) { ViewerQueue queue; // Add some frames for (int i = 0; i < 10; i++) { ViewerPlaybackFrame frame{rational(i, 24), QVariant()}; queue.AppendTimewise(frame, 1); } EXPECT_EQ(queue.size(), 10); // Purge frames before 5/24 queue.PurgeBefore(rational(5, 24), 1); // Should have 5 frames remaining (5, 6, 7, 8, 9) EXPECT_EQ(queue.size(), 5); EXPECT_EQ(queue.front().timestamp, rational(5, 24)); } TEST(ViewerSmokeQueue, PurgeBeforeReverse) { ViewerQueue queue; // Add frames for reverse playback (newest first) for (int i = 9; i >= 0; i--) { ViewerPlaybackFrame frame{rational(i, 24), QVariant()}; queue.AppendTimewise(frame, -1); } EXPECT_EQ(queue.size(), 10); // In reverse playback, front() is the largest timestamp (9/24) // PurgeBefore with negative speed removes frames where front > time queue.PurgeBefore(rational(5, 24), -1); // Should have frames 0-5 remaining (those <= 5/24) EXPECT_EQ(queue.size(), 6); EXPECT_EQ(queue.front().timestamp, rational(5, 24)); } // ============================================================================ // Smoke Test: ViewerSafeMarginInfo // ============================================================================ TEST(ViewerSmokeSafeMargin, DefaultConstruction) { ViewerSafeMarginInfo info; EXPECT_FALSE(info.is_enabled()); EXPECT_FALSE(info.custom_ratio()); EXPECT_DOUBLE_EQ(info.ratio(), 0.0); } TEST(ViewerSmokeSafeMargin, EnabledConstruction) { ViewerSafeMarginInfo info(true); EXPECT_TRUE(info.is_enabled()); EXPECT_FALSE(info.custom_ratio()); } TEST(ViewerSmokeSafeMargin, CustomRatioConstruction) { ViewerSafeMarginInfo info(true, 0.9); EXPECT_TRUE(info.is_enabled()); EXPECT_TRUE(info.custom_ratio()); EXPECT_DOUBLE_EQ(info.ratio(), 0.9); } TEST(ViewerSmokeSafeMargin, EqualityOperators) { ViewerSafeMarginInfo info1(true, 0.9); ViewerSafeMarginInfo info2(true, 0.9); ViewerSafeMarginInfo info3(false, 0.9); ViewerSafeMarginInfo info4(true, 0.8); EXPECT_TRUE(info1 == info2); EXPECT_FALSE(info1 != info2); EXPECT_FALSE(info1 == info3); // Different enabled state EXPECT_FALSE(info1 == info4); // Different ratio EXPECT_TRUE(info1 != info3); } TEST(ViewerSmokeSafeMargin, ZeroRatio) { ViewerSafeMarginInfo info(true, 0.0); EXPECT_TRUE(info.is_enabled()); EXPECT_FALSE(info.custom_ratio()); // 0 ratio means no custom ratio } TEST(ViewerSmokeSafeMargin, CopyConstruction) { ViewerSafeMarginInfo original(true, 0.85); ViewerSafeMarginInfo copy(original); EXPECT_EQ(copy.is_enabled(), original.is_enabled()); EXPECT_EQ(copy.custom_ratio(), original.custom_ratio()); EXPECT_DOUBLE_EQ(copy.ratio(), original.ratio()); } // ============================================================================ // Smoke Test: AudioPlaybackCache (used by preview system) // ============================================================================ TEST(ViewerSmokeAudioCache, DefaultConstruction) { AudioPlaybackCache cache; // Should construct without crashing SUCCEED(); } TEST(ViewerSmokeAudioCache, ParameterSetters) { AudioPlaybackCache cache; AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P); cache.SetParameters(params); // Parameters should be retrievable AudioParams retrieved = cache.GetParameters(); EXPECT_EQ(retrieved.sample_rate(), params.sample_rate()); } TEST(ViewerSmokeAudioCache, ValidateWithRange) { AudioPlaybackCache cache; // Initially no validated ranges TimeRangeList validated = cache.GetValidatedRanges(); EXPECT_TRUE(validated.isEmpty()); } // ============================================================================ // Smoke Test: PreviewAutoCacher (basic lifecycle) // ============================================================================ // NOTE: PreviewAutoCacher requires a QApplication and proper initialization. // These tests are disabled in headless mode. TEST(ViewerSmokeAutoCacher, DISABLED_Construction) { // PreviewAutoCacher requires full GUI environment SUCCEED(); } TEST(ViewerSmokeAutoCacher, DISABLED_SetPlayhead) { // PreviewAutoCacher requires full GUI environment SUCCEED(); } TEST(ViewerSmokeAutoCacher, DISABLED_PauseControls) { // PreviewAutoCacher requires full GUI environment SUCCEED(); } TEST(ViewerSmokeAutoCacher, DISABLED_SetIgnoreCacheRequests) { // PreviewAutoCacher requires full GUI environment SUCCEED(); } TEST(ViewerSmokeAutoCacher, DISABLED_SetDisplayColorProcessor) { // PreviewAutoCacher requires full GUI environment SUCCEED(); } // ============================================================================ // Smoke Test: Rational Time Calculations (core utility) // ============================================================================ TEST(ViewerSmokeRational, DefaultConstruction) { rational r; EXPECT_EQ(r.numerator(), 0); EXPECT_EQ(r.denominator(), 1); } TEST(ViewerSmokeRational, ValueConstruction) { rational r(24, 1); EXPECT_EQ(r.numerator(), 24); EXPECT_EQ(r.denominator(), 1); rational r2(1, 24); EXPECT_EQ(r2.numerator(), 1); EXPECT_EQ(r2.denominator(), 24); } TEST(ViewerSmokeRational, ToDouble) { rational r(1, 2); EXPECT_DOUBLE_EQ(r.toDouble(), 0.5); rational r2(3, 4); EXPECT_DOUBLE_EQ(r2.toDouble(), 0.75); } TEST(ViewerSmokeRational, Arithmetic) { rational r1(1, 2); rational r2(1, 4); rational sum = r1 + r2; EXPECT_EQ(sum.numerator(), 3); EXPECT_EQ(sum.denominator(), 4); rational diff = r1 - r2; EXPECT_EQ(diff.numerator(), 1); EXPECT_EQ(diff.denominator(), 4); } TEST(ViewerSmokeRational, Comparison) { rational r1(1, 2); rational r2(2, 4); rational r3(3, 4); EXPECT_TRUE(r1 == r2); // Equivalent fractions EXPECT_FALSE(r1 == r3); EXPECT_TRUE(r1 < r3); EXPECT_TRUE(r3 > r1); } TEST(ViewerSmokeRational, NullCheck) { rational r; EXPECT_TRUE(r.isNull()); // 0/1 is considered null rational r2(1, 2); EXPECT_FALSE(r2.isNull()); } TEST(ViewerSmokeRational, Flipped) { rational r(24, 1); rational flipped = r.flipped(); EXPECT_EQ(flipped.numerator(), 1); EXPECT_EQ(flipped.denominator(), 24); } // ============================================================================ // Smoke Test: Thread Safety // ============================================================================ TEST(ViewerSmokeThread, ConcurrentTimerAccess) { const int num_threads = 4; const int num_iterations = 100; ViewerPlaybackTimer timer; timer.Start(0, 1, 1.0 / 30.0); std::vector threads; std::atomic success_count{0}; for (int t = 0; t < num_threads; ++t) { threads.emplace_back([&timer, &success_count, num_iterations]() { for (int i = 0; i < num_iterations; ++i) { int64_t ts = timer.GetTimestampNow(); if (ts >= 0) { success_count++; } } }); } for (auto &t : threads) { t.join(); } EXPECT_EQ(success_count.load(), num_threads * num_iterations); } TEST(ViewerSmokeThread, ConcurrentQueueAccess) { const int num_threads = 4; const int num_frames_per_thread = 25; ViewerQueue queue; std::vector threads; std::atomic append_count{0}; for (int t = 0; t < num_threads; ++t) { threads.emplace_back([&queue, &append_count, t, num_frames_per_thread]() { for (int i = 0; i < num_frames_per_thread; ++i) { ViewerPlaybackFrame frame{rational(t * num_frames_per_thread + i, 24), QVariant()}; queue.AppendTimewise(frame, 1); append_count++; } }); } for (auto &t : threads) { t.join(); } EXPECT_EQ(append_count.load(), num_threads * num_frames_per_thread); EXPECT_EQ(queue.size(), num_threads * num_frames_per_thread); } // ============================================================================ // Smoke Test: Integration Scenarios // ============================================================================ TEST(ViewerSmokeIntegration, PlaybackSequenceSimulation) { // Simulate a basic playback sequence ViewerPlaybackTimer timer; ViewerQueue queue; // Start playback at frame 0, 24fps timer.Start(0, 1, 1.0 / 24.0); // Queue some frames for (int i = 0; i < 10; i++) { ViewerPlaybackFrame frame{rational(i, 24), QVariant(i)}; queue.AppendTimewise(frame, 1); } // Get current timestamp int64_t current_ts = timer.GetTimestampNow(); // Find frame closest to current time rational current_time(current_ts, 1); bool found = false; for (const auto &frame : queue) { if (frame.timestamp >= current_time) { found = true; break; } } // Should have frames available EXPECT_FALSE(queue.empty()); } TEST(ViewerSmokeIntegration, SafeMarginWithDifferentAspectRatios) { // Test safe margins for different aspect ratios std::vector ratios = {0.9, 0.85, 0.8, 0.7}; for (double ratio : ratios) { ViewerSafeMarginInfo info(true, ratio); EXPECT_TRUE(info.is_enabled()); EXPECT_TRUE(info.custom_ratio()); EXPECT_DOUBLE_EQ(info.ratio(), ratio); } } TEST(ViewerSmokeIntegration, ReversePlaybackScenario) { ViewerPlaybackTimer timer; ViewerQueue queue; // Start reverse playback from frame 100 timer.Start(100, -1, 1.0 / 24.0); // Queue frames in reverse order for (int i = 100; i >= 90; i--) { ViewerPlaybackFrame frame{rational(i, 24), QVariant(i)}; queue.AppendTimewise(frame, -1); } // Get timestamps - should decrease int64_t ts1 = timer.GetTimestampNow(); QThread::msleep(50); int64_t ts2 = timer.GetTimestampNow(); EXPECT_LT(ts2, ts1); EXPECT_EQ(queue.front().timestamp, rational(100, 24)); } } // namespace test } // namespace viewer } // namespace olive