diff mbox series

[v4,07/10] kunit: string-stream: Decouple string_stream from kunit

Message ID 20230814132309.32641-8-rf@opensource.cirrus.com (mailing list archive)
State New
Delegated to: Brendan Higgins
Headers show
Series kunit: Add dynamically-extending log | expand

Commit Message

Richard Fitzgerald Aug. 14, 2023, 1:23 p.m. UTC
Re-work string_stream so that it is not tied to a struct kunit. This is
to allow using it for the log of struct kunit_suite.

Instead of resource-managing individual allocations the whole string_stream
object can be resource-managed as a single object:

    alloc_string_stream() API is unchanged and takes a pointer to a
    struct kunit but it now registers the returned string_stream object to
    be resource-managed.

    raw_alloc_string_stream() is a new function that allocates a
    bare string_stream without any association to a struct kunit.

    free_string_stream() is a new function that frees a resource-managed
    string_stream allocated by alloc_string_stream().

    raw_free_string_stream() is a new function that frees a non-managed
    string_stream allocated by raw_alloc_string_stream().

The confusing function string_stream_destroy() has been removed. This only
called string_stream_clear() but didn't free the struct string_stream.
Instead string_stream_clear() has been exported, and the new functions use
the more conventional naming of "free" as the opposite of "alloc".

Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
---
 lib/kunit/string-stream-test.c |  2 +-
 lib/kunit/string-stream.c      | 92 +++++++++++++++++++++++-----------
 lib/kunit/string-stream.h      | 12 ++++-
 lib/kunit/test.c               |  2 +-
 4 files changed, 77 insertions(+), 31 deletions(-)

Comments

kernel test robot Aug. 14, 2023, 7:22 p.m. UTC | #1
Hi Richard,

kernel test robot noticed the following build warnings:

