test-scheduler-thread-safe.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // test-scheduler-thread-safe.cpp - Unit tests for _TASK_THREAD_SAFE functionality
  2. // This file contains tests for the TaskScheduler thread-safe operations
  3. // Covers multi-threaded task control, ISR simulation, and request queue operations
  4. //
  5. // THREAD SAFETY TESTING NOTES:
  6. // ============================
  7. // 1. Tests use C++ std::thread for simulating multi-threaded environments
  8. // 2. Queue implementation uses std::queue with mutex protection
  9. // 3. Tests verify that requestAction() safely handles concurrent requests
  10. // 4. StatusRequest signaling from simulated ISRs is tested
  11. // 5. Worker thread task manipulation is verified
  12. //
  13. // Linux/Desktop Implementation:
  14. // - Uses std::queue + std::mutex instead of FreeRTOS queue
  15. // - Simulates ISR context using thread-local storage
  16. // - Compatible with Google Test framework on Ubuntu
  17. #include <gtest/gtest.h>
  18. #include "Arduino.h"
  19. #include "TaskSchedulerDeclarations.h"
  20. #include "test-queue-impl.h"
  21. #include <thread>
  22. #include <atomic>
  23. // Thread-local storage to simulate ISR context
  24. thread_local bool isInISRContext = false;
  25. // ============================================
  26. // Global Test State
  27. // ============================================
  28. std::vector<std::string> test_output;
  29. std::atomic<int> task_execution_count{0};
  30. std::atomic<int> thread1_requests{0};
  31. std::atomic<int> thread2_requests{0};
  32. std::atomic<int> isr_requests{0};
  33. // ============================================
  34. // Test Callbacks
  35. // ============================================
  36. void thread_safe_task_callback() {
  37. task_execution_count++;
  38. test_output.push_back("Thread-safe task executed");
  39. }
  40. void fast_task_callback() {
  41. test_output.push_back("Fast task executed");
  42. }
  43. void interval_test_callback() {
  44. static int counter = 0;
  45. counter++;
  46. test_output.push_back("Interval task #" + std::to_string(counter));
  47. }
  48. // ============================================
  49. // Test Fixture
  50. // ============================================
  51. class ThreadSafeTest : public ::testing::Test {
  52. protected:
  53. void SetUp() override {
  54. test_output.clear();
  55. task_execution_count = 0;
  56. thread1_requests = 0;
  57. thread2_requests = 0;
  58. isr_requests = 0;
  59. clearTaskRequestQueue();
  60. isInISRContext = false;
  61. }
  62. void TearDown() override {
  63. clearTaskRequestQueue();
  64. }
  65. // Helper: Run scheduler for specified duration
  66. void runScheduler(Scheduler& ts, unsigned long duration_ms) {
  67. unsigned long start = millis();
  68. while (millis() - start < duration_ms) {
  69. ts.execute();
  70. delay(1);
  71. }
  72. }
  73. // Helper: Wait for condition with timeout
  74. template<typename Condition>
  75. bool waitForCondition(Condition cond, unsigned long timeout_ms) {
  76. unsigned long start = millis();
  77. while (millis() - start < timeout_ms) {
  78. if (cond()) return true;
  79. delay(10);
  80. }
  81. return false;
  82. }
  83. };
  84. // ============================================
  85. // Basic Queue Tests
  86. // ============================================
  87. TEST_F(ThreadSafeTest, QueueEnqueueDequeue) {
  88. _task_request_t req;
  89. req.req_type = TASK_REQUEST_ENABLE_0;
  90. req.object_ptr = (void*)0x1234;
  91. req.param1 = 100;
  92. // Enqueue
  93. EXPECT_TRUE(_task_enqueue_request(&req));
  94. EXPECT_EQ(getQueueSize(), 1);
  95. // Dequeue
  96. _task_request_t dequeued;
  97. EXPECT_TRUE(_task_dequeue_request(&dequeued));
  98. EXPECT_EQ(getQueueSize(), 0);
  99. // Verify data
  100. EXPECT_EQ(dequeued.req_type, TASK_REQUEST_ENABLE_0);
  101. EXPECT_EQ(dequeued.object_ptr, (void*)0x1234);
  102. EXPECT_EQ(dequeued.param1, 100);
  103. }
  104. TEST_F(ThreadSafeTest, QueueFIFOOrder) {
  105. // Enqueue multiple requests
  106. for (int i = 0; i < 10; i++) {
  107. _task_request_t req;
  108. req.req_type = TASK_REQUEST_ENABLE_0;
  109. req.param1 = i;
  110. EXPECT_TRUE(_task_enqueue_request(&req));
  111. }
  112. EXPECT_EQ(getQueueSize(), 10);
  113. // Dequeue and verify order
  114. for (int i = 0; i < 10; i++) {
  115. _task_request_t req;
  116. EXPECT_TRUE(_task_dequeue_request(&req));
  117. EXPECT_EQ(req.param1, (unsigned long)i);
  118. }
  119. EXPECT_EQ(getQueueSize(), 0);
  120. }
  121. TEST_F(ThreadSafeTest, QueueEmptyDequeue) {
  122. _task_request_t req;
  123. EXPECT_FALSE(_task_dequeue_request(&req));
  124. }
  125. // ============================================
  126. // Thread-Safe Task Control Tests
  127. // ============================================
  128. TEST_F(ThreadSafeTest, RequestActionEnableTask) {
  129. Scheduler ts;
  130. Task testTask(100, 5, &thread_safe_task_callback, &ts, false);
  131. // Task should be disabled initially
  132. EXPECT_FALSE(testTask.isEnabled());
  133. // Request enable via requestAction
  134. bool success = ts.requestAction(&testTask, TASK_REQUEST_ENABLE_0, 0, 0, 0, 0, 0);
  135. EXPECT_TRUE(success);
  136. EXPECT_EQ(getQueueSize(), 1);
  137. // Process requests
  138. ts.execute();
  139. // Task should now be enabled
  140. EXPECT_TRUE(testTask.isEnabled());
  141. EXPECT_EQ(getQueueSize(), 0);
  142. }
  143. TEST_F(ThreadSafeTest, RequestActionDisableTask) {
  144. Scheduler ts;
  145. Task testTask(100, 5, &thread_safe_task_callback, &ts, true);
  146. // Task should be enabled initially
  147. EXPECT_TRUE(testTask.isEnabled());
  148. // Request disable via requestAction
  149. bool success = ts.requestAction(&testTask, TASK_REQUEST_DISABLE_0, 0, 0, 0, 0, 0);
  150. EXPECT_TRUE(success);
  151. // Process requests
  152. ts.execute();
  153. // Task should now be disabled
  154. EXPECT_FALSE(testTask.isEnabled());
  155. }
  156. TEST_F(ThreadSafeTest, RequestActionChangeInterval) {
  157. Scheduler ts;
  158. Task testTask(100, TASK_FOREVER, &thread_safe_task_callback, &ts, true);
  159. EXPECT_EQ(testTask.getInterval(), 100UL);
  160. // Request interval change
  161. ts.requestAction(&testTask, TASK_REQUEST_SETINTERVAL_1, 500, 0, 0, 0, 0);
  162. // Process requests
  163. ts.execute();
  164. // Interval should be updated
  165. EXPECT_EQ(testTask.getInterval(), 500UL);
  166. }
  167. // ============================================
  168. // Multi-Threaded Tests
  169. // ============================================
  170. TEST_F(ThreadSafeTest, ConcurrentEnableRequests) {
  171. Scheduler ts;
  172. Task task1(50, TASK_FOREVER, &thread_safe_task_callback, &ts, false);
  173. Task task2(50, TASK_FOREVER, &fast_task_callback, &ts, false);
  174. // Launch two threads that both try to enable tasks
  175. std::thread t1([&]() {
  176. for (int i = 0; i < 10; i++) {
  177. ts.requestAction(&task1, TASK_REQUEST_ENABLE_0, 0, 0, 0, 0, 0);
  178. thread1_requests++;
  179. std::this_thread::sleep_for(std::chrono::milliseconds(5));
  180. }
  181. });
  182. std::thread t2([&]() {
  183. for (int i = 0; i < 10; i++) {
  184. ts.requestAction(&task2, TASK_REQUEST_ENABLE_0, 0, 0, 0, 0, 0);
  185. thread2_requests++;
  186. std::this_thread::sleep_for(std::chrono::milliseconds(5));
  187. }
  188. });
  189. // Process requests in main thread
  190. std::thread scheduler_thread([&]() {
  191. for (int i = 0; i < 100; i++) {
  192. ts.execute();
  193. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  194. }
  195. });
  196. t1.join();
  197. t2.join();
  198. scheduler_thread.join();
  199. // All requests should have been processed
  200. EXPECT_EQ(thread1_requests.load(), 10);
  201. EXPECT_EQ(thread2_requests.load(), 10);
  202. EXPECT_TRUE(task1.isEnabled());
  203. EXPECT_TRUE(task2.isEnabled());
  204. }
  205. TEST_F(ThreadSafeTest, WorkerThreadChangesInterval) {
  206. Scheduler ts;
  207. Task testTask(100, TASK_FOREVER, &interval_test_callback, &ts, true);
  208. std::atomic<bool> worker_running{true};
  209. // Worker thread that periodically changes interval
  210. std::thread worker([&]() {
  211. int intervals[] = {50, 100, 150, 200, 250};
  212. int idx = 0;
  213. while (worker_running && idx < 5) {
  214. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  215. ts.requestAction(&testTask, TASK_REQUEST_SETINTERVAL_1,
  216. intervals[idx], 0, 0, 0, 0);
  217. idx++;
  218. }
  219. });
  220. // Run scheduler
  221. runScheduler(ts, 600);
  222. worker_running = false;
  223. worker.join();
  224. // Task should still be running
  225. EXPECT_TRUE(testTask.isEnabled());
  226. // Interval should have been changed multiple times
  227. EXPECT_GT(test_output.size(), 3);
  228. }
  229. // ============================================
  230. // Simulated ISR Tests
  231. // ============================================
  232. TEST_F(ThreadSafeTest, ISRTriggersStatusRequest) {
  233. Scheduler ts;
  234. StatusRequest buttonPressed;
  235. Task responseTask(TASK_IMMEDIATE, TASK_ONCE, &fast_task_callback, &ts, false);
  236. // Set up task to wait for status request
  237. responseTask.waitFor(&buttonPressed);
  238. // Simulate ISR in separate thread
  239. std::thread isr_simulation([&]() {
  240. isInISRContext = true; // Mark as ISR context
  241. std::this_thread::sleep_for(std::chrono::milliseconds(50));
  242. // Signal from "ISR"
  243. ts.requestAction(&buttonPressed, TASK_SR_REQUEST_SIGNALCOMPLETE_1, 0, 0, 0, 0, 0);
  244. isr_requests++;
  245. isInISRContext = false;
  246. });
  247. // Run scheduler
  248. runScheduler(ts, 200);
  249. isr_simulation.join();
  250. // Task should have executed in response to signal
  251. EXPECT_EQ(isr_requests.load(), 1);
  252. EXPECT_GT(test_output.size(), 0);
  253. EXPECT_EQ(test_output[0], "Fast task executed");
  254. }
  255. TEST_F(ThreadSafeTest, MultipleISRsWithDifferentRequests) {
  256. Scheduler ts;
  257. Task task1(200, TASK_FOREVER, &thread_safe_task_callback, &ts, false);
  258. Task task2(200, TASK_FOREVER, &fast_task_callback, &ts, false);
  259. // Simulate multiple ISRs
  260. std::thread isr1([&]() {
  261. for (int i = 0; i < 5; i++) {
  262. std::this_thread::sleep_for(std::chrono::milliseconds(30));
  263. ts.requestAction(&task1, TASK_REQUEST_ENABLE_0, 0, 0, 0, 0, 0);
  264. }
  265. });
  266. std::thread isr2([&]() {
  267. for (int i = 0; i < 5; i++) {
  268. std::this_thread::sleep_for(std::chrono::milliseconds(35));
  269. ts.requestAction(&task2, TASK_REQUEST_ENABLE_0, 0, 0, 0, 0, 0);
  270. }
  271. });
  272. // Run scheduler
  273. runScheduler(ts, 300);
  274. isr1.join();
  275. isr2.join();
  276. // Both tasks should be enabled
  277. EXPECT_TRUE(task1.isEnabled());
  278. EXPECT_TRUE(task2.isEnabled());
  279. }
  280. // ============================================
  281. // Stress Tests
  282. // ============================================
  283. TEST_F(ThreadSafeTest, HighFrequencyRequests) {
  284. Scheduler ts;
  285. Task testTask(10, TASK_FOREVER, &thread_safe_task_callback, &ts, true);
  286. std::atomic<int> successful_requests{0};
  287. // Many threads sending requests rapidly
  288. std::vector<std::thread> threads;
  289. for (int t = 0; t < 5; t++) {
  290. threads.emplace_back([&]() {
  291. for (int i = 0; i < 20; i++) {
  292. if (ts.requestAction(&testTask, TASK_REQUEST_SETINTERVAL_1,
  293. 10 + i, 0, 0, 0, 0)) {
  294. successful_requests++;
  295. }
  296. std::this_thread::sleep_for(std::chrono::milliseconds(1));
  297. }
  298. });
  299. }
  300. // Process requests
  301. for (int i = 0; i < 500; i++) {
  302. ts.execute();
  303. delay(1);
  304. }
  305. // Wait for all threads
  306. for (auto& t : threads) {
  307. t.join();
  308. }
  309. // Most requests should have succeeded
  310. EXPECT_GT(successful_requests.load(), 90); // Allow some to fail due to timing
  311. }
  312. TEST_F(ThreadSafeTest, QueueOverflowHandling) {
  313. Scheduler ts;
  314. Task testTask(100, TASK_FOREVER, &thread_safe_task_callback, &ts, true);
  315. // Try to overflow the queue
  316. int successful = 0;
  317. int failed = 0;
  318. for (size_t i = 0; i < MAX_QUEUE_SIZE + 50; i++) {
  319. if (ts.requestAction(&testTask, TASK_REQUEST_SETINTERVAL_1, i, 0, 0, 0, 0)) {
  320. successful++;
  321. } else {
  322. failed++;
  323. }
  324. }
  325. // Should have some failures due to queue limit
  326. EXPECT_GT(failed, 0);
  327. EXPECT_LE(successful, (int)MAX_QUEUE_SIZE);
  328. // Process all queued requests
  329. for (int i = 0; i < 200; i++) {
  330. ts.execute();
  331. delay(1);
  332. }
  333. // Queue should be empty now
  334. EXPECT_EQ(getQueueSize(), 0);
  335. }
  336. // ============================================
  337. // Integration Tests
  338. // ============================================
  339. TEST_F(ThreadSafeTest, RealWorldScenario) {
  340. Scheduler ts;
  341. Task blinkTask(100, TASK_FOREVER, &interval_test_callback, &ts, true);
  342. StatusRequest dataReady;
  343. Task dataTask(TASK_IMMEDIATE, TASK_ONCE, &fast_task_callback, &ts, false);
  344. dataTask.waitFor(&dataReady);
  345. std::atomic<bool> running{true};
  346. // Worker thread: Adjusts blink rate
  347. std::thread worker([&]() {
  348. int count = 0;
  349. while (running && count < 5) {
  350. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  351. ts.requestAction(&blinkTask, TASK_REQUEST_SETINTERVAL_1,
  352. 50 + count * 50, 0, 0, 0, 0);
  353. count++;
  354. }
  355. });
  356. // Simulated ISR: Signals data ready
  357. std::thread isr([&]() {
  358. std::this_thread::sleep_for(std::chrono::milliseconds(250));
  359. ts.requestAction(&dataReady, TASK_SR_REQUEST_SIGNALCOMPLETE_1, 0, 0, 0, 0, 0);
  360. });
  361. // Run scheduler
  362. runScheduler(ts, 600);
  363. running = false;
  364. worker.join();
  365. isr.join();
  366. // Verify both tasks executed
  367. EXPECT_GT(test_output.size(), 2);
  368. // Should have both interval task and fast task executions
  369. bool hasIntervalTask = false;
  370. bool hasFastTask = false;
  371. for (const auto& output : test_output) {
  372. if (output.find("Interval task") != std::string::npos) hasIntervalTask = true;
  373. if (output.find("Fast task") != std::string::npos) hasFastTask = true;
  374. }
  375. EXPECT_TRUE(hasIntervalTask);
  376. EXPECT_TRUE(hasFastTask);
  377. }
  378. // ============================================
  379. // Main
  380. // ============================================
  381. int main(int argc, char **argv) {
  382. ::testing::InitGoogleTest(&argc, argv);
  383. return RUN_ALL_TESTS();
  384. }