diff mbox series

[v4,7/8] tests/unit/test-char: add unit test for the `mux-be` multiplexer

Message ID 20241016102605.459395-8-r.peniaev@gmail.com (mailing list archive)
State New
Headers show
Series chardev: implement backend chardev multiplexing | expand

Commit Message

Roman Penyaev Oct. 16, 2024, 10:26 a.m. UTC
The test is trivial: several backends, 1 `mux-be`, 1 frontend
do the buffer write and read. Pipe is used for EAGAIN verification.

Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
Cc: qemu-devel@nongnu.org
---
 tests/unit/test-char.c | 306 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 304 insertions(+), 2 deletions(-)

Comments

Marc-André Lureau Oct. 16, 2024, 11:36 a.m. UTC | #1
Hi

On Wed, Oct 16, 2024 at 2:28 PM Roman Penyaev <r.peniaev@gmail.com> wrote:
>
> The test is trivial: several backends, 1 `mux-be`, 1 frontend
> do the buffer write and read. Pipe is used for EAGAIN verification.
>
> Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> Cc: qemu-devel@nongnu.org
> ---
>  tests/unit/test-char.c | 306 ++++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 304 insertions(+), 2 deletions(-)

please fix the few leaks (found with --enable-asan)

>
> diff --git a/tests/unit/test-char.c b/tests/unit/test-char.c
> index a1c6bb874c8e..3eb0692b199f 100644
> --- a/tests/unit/test-char.c
> +++ b/tests/unit/test-char.c
> @@ -178,7 +178,7 @@ static void char_ringbuf_test(void)
>      qemu_opts_del(opts);
>  }
>
> -static void char_mux_test(void)
> +static void char_mux_fe_test(void)
>  {
>      QemuOpts *opts;
>      Chardev *chr, *base;
> @@ -359,6 +359,307 @@ static void char_mux_test(void)
>      qmp_chardev_remove("mux-label", &error_abort);
>  }
>
> +static void char_mux_be_test(void)
> +{
> +    QemuOpts *opts;
> +    Chardev *mux_be, *chr1, *chr2, *base;
> +    char *data;
> +    FeHandler h = { 0, false, 0, false, };
> +    Error *error = NULL;
> +    CharBackend chr_be;
> +    int ret, i;
> +
> +#define RB_SIZE 128
> +
> +    /* Create mux-be */
> +    opts = qemu_opts_create(qemu_find_opts("chardev"), "mux0",
> +                            1, &error_abort);
> +    qemu_opt_set(opts, "backend", "mux-be", &error_abort);
> +    mux_be = qemu_chr_new_from_opts(opts, NULL, &error_abort);
> +    g_assert_nonnull(mux_be);
> +    qemu_opts_del(opts);
> +
> +    /* Check maximum allowed backends */
> +    for (i = 0; true; i++) {
> +        char name[8];
> +
> +        snprintf(name, sizeof(name), "chr%d", i);
> +        opts = qemu_opts_create(qemu_find_opts("chardev"), name,
> +                                1, &error_abort);
> +        qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
> +        qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
> +        qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
> +        base = qemu_chr_new_from_opts(opts, NULL, &error);
> +        if (error) {
> +            const char *err_fmt =
> +                "too many uses of multiplexed chardev 'mux0' (maximum is %u)";
> +            unsigned n;
> +
> +            ret = sscanf(error_get_pretty(error), err_fmt, &n);
> +            error_free(error);
> +            error = NULL;
> +            g_assert_cmpint(ret, ==, 1);
> +            g_assert_cmpint(i, ==, n);
> +            break;
> +        }
> +        g_assert_nonnull(base);
> +        qemu_opts_del(opts);
> +    }
> +    /* Finalize mux0 */
> +    qmp_chardev_remove("mux0", &error_abort);
> +
> +    /* Finalize all backends */
> +    while (i--) {
> +        char name[8];
> +        snprintf(name, sizeof(name), "chr%d", i);
> +        qmp_chardev_remove(name, &error_abort);
> +    }
> +
> +    /* Create mux-be */
> +    opts = qemu_opts_create(qemu_find_opts("chardev"), "mux0",
> +                            1, &error_abort);
> +    qemu_opt_set(opts, "backend", "mux-be", &error_abort);
> +    mux_be = qemu_chr_new_from_opts(opts, NULL, &error_abort);
> +    g_assert_nonnull(mux_be);
> +    qemu_opts_del(opts);
> +
> +    /* Create chardev which fails */
> +    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
> +                            1, &error_abort);
> +    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
> +    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
> +    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
> +    qemu_opt_set(opts, "mux", "on", &error_abort);
> +    chr1 = qemu_chr_new_from_opts(opts, NULL, &error);
> +    g_assert_cmpstr(error_get_pretty(error), ==, "chardev: mux and mux-be "
> +                    "can't be used for the same device");
> +    error_free(error);
> +    error = NULL;
> +    qemu_opts_del(opts);
> +
> +    /* Create first chardev */
> +    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
> +                            1, &error_abort);
> +    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
> +    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
> +    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
> +    chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
> +    g_assert_nonnull(chr1);
> +    qemu_opts_del(opts);
> +
> +    /* Create second chardev */
> +    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2",
> +                            1, &error_abort);
> +    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
> +    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
> +    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
> +    chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
> +    g_assert_nonnull(chr2);
> +    qemu_opts_del(opts);
> +
> +    /* Attach mux-be to a frontend */
> +    qemu_chr_fe_init(&chr_be, mux_be, &error_abort);
> +    qemu_chr_fe_set_handlers(&chr_be,
> +                             fe_can_read,
> +                             fe_read,
> +                             fe_event,
> +                             NULL,
> +                             &h,
> +                             NULL, true);
> +
> +    /* Fails second time */
> +    qemu_chr_fe_init(&chr_be, mux_be, &error);
> +    g_assert_cmpstr(error_get_pretty(error), ==, "multiplexed chardev 'mux0' "
> +                    "is already used for multiplexing");
> +    error_free(error);
> +    error = NULL;
> +
> +    /* Write to backend, chr1 */
> +    base = qemu_chr_find("chr1");
> +    g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
> +
> +    qemu_chr_be_write(base, (void *)"hello", 6);
> +    g_assert_cmpint(h.read_count, ==, 6);
> +    g_assert_cmpstr(h.read_buf, ==, "hello");
> +    h.read_count = 0;
> +
> +    /* Write to backend, chr2 */
> +    base = qemu_chr_find("chr2");
> +    g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
> +
> +    qemu_chr_be_write(base, (void *)"olleh", 6);
> +    g_assert_cmpint(h.read_count, ==, 6);
> +    g_assert_cmpstr(h.read_buf, ==, "olleh");
> +    h.read_count = 0;
> +
> +    /* Write to frontend, chr_be */
> +    ret = qemu_chr_fe_write(&chr_be, (void *)"heyhey", 6);
> +    g_assert_cmpint(ret, ==, 6);
> +
> +    data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
> +    g_assert_cmpint(strlen(data), ==, 6);
> +    g_assert_cmpstr(data, ==, "heyhey");
> +    g_free(data);
> +
> +    data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
> +    g_assert_cmpint(strlen(data), ==, 6);
> +    g_assert_cmpstr(data, ==, "heyhey");
> +    g_free(data);
> +
> +
> +#ifndef _WIN32
> +    /*
> +     * Create third chardev to simulate EAGAIN and watcher.
> +     * Mainly copied from char_pipe_test().
> +     */
> +    {
> +        gchar *tmp_path = g_dir_make_tmp("qemu-test-char.XXXXXX", NULL);
> +        gchar *in, *out, *pipe = g_build_filename(tmp_path, "pipe", NULL);
> +        Chardev *chr3;
> +        int fd, len;
> +        char buf[128];
> +
> +        in = g_strdup_printf("%s.in", pipe);
> +        if (mkfifo(in, 0600) < 0) {
> +            abort();
> +        }
> +        out = g_strdup_printf("%s.out", pipe);
> +        if (mkfifo(out, 0600) < 0) {
> +            abort();
> +        }
> +
> +        opts = qemu_opts_create(qemu_find_opts("chardev"), "chr3",
> +                                1, &error_abort);
> +        qemu_opt_set(opts, "backend", "pipe", &error_abort);
> +        qemu_opt_set(opts, "path", pipe, &error_abort);
> +        qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
> +        chr3 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
> +        g_assert_nonnull(chr3);
> +
> +        /* Write to frontend, chr_be */
> +        ret = qemu_chr_fe_write(&chr_be, (void *)"thisis", 6);
> +        g_assert_cmpint(ret, ==, 6);
> +
> +        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 6);
> +        g_assert_cmpstr(data, ==, "thisis");
> +        g_free(data);
> +
> +        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 6);
> +        g_assert_cmpstr(data, ==, "thisis");
> +        g_free(data);
> +
> +        fd = open(out, O_RDWR);
> +        ret = read(fd, buf, sizeof(buf));
> +        g_assert_cmpint(ret, ==, 6);
> +        buf[ret] = 0;
> +        g_assert_cmpstr(buf, ==, "thisis");
> +        close(fd);
> +
> +        /* Add watch. 0 indicates no watches if nothing to wait for */
> +        ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
> +                                    NULL, NULL);
> +        g_assert_cmpint(ret, ==, 0);
> +
> +        /*
> +         * Write to frontend, chr_be, until EAGAIN. Make sure length is
> +         * power of two to fit nicely the whole pipe buffer.
> +         */
> +        len = 0;
> +        while ((ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8))
> +               != -1) {
> +            len += ret;
> +        }
> +        g_assert_cmpint(errno, ==, EAGAIN);
> +
> +        /* Further all writes should cause EAGAIN */
> +        ret = qemu_chr_fe_write(&chr_be, (void *)"b", 1);
> +        g_assert_cmpint(ret, ==, -1);
> +        g_assert_cmpint(errno, ==, EAGAIN);
> +
> +        /*
> +         * Add watch. Non 0 indicates we have a blocked chardev, which
> +         * can wakes us up when write is possible.
> +         */
> +        ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
> +                                    NULL, NULL);
> +        g_assert_cmpint(ret, !=, 0);
> +        g_source_remove(ret);
> +
> +        /* Drain pipe and ring buffers */
> +        fd = open(out, O_RDWR);
> +        while ((ret = read(fd, buf, MIN(sizeof(buf), len))) != -1 && len > 0) {
> +            len -= ret;
> +        }
> +        close(fd);
> +
> +        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 128);
> +        g_free(data);
> +
> +        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 128);
> +        g_free(data);
> +
> +        /*
> +         * Now we are good to go, first repeat "lost" sequence, which
> +         * was already consumed and drained by the ring buffers, but
> +         * pipe have not recieved that yet.
> +         */
> +        ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8);
> +        g_assert_cmpint(ret, ==, 8);
> +
> +        ret = qemu_chr_fe_write(&chr_be, (void *)"streamisrestored", 16);
> +        g_assert_cmpint(ret, ==, 16);
> +
> +        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 16);
> +        /* Only last 16 bytes, see big comment above */
> +        g_assert_cmpstr(data, ==, "streamisrestored");
> +        g_free(data);
> +
> +        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
> +        g_assert_cmpint(strlen(data), ==, 16);
> +        /* Only last 16 bytes, see big comment above */
> +        g_assert_cmpstr(data, ==, "streamisrestored");
> +        g_free(data);
> +
> +        fd = open(out, O_RDWR);
> +        ret = read(fd, buf, sizeof(buf));
> +        g_assert_cmpint(ret, ==, 24);
> +        buf[ret] = 0;
> +        /* Both 8 and 16 bytes */
> +        g_assert_cmpstr(buf, ==, "thisisitstreamisrestored");
> +        close(fd);
> +    }
> +#endif
> +
> +    /* Can't be removed, depends on mux0 */
> +    qmp_chardev_remove("chr1", &error);
> +    g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'chr1' is busy");
> +    error_free(error);
> +    error = NULL;
> +
> +    /* Can't be removed, depends on frontend chr_be */
> +    qmp_chardev_remove("mux0", &error);
> +    g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'mux0' is busy");
> +    error_free(error);
> +    error = NULL;
> +
> +    /* Finalize frontend */
> +    qemu_chr_fe_deinit(&chr_be, false);
> +
> +    /* Finalize mux0 */
> +    qmp_chardev_remove("mux0", &error_abort);
> +
> +    /* Finalize backend chardevs */
> +    qmp_chardev_remove("chr1", &error_abort);
> +    qmp_chardev_remove("chr2", &error_abort);
> +#ifndef _WIN32
> +    qmp_chardev_remove("chr3", &error_abort);
> +#endif
> +}
>
>  static void websock_server_read(void *opaque, const uint8_t *buf, int size)
>  {
> @@ -1506,7 +1807,8 @@ int main(int argc, char **argv)
>      g_test_add_func("/char/null", char_null_test);
>      g_test_add_func("/char/invalid", char_invalid_test);
>      g_test_add_func("/char/ringbuf", char_ringbuf_test);
> -    g_test_add_func("/char/mux", char_mux_test);
> +    g_test_add_func("/char/mux", char_mux_fe_test);
> +    g_test_add_func("/char/mux-be", char_mux_be_test);
>  #ifdef _WIN32
>      g_test_add_func("/char/console/subprocess", char_console_test_subprocess);
>      g_test_add_func("/char/console", char_console_test);
> --
> 2.34.1
>
Roman Penyaev Oct. 17, 2024, 1:48 p.m. UTC | #2
Hi Marc-André,

On Wed, Oct 16, 2024 at 1:36 PM Marc-André Lureau
<marcandre.lureau@redhat.com> wrote:
>
> Hi
>
> On Wed, Oct 16, 2024 at 2:28 PM Roman Penyaev <r.peniaev@gmail.com> wrote:
> >
> > The test is trivial: several backends, 1 `mux-be`, 1 frontend
> > do the buffer write and read. Pipe is used for EAGAIN verification.
> >
> > Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
> > Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>
> > Cc: qemu-devel@nongnu.org
> > ---
> >  tests/unit/test-char.c | 306 ++++++++++++++++++++++++++++++++++++++++-
> >  1 file changed, 304 insertions(+), 2 deletions(-)
>
> please fix the few leaks (found with --enable-asan)

Did not know about this option. Should be easy, thanks.

--
Roman
diff mbox series

Patch

diff --git a/tests/unit/test-char.c b/tests/unit/test-char.c
index a1c6bb874c8e..3eb0692b199f 100644
--- a/tests/unit/test-char.c
+++ b/tests/unit/test-char.c
@@ -178,7 +178,7 @@  static void char_ringbuf_test(void)
     qemu_opts_del(opts);
 }
 
