Re: [RFC PATCH v2] kunit: Add "hooks" to call into KUnit when it's built as a module

From: David Gow
Date: Thu Jan 26 2023 - 20:32:05 EST


On Fri, 27 Jan 2023 at 08:49, Brendan Higgins <brendanhiggins@xxxxxxxxxx> wrote:
>
> On Tue, Jan 24, 2023 at 3:04 AM 'David Gow' via KUnit Development
> <kunit-dev@xxxxxxxxxxxxxxxx> wrote:
> >
> > KUnit has several macros and functions intended for use from non-test
> > code. These hooks, currently the kunit_get_current_test() and
> > kunit_fail_current_test() macros, didn't work when CONFIG_KUNIT=m.
> >
> > In order to support this case, the required functions and static data
> > need to be available unconditionally, even when KUnit itself is not
> > built-in. The new 'hooks.c' file is therefore always included, and has
> > both the static key required for kunit_get_current_test(), and a
> > function pointer to the real implementation of
> > __kunit_fail_current_test(), which is populated when the KUnit module is
> > loaded.
> >
> > A new header, kunit/hooks-table.h, contains a table of all hooks, and is
> > repeatedly included with different definitions of the KUNIT_HOOK() in
> > order to automatically generate the needed function pointer tables. When
> > KUnit is disabled, or the module is not loaded, these function pointers
> > are all NULL. This shouldn't be a problem, as they're all used behind
> > wrappers which check kunit_running and/or that the pointer is non-NULL.
> >
> > This can then be extended for future features which require similar
> > "hook" behaviour, such as static stubs:
> > https://lore.kernel.org/all/20221208061841.2186447-1-davidgow@xxxxxxxxxx/
>
> Devilishly clever. Maybe too clever, but I don't have any better ideas, so:
>
> Reviewed-by: Brendan Higgins <brendanhiggins@xxxxxxxxxx>
>
> Nevertheless, see my comments below:
>

Thanks for checking this out! I definitely agree that this is verging
on over-complicated, but I do think it'll be worth it.

I'll send out a proper v1 in a day or two with your comments below
addressed (and a few other minor issues).
One option is to just include this in the same series as the static
stub feature (the next version of which will use it), but that may
just make things more confusing.

> > Signed-off-by: David Gow <davidgow@xxxxxxxxxx>
> > ---
> >
> > This is basically a prerequisite for the stub features working when
> > KUnit is built as a module, and should nicely make a few other tests
> > work then, too.
> >
> > v2 adds a slightly-excessive macro-based system for defining hooks. This
> > made adding the static stub hooks absolutely trivial, and the complexity
> > is totally hidden from the user (being an internal KUnit implementation
> > detail), so I'm more comfortable with this than some other macro magic.
> >
> > It does however result in a huge number of checkpatch.pl errors, as
> > we're using macros in unconventional ways, and checkpatch just can't
> > work out the syntax. These are mostly "Macros with complex values should
> > be enclosed in parentheses", "Macros with multiple statements should be
> > enclosed in a do - while loop", and similar, which don't apply due to
> > the macros not being expressions: they are mostly declarations or
> > assignment statements. There are a few others where checkpatch thinks
> > that the return value is the function name and similar, so complains
> > about the style.
>
> Shuah, what are your thoughts here? I think it's OK, but I don't want
> to go any further down this path unless you are OK with it too.
>
> > Open questions:
> > - Is this macro-based system worth it, or was v1 better?
>
> I think this is definitely better if we had more than one function to
> hook. With just one function - I am less confident, but I like having
> a set way to do it.

Yeah, I agree that it's not worth it for just one function (hence the
first RFC just hardcoding these), but I think it pays of surprisingly
quickly as we add more.

In particular, the static stub code lives in a different file, so
would've needed an extra header for the "_impl" version anyway, as
it'd need to be accessible from the test.c init code which sets up the
function pointers. Once we're adding several headers, each with very
closely related definitions, this way already looks like a bit of a
win.

(And I have plans to add some more hooks going forward, particularly
for "data stubbing", as well as to expose some way of looking up
resources.)

>
> > - Should we rename test-bug.h to hooks.h or similar.
> > (I think so, but would rather do it in a separate patch, to make it
> > easier to review. There are a few includes of it scattered about.)
>
> Agreed, that confused me at first.