[auto build test WARNING on shuah-kselftest/kunit]
[also build test WARNING on shuah-kselftest/kunit-fixes linus/master v6.5-rc6 next-20230809]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Richard-Fitzgerald/kunit-string-stream-Improve-testing-of-string_stream/20230814-212947
base:   https://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest.git kunit
patch link:    https://lore.kernel.org/r/20230814132309.32641-8-rf%40opensource.cirrus.com
patch subject: [PATCH v4 07/10] kunit: string-stream: Decouple string_stream from kunit
config: hexagon-randconfig-r014-20230815 (https://download.01.org/0day-ci/archive/20230815/202308150347.LvFXkSdz-lkp@intel.com/config)
compiler: clang version 17.0.0 (https://github.com/llvm/llvm-project.git 4a5ac14ee968ff0ad5d2cc1ffa0299048db4c88a)
reproduce: (https://download.01.org/0day-ci/archive/20230815/202308150347.LvFXkSdz-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202308150347.LvFXkSdz-lkp@intel.com/

All warnings (new ones prefixed by >>):

>> lib/kunit/string-stream.c:199:38: warning: cast from 'void (*)(struct string_stream *)' to 'kunit_action_t *' (aka 'void (*)(void *)') converts to incompatible function type [-Wcast-function-type-strict]
     199 |         if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
         |                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/linux/compiler.h:55:47: note: expanded from macro 'if'
      55 | #define if(cond, ...) if ( __trace_if_var( !!(cond , ## __VA_ARGS__) ) )
         |                                               ^~~~
   include/linux/compiler.h:57:52: note: expanded from macro '__trace_if_var'
      57 | #define __trace_if_var(cond) (__builtin_constant_p(cond) ? (cond) : __trace_if_value(cond))
         |                                                    ^~~~
>> lib/kunit/string-stream.c:199:38: warning: cast from 'void (*)(struct string_stream *)' to 'kunit_action_t *' (aka 'void (*)(void *)') converts to incompatible function type [-Wcast-function-type-strict]
     199 |         if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
         |                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/linux/compiler.h:55:47: note: expanded from macro 'if'
      55 | #define if(cond, ...) if ( __trace_if_var( !!(cond , ## __VA_ARGS__) ) )
         |                                               ^~~~
   include/linux/compiler.h:57:61: note: expanded from macro '__trace_if_var'
      57 | #define __trace_if_var(cond) (__builtin_constant_p(cond) ? (cond) : __trace_if_value(cond))
         |                                                             ^~~~
>> lib/kunit/string-stream.c:199:38: warning: cast from 'void (*)(struct string_stream *)' to 'kunit_action_t *' (aka 'void (*)(void *)') converts to incompatible function type [-Wcast-function-type-strict]
     199 |         if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
         |                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/linux/compiler.h:55:47: note: expanded from macro 'if'
      55 | #define if(cond, ...) if ( __trace_if_var( !!(cond , ## __VA_ARGS__) ) )
         |                                               ^~~~
   include/linux/compiler.h:57:86: note: expanded from macro '__trace_if_var'
      57 | #define __trace_if_var(cond) (__builtin_constant_p(cond) ? (cond) : __trace_if_value(cond))
         |                                                                                      ^~~~
   include/linux/compiler.h:68:3: note: expanded from macro '__trace_if_value'
      68 |         (cond) ?                                        \
         |          ^~~~
   lib/kunit/string-stream.c:210:29: warning: cast from 'void (*)(struct string_stream *)' to 'kunit_action_t *' (aka 'void (*)(void *)') converts to incompatible function type [-Wcast-function-type-strict]
     210 |         kunit_release_action(test, (kunit_action_t *)raw_free_string_stream, (void *)stream);
         |                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   4 warnings generated.


vim +199 lib/kunit/string-stream.c

   188	
   189	struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp)
   190	{
   191		struct string_stream *stream;
   192	
   193		stream = raw_alloc_string_stream(gfp);
   194		if (IS_ERR(stream))
   195			return stream;
   196	
   197		stream->test = test;
   198	
 > 199		if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
   200			return ERR_PTR(-ENOMEM);
   201	
   202		return stream;
   203	}
   204
David Gow Aug. 15, 2023, 9:16 a.m. UTC | #2
On Mon, 14 Aug 2023 at 21:23, Richard Fitzgerald
<rf@opensource.cirrus.com> wrote:
>
> Re-work string_stream so that it is not tied to a struct kunit. This is
> to allow using it for the log of struct kunit_suite.
>
> Instead of resource-managing individual allocations the whole string_stream
> object can be resource-managed as a single object:
>
>     alloc_string_stream() API is unchanged and takes a pointer to a
>     struct kunit but it now registers the returned string_stream object to
>     be resource-managed.
>
>     raw_alloc_string_stream() is a new function that allocates a
>     bare string_stream without any association to a struct kunit.
>
>     free_string_stream() is a new function that frees a resource-managed
>     string_stream allocated by alloc_string_stream().
>
>     raw_free_string_stream() is a new function that frees a non-managed
>     string_stream allocated by raw_alloc_string_stream().
>
> The confusing function string_stream_destroy() has been removed. This only
> called string_stream_clear() but didn't free the struct string_stream.
> Instead string_stream_clear() has been exported, and the new functions use
> the more conventional naming of "free" as the opposite of "alloc".
>
> Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
> ---

I'm in favour of this. Should we go further and get rid of the struct
kunit member from string_stream totally?

Also, note that the kunit_action_t casting is causing warnings on some
clang configs (and technically isn't valid C). Personally, I still
like it, but expect more emails from the kernel test robot and others.

Reviewed-by: David Gow <davidgow@google.com>

>  lib/kunit/string-stream-test.c |  2 +-
>  lib/kunit/string-stream.c      | 92 +++++++++++++++++++++++-----------
>  lib/kunit/string-stream.h      | 12 ++++-
>  lib/kunit/test.c               |  2 +-
>  4 files changed, 77 insertions(+), 31 deletions(-)
>
> diff --git a/lib/kunit/string-stream-test.c b/lib/kunit/string-stream-test.c
> index 8a30bb7d5fb7..437aa4b3179d 100644
> --- a/lib/kunit/string-stream-test.c
> +++ b/lib/kunit/string-stream-test.c
> @@ -200,7 +200,7 @@ static void string_stream_append_test(struct kunit *test)
>                            combined_content);
>
>         /* Append content of non-empty stream to empty stream */
> -       string_stream_destroy(stream_1);
> +       string_stream_clear(stream_1);
>
>         stream_1 = alloc_string_stream(test, GFP_KERNEL);
>         KUNIT_ASSERT_NOT_ERR_OR_NULL(test, stream_1);
> diff --git a/lib/kunit/string-stream.c b/lib/kunit/string-stream.c
> index eb673d3ea3bd..06104a729b45 100644
> --- a/lib/kunit/string-stream.c
> +++ b/lib/kunit/string-stream.c
> @@ -13,30 +13,28 @@
>  #include "string-stream.h"
>
>
> -static struct string_stream_fragment *alloc_string_stream_fragment(
> -               struct kunit *test, int len, gfp_t gfp)
> +static struct string_stream_fragment *alloc_string_stream_fragment(int len, gfp_t gfp)
>  {
>         struct string_stream_fragment *frag;
>
> -       frag = kunit_kzalloc(test, sizeof(*frag), gfp);
> +       frag = kzalloc(sizeof(*frag), gfp);
>         if (!frag)
>                 return ERR_PTR(-ENOMEM);
>
> -       frag->fragment = kunit_kmalloc(test, len, gfp);
> +       frag->fragment = kmalloc(len, gfp);
>         if (!frag->fragment) {
> -               kunit_kfree(test, frag);
> +               kfree(frag);
>                 return ERR_PTR(-ENOMEM);
>         }
>
>         return frag;
>  }
>
> -static void string_stream_fragment_destroy(struct kunit *test,
> -                                          struct string_stream_fragment *frag)
> +static void string_stream_fragment_destroy(struct string_stream_fragment *frag)
>  {
>         list_del(&frag->node);
> -       kunit_kfree(test, frag->fragment);
> -       kunit_kfree(test, frag);
> +       kfree(frag->fragment);
> +       kfree(frag);
>  }
>
>  int string_stream_vadd(struct string_stream *stream,
> @@ -65,9 +63,7 @@ int string_stream_vadd(struct string_stream *stream,
>         /* Need space for null byte. */
>         buf_len++;
>
> -       frag_container = alloc_string_stream_fragment(stream->test,
> -                                                     buf_len,
> -                                                     stream->gfp);
> +       frag_container = alloc_string_stream_fragment(buf_len, stream->gfp);
>         if (IS_ERR(frag_container))
>                 return PTR_ERR(frag_container);
>
> @@ -102,7 +98,7 @@ int string_stream_add(struct string_stream *stream, const char *fmt, ...)
>         return result;
>  }
>
> -static void string_stream_clear(struct string_stream *stream)
> +void string_stream_clear(struct string_stream *stream)
>  {
>         struct string_stream_fragment *frag_container, *frag_container_safe;
>
> @@ -111,16 +107,28 @@ static void string_stream_clear(struct string_stream *stream)
>                                  frag_container_safe,
>                                  &stream->fragments,
>                                  node) {
> -               string_stream_fragment_destroy(stream->test, frag_container);
> +               string_stream_fragment_destroy(frag_container);
>         }
>         stream->length = 0;
>         spin_unlock(&stream->lock);
>  }
>
> +static void _string_stream_concatenate_to_buf(struct string_stream *stream,
> +                                             char *buf, size_t buf_len)
> +{
> +       struct string_stream_fragment *frag_container;
> +
> +       buf[0] = '\0';
> +
> +       spin_lock(&stream->lock);
> +       list_for_each_entry(frag_container, &stream->fragments, node)
> +               strlcat(buf, frag_container->fragment, buf_len);
> +       spin_unlock(&stream->lock);
> +}
> +
>  char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
>                                gfp_t gfp)
>  {
> -       struct string_stream_fragment *frag_container;
>         size_t buf_len = stream->length + 1; /* +1 for null byte. */
>         char *buf;
>
> @@ -128,10 +136,7 @@ char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
>         if (!buf)
>                 return NULL;
>
> -       spin_lock(&stream->lock);
> -       list_for_each_entry(frag_container, &stream->fragments, node)
> -               strlcat(buf, frag_container->fragment, buf_len);
> -       spin_unlock(&stream->lock);
> +       _string_stream_concatenate_to_buf(stream, buf, buf_len);
>
>         return buf;
>  }
> @@ -139,13 +144,20 @@ char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
>  int string_stream_append(struct string_stream *stream,
>                          struct string_stream *other)
>  {
> -       const char *other_content;
> +       size_t other_buf_len = other->length + 1; /* +1 for null byte. */
> +       char *other_buf;
> +       int ret;
>
> -       other_content = string_stream_get_string(other->test, other, other->gfp);
> -       if (!other_content)
> +       other_buf = kmalloc(other_buf_len, GFP_KERNEL);
> +       if (!other_buf)
>                 return -ENOMEM;
>
> -       return string_stream_add(stream, other_content);
> +       _string_stream_concatenate_to_buf(other, other_buf, other_buf_len);
> +
> +       ret = string_stream_add(stream, other_buf);
> +       kfree(other_buf);
> +
> +       return ret;
>  }
>
>  bool string_stream_is_empty(struct string_stream *stream)
> @@ -153,23 +165,47 @@ bool string_stream_is_empty(struct string_stream *stream)
>         return list_empty(&stream->fragments);
>  }
>
> -struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp)
> +void raw_free_string_stream(struct string_stream *stream)
> +{
> +       string_stream_clear(stream);
> +       kfree(stream);
> +}
> +
> +struct string_stream *raw_alloc_string_stream(gfp_t gfp)
>  {
>         struct string_stream *stream;
>
> -       stream = kunit_kzalloc(test, sizeof(*stream), gfp);
> +       stream = kzalloc(sizeof(*stream), gfp);
>         if (!stream)
>                 return ERR_PTR(-ENOMEM);
>
>         stream->gfp = gfp;
> -       stream->test = test;
>         INIT_LIST_HEAD(&stream->fragments);
>         spin_lock_init(&stream->lock);
>
>         return stream;
>  }
>
> -void string_stream_destroy(struct string_stream *stream)
> +struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp)
>  {
> -       string_stream_clear(stream);
> +       struct string_stream *stream;
> +
> +       stream = raw_alloc_string_stream(gfp);
> +       if (IS_ERR(stream))
> +               return stream;
> +
> +       stream->test = test;
> +
> +       if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
> +               return ERR_PTR(-ENOMEM);

As the kernel test robot notes, this sort of function pointer casting
is not technically valid C, and some compiler setups are starting to
warn on it.

Personally, I'm still okay with it, but expect a bunch of robot email
complaining about it if it lands. If more compilers / configs start
warning, though, I'll try to fix all callers of the KUnit action
functions which are affected.

> +
> +       return stream;
> +}
> +
> +void free_string_stream(struct kunit *test, struct string_stream *stream)
> +{
> +       if (!stream)
> +               return;
> +
> +       kunit_release_action(test, (kunit_action_t *)raw_free_string_stream, (void *)stream);

(As above.)

>  }
> diff --git a/lib/kunit/string-stream.h b/lib/kunit/string-stream.h
> index 6b4a747881c5..fbeda486382a 100644
> --- a/lib/kunit/string-stream.h
> +++ b/lib/kunit/string-stream.h
> @@ -23,14 +23,24 @@ struct string_stream {
>         struct list_head fragments;
>         /* length and fragments are protected by this lock */
>         spinlock_t lock;
> +
> +       /*
> +        * Pointer to kunit this stream is associated with, or NULL if
> +        * not associated with a kunit.
> +        */
>         struct kunit *test;
> +

Can we just get rid of this totally? I don't think anything's actually
using it now. (Or have I missed something?)


>         gfp_t gfp;
>         bool append_newlines;
>  };
>
>  struct kunit;
>
> +struct string_stream *raw_alloc_string_stream(gfp_t gfp);
> +void raw_free_string_stream(struct string_stream *stream);
> +
>  struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp);
> +void free_string_stream(struct kunit *test, struct string_stream *stream);
>
>  int __printf(2, 3) string_stream_add(struct string_stream *stream,
>                                      const char *fmt, ...);
> @@ -47,7 +57,7 @@ int string_stream_append(struct string_stream *stream,
>
>  bool string_stream_is_empty(struct string_stream *stream);
>
> -void string_stream_destroy(struct string_stream *stream);
> +void string_stream_clear(struct string_stream *stream);
>
>  static inline void string_stream_set_append_newlines(struct string_stream *stream,
>                                                      bool append_newlines)
> diff --git a/lib/kunit/test.c b/lib/kunit/test.c
> index 520e15f49d0d..4b69d12dfc96 100644
> --- a/lib/kunit/test.c
> +++ b/lib/kunit/test.c
> @@ -322,7 +322,7 @@ static void kunit_fail(struct kunit *test, const struct kunit_loc *loc,
>
>         kunit_print_string_stream(test, stream);
>
> -       string_stream_destroy(stream);
> +       free_string_stream(test, stream);
>  }
>
>  void __noreturn __kunit_abort(struct kunit *test)
> --
> 2.30.2
>
Richard Fitzgerald Aug. 15, 2023, 10:51 a.m. UTC | #3
On 15/8/23 10:16, David Gow wrote:
> On Mon, 14 Aug 2023 at 21:23, Richard Fitzgerald
> <rf@opensource.cirrus.com> wrote:
>>
>> Re-work string_stream so that it is not tied to a struct kunit. This is
>> to allow using it for the log of struct kunit_suite.
>>
>> Instead of resource-managing individual allocations the whole string_stream
>> object can be resource-managed as a single object:
>>
>>      alloc_string_stream() API is unchanged and takes a pointer to a
>>      struct kunit but it now registers the returned string_stream object to
>>      be resource-managed.
>>
>>      raw_alloc_string_stream() is a new function that allocates a
>>      bare string_stream without any association to a struct kunit.
>>
>>      free_string_stream() is a new function that frees a resource-managed
>>      string_stream allocated by alloc_string_stream().
>>
>>      raw_free_string_stream() is a new function that frees a non-managed
>>      string_stream allocated by raw_alloc_string_stream().
>>
>> The confusing function string_stream_destroy() has been removed. This only
>> called string_stream_clear() but didn't free the struct string_stream.
>> Instead string_stream_clear() has been exported, and the new functions use
>> the more conventional naming of "free" as the opposite of "alloc".
>>
>> Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
>> ---
> 
> I'm in favour of this. Should we go further and get rid of the struct
> kunit member from string_stream totally?
>

I can do that. I was worried about some hairy-looking code in assert.c
that used stream->test. But I've just looked at it again and it's
really quite simple, and doesn't even need ->test. is_literal()
allocates a temporary managed buffer, but it frees it before returning
so it doesn't need to be managed.

> Also, note that the kunit_action_t casting is causing warnings on some
> clang configs (and technically isn't valid C). Personally, I still
> like it, but expect more emails from the kernel test robot and others.
> 

I can send a new version to fix that.
David Gow Aug. 17, 2023, 6:24 a.m. UTC | #4
On Tue, 15 Aug 2023 at 18:51, Richard Fitzgerald
<rf@opensource.cirrus.com> wrote:
>
> On 15/8/23 10:16, David Gow wrote:
> > On Mon, 14 Aug 2023 at 21:23, Richard Fitzgerald
> > <rf@opensource.cirrus.com> wrote:
> >>
> >> Re-work string_stream so that it is not tied to a struct kunit. This is
> >> to allow using it for the log of struct kunit_suite.
> >>
> >> Instead of resource-managing individual allocations the whole string_stream
> >> object can be resource-managed as a single object:
> >>
> >>      alloc_string_stream() API is unchanged and takes a pointer to a
> >>      struct kunit but it now registers the returned string_stream object to
> >>      be resource-managed.
> >>
> >>      raw_alloc_string_stream() is a new function that allocates a
> >>      bare string_stream without any association to a struct kunit.
> >>
> >>      free_string_stream() is a new function that frees a resource-managed
> >>      string_stream allocated by alloc_string_stream().
> >>
> >>      raw_free_string_stream() is a new function that frees a non-managed
> >>      string_stream allocated by raw_alloc_string_stream().
> >>
> >> The confusing function string_stream_destroy() has been removed. This only
> >> called string_stream_clear() but didn't free the struct string_stream.
> >> Instead string_stream_clear() has been exported, and the new functions use
> >> the more conventional naming of "free" as the opposite of "alloc".
> >>
> >> Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com>
> >> ---
> >
> > I'm in favour of this. Should we go further and get rid of the struct
> > kunit member from string_stream totally?
> >
>
> I can do that. I was worried about some hairy-looking code in assert.c
> that used stream->test. But I've just looked at it again and it's
> really quite simple, and doesn't even need ->test. is_literal()
> allocates a temporary managed buffer, but it frees it before returning
> so it doesn't need to be managed.
>

Yeah, let's get rid of that. Having a stream->kunit exist but be NULL
half the time is asking for issues down the line.

> > Also, note that the kunit_action_t casting is causing warnings on some
> > clang configs (and technically isn't valid C). Personally, I still
> > like it, but expect more emails from the kernel test robot and others.
> >
>
> I can send a new version to fix that.
>

That's probably best. If you want to keep it as-is, I'll fight for it,
but it's probably better to err on the side of not introducing the
warnings.

Thanks,
-- David
diff mbox series

Patch

diff --git a/lib/kunit/string-stream-test.c b/lib/kunit/string-stream-test.c
index 8a30bb7d5fb7..437aa4b3179d 100644
--- a/lib/kunit/string-stream-test.c
+++ b/lib/kunit/string-stream-test.c
@@ -200,7 +200,7 @@  static void string_stream_append_test(struct kunit *test)
 			   combined_content);
 
 	/* Append content of non-empty stream to empty stream */
-	string_stream_destroy(stream_1);
+	string_stream_clear(stream_1);
 
 	stream_1 = alloc_string_stream(test, GFP_KERNEL);
 	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, stream_1);
diff --git a/lib/kunit/string-stream.c b/lib/kunit/string-stream.c
index eb673d3ea3bd..06104a729b45 100644
--- a/lib/kunit/string-stream.c
+++ b/lib/kunit/string-stream.c
@@ -13,30 +13,28 @@ 
 #include "string-stream.h"
 
 
-static struct string_stream_fragment *alloc_string_stream_fragment(
-		struct kunit *test, int len, gfp_t gfp)
+static struct string_stream_fragment *alloc_string_stream_fragment(int len, gfp_t gfp)
 {
 	struct string_stream_fragment *frag;
 
-	frag = kunit_kzalloc(test, sizeof(*frag), gfp);
+	frag = kzalloc(sizeof(*frag), gfp);
 	if (!frag)
 		return ERR_PTR(-ENOMEM);
 
-	frag->fragment = kunit_kmalloc(test, len, gfp);
+	frag->fragment = kmalloc(len, gfp);
 	if (!frag->fragment) {
-		kunit_kfree(test, frag);
+		kfree(frag);
 		return ERR_PTR(-ENOMEM);
 	}
 
 	return frag;
 }
 
-static void string_stream_fragment_destroy(struct kunit *test,
-					   struct string_stream_fragment *frag)
+static void string_stream_fragment_destroy(struct string_stream_fragment *frag)
 {
 	list_del(&frag->node);
-	kunit_kfree(test, frag->fragment);
-	kunit_kfree(test, frag);
+	kfree(frag->fragment);
+	kfree(frag);
 }
 
 int string_stream_vadd(struct string_stream *stream,
@@ -65,9 +63,7 @@  int string_stream_vadd(struct string_stream *stream,
 	/* Need space for null byte. */
 	buf_len++;
 
-	frag_container = alloc_string_stream_fragment(stream->test,
-						      buf_len,
-						      stream->gfp);
+	frag_container = alloc_string_stream_fragment(buf_len, stream->gfp);
 	if (IS_ERR(frag_container))
 		return PTR_ERR(frag_container);
 
@@ -102,7 +98,7 @@  int string_stream_add(struct string_stream *stream, const char *fmt, ...)
 	return result;
 }
 
-static void string_stream_clear(struct string_stream *stream)
+void string_stream_clear(struct string_stream *stream)
 {
 	struct string_stream_fragment *frag_container, *frag_container_safe;
 
@@ -111,16 +107,28 @@  static void string_stream_clear(struct string_stream *stream)
 				 frag_container_safe,
 				 &stream->fragments,
 				 node) {
-		string_stream_fragment_destroy(stream->test, frag_container);
+		string_stream_fragment_destroy(frag_container);
 	}
 	stream->length = 0;
 	spin_unlock(&stream->lock);
 }
 
+static void _string_stream_concatenate_to_buf(struct string_stream *stream,
+					      char *buf, size_t buf_len)
+{
+	struct string_stream_fragment *frag_container;
+
+	buf[0] = '\0';
+
+	spin_lock(&stream->lock);
+	list_for_each_entry(frag_container, &stream->fragments, node)
+		strlcat(buf, frag_container->fragment, buf_len);
+	spin_unlock(&stream->lock);
+}
+
 char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
 			       gfp_t gfp)
 {
-	struct string_stream_fragment *frag_container;
 	size_t buf_len = stream->length + 1; /* +1 for null byte. */
 	char *buf;
 
@@ -128,10 +136,7 @@  char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
 	if (!buf)
 		return NULL;
 
-	spin_lock(&stream->lock);
-	list_for_each_entry(frag_container, &stream->fragments, node)
-		strlcat(buf, frag_container->fragment, buf_len);
-	spin_unlock(&stream->lock);
+	_string_stream_concatenate_to_buf(stream, buf, buf_len);
 
 	return buf;
 }