-static void char_mux_test(void)
+static void char_mux_fe_test(void)
 {
     QemuOpts *opts;
     Chardev *chr, *base;
@@ -359,6 +359,307 @@  static void char_mux_test(void)
     qmp_chardev_remove("mux-label", &error_abort);
 }
 
+static void char_mux_be_test(void)
+{
+    QemuOpts *opts;
+    Chardev *mux_be, *chr1, *chr2, *base;
+    char *data;
+    FeHandler h = { 0, false, 0, false, };
+    Error *error = NULL;
+    CharBackend chr_be;
+    int ret, i;
+
+#define RB_SIZE 128
+
+    /* Create mux-be */
+    opts = qemu_opts_create(qemu_find_opts("chardev"), "mux0",
+                            1, &error_abort);
+    qemu_opt_set(opts, "backend", "mux-be", &error_abort);
+    mux_be = qemu_chr_new_from_opts(opts, NULL, &error_abort);
+    g_assert_nonnull(mux_be);
+    qemu_opts_del(opts);
+
+    /* Check maximum allowed backends */
+    for (i = 0; true; i++) {
+        char name[8];
+
+        snprintf(name, sizeof(name), "chr%d", i);
+        opts = qemu_opts_create(qemu_find_opts("chardev"), name,
+                                1, &error_abort);
+        qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
+        qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
+        qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
+        base = qemu_chr_new_from_opts(opts, NULL, &error);
+        if (error) {
+            const char *err_fmt =
+                "too many uses of multiplexed chardev 'mux0' (maximum is %u)";
+            unsigned n;
+
+            ret = sscanf(error_get_pretty(error), err_fmt, &n);
+            error_free(error);
+            error = NULL;
+            g_assert_cmpint(ret, ==, 1);
+            g_assert_cmpint(i, ==, n);
+            break;
+        }
+        g_assert_nonnull(base);
+        qemu_opts_del(opts);
+    }
+    /* Finalize mux0 */
+    qmp_chardev_remove("mux0", &error_abort);
+
+    /* Finalize all backends */
+    while (i--) {
+        char name[8];
+        snprintf(name, sizeof(name), "chr%d", i);
+        qmp_chardev_remove(name, &error_abort);
+    }
+
+    /* Create mux-be */
+    opts = qemu_opts_create(qemu_find_opts("chardev"), "mux0",
+                            1, &error_abort);
+    qemu_opt_set(opts, "backend", "mux-be", &error_abort);
+    mux_be = qemu_chr_new_from_opts(opts, NULL, &error_abort);
+    g_assert_nonnull(mux_be);
+    qemu_opts_del(opts);
+
+    /* Create chardev which fails */
+    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
+                            1, &error_abort);
+    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
+    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
+    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
+    qemu_opt_set(opts, "mux", "on", &error_abort);
+    chr1 = qemu_chr_new_from_opts(opts, NULL, &error);
+    g_assert_cmpstr(error_get_pretty(error), ==, "chardev: mux and mux-be "
+                    "can't be used for the same device");
+    error_free(error);
+    error = NULL;
+    qemu_opts_del(opts);
+
+    /* Create first chardev */
+    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr1",
+                            1, &error_abort);
+    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
+    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
+    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
+    chr1 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
+    g_assert_nonnull(chr1);
+    qemu_opts_del(opts);
+
+    /* Create second chardev */
+    opts = qemu_opts_create(qemu_find_opts("chardev"), "chr2",
+                            1, &error_abort);
+    qemu_opt_set(opts, "backend", "ringbuf", &error_abort);
+    qemu_opt_set(opts, "size", stringify(RB_SIZE), &error_abort);
+    qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
+    chr2 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
+    g_assert_nonnull(chr2);
+    qemu_opts_del(opts);
+
+    /* Attach mux-be to a frontend */
+    qemu_chr_fe_init(&chr_be, mux_be, &error_abort);
+    qemu_chr_fe_set_handlers(&chr_be,
+                             fe_can_read,
+                             fe_read,
+                             fe_event,
+                             NULL,
+                             &h,
+                             NULL, true);
+
+    /* Fails second time */
+    qemu_chr_fe_init(&chr_be, mux_be, &error);
+    g_assert_cmpstr(error_get_pretty(error), ==, "multiplexed chardev 'mux0' "
+                    "is already used for multiplexing");
+    error_free(error);
+    error = NULL;
+
+    /* Write to backend, chr1 */
+    base = qemu_chr_find("chr1");
+    g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
+
+    qemu_chr_be_write(base, (void *)"hello", 6);
+    g_assert_cmpint(h.read_count, ==, 6);
+    g_assert_cmpstr(h.read_buf, ==, "hello");
+    h.read_count = 0;
+
+    /* Write to backend, chr2 */
+    base = qemu_chr_find("chr2");
+    g_assert_cmpint(qemu_chr_be_can_write(base), !=, 0);
+
+    qemu_chr_be_write(base, (void *)"olleh", 6);
+    g_assert_cmpint(h.read_count, ==, 6);
+    g_assert_cmpstr(h.read_buf, ==, "olleh");
+    h.read_count = 0;
+
+    /* Write to frontend, chr_be */
+    ret = qemu_chr_fe_write(&chr_be, (void *)"heyhey", 6);
+    g_assert_cmpint(ret, ==, 6);
+
+    data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
+    g_assert_cmpint(strlen(data), ==, 6);
+    g_assert_cmpstr(data, ==, "heyhey");
+    g_free(data);
+
+    data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
+    g_assert_cmpint(strlen(data), ==, 6);
+    g_assert_cmpstr(data, ==, "heyhey");
+    g_free(data);
+
+
+#ifndef _WIN32
+    /*
+     * Create third chardev to simulate EAGAIN and watcher.
+     * Mainly copied from char_pipe_test().
+     */
+    {
+        gchar *tmp_path = g_dir_make_tmp("qemu-test-char.XXXXXX", NULL);
+        gchar *in, *out, *pipe = g_build_filename(tmp_path, "pipe", NULL);
+        Chardev *chr3;
+        int fd, len;
+        char buf[128];
+
+        in = g_strdup_printf("%s.in", pipe);
+        if (mkfifo(in, 0600) < 0) {
+            abort();
+        }
+        out = g_strdup_printf("%s.out", pipe);
+        if (mkfifo(out, 0600) < 0) {
+            abort();
+        }
+
+        opts = qemu_opts_create(qemu_find_opts("chardev"), "chr3",
+                                1, &error_abort);
+        qemu_opt_set(opts, "backend", "pipe", &error_abort);
+        qemu_opt_set(opts, "path", pipe, &error_abort);
+        qemu_opt_set(opts, "mux-be-id", "mux0", &error_abort);
+        chr3 = qemu_chr_new_from_opts(opts, NULL, &error_abort);
+        g_assert_nonnull(chr3);
+
+        /* Write to frontend, chr_be */
+        ret = qemu_chr_fe_write(&chr_be, (void *)"thisis", 6);
+        g_assert_cmpint(ret, ==, 6);
+
+        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 6);
+        g_assert_cmpstr(data, ==, "thisis");
+        g_free(data);
+
+        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 6);
+        g_assert_cmpstr(data, ==, "thisis");
+        g_free(data);
+
+        fd = open(out, O_RDWR);
+        ret = read(fd, buf, sizeof(buf));
+        g_assert_cmpint(ret, ==, 6);
+        buf[ret] = 0;
+        g_assert_cmpstr(buf, ==, "thisis");
+        close(fd);
+
+        /* Add watch. 0 indicates no watches if nothing to wait for */
+        ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
+                                    NULL, NULL);
+        g_assert_cmpint(ret, ==, 0);
+
+        /*
+         * Write to frontend, chr_be, until EAGAIN. Make sure length is
+         * power of two to fit nicely the whole pipe buffer.
+         */
+        len = 0;
+        while ((ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8))
+               != -1) {
+            len += ret;
+        }
+        g_assert_cmpint(errno, ==, EAGAIN);
+
+        /* Further all writes should cause EAGAIN */
+        ret = qemu_chr_fe_write(&chr_be, (void *)"b", 1);
+        g_assert_cmpint(ret, ==, -1);
+        g_assert_cmpint(errno, ==, EAGAIN);
+
+        /*
+         * Add watch. Non 0 indicates we have a blocked chardev, which
+         * can wakes us up when write is possible.
+         */
+        ret = qemu_chr_fe_add_watch(&chr_be, G_IO_OUT | G_IO_HUP,
+                                    NULL, NULL);
+        g_assert_cmpint(ret, !=, 0);
+        g_source_remove(ret);
+
+        /* Drain pipe and ring buffers */
+        fd = open(out, O_RDWR);
+        while ((ret = read(fd, buf, MIN(sizeof(buf), len))) != -1 && len > 0) {
+            len -= ret;
+        }
+        close(fd);
+
+        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 128);
+        g_free(data);
+
+        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 128);
+        g_free(data);
+
+        /*
+         * Now we are good to go, first repeat "lost" sequence, which
+         * was already consumed and drained by the ring buffers, but
+         * pipe have not recieved that yet.
+         */
+        ret = qemu_chr_fe_write(&chr_be, (void *)"thisisit", 8);
+        g_assert_cmpint(ret, ==, 8);
+
+        ret = qemu_chr_fe_write(&chr_be, (void *)"streamisrestored", 16);
+        g_assert_cmpint(ret, ==, 16);
+
+        data = qmp_ringbuf_read("chr1", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 16);
+        /* Only last 16 bytes, see big comment above */
+        g_assert_cmpstr(data, ==, "streamisrestored");
+        g_free(data);
+
+        data = qmp_ringbuf_read("chr2", RB_SIZE, false, 0, &error_abort);
+        g_assert_cmpint(strlen(data), ==, 16);
+        /* Only last 16 bytes, see big comment above */
+        g_assert_cmpstr(data, ==, "streamisrestored");
+        g_free(data);
+
+        fd = open(out, O_RDWR);
+        ret = read(fd, buf, sizeof(buf));
+        g_assert_cmpint(ret, ==, 24);
+        buf[ret] = 0;
+        /* Both 8 and 16 bytes */
+        g_assert_cmpstr(buf, ==, "thisisitstreamisrestored");
+        close(fd);
+    }
+#endif
+
+    /* Can't be removed, depends on mux0 */
+    qmp_chardev_remove("chr1", &error);
+    g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'chr1' is busy");
+    error_free(error);
+    error = NULL;
+
+    /* Can't be removed, depends on frontend chr_be */
+    qmp_chardev_remove("mux0", &error);
+    g_assert_cmpstr(error_get_pretty(error), ==, "Chardev 'mux0' is busy");
+    error_free(error);
+    error = NULL;
+
+    /* Finalize frontend */
+    qemu_chr_fe_deinit(&chr_be, false);
+
+    /* Finalize mux0 */
+    qmp_chardev_remove("mux0", &error_abort);
+
+    /* Finalize backend chardevs */
+    qmp_chardev_remove("chr1", &error_abort);
+    qmp_chardev_remove("chr2", &error_abort);
+#ifndef _WIN32
+    qmp_chardev_remove("chr3", &error_abort);
+#endif
+}
 
 static void websock_server_read(void *opaque, const uint8_t *buf, int size)
 {
@@ -1506,7 +1807,8 @@  int main(int argc, char **argv)
     g_test_add_func("/char/null", char_null_test);
     g_test_add_func("/char/invalid", char_invalid_test);
     g_test_add_func("/char/ringbuf", char_ringbuf_test);
-    g_test_add_func("/char/mux", char_mux_test);
+    g_test_add_func("/char/mux", char_mux_fe_test);
+    g_test_add_func("/char/mux-be", char_mux_be_test);
 #ifdef _WIN32
     g_test_add_func("/char/console/subprocess", char_console_test_subprocess);
     g_test_add_func("/char/console", char_console_test);