The other option we could do is to add "hooks.h" now, and temporarily
make "test-bug.h" just transitively include that until we clean up any
existing users.

>
> > - Is making these NULL when KUnit isn't around sensible, or should we
> > auto-generate a "default" implementation. This is a pretty easy
> > extension to the macros here.
> > (I think NULL is good for now, as we have wrappers for these anyway.
> > If we want to change our minds later as we add more hooks, it's easy.)
>
> Yeah, I'm fine with either.
>

I think we'll leave it as-is until we have enough hooks that it
justifies even more complexity.

> > - Any other thoughts?
>
> My primary concern was with the naming of test-bug.h, but you said
> you'd handle that in another patch, which makes sense to me.
>
> I also want to make sure Shuah is OK with the checkpatch warnings.
>

Yeah, the checkpatch warnings are the biggest worry I have. There are
good reasons to consider them false positives, but it still leaves a
bit of a bad taste in my mouth.

Here's the full list, with some notes:
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#116:
new file mode 100644

(The new files are in KUnit directories, so are already covered by MAINTAINERS)

ERROR: Macros with complex values should be enclosed in parentheses
#196: FILE: include/kunit/test-bug.h:57:
+#define KUNIT_HOOK(name, retval, args) \
+ extern retval (*name)args

(This is a global variable declaration, so can't be enclosed with
parentheses or do {} while (0))

WARNING: space prohibited between function name and open parenthesis '('
#197: FILE: include/kunit/test-bug.h:58:
+ extern retval (*name)args

(retval is not the function name, it's the return value. name is the
name, the parentheses are for the function pointer syntax.)

ERROR: Macros with complex values should be enclosed in parentheses
#212: FILE: include/kunit/test-bug.h:71:
+#define KUNIT_HOOK(name, retval, args) \
+ static retval (*name)args = NULL

(This is a global variable declaration, so can't be enclosed with
parentheses or do {} while (0))

WARNING: space prohibited between function name and open parenthesis '('
#213: FILE: include/kunit/test-bug.h:72:
+ static retval (*name)args = NULL

(retval is not the function name, it's the return value. name is the
name, the parentheses are for the function pointer syntax.)

WARNING: EXPORT_SYMBOL(foo); should immediately follow its function/variable
#271: FILE: lib/kunit/hooks.c:18:
+EXPORT_SYMBOL(kunit_running);

(This does immediately follow the DEFINE_STATIC_KEY macro)

ERROR: Macros with multiple statements should be enclosed in a do - while loop
#274: FILE: lib/kunit/hooks.c:21:
+#define KUNIT_HOOK(name, retval, args) \
+ retval (*name)args; \
+ EXPORT_SYMBOL(name)

(These are global declarations, so can't live in a do {} while (0) loop)

WARNING: space prohibited between function name and open parenthesis '('
#275: FILE: lib/kunit/hooks.c:22:
+ retval (*name)args; \

(retval is not the function name, it's the return value. name is the
name, the parentheses are for the function pointer syntax.)

ERROR: Macros with complex values should be enclosed in parentheses
#314: FILE: lib/kunit/test.c:774:
+#define KUNIT_HOOK(name, retval, args) \
+ extern retval name##_impl args

(This is a global variable declaration, so can't be enclosed with
parentheses or do {} while (0))

ERROR: Macros with complex values should be enclosed in parentheses
#321: FILE: lib/kunit/test.c:781:
+#define KUNIT_HOOK(name, retval, args) \
+ name = name##_impl

(I _guess_ this one and the following could be put in a do {} while
(0) loop, but parentheses wouldn't work, as this is an assignment.)

ERROR: Macros with complex values should be enclosed in parentheses
#333: FILE: lib/kunit/test.c:797:
+#define KUNIT_HOOK(name, retval, args) \
+ name = NULL

(I _guess_ this one and the previous could be put in a do {} while (0)
loop, but parentheses wouldn't work, as this is an assignment.)

total: 6 errors, 5 warnings, 211 lines checked


> I did find two nits below:
>
> > Cheers,
> > -- David
> >
> > Changes since RFC v1:
> > https://lore.kernel.org/all/20230117142737.246446-1-davidgow@xxxxxxxxxx/
> > - Major refit to auto-generate the hook code using macros.
> > - (Note that previous Reviewed-by tags have not been added, as this is a
> > big enough change it probably needs a re-reviews. Thanks Rae for
> > reviewing RFC v1 previously, though!)
> > ---
> > Documentation/dev-tools/kunit/usage.rst | 14 +++++-----
> > include/kunit/hooks-table.h | 34 +++++++++++++++++++++++++
> > include/kunit/test-bug.h | 24 +++++++++--------
> > lib/Makefile | 4 +++
> > lib/kunit/Makefile | 3 +++
> > lib/kunit/hooks.c | 27 ++++++++++++++++++++
> > lib/kunit/test.c | 22 +++++++++++-----
> > 7 files changed, 103 insertions(+), 25 deletions(-)
> > create mode 100644 include/kunit/hooks-table.h
> > create mode 100644 lib/kunit/hooks.c
> >
> > diff --git a/Documentation/dev-tools/kunit/usage.rst b/Documentation/dev-tools/kunit/usage.rst
> > index 48f8196d5aad..6424493b93cb 100644
> > --- a/Documentation/dev-tools/kunit/usage.rst
> > +++ b/Documentation/dev-tools/kunit/usage.rst
> > @@ -648,10 +648,9 @@ We can do this via the ``kunit_test`` field in ``task_struct``, which we can
> > access using the ``kunit_get_current_test()`` function in ``kunit/test-bug.h``.
> >
> > ``kunit_get_current_test()`` is safe to call even if KUnit is not enabled. If
> > -KUnit is not enabled, was built as a module (``CONFIG_KUNIT=m``), or no test is
> > -running in the current task, it will return ``NULL``. This compiles down to
> > -either a no-op or a static key check, so will have a negligible performance
> > -impact when no test is running.
> > +KUnit is not enabled, or if no test is running in the current task, it will
> > +return ``NULL``. This compiles down to either a no-op or a static key check,
> > +so will have a negligible performance impact when no test is running.
> >
> > The example below uses this to implement a "mock" implementation of a function, ``foo``:
> >
> > @@ -726,8 +725,7 @@ structures as shown below:
> > #endif
> >
> > ``kunit_fail_current_test()`` is safe to call even if KUnit is not enabled. If
> > -KUnit is not enabled, was built as a module (``CONFIG_KUNIT=m``), or no test is
> > -running in the current task, it will do nothing. This compiles down to either a
> > -no-op or a static key check, so will have a negligible performance impact when
> > -no test is running.
> > +KUnit is not enabled, or if no test is running in the current task, it will do
> > +nothing. This compiles down to either a no-op or a static key check, so will
> > +have a negligible performance impact when no test is running.
> >
> > diff --git a/include/kunit/hooks-table.h b/include/kunit/hooks-table.h
> > new file mode 100644
> > index 000000000000..0b5eafd199ed
> > --- /dev/null
> > +++ b/include/kunit/hooks-table.h
> > @@ -0,0 +1,34 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +/*
> > + * KUnit 'Hooks' function pointer table
> > + *
> > + * This file is included multiple times, each time with a different definition
> > + * of KUNIT_HOOK. This provides one place where all of the hooks can be listed
> > + * which can then be converted into function / implementation declarations, or
> > + * code to set function pointers.
> > + *
> > + * Copyright (C) 2023, Google LLC.
> > + * Author: David Gow <davidgow@xxxxxxxxxx>
> > + */
> > +
> > +/*
> > + * To declare a hook, use:
> > + * KUNIT_HOOK(name, retval, args), where:
> > + * - name: the function name of the exported hook
> > + * - retval: the type of the return value of the hook
> > + * - args: the arguments to the hook, of the form (int a, int b)
> > + *
> > + * Note that the argument list should be contained within the brackets (),
> > + * and that the implementation of the hook should be in a <name>_impl
> > + * function, which should not be declared static, but need not be exported.
> > + */
> > +
> > +#ifndef KUNIT_HOOK
> > +#error KUNIT_HOOK must be defined before including the hooks table
> > +#endif
> > +
> > +KUNIT_HOOK(__kunit_fail_current_test, __printf(3, 4) void,
> > + (const char *file, int line, const char *fmt, ...));
> > +
> > +/* Undefine KUNIT_HOOK at the end, ready for the next use. */
> > +#undef KUNIT_HOOK
> > diff --git a/include/kunit/test-bug.h b/include/kunit/test-bug.h
> > index c1b2e14eab64..3203ffc0a08b 100644
> > --- a/include/kunit/test-bug.h
> > +++ b/include/kunit/test-bug.h
> > @@ -1,6 +1,6 @@
> > /* SPDX-License-Identifier: GPL-2.0 */
> > /*
> > - * KUnit API allowing dynamic analysis tools to interact with KUnit tests
> > + * KUnit API providing hooks for non-test code to interact with tests.
> > *
> > * Copyright (C) 2020, Google LLC.
> > * Author: Uriel Guajardo <urielguajardo@xxxxxxxxxx>
> > @@ -9,7 +9,7 @@
> > #ifndef _KUNIT_TEST_BUG_H
> > #define _KUNIT_TEST_BUG_H
> >
> > -#if IS_BUILTIN(CONFIG_KUNIT)
> > +#if IS_ENABLED(CONFIG_KUNIT)
> >
> > #include <linux/jump_label.h> /* For static branch */
> > #include <linux/sched.h>
> > @@ -43,20 +43,21 @@ static inline struct kunit *kunit_get_current_test(void)
> > * kunit_fail_current_test() - If a KUnit test is running, fail it.
> > *
> > * If a KUnit test is running in the current task, mark that test as failed.
> > - *
> > - * This macro will only work if KUnit is built-in (though the tests
> > - * themselves can be modules). Otherwise, it compiles down to nothing.
> > */
> > #define kunit_fail_current_test(fmt, ...) do { \
> > if (static_branch_unlikely(&kunit_running)) { \
> > + /* Guaranteed to be non-NULL when kunit_running true*/ \
> > __kunit_fail_current_test(__FILE__, __LINE__, \
> > fmt, ##__VA_ARGS__); \
> > } \
> > } while (0)
> >
> >
> > -extern __printf(3, 4) void __kunit_fail_current_test(const char *file, int line,
> > - const char *fmt, ...);
> > +/* Declare all of the available hooks. */
> > +#define KUNIT_HOOK(name, retval, args) \
> > + extern retval (*name)args
> > +
> > +#include "kunit/hooks-table.h"
> >
> > #else
> >
> > @@ -66,10 +67,11 @@ static inline struct kunit *kunit_get_current_test(void) { return NULL; }
> > #define kunit_fail_current_test(fmt, ...) \
> > __kunit_fail_current_test(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
> >
> > -static inline __printf(3, 4) void __kunit_fail_current_test(const char *file, int line,
> > - const char *fmt, ...)
> > -{
> > -}
> > +/* No-op stubs if KUnit is not enabled. */
> > +#define KUNIT_HOOK(name, retval, args) \
> > + static retval (*name)args = NULL
> > +
> > +#include "kunit/hooks-table.h"
> >
> > #endif
> >
> > diff --git a/lib/Makefile b/lib/Makefile
> > index 4d9461bfea42..9031de6ca73c 100644
> > --- a/lib/Makefile
> > +++ b/lib/Makefile
> > @@ -126,6 +126,10 @@ CFLAGS_test_fpu.o += $(FPU_CFLAGS)
> > obj-$(CONFIG_TEST_LIVEPATCH) += livepatch/
> >
> > obj-$(CONFIG_KUNIT) += kunit/
> > +# Include the KUnit hooks unconditionally. They'll compile to nothing if
> > +# CONFIG_KUNIT=n, otherwise will be a small table of static data (static key,
> > +# function pointers) which need to be built-in even when KUnit is a module.
> > +obj-y += kunit/hooks.o
> >
> > ifeq ($(CONFIG_DEBUG_KOBJECT),y)
> > CFLAGS_kobject.o += -DDEBUG
> > diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
> > index 29aff6562b42..deeb46cc879b 100644
> > --- a/lib/kunit/Makefile
> > +++ b/lib/kunit/Makefile
> > @@ -11,6 +11,9 @@ ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
> > kunit-objs += debugfs.o
> > endif
> >
> > +# KUnit 'hooks' are built-in even when KUnit is built as a module.
> > +lib-y += hooks.o
> > +
> > obj-$(CONFIG_KUNIT_TEST) += kunit-test.o
> >
> > # string-stream-test compiles built-in only.
> > diff --git a/lib/kunit/hooks.c b/lib/kunit/hooks.c
> > new file mode 100644
> > index 000000000000..29e81614f486
> > --- /dev/null
> > +++ b/lib/kunit/hooks.c
> > @@ -0,0 +1,27 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * KUnit 'Hooks' implementation.
> > + *
> > + * This file contains code / structures which should be built-in even when
> > + * KUnit itself is built as a module.
> > + *
> > + * Copyright (C) 2022, Google LLC.
> > + * Author: David Gow <davidgow@xxxxxxxxxx>
> > + */
> > +
> > +/* This file is always built-in, so make sure it's empty if CONFIG_KUNIT=n */
> > +#if IS_ENABLED(CONFIG_KUNIT)
> > +
> > +#include <kunit/test-bug.h>
> > +
> > +DEFINE_STATIC_KEY_FALSE(kunit_running);
> > +EXPORT_SYMBOL(kunit_running);
> > +
> > +/* Function pointers for hooks. */
> > +#define KUNIT_HOOK(name, retval, args) \
> > + retval (*name)args; \
> > + EXPORT_SYMBOL(name)
> > +
> > +#include "kunit/hooks-table.h"
> > +
> > +#endif
> > diff --git a/lib/kunit/test.c b/lib/kunit/test.c
> > index c9ebf975e56b..b6c88f722b68 100644
> > --- a/lib/kunit/test.c
> > +++ b/lib/kunit/test.c
> > @@ -20,13 +20,10 @@
> > #include "string-stream.h"
> > #include "try-catch-impl.h"
> >
> > -DEFINE_STATIC_KEY_FALSE(kunit_running);
> > -
> > -#if IS_BUILTIN(CONFIG_KUNIT)
> > /*
> > * Fail the current test and print an error message to the log.
> > */
> > -void __kunit_fail_current_test(const char *file, int line, const char *fmt, ...)
>
> nit: I think it would be good to add a comment here about this being a
> hooked function or something.

Makes sense, will do.

>
> > +void __kunit_fail_current_test_impl(const char *file, int line, const char *fmt, ...)
> > {
> > va_list args;
> > int len;
> > @@ -53,8 +50,6 @@ void __kunit_fail_current_test(const char *file, int line, const char *fmt, ...)
> > kunit_err(current->kunit_test, "%s:%d: %s", file, line, buffer);
> > kunit_kfree(current->kunit_test, buffer);
> > }
> > -EXPORT_SYMBOL_GPL(__kunit_fail_current_test);
> > -#endif
> >
> > /*
> > * Enable KUnit tests to run.
> > @@ -775,8 +770,18 @@ void kunit_cleanup(struct kunit *test)
> > }
> > EXPORT_SYMBOL_GPL(kunit_cleanup);
> >
> > +/* Declarations for the hook implemetnations */
>
> nit: spelling
>

Nice catch, thanks!

> > +#define KUNIT_HOOK(name, retval, args) \
> > + extern retval name##_impl args
> > +#include "kunit/hooks-table.h"
> > +
> > static int __init kunit_init(void)
> > {
> > + /* Install the KUnit hook functions. */
> > +#define KUNIT_HOOK(name, retval, args) \
> > + name = name##_impl
> > +#include "kunit/hooks-table.h"
> > +
> > kunit_debugfs_init();
> > #ifdef CONFIG_MODULES
> > return register_module_notifier(&kunit_mod_nb);
> > @@ -788,6 +793,11 @@ late_initcall(kunit_init);
> >
> > static void __exit kunit_exit(void)
> > {
> > + /* Remove the KUnit hook functions. */
> > +#define KUNIT_HOOK(name, retval, args) \
> > + name = NULL
> > +#include "kunit/hooks-table.h"
> > +
> > #ifdef CONFIG_MODULES
> > unregister_module_notifier(&kunit_mod_nb);
> > #endif
> > --
> > 2.39.0.246.g2a6d74b583-goog