99#include < latch>
1010#include < span>
1111#include < string_view>
12+ #include < thread>
1213
1314#include < m/debugging/dbg_format.h>
1415#include < m/threadpool/threadpool.h>
@@ -34,3 +35,147 @@ TEST(PeriodicTimer, Test1)
3435 EXPECT_GT (counter, 17 );
3536 EXPECT_LT (counter, 22 );
3637}
38+
39+ // ---------------------------------------------------------------------------
40+ // is_set() tests
41+ // ---------------------------------------------------------------------------
42+
43+ TEST (PeriodicTimer, IsSetInitiallyFalse)
44+ {
45+ auto t1 = m::threadpool->create_periodic_timer ([]() {});
46+ EXPECT_FALSE (t1->is_set ());
47+ }
48+
49+ TEST (PeriodicTimer, IsSetTrueAfterSet)
50+ {
51+ std::latch fired (1 );
52+
53+ auto t1 = m::threadpool->create_periodic_timer ([&]() {
54+ fired.count_down (); // signal first firing
55+ });
56+
57+ EXPECT_FALSE (t1->is_set ());
58+ t1->set (100ms);
59+ EXPECT_TRUE (t1->is_set ());
60+
61+ // Wait for at least one callback so we know the timer is operating.
62+ fired.wait ();
63+
64+ // Still set (periodic timers keep firing until stopped).
65+ EXPECT_TRUE (t1->is_set ());
66+
67+ t1->stop ();
68+ t1->wait ();
69+ }
70+
71+ // ---------------------------------------------------------------------------
72+ // stop() regression tests (bug: m_set_count was never incremented)
73+ // ---------------------------------------------------------------------------
74+
75+ TEST (PeriodicTimer, StopAfterSetSucceeds)
76+ {
77+ // Regression: before the fix, do_stop() always threw because m_set_count
78+ // was never incremented in do_set(), making the "not started" guard always
79+ // true even on a running timer.
80+ auto t1 = m::threadpool->create_periodic_timer ([]() {});
81+ t1->set (100ms);
82+ EXPECT_NO_THROW (t1->stop ());
83+ t1->wait ();
84+ }
85+
86+ TEST (PeriodicTimer, IsSetFalseAfterStop)
87+ {
88+ auto t1 = m::threadpool->create_periodic_timer ([]() {});
89+ t1->set (100ms);
90+ t1->stop ();
91+ EXPECT_FALSE (t1->is_set ());
92+ t1->wait ();
93+ }
94+
95+ TEST (PeriodicTimer, WaitCompletesAfterStop)
96+ {
97+ auto t1 = m::threadpool->create_periodic_timer ([]() {});
98+ t1->set (100ms);
99+ t1->stop ();
100+ t1->wait (); // must not hang
101+ }
102+
103+ TEST (PeriodicTimer, DoubleStopThrows)
104+ {
105+ // Stopping an already-stopped timer must throw.
106+ auto t1 = m::threadpool->create_periodic_timer ([]() {});
107+ t1->set (100ms);
108+ t1->stop ();
109+ t1->wait ();
110+ EXPECT_ANY_THROW (t1->stop ());
111+ }
112+
113+ TEST (PeriodicTimer, StopPreventsFurtherFiring)
114+ {
115+ std::atomic<uintmax_t > counter{};
116+
117+ auto t1 = m::threadpool->create_periodic_timer ([&]() { counter.fetch_add (1 ); });
118+
119+ t1->set (50ms);
120+ std::this_thread::sleep_for (250ms); // let it fire a few times
121+
122+ t1->stop ();
123+ t1->wait ();
124+
125+ auto const count_at_stop = counter.load ();
126+ EXPECT_GT (count_at_stop, 0u );
127+
128+ // After stop + wait no further callbacks should arrive.
129+ std::this_thread::sleep_for (200ms);
130+ EXPECT_EQ (counter.load (), count_at_stop);
131+ }
132+
133+ // ---------------------------------------------------------------------------
134+ // Deadlock regression: callback must not hold internal mutex when invoked
135+ // ---------------------------------------------------------------------------
136+
137+ TEST (PeriodicTimer, CallbackCanCallIsSetWithoutDeadlock)
138+ {
139+ // Before the fix, on_tp_timer called user code while holding m_mutex.
140+ // do_is_set() on Windows delegates to ::IsThreadpoolTimerSet() (no m_mutex),
141+ // but the test demonstrates that is_set() is callable from the callback.
142+ std::latch checked (1 );
143+ m::periodic_timer* timer_ptr = nullptr ;
144+
145+ auto t1 = m::threadpool->create_periodic_timer ([&]() {
146+ if (timer_ptr != nullptr )
147+ {
148+ (void )timer_ptr->is_set ();
149+ checked.count_down ();
150+ }
151+ });
152+
153+ timer_ptr = t1.get ();
154+ t1->set (50ms);
155+ checked.wait (); // block until callback has run and checked is_set()
156+
157+ t1->stop ();
158+ t1->wait ();
159+ }
160+
161+ TEST (PeriodicTimer, StopCanBeCalledFromCallbackWithoutDeadlock)
162+ {
163+ // Regression: on_tp_timer held m_mutex across the user callback.
164+ // stop() also takes m_mutex, so calling stop() from the callback would
165+ // deadlock. After the fix the callback is invoked with the lock released.
166+ std::latch stopped (1 );
167+ std::unique_ptr<m::periodic_timer> t1;
168+
169+ t1 = m::threadpool->create_periodic_timer ([&]() {
170+ // Stop from within the callback – must not deadlock.
171+ if (stopped.try_wait ())
172+ return ; // already stopped on a previous re-entry
173+
174+ t1->stop ();
175+ stopped.count_down ();
176+ });
177+
178+ t1->set (50ms);
179+ stopped.wait (); // block until callback has called stop()
180+ t1->wait ();
181+ }
0 commit comments