@@ -139,13 +144,20 @@  char *string_stream_get_string(struct kunit *test, struct string_stream *stream,
 int string_stream_append(struct string_stream *stream,
 			 struct string_stream *other)
 {
-	const char *other_content;
+	size_t other_buf_len = other->length + 1; /* +1 for null byte. */
+	char *other_buf;
+	int ret;
 
-	other_content = string_stream_get_string(other->test, other, other->gfp);
-	if (!other_content)
+	other_buf = kmalloc(other_buf_len, GFP_KERNEL);
+	if (!other_buf)
 		return -ENOMEM;
 
-	return string_stream_add(stream, other_content);
+	_string_stream_concatenate_to_buf(other, other_buf, other_buf_len);
+
+	ret = string_stream_add(stream, other_buf);
+	kfree(other_buf);
+
+	return ret;
 }
 
 bool string_stream_is_empty(struct string_stream *stream)
@@ -153,23 +165,47 @@  bool string_stream_is_empty(struct string_stream *stream)
 	return list_empty(&stream->fragments);
 }
 
-struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp)
+void raw_free_string_stream(struct string_stream *stream)
+{
+	string_stream_clear(stream);
+	kfree(stream);
+}
+
+struct string_stream *raw_alloc_string_stream(gfp_t gfp)
 {
 	struct string_stream *stream;
 
-	stream = kunit_kzalloc(test, sizeof(*stream), gfp);
+	stream = kzalloc(sizeof(*stream), gfp);
 	if (!stream)
 		return ERR_PTR(-ENOMEM);
 
 	stream->gfp = gfp;
-	stream->test = test;
 	INIT_LIST_HEAD(&stream->fragments);
 	spin_lock_init(&stream->lock);
 
 	return stream;
 }
 
