]> www.infradead.org Git - users/sagi/libnvme.git/commitdiff
test: add infra for mocking passthru ioctls
authorCaleb Sander <csander@purestorage.com>
Sun, 30 Jul 2023 23:08:59 +0000 (17:08 -0600)
committerDaniel Wagner <wagi@monom.org>
Fri, 11 Aug 2023 10:04:21 +0000 (12:04 +0200)
Functions issuing admin/IO passthru ioctls sorely lack unit tests.
It would be great for unit tests not to need a real NVMe controller.
It's also useful to be able to test responses to commands
that might be impossible to trigger with real controllers.

To that end, implement infrastructure for mocking ioctl(),
allowing tests to set expectations for the NVMe passthru ioctls
that will be issued and control the corresponding responses.

The mock library can be used with LD_PRELOAD so that libnvme's ioctl()
calls are redirected from libc. No changes in libnvme itself are needed.

Signed-off-by: Caleb Sander <csander@purestorage.com>
test/ioctl/meson.build [new file with mode: 0644]
test/ioctl/mock.c [new file with mode: 0644]
test/ioctl/mock.h [new file with mode: 0644]
test/ioctl/util.c [new file with mode: 0644]
test/ioctl/util.h [new file with mode: 0644]
test/meson.build

diff --git a/test/ioctl/meson.build b/test/ioctl/meson.build
new file mode 100644 (file)
index 0000000..d3fe6fd
--- /dev/null
@@ -0,0 +1,4 @@
+mock_ioctl = library(
+    'mock-ioctl',
+    ['mock.c', 'util.c'],
+)
diff --git a/test/ioctl/mock.c b/test/ioctl/mock.c
new file mode 100644 (file)
index 0000000..e917244
--- /dev/null
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "mock.h"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdarg.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#include "../../src/nvme/ioctl.h"
+#include "util.h"
+
+struct mock_cmds {
+       const char *name;
+       const struct mock_cmd *cmds;
+       size_t remaining_cmds;
+};
+
+static int mock_fd = -1;
+static struct mock_cmds mock_admin_cmds = {.name = "admin"};
+static struct mock_cmds mock_io_cmds = {.name = "IO"};
+
+static void set_mock_cmds(
+       struct mock_cmds *mock_cmds, const struct mock_cmd *cmds, size_t len)
+{
+       mock_cmds->cmds = cmds;
+       mock_cmds->remaining_cmds = len;
+}
+
+static void mock_cmds_done(const struct mock_cmds *mock_cmds)
+{
+       check(!mock_cmds->remaining_cmds,
+             "%zu %s commands not executed",
+             mock_cmds->remaining_cmds, mock_cmds->name);
+}
+
+void set_mock_fd(int fd)
+{
+       mock_fd = fd;
+}
+
+void set_mock_admin_cmds(const struct mock_cmd *cmds, size_t len)
+{
+       set_mock_cmds(&mock_admin_cmds, cmds, len);
+}
+
+void set_mock_io_cmds(const struct mock_cmd *cmds, size_t len)
+{
+       set_mock_cmds(&mock_io_cmds, cmds, len);
+}
+
+void end_mock_cmds(void)
+{
+       mock_cmds_done(&mock_admin_cmds);
+       mock_cmds_done(&mock_io_cmds);
+}
+
+#define execute_ioctl(cmd, mock_cmd) ({ \
+       check((cmd)->opcode == (mock_cmd)->opcode, \
+             "got opcode %" PRIu8 ", expected %" PRIu8, \
+             (cmd)->opcode, (mock_cmd)->opcode); \
+       check((cmd)->flags == (mock_cmd)->flags, \
+             "got flags %" PRIu8 ", expected %" PRIu8, \
+             (cmd)->flags, (mock_cmd)->flags); \
+       check((cmd)->nsid == (mock_cmd)->nsid, \
+             "got nsid %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->nsid, (mock_cmd)->nsid); \
+       check((cmd)->cdw2 == (mock_cmd)->cdw2, \
+             "got cdw2 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw2, (mock_cmd)->cdw2); \
+       check((cmd)->cdw3 == (mock_cmd)->cdw3, \
+             "got cdw3 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw3, (mock_cmd)->cdw3); \
+       check((cmd)->metadata_len == (mock_cmd)->metadata_len, \
+             "got metadata_len %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->metadata_len, (mock_cmd)->metadata_len); \
+       cmp((void const *)(uintptr_t)(cmd)->metadata, \
+           (mock_cmd)->metadata, \
+           (cmd)->metadata_len, \
+           "incorrect metadata"); \
+       __u32 data_len = (cmd)->data_len; \
+       check(data_len == (mock_cmd)->data_len, \
+             "got data_len %" PRIu32 ", expected %" PRIu32, \
+             data_len, (mock_cmd)->data_len); \
+       void *data = (void *)(uintptr_t)(cmd)->addr; \
+       if ((mock_cmd)->in_data) { \
+               cmp(data, (mock_cmd)->in_data, data_len, "incorrect data"); \
+       } \
+       check((cmd)->cdw10 == (mock_cmd)->cdw10, \
+             "got cdw10 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw10, (mock_cmd)->cdw10); \
+       check((cmd)->cdw11 == (mock_cmd)->cdw11, \
+             "got cdw11 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw11, (mock_cmd)->cdw11); \
+       check((cmd)->cdw12 == (mock_cmd)->cdw12, \
+             "got cdw12 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw12, (mock_cmd)->cdw12); \
+       check((cmd)->cdw13 == (mock_cmd)->cdw13, \
+             "got cdw13 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw13, (mock_cmd)->cdw13); \
+       check((cmd)->cdw14 == (mock_cmd)->cdw14, \
+             "got cdw14 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw14, (mock_cmd)->cdw14); \
+       check((cmd)->cdw15 == (mock_cmd)->cdw15, \
+             "got cdw15 %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->cdw15, (mock_cmd)->cdw15); \
+       check((cmd)->timeout_ms == (mock_cmd)->timeout_ms, \
+             "got timeout_ms %" PRIu32 ", expected %" PRIu32, \
+             (cmd)->timeout_ms, (mock_cmd)->timeout_ms); \
+       (cmd)->result = (mock_cmd)->result; \
+       if ((mock_cmd)->out_data) { \
+               memcpy(data, (mock_cmd)->out_data, data_len); \
+       } \
+})
+
+int ioctl(int fd, unsigned long request, ...)
+{
+       struct mock_cmds *mock_cmds;
+       bool result64;
+       const struct mock_cmd *mock_cmd;
+       va_list args;
+       void *cmd;
+
+       check(fd == mock_fd, "got fd %d, expected %d", fd, mock_fd);
+       switch (request) {
+       case NVME_IOCTL_ADMIN_CMD:
+               mock_cmds = &mock_admin_cmds;
+               result64 = false;
+               break;
+       case NVME_IOCTL_ADMIN64_CMD:
+               mock_cmds = &mock_admin_cmds;
+               result64 = true;
+               break;
+       case NVME_IOCTL_IO_CMD:
+               mock_cmds = &mock_io_cmds;
+               result64 = false;
+               break;
+       case NVME_IOCTL_IO64_CMD:
+               mock_cmds = &mock_io_cmds;
+               result64 = true;
+               break;
+       default:
+               fail("unexpected %s %lu", __func__, request);
+       }
+       check(mock_cmds->remaining_cmds,
+             "unexpected %s command", mock_cmds->name);
+       mock_cmd = mock_cmds->cmds++;
+       mock_cmds->remaining_cmds--;
+
+       va_start(args, request);
+       cmd = va_arg(args, void *);
+       va_end(args);
+       if (result64) {
+               execute_ioctl((struct nvme_passthru_cmd64 *)cmd, mock_cmd);
+       } else {
+               check((uint32_t)mock_cmd->result == mock_cmd->result,
+                     "expected 64-bit %s for result %" PRIu64,
+                     __func__, mock_cmd->result);
+               execute_ioctl((struct nvme_passthru_cmd *)cmd, mock_cmd);
+       }
+       if (mock_cmd->err < 0) {
+               errno = -mock_cmd->err;
+               return -1;
+       }
+
+       return mock_cmd->err;
+}
diff --git a/test/ioctl/mock.h b/test/ioctl/mock.h
new file mode 100644 (file)
index 0000000..192eba8
--- /dev/null
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef _LIBNVME_TEST_IOCTL_MOCK_H
+#define _LIBNVME_TEST_IOCTL_MOCK_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * struct mock_cmd - a mock NVMe passthru ioctl() invocation
+ * @opcode: the expected `opcode` passed to ioctl()
+ * @flags: the expected `flags` passed to ioctl()
+ * @nsid: the expected `nsid` passed to ioctl()
+ * @cdw2: the expected `cdw2` passed to ioctl()
+ * @cdw3: the expected `cdw3` passed to ioctl()
+ * @metadata: the expected `metadata` of length `metadata_len` passed to ioctl()
+ * @in_data: the expected `addr` of length `data_len` passed to ioctl().
+ *           Set this to NULL to skip checking the data,
+ *           for example if the command is in the read direction.
+ * @metadata_len: the expected `metadata_len` passed to ioctl()
+ * @data_len: the expected `data_len` passed to ioctl()
+ * @cdw10: the expected `cdw10` passed to ioctl()
+ * @cdw11: the expected `cdw11` passed to ioctl()
+ * @cdw12: the expected `cdw12` passed to ioctl()
+ * @cdw13: the expected `cdw13` passed to ioctl()
+ * @cdw14: the expected `cdw14` passed to ioctl()
+ * @cdw15: the expected `cdw15` passed to ioctl()
+ * @timeout_ms: the expected `timeout_ms` passed to ioctl()
+ * @out_data: if not NULL, `data_len` bytes to copy to the caller's `addr`
+ * @result: copied to the caller's `result`.
+ *       If `result` doesn't fit in a u32, the ioctl() must be the 64-bit one.
+ * @err: If negative, ioctl() returns -1 and sets `errno` to `-err`.
+ *       Otherwise, ioctl() returns `err`, representing a NVMe status code.
+ */
+struct mock_cmd {
+       uint8_t opcode;
+       uint8_t flags;
+       uint32_t nsid;
+       uint32_t cdw2;
+       uint32_t cdw3;
+       const void *metadata;
+       const void *in_data;
+       uint32_t metadata_len;
+       uint32_t data_len;
+       uint32_t cdw10;
+       uint32_t cdw11;
+       uint32_t cdw12;
+       uint32_t cdw13;
+       uint32_t cdw14;
+       uint32_t cdw15;
+       uint32_t timeout_ms;
+       const void *out_data;
+       uint64_t result;
+       int err;
+};
+
+/**
+ * set_mock_fd() - sets the expected file descriptor for NVMe passthru ioctls()
+ * @fd: file descriptor expected to be passed to ioctl()
+ */
+void set_mock_fd(int fd);
+
+/**
+ * set_mock_admin_cmds() - mocks NVMe admin passthru ioctl() invocations
+ * @cmds: pointer to start of the mock_cmd slice
+ * @len: length of the mock_cmd slice (number of ioctl() invocations)
+ *
+ * Provides a sequence of mocks for NVMe admin passthru ioctl() invocations.
+ * Each ioctl() consumes the next mock from the sequence.
+ * Its arguments are checked against the mock's expected arguments,
+ * aborting the process if unexpected arguments are passed.
+ * The mock results (return value, NVMe result and data)
+ * are returned from the ioctl().
+ *
+ * Analogous to set_mock_io_cmds(), but for admin commands.
+ * Both admin and IO mocks can be active at the same time.
+ */
+void set_mock_admin_cmds(const struct mock_cmd *cmds, size_t len);
+
+/**
+ * set_mock_io_cmds() - mocks NVMe IO passthru ioctl() invocations
+ * @cmds: pointer to start of the mock_cmd slice
+ * @len: length of the mock_cmd slice (number of ioctl() invocations)
+ *
+ * Provides a sequence of mocks for NVMe IO passthru ioctl() invocations.
+ * Each ioctl() consumes the next mock from the sequence.
+ * Its arguments are checked against the mock's expected arguments,
+ * aborting the process if unexpected arguments are passed.
+ * The mock results (return value, NVMe result and data)
+ * are returned from the ioctl().
+ *
+ * Analogous to set_mock_admin_cmds(), but for IO commands.
+ * Both admin and IO mocks can be active at the same time.
+ */
+void set_mock_io_cmds(const struct mock_cmd *cmds, size_t len);
+
+/**
+ * end_mock_cmds() - finishes mocking NVMe passthru ioctl() invocations
+ *
+ * Checks that all mock ioctl() invocations were performed.
+ */
+void end_mock_cmds(void);
+
+#endif /* #ifndef _LIBNVME_TEST_IOCTL_MOCK_H */
diff --git a/test/ioctl/util.c b/test/ioctl/util.c
new file mode 100644 (file)
index 0000000..7fc551c
--- /dev/null
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "util.h"
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void hexdump(const uint8_t *buf, size_t len)
+{
+       size_t i = 0;
+
+       if (!len)
+               return;
+
+       for (;;) {
+               fprintf(stderr, "%02X", buf[i++]);
+               if (i >= len)
+                       break;
+
+               fputc(i % 16 > 0 ? ' ' : '\n', stderr);
+       }
+       fputc('\n', stderr);
+}
+
+void fail(const char *fmt, ...)
+{
+       va_list args;
+
+       va_start(args, fmt);
+       vfprintf(stderr, fmt, args);
+       va_end(args);
+       fputc('\n', stderr);
+       abort();
+}
+
+void cmp(const void *actual, const void *expected, size_t len, const char *msg)
+{
+       if (memcmp(actual, expected, len) == 0)
+               return;
+
+       fputs(msg, stderr);
+       fputs("\nactual:\n", stderr);
+       hexdump(actual, len);
+       fputs("expected:\n", stderr);
+       hexdump(expected, len);
+       abort();
+}
diff --git a/test/ioctl/util.h b/test/ioctl/util.h
new file mode 100644 (file)
index 0000000..159a347
--- /dev/null
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef _LIBNVME_TEST_IOCTL_UTIL_H
+#define _LIBNVME_TEST_IOCTL_UTIL_H
+
+#include <stddef.h>
+#include <stdnoreturn.h>
+
+noreturn void fail(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
+
+#define check(condition, fmt...) ((condition) || (fail(fmt), 0))
+
+void cmp(const void *actual, const void *expected, size_t len, const char *msg);
+
+#endif /* #ifndef _LIBNVME_TEST_IOCTL_UTIL_H */
index 49cd1acc74d09046aa30cc087f7d01322d0ba1bd..a8cfe9f950b59acdda343533e4e7d6bf47b52f08 100644 (file)
@@ -84,4 +84,5 @@ if conf.get('HAVE_NETDB')
     test('Test util.c', test_util)
 endif
 
+subdir('ioctl')
 subdir('nbft')