The branch main has been updated by melifaro:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=3e5d0784b9b5296bda801add034b057ad68237f7

commit 3e5d0784b9b5296bda801add034b057ad68237f7
Author:     Alexander V. Chernikov <melif...@freebsd.org>
AuthorDate: 2023-04-14 15:25:50 +0000
Commit:     Alexander V. Chernikov <melif...@freebsd.org>
CommitDate: 2023-04-14 15:47:55 +0000

    Testing: add framework for the kernel unit tests.
    
    This changes intends to reduce the bar to the kernel unit-testing by
     introducing a new kernel-testing framework ("ktest") based on Netlink,
     loadable test modules and python test suite integration.
    
    This framework provides the following features:
    * Integration to the FreeBSD test suite
    * Automatic test discovery
    * Automatic test module loading
    * Minimal boiler-plate code in both kernel and userland
    * Passing any metadata to the test
    * Convenient environment pre-setup using python testing framework
    * Streaming messages from the kernel to the userland
    * Running tests in the dedicated taskqueues
    * Skipping or parametrizing tests
    
    Differential Revision: https://reviews.freebsd.org/D39385
    MFC after:      2 weeks
---
 sys/modules/ktest/Makefile                      |   7 +
 sys/modules/ktest/ktest/Makefile                |  14 +
 sys/modules/ktest/ktest_example/Makefile        |  13 +
 sys/tests/ktest.c                               | 414 ++++++++++++++++++++++++
 sys/tests/ktest.h                               | 141 ++++++++
 sys/tests/ktest_example.c                       | 134 ++++++++
 tests/atf_python/Makefile                       |   2 +-
 tests/atf_python/atf_pytest.py                  |   6 +
 tests/atf_python/ktest.py                       | 173 ++++++++++
 tests/atf_python/sys/netlink/attrs.py           |   2 +
 tests/atf_python/sys/netlink/base_headers.py    |   7 +
 tests/atf_python/sys/netlink/netlink.py         |   2 +-
 tests/atf_python/sys/netlink/netlink_generic.py | 118 +++++++
 tests/atf_python/utils.py                       |   5 +
 tests/conftest.py                               |   6 +
 tests/examples/Makefile                         |   1 +
 tests/examples/test_ktest_example.py            |  35 ++
 17 files changed, 1078 insertions(+), 2 deletions(-)