-void string_stream_destroy(struct string_stream *stream)
+struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp)
 {
-	string_stream_clear(stream);
+	struct string_stream *stream;
+
+	stream = raw_alloc_string_stream(gfp);
+	if (IS_ERR(stream))
+		return stream;
+
+	stream->test = test;
+
+	if (kunit_add_action_or_reset(test, (kunit_action_t *)raw_free_string_stream, stream) != 0)
+		return ERR_PTR(-ENOMEM);
+
+	return stream;
+}
+
+void free_string_stream(struct kunit *test, struct string_stream *stream)
+{
+	if (!stream)
+		return;
+
+	kunit_release_action(test, (kunit_action_t *)raw_free_string_stream, (void *)stream);
 }
diff --git a/lib/kunit/string-stream.h b/lib/kunit/string-stream.h
index 6b4a747881c5..fbeda486382a 100644
--- a/lib/kunit/string-stream.h
+++ b/lib/kunit/string-stream.h
@@ -23,14 +23,24 @@  struct string_stream {
 	struct list_head fragments;
 	/* length and fragments are protected by this lock */
 	spinlock_t lock;
+
+	/*
+	 * Pointer to kunit this stream is associated with, or NULL if
+	 * not associated with a kunit.
+	 */
 	struct kunit *test;
+
 	gfp_t gfp;
 	bool append_newlines;
 };
 
 struct kunit;
 
