| /* |
| * SPDX-FileCopyrightText: Copyright OpenBMC Authors |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include "SerialQueue.hpp" |
| |
| #include <boost/asio/io_context.hpp> |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| |
| namespace |
| { |
| |
| class SerialQueueTest : public ::testing::Test |
| { |
| protected: |
| // Shared across the fixture's tests so the binary creates a single |
| // io_uring instance. A per-test io_context exhausts the io_uring / locked |
| // memory budget in constrained CI containers (io_uring_queue_init ENOMEM). |
| inline static boost::asio::io_context io; |
| std::shared_ptr<SerialQueue> queue = std::make_shared<SerialQueue>(io); |
| |
| // The io_context is shared, so reset its stopped state before each test; |
| // poll() leaves it stopped once drained. |
| void SetUp() override |
| { |
| io.restart(); |
| } |
| |
| // restart() before each poll so successive pumps process newly posted |
| // handlers. |
| static void pump() |
| { |
| io.restart(); |
| io.poll(); |
| } |
| }; |
| |
| TEST_F(SerialQueueTest, SubmitOnIdleRunsImmediately) |
| { |
| bool ran = false; |
| queue->submit([&ran](SerialQueue::ReleaseHandle /*handle*/) { |
| ran = true; |
| }); |
| EXPECT_TRUE(ran); |
| } |
| |
| TEST_F(SerialQueueTest, SecondSubmitQueuedUntilFirstReleases) |
| { |
| std::optional<SerialQueue::ReleaseHandle> heldA; |
| bool ranA = false; |
| bool ranB = false; |
| |
| queue->submit([&](SerialQueue::ReleaseHandle handle) { |
| ranA = true; |
| heldA = std::move(handle); // keep the slot reserved |
| }); |
| queue->submit([&](SerialQueue::ReleaseHandle /*handle*/) { ranB = true; }); |
| |
| EXPECT_TRUE(ranA); |
| EXPECT_FALSE(ranB); // B is queued behind A |
| |
| heldA.reset(); // release A's slot -> posts B |
| EXPECT_FALSE(ranB); // dispatch is via io.post(), not inline |
| io.poll(); |
| EXPECT_TRUE(ranB); |
| } |
| |
| TEST_F(SerialQueueTest, TasksRunInFifoOrder) |
| { |
| std::vector<int> order; |
| std::optional<SerialQueue::ReleaseHandle> held; |
| |
| auto makeTask = [&](int id) { |
| return [&order, &held, id](SerialQueue::ReleaseHandle handle) { |
| order.push_back(id); |
| held = std::move(handle); |
| }; |
| }; |
| |
| queue->submit(makeTask(1)); |
| queue->submit(makeTask(2)); |
| queue->submit(makeTask(3)); |
| |
| // Task 1 ran inline; release sequentially, pumping between each. |
| held.reset(); |
| pump(); |
| held.reset(); |
| pump(); |
| held.reset(); |
| pump(); |
| |
| ASSERT_EQ(order.size(), 3U); |
| EXPECT_EQ(order[0], 1); |
| EXPECT_EQ(order[1], 2); |
| EXPECT_EQ(order[2], 3); |
| } |
| |
| TEST_F(SerialQueueTest, ReleaseHandleMoveTransfersOwnership) |
| { |
| std::optional<SerialQueue::ReleaseHandle> heldA; |
| bool ranB = false; |
| |
| queue->submit([&](SerialQueue::ReleaseHandle handle) { |
| heldA = std::move(handle); |
| }); |
| queue->submit([&](SerialQueue::ReleaseHandle /*handle*/) { ranB = true; }); |
| |
| // Move ownership to another handle; the moved-from slot must not release. |
| std::optional<SerialQueue::ReleaseHandle> heldMoved = std::move(heldA); |
| // NOLINTNEXTLINE(bugprone-use-after-move): moved-from reset is the point |
| heldA.reset(); |
| pump(); |
| EXPECT_FALSE(ranB); |
| |
| // Releasing the owning handle dispatches B exactly once. |
| heldMoved.reset(); |
| pump(); |
| EXPECT_TRUE(ranB); |
| } |
| |
| TEST_F(SerialQueueTest, EmptyQueueReleaseResetsInFlight) |
| { |
| bool ranFirst = false; |
| queue->submit([&ranFirst](SerialQueue::ReleaseHandle /*handle*/) { |
| ranFirst = true; |
| }); |
| EXPECT_TRUE(ranFirst); |
| |
| // Queue returned to idle; a later submit must again run inline. |
| bool ranSecond = false; |
| queue->submit([&ranSecond](SerialQueue::ReleaseHandle /*handle*/) { |
| ranSecond = true; |
| }); |
| EXPECT_TRUE(ranSecond); |
| } |
| |
| TEST_F(SerialQueueTest, DestroyQueueWithPendingTasksNoCrash) |
| { |
| std::optional<SerialQueue::ReleaseHandle> held; |
| |
| queue->submit([&](SerialQueue::ReleaseHandle handle) { |
| held = std::move(handle); |
| }); |
| queue->submit([](SerialQueue::ReleaseHandle /*handle*/) {}); |
| |
| // Drop the queue while a handle is still outstanding. |
| queue.reset(); |
| EXPECT_NO_THROW(held.reset()); // ReleaseHandle::reset no-ops if queue gone |
| EXPECT_NO_THROW(io.poll()); |
| } |
| |
| } // namespace |