    /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
    /*
     * This file is part of the LibreOffice project.
     *
     * This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/.
     */
    /*
     * Timers are evil beasts across platforms...
     */

    #include <test/bootstrapfixture.hxx>

    #include <osl/thread.hxx>
    #include <salhelper/thread.hxx>
    #include <chrono>

    #include <vcl/timer.hxx>
    #include <vcl/idle.hxx>
    #include <vcl/svapp.hxx>
    #include <vcl/scheduler.hxx>
    #include <svdata.hxx>
    #include <salinst.hxx>

    // #define TEST_WATCHDOG

    // Enables timer tests that appear to provoke windows under load unduly.
    //#define TEST_TIMERPRECISION

    /// Avoid our timer tests just wedging the build if they fail.
    class WatchDog : public osl::Thread
    {
        sal_Int32 mnSeconds;
    public:
        explicit WatchDog(sal_Int32 nSeconds) :
            Thread(),
            mnSeconds( nSeconds )
        {
            create();
        }
        virtual void SAL_CALL run() override
        {
            osl::Thread::wait( std::chrono::seconds(mnSeconds) );
            fprintf(stderr, "ERROR: WatchDog timer thread expired, failing the test!\n");
            fflush(stderr);
            CPPUNIT_ASSERT_MESSAGE("watchdog triggered", false);
        }
    };

    static WatchDog * aWatchDog = new WatchDog( 120 ); // random high number in secs

    class TimerTest : public test::BootstrapFixture
    {
    public:
        TimerTest() : BootstrapFixture(true, false) {}

        void testIdle();
    #ifndef WIN32
        void testIdleMainloop();
    #endif
    #ifdef TEST_WATCHDOG
        void testWatchdog();
    #endif
        void testDurations();
    #ifdef TEST_TIMERPRECISION
        void testAutoTimer();
        void testMultiAutoTimers();
    #endif
        void testAutoTimerStop();
        void testNestedTimer();
        void testSlowTimerCallback();
        void testTriggerIdleFromIdle();
        void testInvokedReStart();
        void testPriority();
        void testRoundRobin();

        CPPUNIT_TEST_SUITE(TimerTest);
        CPPUNIT_TEST(testIdle);
    #ifndef WIN32
        CPPUNIT_TEST(testIdleMainloop);
    #endif
    #ifdef TEST_WATCHDOG
        CPPUNIT_TEST(testWatchdog);
    #endif
        CPPUNIT_TEST(testDurations);
    #ifdef TEST_TIMERPRECISION
        CPPUNIT_TEST(testAutoTimer);
        CPPUNIT_TEST(testMultiAutoTimers);
    #endif
        CPPUNIT_TEST(testAutoTimerStop);
        CPPUNIT_TEST(testNestedTimer);
        CPPUNIT_TEST(testSlowTimerCallback);
        CPPUNIT_TEST(testTriggerIdleFromIdle);
        CPPUNIT_TEST(testInvokedReStart);
        CPPUNIT_TEST(testPriority);
        CPPUNIT_TEST(testRoundRobin);

        CPPUNIT_TEST_SUITE_END();
    };

    #ifdef TEST_WATCHDOG
    void TimerTest::testWatchdog()
    {
        // out-wait the watchdog.
        osl::Thread::wait( std::chrono::seconds(12) );
    }
    #endif

    class IdleBool : public Idle
    {
        bool &mrBool;
    public:
        explicit IdleBool( bool &rBool ) :
            Idle( "IdleBool" ), mrBool( rBool )
        {
            SetPriority( TaskPriority::LOWEST );
            Start();
            mrBool = false;
        }
        virtual void Invoke() override
        {
            mrBool = true;
            Application::EndYield();
        }
    };

    void TimerTest::testIdle()
    {
        bool bTriggered = false;
        IdleBool aTest( bTriggered );
        Scheduler::ProcessEventsToIdle();
        CPPUNIT_ASSERT_MESSAGE("idle triggered", bTriggered);
    }

    #ifndef WIN32
    // tdf#91727
    void TimerTest::testIdleMainloop()
    {
        bool bTriggered = false;
        IdleBool aTest( bTriggered );
        // coverity[loop_top] - Application::Yield allows the timer to fire and toggle bDone
        while (!bTriggered)
        {
            ImplSVData* pSVData = ImplGetSVData();

            // can't test this via Application::Yield since this
            // also processes all tasks directly via the scheduler.
            pSVData->maAppData.mnDispatchLevel++;
            pSVData->mpDefInst->DoYield(true, false);
            pSVData->maAppData.mnDispatchLevel--;
        }
        CPPUNIT_ASSERT_MESSAGE("mainloop idle triggered", bTriggered);
    }
    #endif

    class TimerBool : public Timer
    {
        bool &mrBool;
    public:
        TimerBool( sal_uLong nMS, bool &rBool ) :
            Timer( "TimerBool" ), mrBool( rBool )
        {
            SetTimeout( nMS );
            Start();
            mrBool = false;
        }
        virtual void Invoke() override
        {
            mrBool = true;
            Application::EndYield();
        }
    };

    void TimerTest::testDurations()
    {
        static const sal_uLong aDurations[] = { 0, 1, 500, 1000 };
        for (size_t i = 0; i < SAL_N_ELEMENTS( aDurations ); i++)
        {
            bool bDone = false;
            TimerBool aTimer( aDurations[i], bDone );
            // coverity[loop_top] - Application::Yield allows the timer to fire and toggle bDone
            while( !bDone )
            {
                Application::Yield();
            }
        }
    }

    class AutoTimerCount : public AutoTimer
    {
        sal_Int32 &mrCount;
        const sal_Int32 mnMaxCount;

    public:
        AutoTimerCount( sal_uLong nMS, sal_Int32 &rCount,
                        const sal_Int32 nMaxCount = -1 )
            : AutoTimer( "AutoTimerCount" )
            , mrCount( rCount )
            , mnMaxCount( nMaxCount )
        {
            SetTimeout( nMS );
            Start();
            mrCount = 0;
        }

        virtual void Invoke() override
        {
            ++mrCount;
            CPPUNIT_ASSERT( mnMaxCount < 0 || mrCount <= mnMaxCount );
            if ( mrCount == mnMaxCount )
                Stop();
        }
    };

    #ifdef TEST_TIMERPRECISION

    void TimerTest::testAutoTimer()
    {
        const sal_Int32 nDurationMs = 30;
        const sal_Int32 nEventsCount = 5;
        const double exp = (nDurationMs * nEventsCount);

        sal_Int32 nCount = 0;
        std::ostringstream msg;

        // Repeat when we have random latencies.
        // This is expected on non-realtime OSes.
        for (int i = 0; i < 10; ++i)
        {
            const auto start = std::chrono::high_resolution_clock::now();
            nCount = 0;
            AutoTimerCount aCount(nDurationMs, nCount);
            while (nCount < nEventsCount) {
                Application::Yield();
            }

            const auto end = std::chrono::high_resolution_clock::now();
            double dur = std::chrono::duration<double, std::milli>(end - start).count();

            msg << std::setprecision(2) << std::fixed
                << "periodic multi-timer - dur: "
                << dur << " (" << exp << ") ms." << std::endl;

            // +/- 20% should be reasonable enough a margin.
            if (dur >= (exp * 0.8) && dur <= (exp * 1.2))
            {
                // Success.
                return;
            }
        }

        CPPUNIT_FAIL(msg.str().c_str());
    }

    void TimerTest::testMultiAutoTimers()
    {
        // The behavior of the timers change drastically
        // when multiple timers are present.
        // The worst, in my tests, is when two
        // timers with 1ms period exist with a
        // third of much longer period.

        const sal_Int32 nDurationMsX = 5;
        const sal_Int32 nDurationMsY = 10;
        const sal_Int32 nDurationMs = 40;
        const sal_Int32 nEventsCount = 5;
        const double exp = (nDurationMs * nEventsCount);
        const double expX = (exp / nDurationMsX);
        const double expY = (exp / nDurationMsY);

        sal_Int32 nCountX = 0;
        sal_Int32 nCountY = 0;
        sal_Int32 nCount = 0;
        std::ostringstream msg;

        // Repeat when we have random latencies.
        // This is expected on non-realtime OSes.
        for (int i = 0; i < 10; ++i)
        {
            nCountX = 0;
            nCountY = 0;
            nCount = 0;

            const auto start = std::chrono::high_resolution_clock::now();
            AutoTimerCount aCountX(nDurationMsX, nCountX);
            AutoTimerCount aCountY(nDurationMsY, nCountY);

            AutoTimerCount aCount(nDurationMs, nCount);
            // coverity[loop_top] - Application::Yield allows the timer to fire and toggle nCount
            while (nCount < nEventsCount) {
                Application::Yield();
            }

            const auto end = std::chrono::high_resolution_clock::now();
            double dur = std::chrono::duration<double, std::milli>(end - start).count();

            msg << std::setprecision(2) << std::fixed << "periodic multi-timer - dur: "
                << dur << " (" << exp << ") ms, nCount: " << nCount
                << " (" << nEventsCount << "), nCountX: " << nCountX
                << " (" << expX << "), nCountY: " << nCountY
                << " (" << expY << ")." << std::endl;

            // +/- 20% should be reasonable enough a margin.
            if (dur >= (exp * 0.8) && dur <= (exp * 1.2) &&
                nCountX >= (expX * 0.8) && nCountX <= (expX * 1.2) &&
                nCountY >= (expY * 0.8) && nCountY <= (expY * 1.2))
            {
                // Success.
                return;
            }
        }

        CPPUNIT_FAIL(msg.str().c_str());
    }
    #endif // TEST_TIMERPRECISION

    void TimerTest::testAutoTimerStop()
    {
        sal_Int32 nTimerCount = 0;
        const sal_Int32 nMaxCount = 5;
        AutoTimerCount aAutoTimer( 0, nTimerCount, nMaxCount );
        // coverity[loop_top] - Application::Yield allows the timer to fire and increment TimerCount
        while (nMaxCount != nTimerCount)
            Application::Yield();
        CPPUNIT_ASSERT( !aAutoTimer.IsActive() );
        CPPUNIT_ASSERT( !Application::Reschedule() );
    }

    class YieldTimer : public Timer
    {
    public:
        explicit YieldTimer( sal_uLong nMS ) : Timer( "YieldTimer" )
        {
            SetTimeout( nMS );
            Start();
        }
        virtual void Invoke() override
        {
            for (int i = 0; i < 100; i++)
                Application::Yield();
        }
    };

    void TimerTest::testNestedTimer()
    {
        sal_Int32 nCount = 0;
        YieldTimer aCount(5);
        AutoTimerCount aCountUp( 3, nCount );
        // coverity[loop_top] - Application::Yield allows the timer to fire and increment nCount
        while (nCount < 20)
            Application::Yield();
    }

    class SlowCallbackTimer : public Timer
    {
        bool &mbSlow;
    public:
        SlowCallbackTimer( sal_uLong nMS, bool &bBeenSlow ) :
            Timer( "SlowCallbackTimer" ), mbSlow( bBeenSlow )
        {
            SetTimeout( nMS );
            Start();
            mbSlow = false;
        }
        virtual void Invoke() override
        {
            osl::Thread::wait( std::chrono::seconds(1) );
            mbSlow = true;
        }
    };

    void TimerTest::testSlowTimerCallback()
    {
        bool bBeenSlow = false;
        sal_Int32 nCount = 0;
        AutoTimerCount aHighFreq(1, nCount);
        SlowCallbackTimer aSlow(250, bBeenSlow);
        // coverity[loop_top] - Application::Yield allows the timer to fire and toggle bBeenSlow
        while (!bBeenSlow)
            Application::Yield();
        // coverity[loop_top] - Application::Yield allows the timer to fire and increment nCount
        while (nCount < 200)
            Application::Yield();
    }

    class TriggerIdleFromIdle : public Idle
    {
        bool* mpTriggered;
        TriggerIdleFromIdle* mpOther;
    public:
        explicit TriggerIdleFromIdle( bool* pTriggered, TriggerIdleFromIdle* pOther ) :
            Idle( "TriggerIdleFromIdle" ), mpTriggered(pTriggered), mpOther(pOther)
        {
        }
        virtual void Invoke() override
        {
            Start();
            if (mpOther)
                mpOther->Start();
            Application::Yield();
            if (mpTriggered)
                *mpTriggered = true;
        }
    };

    void TimerTest::testTriggerIdleFromIdle()
    {
        bool bTriggered1 = false;
        bool bTriggered2 = false;
        TriggerIdleFromIdle aTest2( &bTriggered2, nullptr );
        TriggerIdleFromIdle aTest1( &bTriggered1, &aTest2 );
        aTest1.Start();
        Application::Yield();
        CPPUNIT_ASSERT_MESSAGE("idle not triggered", bTriggered1);
        CPPUNIT_ASSERT_MESSAGE("idle not triggered", bTriggered2);
    }

    class IdleInvokedReStart : public Idle
    {
        sal_Int32 &mrCount;
    public:
        IdleInvokedReStart( sal_Int32 &rCount )
            : Idle( "IdleInvokedReStart" ), mrCount( rCount )
        {
            Start();
        }
        virtual void Invoke() override
        {
            mrCount++;
            if ( mrCount < 2 )
                Start();
        }
    };

    void TimerTest::testInvokedReStart()
    {
        sal_Int32 nCount = 0;
        IdleInvokedReStart aIdle( nCount );
        Scheduler::ProcessEventsToIdle();
        CPPUNIT_ASSERT_EQUAL( sal_Int32(2), nCount );
    }

    class IdleSerializer : public Idle
    {
        sal_uInt32 mnPosition;
        sal_uInt32 &mrProcesed;
    public:
        IdleSerializer( const sal_Char *pDebugName,
                        sal_uInt32 nPosition, sal_uInt32 &rProcesed )
            : Idle( pDebugName )
            , mnPosition( nPosition )
            , mrProcesed( rProcesed )
        {
            Start();
        }
        virtual void Invoke() override
        {
            ++mrProcesed;
            CPPUNIT_ASSERT_EQUAL_MESSAGE( "Ignored prio", mnPosition, mrProcesed );
        }
    };

    void TimerTest::testPriority()
    {
        // scope, so tasks are deleted
        {
            // Start: 1st Idle low, 2nd high
            sal_uInt32 nProcessed = 0;
            IdleSerializer aLowPrioIdle( "IdleSerializer LowPrio", 2, nProcessed );
            aLowPrioIdle.SetPriority( TaskPriority::LOWEST );
            IdleSerializer aHighPrioIdle( "IdleSerializer HighPrio", 1, nProcessed );
            aHighPrioIdle.SetPriority( TaskPriority::HIGHEST );
            Scheduler::ProcessEventsToIdle();
            CPPUNIT_ASSERT_EQUAL_MESSAGE( "Not all idles processed", sal_uInt32(2), nProcessed );
        }

        {
            // Start: 1st Idle high, 2nd low
            sal_uInt32 nProcessed = 0;
            IdleSerializer aHighPrioIdle( "IdleSerializer HighPrio", 1, nProcessed );
            aHighPrioIdle.SetPriority( TaskPriority::HIGHEST );
            IdleSerializer aLowPrioIdle( "IdleSerializer LowPrio", 2, nProcessed );
            aLowPrioIdle.SetPriority( TaskPriority::LOWEST );
            Scheduler::ProcessEventsToIdle();
            CPPUNIT_ASSERT_EQUAL_MESSAGE( "Not all idles processed", sal_uInt32(2), nProcessed );
        }
    }

    class TestAutoIdleRR : public AutoIdle
    {
        sal_uInt32 &mrCount;

        DECL_LINK( IdleRRHdl, Timer *, void );

    public:
        TestAutoIdleRR( sal_uInt32 &rCount,
                        const sal_Char *pDebugName )
            : AutoIdle( pDebugName )
            , mrCount( rCount )
        {
            CPPUNIT_ASSERT_EQUAL( sal_uInt32(0), mrCount );
            SetInvokeHandler( LINK( this, TestAutoIdleRR, IdleRRHdl ) );
            Start();
        }
    };

    IMPL_LINK_NOARG(TestAutoIdleRR, IdleRRHdl, Timer *, void)
    {
        ++mrCount;
        if ( mrCount == 3 )
            Stop();
    }

    void TimerTest::testRoundRobin()
    {
        sal_uInt32 nCount1 = 0, nCount2 = 0;
        TestAutoIdleRR aIdle1( nCount1, "TestAutoIdleRR aIdle1" ),
                       aIdle2( nCount2, "TestAutoIdleRR aIdle2" );
        while ( Application::Reschedule() )
        {
            CPPUNIT_ASSERT( nCount1 == nCount2 || nCount1 - 1 == nCount2 );
            CPPUNIT_ASSERT( nCount1 <= 3 );
            CPPUNIT_ASSERT( nCount2 <= 3 );
        }
        CPPUNIT_ASSERT( 3 == nCount1 && 3 == nCount2 );
    }

    CPPUNIT_TEST_SUITE_REGISTRATION(TimerTest);

    CPPUNIT_PLUGIN_IMPLEMENT();

    /* vim:set shiftwidth=4 softtabstop=4 expandtab: */

↑ V654 The condition 'nCount < 20' of loop is always true.

↑ V654 The condition 'nCount < 200' of loop is always true.

↑ V776 Potentially infinite loop. The variable in the loop exit condition '!bBeenSlow' does not change its value between iterations.

↑ V776 Potentially infinite loop. The variable in the loop exit condition 'nMaxCount != nTimerCount' does not change its value between iterations.