+struct string_stream *raw_alloc_string_stream(gfp_t gfp);
+void raw_free_string_stream(struct string_stream *stream);
+
 struct string_stream *alloc_string_stream(struct kunit *test, gfp_t gfp);
+void free_string_stream(struct kunit *test, struct string_stream *stream);
 
 int __printf(2, 3) string_stream_add(struct string_stream *stream,
 				     const char *fmt, ...);
@@ -47,7 +57,7 @@  int string_stream_append(struct string_stream *stream,
 
 bool string_stream_is_empty(struct string_stream *stream);
 
-void string_stream_destroy(struct string_stream *stream);
+void string_stream_clear(struct string_stream *stream);
 
 static inline void string_stream_set_append_newlines(struct string_stream *stream,
 						     bool append_newlines)
diff --git a/lib/kunit/test.c b/lib/kunit/test.c
index 520e15f49d0d..4b69d12dfc96 100644
--- a/lib/kunit/test.c
+++ b/lib/kunit/test.c
@@ -322,7 +322,7 @@  static void kunit_fail(struct kunit *test, const struct kunit_loc *loc,
 
 	kunit_print_string_stream(test, stream);
 
-	string_stream_destroy(stream);
+	free_string_stream(test, stream);
 }
 
 void __noreturn __kunit_abort(struct kunit *test)