blob: 7a646e2f5331767fc914dee28bcef9ba2000be5e [file] [log] [blame]
#include <fmt/format.h>
#include <poll.h>
#include <sys/utsname.h>
#include <stdplus/io_uring.hpp>
#include <stdplus/util/cexec.hpp>
#include <array>
#include <charconv>
#include <chrono>
#include <optional>
#include <string_view>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace stdplus
{
using testing::_;
static std::string_view extractVerNum(std::string_view& str)
{
auto ret = str.substr(0, str.find('.'));
str.remove_prefix(ret.size() + (ret.size() == str.size() ? 0 : 1));
return ret;
}
template <typename T>
static T intFromStr(std::string_view str)
{
T ret;
auto res = std::from_chars(str.data(), str.data() + str.size(), ret);
if (res.ec != std::errc{} || res.ptr != str.data() + str.size())
{
throw std::invalid_argument("Bad kernel version");
}
return ret;
}
static bool isKernelSafe(std::string_view ver, uint8_t smajor, uint8_t sminor)
{
auto major = intFromStr<uint8_t>(extractVerNum(ver));
auto minor = intFromStr<uint8_t>(extractVerNum(ver));
return major > smajor || (major == smajor && minor >= sminor);
}
TEST(KernelInfo, VersionSafe)
{
EXPECT_THROW(isKernelSafe("a", 5, 10), std::invalid_argument);
EXPECT_THROW(isKernelSafe("3a.3b.c", 5, 10), std::invalid_argument);
EXPECT_FALSE(isKernelSafe("4.11.20-nfd", 5, 10));
EXPECT_FALSE(isKernelSafe("4.11.20", 5, 10));
EXPECT_FALSE(isKernelSafe("4.11", 5, 10));
EXPECT_FALSE(isKernelSafe("5.9.0", 5, 10));
EXPECT_TRUE(isKernelSafe("5.10.1", 5, 10));
EXPECT_TRUE(isKernelSafe("6.0.0", 5, 10));
EXPECT_TRUE(isKernelSafe("6.0.0-abc", 5, 10));
}
static bool checkKernelSafe(uint8_t smajor, uint8_t sminor)
{
utsname uts;
CHECK_ERRNO(uname(&uts), "uname");
return isKernelSafe(uts.release, smajor, sminor);
}
TEST(KernelInfo, Print)
{
utsname uts;
ASSERT_NO_THROW(CHECK_ERRNO(uname(&uts), "uname"));
fmt::print("{} {} {} {} {}", uts.sysname, uts.nodename, uts.release,
uts.version, uts.machine);
}
TEST(Convert, ChronoToKTS)
{
const auto ns = 700;
const auto s = 5;
const auto kts =
chronoToKTS(std::chrono::nanoseconds(ns) + std::chrono::seconds(s));
EXPECT_EQ(kts.tv_sec, s);
EXPECT_EQ(kts.tv_nsec, ns);
}
class MockHandler : public IoUring::CQEHandler
{
public:
MOCK_METHOD(void, handleCQE, (io_uring_cqe&), (noexcept, override));
};
class IoUringTest : public testing::Test
{
protected:
static void SetUpTestCase()
{
io_uring r;
if (io_uring_queue_init(1, &r, 0) == -ENOSYS)
{
// Not supported, skip running this test
exit(77);
}
io_uring_queue_exit(&r);
}
std::array<testing::StrictMock<MockHandler>, 2> h;
IoUring ring;
void testFdWrite(int fd, int flags,
std::optional<int> expected_res = std::nullopt)
{
auto& sqe = ring.getSQE();
std::string_view data = "Test\n";
io_uring_prep_write(&sqe, fd, data.data(), data.size(), 0);
sqe.flags |= flags;
ring.setHandler(sqe, &h[0]);
ring.submit();
ring.wait();
EXPECT_CALL(h[0], handleCQE(_)).WillOnce([&](io_uring_cqe& cqe) {
EXPECT_EQ(cqe.res, expected_res.value_or(data.size()));
});
ring.process();
testing::Mock::VerifyAndClearExpectations(&h[0]);
}
};
TEST_F(IoUringTest, NullHandler)
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.submit();
ring.process();
}
TEST_F(IoUringTest, HandlerCalled)
{
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.setHandler(sqe, &h[0]);
}
// Nothing should happen without submission
ring.process();
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.setHandler(sqe, &h[1]);
}
// Handle all of the outstanding requests
ring.submit();
EXPECT_CALL(h[0], handleCQE(_));
EXPECT_CALL(h[1], handleCQE(_));
ring.process();
testing::Mock::VerifyAndClearExpectations(&h[0]);
testing::Mock::VerifyAndClearExpectations(&h[1]);
}
TEST_F(IoUringTest, HandlerReplacement)
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.setHandler(sqe, &h[0]);
// Setting a new handler should cancel the old one
EXPECT_CALL(h[0], handleCQE(_));
ring.setHandler(sqe, &h[1]);
testing::Mock::VerifyAndClearExpectations(&h[0]);
// Setting a null handler should cancel the old one
EXPECT_CALL(h[1], handleCQE(_));
ring.setHandler(sqe, nullptr);
testing::Mock::VerifyAndClearExpectations(&h[1]);
// Set it back twice and make sure it isn't recognized idempotently
ring.setHandler(sqe, &h[1]);
ring.setHandler(sqe, &h[1]);
// Make sure it still works
ring.submit();
EXPECT_CALL(h[1], handleCQE(_));
ring.process();
testing::Mock::VerifyAndClearExpectations(&h[1]);
}
TEST_F(IoUringTest, EventFd)
{
auto& efd = ring.getEventFd();
for (size_t i = 0; i < h.size(); ++i)
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.setHandler(sqe, &h[i]);
ring.submit();
}
// Our event fd should become ready
pollfd pfd;
pfd.fd = efd.get();
pfd.events = POLLIN;
ASSERT_EQ(1, poll(&pfd, 1, 100));
// Handle all of the outstanding requests
EXPECT_CALL(h[0], handleCQE(_));
EXPECT_CALL(h[1], handleCQE(_));
ring.processEvents();
testing::Mock::VerifyAndClearExpectations(&h[0]);
testing::Mock::VerifyAndClearExpectations(&h[1]);
// Our event fd should be empty
ASSERT_EQ(0, poll(&pfd, 1, 100));
}
TEST_F(IoUringTest, Wait)
{
auto& sqe = ring.getSQE();
auto kts = chronoToKTS(std::chrono::milliseconds(1));
io_uring_prep_timeout(&sqe, &kts, 0, 0);
ring.setHandler(sqe, &h[0]);
ring.submit();
ring.wait(std::chrono::seconds(1));
EXPECT_CALL(h[0], handleCQE(_));
}
TEST_F(IoUringTest, HandleCalledOnDestroy)
{
auto& sqe = ring.getSQE();
io_uring_prep_nop(&sqe);
ring.setHandler(sqe, &h[0]);
EXPECT_CALL(h[0], handleCQE(_));
}
TEST_F(IoUringTest, RegisterFiles)
{
// Earlier kernels had buggy support for registered files
if (!checkKernelSafe(5, 10))
{
GTEST_SKIP();
}
std::optional<IoUring::FileHandle> fh;
// Slots are always allocated linearly and re-used if invalidated
fh = ring.registerFile(STDERR_FILENO);
EXPECT_GT(ring.getFiles().size(), 1);
EXPECT_EQ(*fh, 0);
fh = ring.registerFile(STDOUT_FILENO);
EXPECT_EQ(*fh, 1);
EXPECT_GT(ring.getFiles().size(), 2);
EXPECT_EQ(ring.getFiles()[0], -1);
EXPECT_EQ(ring.getFiles()[1], STDOUT_FILENO);
// The first handle should have dropped and can be replaced
fh = ring.registerFile(STDERR_FILENO);
EXPECT_EQ(*fh, 0);
EXPECT_GT(ring.getFiles().size(), 1);
EXPECT_EQ(ring.getFiles()[0], STDERR_FILENO);
EXPECT_EQ(ring.getFiles()[1], -1);
// We should be able to write to stderr via the fixed file and regular fd
testFdWrite(STDERR_FILENO, 0);
testFdWrite(*fh, IOSQE_FIXED_FILE);
// Without registration we should only be able to write to the regular fd
fh.reset();
testFdWrite(STDERR_FILENO, 0);
testFdWrite(*fh, IOSQE_FIXED_FILE, -EBADF);
std::vector<IoUring::FileHandle> fhs;
EXPECT_LT(ring.getFiles().size(), 9);
ring.reserveFiles(9);
EXPECT_EQ(ring.getFiles().size(), 9);
for (size_t i = 0; i < 9; ++i)
{
fhs.emplace_back(ring.registerFile(STDERR_FILENO));
testFdWrite(fhs.back(), IOSQE_FIXED_FILE);
}
EXPECT_EQ(ring.getFiles().size(), 9);
fhs.emplace_back(ring.registerFile(STDERR_FILENO));
testFdWrite(fhs.back(), IOSQE_FIXED_FILE);
EXPECT_GE(ring.getFiles().size(), 10);
}
} // namespace stdplus