diff --git a/sys/modules/ktest/Makefile b/sys/modules/ktest/Makefile
new file mode 100644
index 000000000000..21c94caabc30
--- /dev/null
+++ b/sys/modules/ktest/Makefile
@@ -0,0 +1,7 @@
+SYSDIR?=${SRCTOP}/sys
+.include "${SYSDIR}/conf/kern.opts.mk"
+
+SUBDIR=        ktest \
+       ktest_example
+
+.include <bsd.subdir.mk>
diff --git a/sys/modules/ktest/ktest/Makefile b/sys/modules/ktest/ktest/Makefile
new file mode 100644
index 000000000000..86ed957ac2b7
--- /dev/null
+++ b/sys/modules/ktest/ktest/Makefile
@@ -0,0 +1,14 @@
+# $FreeBSD$
+
+PACKAGE=       tests
+
+SYSDIR?=${SRCTOP}/sys
+.include "${SYSDIR}/conf/kern.opts.mk"
+
+.PATH: ${SYSDIR}/tests
+
+KMOD=  ktest
+SRCS=  ktest.c
+SRCS+= opt_netlink.h
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/ktest/ktest_example/Makefile 
b/sys/modules/ktest/ktest_example/Makefile
new file mode 100644
index 000000000000..b4a3e778e2ed
--- /dev/null
+++ b/sys/modules/ktest/ktest_example/Makefile
@@ -0,0 +1,13 @@
+# $FreeBSD$
+
+PACKAGE=       tests
+
+SYSDIR?=${SRCTOP}/sys
+.include "${SYSDIR}/conf/kern.opts.mk"
+
+.PATH: ${SYSDIR}/tests
+
+KMOD=  ktest_example
+SRCS=  ktest_example.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/tests/ktest.c b/sys/tests/ktest.c
new file mode 100644
index 000000000000..fcb40130bcef
--- /dev/null
+++ b/sys/tests/ktest.c
@@ -0,0 +1,414 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Alexander V. Chernikov
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "opt_netlink.h"
+
+#include <sys/param.h>
+#include <sys/refcount.h>
+#include <sys/types.h>
+#include <sys/kernel.h>
+#include <sys/lock.h>
+#include <sys/malloc.h>
+#include <sys/module.h>
+#include <sys/socket.h>
+#include <sys/priv.h>
+
+#include <netlink/netlink.h>
+#include <netlink/netlink_ctl.h>
+#include <netlink/netlink_generic.h>
+#include <netlink/netlink_message_parser.h>
+
+#include <machine/stdarg.h>
+#include <tests/ktest.h>
+
+struct mtx ktest_mtx;
+#define        KTEST_LOCK()            mtx_lock(&ktest_mtx)
+#define        KTEST_UNLOCK()          mtx_unlock(&ktest_mtx)
+#define        KTEST_LOCK_ASSERT()     mtx_assert(&ktest_mtx, MA_OWNED)
+
+MTX_SYSINIT(ktest_mtx, &ktest_mtx, "ktest mutex", MTX_DEF);
+
+struct ktest_module {
+       struct ktest_module_info        *info;
+       volatile u_int                  refcount;
+       TAILQ_ENTRY(ktest_module)       entries;
+};
+static TAILQ_HEAD(, ktest_module) module_list = 
TAILQ_HEAD_INITIALIZER(module_list);
+
+struct nl_ktest_parsed {
+       char            *mod_name;
+       char            *test_name;
+       struct nlattr   *test_meta;
+};
+
+#define        _IN(_field)     offsetof(struct genlmsghdr, _field)
+#define        _OUT(_field)    offsetof(struct nl_ktest_parsed, _field)
+
+static const struct nlattr_parser nla_p_get[] = {
+       { .type = KTEST_ATTR_MOD_NAME, .off = _OUT(mod_name), .cb = 
nlattr_get_string },
+       { .type = KTEST_ATTR_TEST_NAME, .off = _OUT(test_name), .cb = 
nlattr_get_string },
+       { .type = KTEST_ATTR_TEST_META, .off = _OUT(test_meta), .cb = 
nlattr_get_nla },
+};
+static const struct nlfield_parser nlf_p_get[] = {
+};
+NL_DECLARE_PARSER(ktest_parser, struct genlmsghdr, nlf_p_get, nla_p_get);
+#undef _IN
+#undef _OUT
+
+static bool
+create_reply(struct nl_writer *nw, struct nlmsghdr *hdr, int cmd)
+{
+       if (!nlmsg_reply(nw, hdr, sizeof(struct genlmsghdr)))
+               return (false);
+
+       struct genlmsghdr *ghdr_new = nlmsg_reserve_object(nw, struct 
genlmsghdr);
+       ghdr_new->cmd = cmd;
+       ghdr_new->version = 0;
+       ghdr_new->reserved = 0;
+
+       return (true);
+}
+
+static int
+dump_mod_test(struct nlmsghdr *hdr, struct nl_pstate *npt,
+    struct ktest_module *mod, const struct ktest_test_info *test_info)
+{
+       struct nl_writer *nw = npt->nw;
+
+       if (!create_reply(nw, hdr, KTEST_CMD_NEWTEST))
+               goto enomem;
+
+       nlattr_add_string(nw, KTEST_ATTR_MOD_NAME, mod->info->name);
+       nlattr_add_string(nw, KTEST_ATTR_TEST_NAME, test_info->name);
+       nlattr_add_string(nw, KTEST_ATTR_TEST_DESCR, test_info->desc);
+
+       if (nlmsg_end(nw))
+               return (0);
+enomem:
+       nlmsg_abort(nw);
+       return (ENOMEM);
+}
+
+static int
+dump_mod_tests(struct nlmsghdr *hdr, struct nl_pstate *npt,
+    struct ktest_module *mod, struct nl_ktest_parsed *attrs)
+{
+       for (int i = 0; i < mod->info->num_tests; i++) {
+               const struct ktest_test_info *test_info = &mod->info->tests[i];
+               if (attrs->test_name != NULL && strcmp(attrs->test_name, 
test_info->name))
+                       continue;
+               int error = dump_mod_test(hdr, npt, mod, test_info);
+               if (error != 0)
+                       return (error);
+       }
+
+       return (0);
+}
+
+static int
+dump_tests(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+       struct nl_ktest_parsed attrs = { };
+       struct ktest_module *mod;
+       int error;
+
+       error = nl_parse_nlmsg(hdr, &ktest_parser, npt, &attrs);
+       if (error != 0)
+               return (error);
+
+       hdr->nlmsg_flags |= NLM_F_MULTI;
+
+       KTEST_LOCK();
+       TAILQ_FOREACH(mod, &module_list, entries) {
+               if (attrs.mod_name && strcmp(attrs.mod_name, mod->info->name))
+                       continue;
+               error = dump_mod_tests(hdr, npt, mod, &attrs);
+               if (error != 0)
+                       break;
+       }
+       KTEST_UNLOCK();
+
+       if (!nlmsg_end_dump(npt->nw, error, hdr)) {
+               //NL_LOG(LOG_DEBUG, "Unable to finalize the dump");
+               return (ENOMEM);
+       }
+
+       return (error);
+}
+
+static int
+run_test(struct nlmsghdr *hdr, struct nl_pstate *npt)
+{
+       struct nl_ktest_parsed attrs = { };
+       struct ktest_module *mod;
+       int error;
+
+       error = nl_parse_nlmsg(hdr, &ktest_parser, npt, &attrs);
+       if (error != 0)
+               return (error);
+
+       if (attrs.mod_name == NULL) {
+               nlmsg_report_err_msg(npt, "KTEST_ATTR_MOD_NAME not set");
+               return (EINVAL);
+       }
+
+       if (attrs.test_name == NULL) {
+               nlmsg_report_err_msg(npt, "KTEST_ATTR_TEST_NAME not set");
+               return (EINVAL);
+       }
+
+       const struct ktest_test_info *test = NULL;
+
+       KTEST_LOCK();
+       TAILQ_FOREACH(mod, &module_list, entries) {
+               if (strcmp(attrs.mod_name, mod->info->name))
+                       continue;
+
+               const struct ktest_module_info *info = mod->info;
+
+               for (int i = 0; i < info->num_tests; i++) {
+                       const struct ktest_test_info *test_info = 
&info->tests[i];
+
+                       if (!strcmp(attrs.test_name, test_info->name)) {
+                               test = test_info;
+                               break;
+                       }
+               }
+               break;
+       }
+       if (test != NULL)
+               refcount_acquire(&mod->refcount);
+       KTEST_UNLOCK();
+
+       if (test == NULL)
+               return (ESRCH);
+
+       /* Run the test */
+       struct ktest_test_context ctx = {
+               .npt = npt,
+               .hdr = hdr,
+               .buf = npt_alloc(npt, KTEST_MAX_BUF),
+               .bufsize = KTEST_MAX_BUF,
+       };
+
+       if (ctx.buf == NULL) {
+               //NL_LOG(LOG_DEBUG, "unable to allocate temporary buffer");
+               return (ENOMEM);
+       }
+
+       if (test->parse != NULL && attrs.test_meta != NULL) {
+               error = test->parse(&ctx, attrs.test_meta);
+               if (error != 0)
+                       return (error);
+       }
+
+       hdr->nlmsg_flags |= NLM_F_MULTI;
+
+       KTEST_LOG_LEVEL(&ctx, LOG_INFO, "start running %s", test->name);
+       error = test->func(&ctx);
+       KTEST_LOG_LEVEL(&ctx, LOG_INFO, "end running %s", test->name);
+
+       refcount_release(&mod->refcount);
+
+       if (!nlmsg_end_dump(npt->nw, error, hdr)) {
+               //NL_LOG(LOG_DEBUG, "Unable to finalize the dump");
+               return (ENOMEM);
+       }
+
+       return (error);
+}
+
+
+/* USER API */
+static void
+register_test_module(struct ktest_module_info *info)
+{
+       struct ktest_module *mod = malloc(sizeof(*mod), M_TEMP, M_WAITOK | 
M_ZERO);
+
+       mod->info = info;
+       info->module_ptr = mod;
+       KTEST_LOCK();
+       TAILQ_INSERT_TAIL(&module_list, mod, entries);
+       KTEST_UNLOCK();
+}
+
+static void
+unregister_test_module(struct ktest_module_info *info)
+{
+       struct ktest_module *mod = info->module_ptr;
+
+       info->module_ptr = NULL;
+
+       KTEST_LOCK();
+       TAILQ_REMOVE(&module_list, mod, entries);
+       KTEST_UNLOCK();
+
+       free(mod, M_TEMP);
+}
+
+static bool
+can_unregister(struct ktest_module_info *info)
+{
+       struct ktest_module *mod = info->module_ptr;
+
+       return (refcount_load(&mod->refcount) == 0);
+}
+
+int
+ktest_default_modevent(module_t mod, int type, void *arg)
+{
+       struct ktest_module_info *info = (struct ktest_module_info *)arg;
+       int error = 0;
+
+       switch (type) {
+       case MOD_LOAD:
+               register_test_module(info);
+               break;
+       case MOD_UNLOAD:
+               if (!can_unregister(info))
+                       return (EBUSY);
+               unregister_test_module(info);
+               break;
+       default:
+               error = EOPNOTSUPP;
+               break;
+       }
+       return (error);
+}
+
+bool
+ktest_start_msg(struct ktest_test_context *ctx)
+{
+       return (create_reply(ctx->npt->nw, ctx->hdr, KTEST_CMD_NEWMESSAGE));
+}
+
+void
+ktest_add_msg_meta(struct ktest_test_context *ctx, const char *func,
+    const char *fname, int line)
+{
+       struct nl_writer *nw = ctx->npt->nw;
+       struct timespec ts;
+
+       nanouptime(&ts);
+       nlattr_add(nw, KTEST_MSG_ATTR_TS, sizeof(ts), &ts);
+
+       nlattr_add_string(nw, KTEST_MSG_ATTR_FUNC, func);
+       nlattr_add_string(nw, KTEST_MSG_ATTR_FILE, fname);
+       nlattr_add_u32(nw, KTEST_MSG_ATTR_LINE, line);
+}
+
+void
+ktest_add_msg_text(struct ktest_test_context *ctx, int msg_level,
+    const char *fmt, ...)
+{
+       va_list ap;
+
+       va_start(ap, fmt);
+       vsnprintf(ctx->buf, ctx->bufsize, fmt, ap);
+       va_end(ap);
+
+       nlattr_add_u8(ctx->npt->nw, KTEST_MSG_ATTR_LEVEL, msg_level);
+       nlattr_add_string(ctx->npt->nw, KTEST_MSG_ATTR_TEXT, ctx->buf);
+}
+
+void
+ktest_end_msg(struct ktest_test_context *ctx)
+{
+       nlmsg_end(ctx->npt->nw);
+}
+
+/* Module glue */
+
+static const struct nlhdr_parser *all_parsers[] = { &ktest_parser };
+
+static const struct genl_cmd ktest_cmds[] = {
+       {
+               .cmd_num = KTEST_CMD_LIST,
+               .cmd_name = "KTEST_CMD_LIST",
+               .cmd_cb = dump_tests,
+               .cmd_flags = GENL_CMD_CAP_DO | GENL_CMD_CAP_DUMP | 
GENL_CMD_CAP_HASPOL,
+       },
+       {
+               .cmd_num = KTEST_CMD_RUN,
+               .cmd_name = "KTEST_CMD_RUN",
+               .cmd_cb = run_test,
+               .cmd_flags = GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL,
+               .cmd_priv = PRIV_KLD_LOAD,
+       },
+};
+
+static void
+ktest_nl_register(void)
+{
+       bool ret __diagused;
+       int family_id __diagused;
+
+       NL_VERIFY_PARSERS(all_parsers);
+       family_id = genl_register_family(KTEST_FAMILY_NAME, 0, 1, 
KTEST_CMD_MAX);
+       MPASS(family_id != 0);
+
+       ret = genl_register_cmds(KTEST_FAMILY_NAME, ktest_cmds, 
NL_ARRAY_LEN(ktest_cmds));
+       MPASS(ret);
+}
+
+static void
+ktest_nl_unregister(void)
+{
+       MPASS(TAILQ_EMPTY(&module_list));
+
+       genl_unregister_family(KTEST_FAMILY_NAME);
+}
+
+static int
+ktest_modevent(module_t mod, int type, void *unused)
+{
+       int error = 0;
+
+       switch (type) {
+       case MOD_LOAD:
+               ktest_nl_register();
+               break;
+       case MOD_UNLOAD:
+               ktest_nl_unregister();
+               break;
+       default:
+               error = EOPNOTSUPP;
+               break;
+       }
+       return (error);
+}
+
+static moduledata_t ktestmod = {
+        "ktest",
+        ktest_modevent,
+        0
+};
+
+DECLARE_MODULE(ktestmod, ktestmod, SI_SUB_PSEUDO, SI_ORDER_ANY);
+MODULE_VERSION(ktestmod, 1);
+MODULE_DEPEND(ktestmod, netlink, 1, 1, 1);
+
diff --git a/sys/tests/ktest.h b/sys/tests/ktest.h
new file mode 100644
index 000000000000..feadb800551b
--- /dev/null
+++ b/sys/tests/ktest.h
@@ -0,0 +1,141 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Alexander V. Chernikov
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef        SYS_TESTS_KTEST_H_
+#define SYS_TESTS_KTEST_H_
+
+#ifdef _KERNEL
+
+#include <sys/param.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/syslog.h>
+
+struct nlattr;
+struct nl_pstate;
+struct nlmsghdr;
+
+struct ktest_test_context {
+       void                    *arg;
+       struct nl_pstate        *npt;
+       struct nlmsghdr         *hdr;
+       char                    *buf;
+       size_t                  bufsize;
+};
+
+typedef int (*ktest_run_t)(struct ktest_test_context *ctx);
+typedef int (*ktest_parse_t)(struct ktest_test_context *ctx, struct nlattr 
*container);
+
+struct ktest_test_info {
+       const char      *name;
+       const char      *desc;
+       ktest_run_t     func;
+       ktest_parse_t   parse;
+};
+
+struct ktest_module_info {
+       const char                      *name;
+       const struct ktest_test_info    *tests;
+       int                             num_tests;
+       void                            *module_ptr;
+};
+
+int ktest_default_modevent(module_t mod, int type, void *arg);
+
+bool ktest_start_msg(struct ktest_test_context *ctx);
+void ktest_add_msg_meta(struct ktest_test_context *ctx, const char *func,
+    const char *fname, int line);
+void ktest_add_msg_text(struct ktest_test_context *ctx, int msg_level,
+    const char *fmt, ...);
+void ktest_end_msg(struct ktest_test_context *ctx);
+
+#define        KTEST_LOG_LEVEL(_ctx, _l, _fmt, ...) {                          
\
+       if (ktest_start_msg(_ctx)) {                                    \
+               ktest_add_msg_meta(_ctx, __func__, __FILE__, __LINE__); \
+               ktest_add_msg_text(_ctx, _l, _fmt, ## __VA_ARGS__);     \
+               ktest_end_msg(_ctx);                                    \
+       }                                                               \
+}
+
+#define        KTEST_LOG(_ctx, _fmt, ...)                                      
\
+       KTEST_LOG_LEVEL(_ctx, LOG_DEBUG, _fmt, ## __VA_ARGS__)
+
+#define KTEST_MAX_BUF  512
+
+#define        KTEST_MODULE_DECLARE(_n, _t)                                    
\
+static struct ktest_module_info _module_info = {                       \
+       .name = #_n,                                                    \
+       .tests = _t,                                                    \
+       .num_tests = nitems(_t),                                        \
+};                                                                     \
+                                                                       \
+static moduledata_t _module_data = {                                   \
+        "__"  #_n  "_module",                                          \
+        ktest_default_modevent,                                                
\
+        &_module_info,                                                 \
+};                                                                     \
+                                                                       \
+DECLARE_MODULE(ktest_##_n, _module_data, SI_SUB_PSEUDO, SI_ORDER_ANY); \
+MODULE_VERSION(ktest_##_n, 1);                                         \
+MODULE_DEPEND(ktest_##_n, ktestmod, 1, 1, 1);                          \
+
+#endif /* _KERNEL */
+
+/* genetlink definitions */
+#define KTEST_FAMILY_NAME      "ktest"
+
+/* commands */
+enum {
+       KTEST_CMD_UNSPEC        = 0,
+       KTEST_CMD_LIST          = 1,
+       KTEST_CMD_RUN           = 2,
+       KTEST_CMD_NEWTEST       = 3,
+       KTEST_CMD_NEWMESSAGE    = 4,
+       __KTEST_CMD_MAX,
+};
+#define        KTEST_CMD_MAX   (__KTEST_CMD_MAX - 1)
+
+enum ktest_attr_type_t {
+       KTEST_ATTR_UNSPEC,
+       KTEST_ATTR_MOD_NAME     = 1,    /* string: test module name */
+       KTEST_ATTR_TEST_NAME    = 2,    /* string: test name */
+       KTEST_ATTR_TEST_DESCR   = 3,    /* string: test description */
+       KTEST_ATTR_TEST_META    = 4,    /* nested: container with test-specific 
metadata */
+};
+
+enum ktest_msg_attr_type_t {
+       KTEST_MSG_ATTR_UNSPEC,
+       KTEST_MSG_ATTR_TS       = 1,    /* struct timespec */
+       KTEST_MSG_ATTR_FUNC     = 2,    /* string: function name */
+       KTEST_MSG_ATTR_FILE     = 3,    /* string: file name */
+       KTEST_MSG_ATTR_LINE     = 4,    /* u32: line in the file */
+       KTEST_MSG_ATTR_TEXT     = 5,    /* string: actual message data */
+       KTEST_MSG_ATTR_LEVEL    = 6,    /* u8: syslog loglevel */
+       KTEST_MSG_ATTR_META     = 7,    /* nested: message metadata */
+};
+
+#endif
diff --git a/sys/tests/ktest_example.c b/sys/tests/ktest_example.c
new file mode 100644
index 000000000000..7cccaad7a855
--- /dev/null
+++ b/sys/tests/ktest_example.c
@@ -0,0 +1,134 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Alexander V. Chernikov
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <tests/ktest.h>
+#include <sys/cdefs.h>
+#include <sys/systm.h>
+
+
+static int
+test_something(struct ktest_test_context *ctx)
+{
+       KTEST_LOG(ctx, "I'm here, [%s]", __func__);
+
+       pause("sleeping...", hz / 10);
+
+       KTEST_LOG(ctx, "done");
+
+       return (0);
+}
+
+static int
+test_something_else(struct ktest_test_context *ctx)
+{
+       return (0);
+}
+
+static int
+test_failed(struct ktest_test_context *ctx)
+{
+       return (EBUSY);
+}
+
+static int
+test_failed2(struct ktest_test_context *ctx)
+{
+       KTEST_LOG(ctx, "failed because it always fails");
+       return (EBUSY);
+}
+
+#include <sys/malloc.h>
+#include <netlink/netlink.h>
+#include <netlink/netlink_ctl.h>
+
+struct test1_attrs {
+       uint32_t        arg1;
+       uint32_t        arg2;
+       char            *text;
+};
+
+#define        _OUT(_field)    offsetof(struct test1_attrs, _field)
+static const struct nlattr_parser nla_p_test1[] = {
+       { .type = 1, .off = _OUT(arg1), .cb = nlattr_get_uint32 },
+       { .type = 2, .off = _OUT(arg2), .cb = nlattr_get_uint32 },
+       { .type = 3, .off = _OUT(text), .cb = nlattr_get_string },
+};
+#undef _OUT
+NL_DECLARE_ATTR_PARSER(test1_parser, nla_p_test1);
+
+static int
+test_with_params_parser(struct ktest_test_context *ctx, struct nlattr *nla)
+{
+       struct test1_attrs *attrs = npt_alloc(ctx->npt, sizeof(*attrs));
+
+       ctx->arg = attrs;
+       if (attrs != NULL)
+               return (nl_parse_nested(nla, &test1_parser, ctx->npt, attrs));
+       return (ENOMEM);
+}
+
+static int
+test_with_params(struct ktest_test_context *ctx)
+{
+       struct test1_attrs *attrs = ctx->arg;
+
+       if (attrs->text != NULL)
+               KTEST_LOG(ctx, "Get '%s'", attrs->text);
+       KTEST_LOG(ctx, "%u + %u = %u", attrs->arg1, attrs->arg2,
+           attrs->arg1 + attrs->arg2);
+       return (0);
+}
+
+static const struct ktest_test_info tests[] = {
+       {
+               .name = "test_something",
+               .desc = "example description",
+               .func = &test_something,
+       },
+       {
+               .name = "test_something_else",
+               .desc = "example description 2",
+               .func = &test_something_else,
+       },
+       {
+               .name = "test_failed",
+               .desc = "always failing test",
+               .func = &test_failed,
+       },
+       {
+               .name = "test_failed2",
+               .desc = "always failing test",
+               .func = &test_failed2,
+       },
+       {
+               .name = "test_with_params",
+               .desc = "test summing integers",
+               .func = &test_with_params,
+               .parse = &test_with_params_parser,
+       },
+};
+KTEST_MODULE_DECLARE(ktest_example, tests);
diff --git a/tests/atf_python/Makefile b/tests/atf_python/Makefile
index 1a2fec387eda..889cdcdf9592 100644
--- a/tests/atf_python/Makefile
+++ b/tests/atf_python/Makefile
@@ -2,7 +2,7 @@
 
 .PATH: ${.CURDIR}
 
-FILES= __init__.py atf_pytest.py utils.py
+FILES= __init__.py atf_pytest.py ktest.py utils.py
 SUBDIR=        sys
 
 .include <bsd.own.mk>
diff --git a/tests/atf_python/atf_pytest.py b/tests/atf_python/atf_pytest.py
index 0dd3a225b73d..19b5f88fa200 100644
--- a/tests/atf_python/atf_pytest.py
+++ b/tests/atf_python/atf_pytest.py
@@ -6,6 +6,7 @@ from typing import NamedTuple
 from typing import Optional
 from typing import Tuple
 
+from atf_python.ktest import generate_ktests
 from atf_python.utils import nodeid_to_method_name
 
 import pytest
@@ -42,6 +43,8 @@ class ATFTestObj(object):
 
     def _get_test_description(self, obj):
         """Returns first non-empty line from func docstring or func name"""
+        if getattr(obj, "descr", None) is not None:
+            return getattr(obj, "descr")
         docstr = obj.function.__doc__
         if docstr:
             for line in docstr.split("\n"):
@@ -163,6 +166,9 @@ class ATFHandler(object):
         items.clear()
         items.extend(new_items)
 
+    def expand_tests(self, collector, name, obj):
+        return generate_ktests(collector, name, obj)
+
     def modify_tests(self, items, config):
         if config.option.atf_cleanup:
             self._generate_test_cleanups(items)
diff --git a/tests/atf_python/ktest.py b/tests/atf_python/ktest.py
new file mode 100644
index 000000000000..4cd9970aaec1
--- /dev/null
+++ b/tests/atf_python/ktest.py
@@ -0,0 +1,173 @@
+import logging
+import time
+from typing import NamedTuple
+
+import pytest
+from atf_python.sys.netlink.attrs import NlAttrNested
+from atf_python.sys.netlink.attrs import NlAttrStr
+from atf_python.sys.netlink.netlink import NetlinkMultipartIterator
+from atf_python.sys.netlink.netlink import NlHelper
+from atf_python.sys.netlink.netlink import Nlsock
+from atf_python.sys.netlink.netlink_generic import KtestAttrType
+from atf_python.sys.netlink.netlink_generic import KtestInfoMessage
+from atf_python.sys.netlink.netlink_generic import KtestLogMsgType
+from atf_python.sys.netlink.netlink_generic import KtestMsgAttrType
+from atf_python.sys.netlink.netlink_generic import KtestMsgType
+from atf_python.sys.netlink.netlink_generic import timespec
+from atf_python.sys.netlink.utils import NlConst
+from atf_python.utils import BaseTest
+from atf_python.utils import libc
+from atf_python.utils import nodeid_to_method_name
+
+
+datefmt = "%H:%M:%S"
+fmt = "%(asctime)s.%(msecs)03d %(filename)s:%(funcName)s:%(lineno)d 
%(message)s"
+logging.basicConfig(level=logging.DEBUG, format=fmt, datefmt=datefmt)
+logger = logging.getLogger("ktest")
+
+
+NETLINK_FAMILY = "ktest"
+
+
+class KtestItem(pytest.Item):
+    def __init__(self, *, descr, kcls, **kwargs):
+        super().__init__(**kwargs)
+        self.descr = descr
+        self._kcls = kcls
+
+    def runtest(self):
+        self._kcls().runtest()
+
+
+class KtestCollector(pytest.Class):
+    def collect(self):
+        obj = self.obj
+        exclude_names = set([n for n in dir(obj) if not n.startswith("_")])
+
+        autoload = obj.KTEST_MODULE_AUTOLOAD
+        module_name = obj.KTEST_MODULE_NAME
+        loader = KtestLoader(module_name, autoload)
+        ktests = loader.load_ktests()
+        if not ktests:
+            return
+
+        orig = pytest.Class.from_parent(self.parent, name=self.name, obj=obj)
+        for py_test in orig.collect():
+            yield py_test
+
+        for ktest in ktests:
+            name = ktest["name"]
+            descr = ktest["desc"]
+            if name in exclude_names:
+                continue
+            yield KtestItem.from_parent(self, name=name, descr=descr, kcls=obj)
+
+
+class KtestLoader(object):
+    def __init__(self, module_name: str, autoload: bool):
+        self.module_name = module_name
+        self.autoload = autoload
+        self.helper = NlHelper()
+        self.nlsock = Nlsock(NlConst.NETLINK_GENERIC, self.helper)
+        self.family_id = self._get_family_id()
+
+    def _get_family_id(self):
+        try:
+            family_id = self.nlsock.get_genl_family_id(NETLINK_FAMILY)
+        except ValueError:
+            if self.autoload:
+                libc.kldload(self.module_name)
+                family_id = self.nlsock.get_genl_family_id(NETLINK_FAMILY)
+            else:
+                raise
+        return family_id
+
+    def _load_ktests(self):
+        msg = KtestInfoMessage(self.helper, self.family_id, 
KtestMsgType.KTEST_CMD_LIST)
+        msg.set_request()
+        msg.add_nla(NlAttrStr(KtestAttrType.KTEST_ATTR_MOD_NAME, 
self.module_name))
+        self.nlsock.write_message(msg, verbose=False)
+        nlmsg_seq = msg.nl_hdr.nlmsg_seq
+
+        ret = []
+        for rx_msg in NetlinkMultipartIterator(self.nlsock, nlmsg_seq, 
self.family_id):
+            # test_msg.print_message()
+            tst = {
+                "mod_name": 
rx_msg.get_nla(KtestAttrType.KTEST_ATTR_MOD_NAME).text,
+                "name": 
rx_msg.get_nla(KtestAttrType.KTEST_ATTR_TEST_NAME).text,
+                "desc": 
rx_msg.get_nla(KtestAttrType.KTEST_ATTR_TEST_DESCR).text,
+            }
+            ret.append(tst)
+        return ret
+
+    def load_ktests(self):
+        ret = self._load_ktests()
+        if not ret and self.autoload:
+            libc.kldload(self.module_name)
+            ret = self._load_ktests()
+        return ret
+
+
+def generate_ktests(collector, name, obj):
+    if getattr(obj, "KTEST_MODULE_NAME", None) is not None:
+        return KtestCollector.from_parent(collector, name=name, obj=obj)
+    return None
+
+
+class BaseKernelTest(BaseTest):
+    KTEST_MODULE_AUTOLOAD = True
+    KTEST_MODULE_NAME = None
+
+    def _get_record_time(self, msg) -> float:
+        timespec = msg.get_nla(KtestMsgAttrType.KTEST_MSG_ATTR_TS).ts
+        epoch_ktime = timespec.tv_sec * 1.0 + timespec.tv_nsec * 1.0 / 
1000000000
+        if not hasattr(self, "_start_epoch"):
+            self._start_ktime = epoch_ktime
+            self._start_time = time.time()
+            epoch_time = self._start_time
+        else:
+            epoch_time = time.time() - self._start_time + epoch_ktime
*** 315 LINES SKIPPED ***

Reply via email to