The following patches will touch HMP readline completion for migration
parameters, add a test case.

The test puts the monitor on a chardev via socket and bypasses qtest
facilities because it needs to emit raw codes to readline. It
therefore requires a couple of new helpers to read/write to the
monitor socket.

Usage:
QTEST_QEMU_BINARY=./qemu-system-x86_64 \
./tests/qtest/migration-test --full -p /x86_64/migration/hmp/completion

Acked-by: Peter Xu <[email protected]>
Signed-off-by: Fabiano Rosas <[email protected]>
---
 tests/qtest/migration/misc-tests.c | 216 +++++++++++++++++++++++++++++
 1 file changed, 216 insertions(+)

diff --git a/tests/qtest/migration/misc-tests.c 
b/tests/qtest/migration/misc-tests.c
index 02469a92ff..41bf123359 100644
--- a/tests/qtest/migration/misc-tests.c
+++ b/tests/qtest/migration/misc-tests.c
@@ -11,6 +11,8 @@
  */
 
 #include "qemu/osdep.h"
+#include "qemu/cutils.h"
+#include "qemu/sockets.h"
 #include "qapi/error.h"
 #include "qobject/qjson.h"
 #include "libqtest.h"
@@ -78,6 +80,22 @@ static bool token_list_is_substr(char **smaller, char 
**larger, int *last)
     return match;
 }
 
+static void assert_hmp_match_text(const char *str, const char *text)
+{
+    g_auto(GStrv) tok_str = g_strsplit_set(str, " ", -1);
+    g_auto(GStrv) tok_txt = g_strsplit_set(text, " \r\n", -1);
+    int idx;
+
+    if (token_list_is_substr(tok_str, tok_txt, &idx)) {
+        return;
+    }
+
+    g_test_message("HMP output mismatch for entry at line %d:", 
test_case_line);
+    g_test_message("expected vs. found (whitespace ignored):\n\n%s\n---\n%s",
+                   str, text);
+    g_assert_not_reached();
+}
+
 static void assert_hmp_match_line(const char *str, const char *text)
 {
     g_auto(GStrv) tok_str = g_strsplit_set(str, " ", -1);
@@ -216,6 +234,202 @@ static void test_hmp_migration_parameters(char *name, 
MigrateCommon *args)
 
     qtest_quit(qts);
 }
+
+static void hmp_sock_write(int fd, const char *buf)
+{
+    size_t sz = strlen(buf);
+
+    assert(fd > 0);
+    assert(write(fd, buf, sz) == sz);
+}
+
+static void hmp_sock_read(int fd, char *buf, size_t buf_sz)
+{
+    char *p = buf;
+    size_t sz = buf_sz - 1;
+
+    assert(fd >= 0);
+    memset(buf, 0, buf_sz);
+
+    while (sz > 0) {
+        ssize_t r = read(fd, p, sz);
+        char *prompt;
+
+        if (!r) {
+            break;
+        } else if (r < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            g_assert_not_reached();
+        }
+
+        p += r;
+        sz -= r;
+
+        /* stop reading after the next prompt appears */
+        prompt = strstr(buf, "(qemu) ");
+        if (prompt) {
+            *prompt = '\0';
+            break;
+        }
+    }
+}
+
+static gint comp(gconstpointer a, gconstpointer b)
+{
+    return qemu_pstrcmp0((const char **)a, (const char **)b);
+}
+
+static char *get_migration_opts_sorted(QTestState *qts, const char *opt)
+{
+    GPtrArray *header = g_ptr_array_new_with_free_func(g_free);
+    GPtrArray *opts = g_ptr_array_new();
+    char *sorted_str;
+
+    if (g_str_equal(opt, "@params@")) {
+        const QDictEntry *e;
+        QDict *rsp;
+
+        g_ptr_array_add(header, g_strdup("migrate_set_parameter"));
+
+        rsp = qtest_qmp_assert_success_ref(
+            qts, "{ 'execute': 'query-migrate-parameters' }");
+
+        for (e = qdict_first(rsp); e; e = qdict_next(rsp, e)) {
+            g_ptr_array_add(opts, g_strdup(qdict_entry_key(e)));
+        }
+
+        qobject_unref(rsp);
+
+        /*
+         * query_migrate has this as optional, but for completion we want
+         * it present.
+         */
+        g_ptr_array_add(opts, g_strdup("block-bitmap-mapping"));
+
+    } else if (g_str_equal(opt, "@caps@")) {
+        int i;
+
+        g_ptr_array_add(header, g_strdup("migrate_set_capability"));
+
+        for (i = 0; i < MIGRATION_CAPABILITY__MAX; i++) {
+            g_ptr_array_add(opts,
+                            g_strdup(MigrationCapability_lookup.array[i]));
+        }
+    } else {
+        g_assert_not_reached();
+    }
+
+    g_ptr_array_sort(opts, comp);
+    g_ptr_array_extend_and_steal(header, opts);
+
+    g_ptr_array_add(header, NULL);
+    sorted_str = g_strjoinv(" ", (char **)header->pdata);
+
+    g_ptr_array_unref(header);
+
+    return sorted_str;
+}
+
+static void hmp_completion_single(QTestState *qts, int fd,
+                                  const struct HMPTestData *t)
+{
+    g_autofree char *exp = NULL;
+    char buf[8192];
+    char *output;
+
+    test_case_line = t->line;
+
+    if (g_str_has_prefix(t->output1, "@")) {
+        exp = get_migration_opts_sorted(qts, t->output1);
+    } else {
+        exp = g_strdup(t->output1);
+    }
+
+    hmp_sock_write(fd, t->input1);
+    hmp_sock_read(fd, buf, sizeof(buf));
+
+    /*
+     * readline first rewrites the input to the common root of the
+     * completions, then outputs the completion suggestions:
+     *
+     * (qemu) info migr<TAB>
+     * (qemu) migrate migrate_parameters
+     * migrate_capabilities ...
+     */
+    output = strstr(buf, t->input2);
+    assert_hmp_match_text(exp, output);
+
+    /* ^U backward kill line */
+    hmp_sock_write(fd, "\x15");
+}
+
+static void test_hmp_completion(char *name, MigrateCommon *args)
+{
+    g_autofree char *cmdline;
+    char buf[1024];
+    QTestState *qts;
+    int sockfds[2];
+    /*
+     * .input1:  partial string with an ending TAB (as if pressed by
+     *           the user).
+     * .input2:  common root of the completions, i.e. what the partial
+     *           part of .input1 string completes to.
+     * .output1: full list of completion suggestions for the string
+     *           in .input2.
+     * E.g:
+     * (qemu) .input1
+     * <after TAB>
+     * (qemu) .input2
+     * .output1
+     */
+    HMPTestData completion_cases[] = {
+        TEST("migra\t",
+             "migrate",
+             "migrate migrate_cancel migrate_continue migrate_incoming "
+             "migrate_pause migrate_recover migrate_set_capability "
+             "migrate_set_parameter migrate_start_postcopy"),
+
+        /*
+         * Note QEMU doesn't keep 'info' when offering the completions
+         * suggestions.
+         */
+        TEST("info migra\t",
+             "migrate",
+             "migrate migrate_capabilities migrate_parameters"),
+
+        TEST("migrate_se\t",
+             "migrate_set_",
+             "migrate_set_capability migrate_set_parameter"),
+
+        /*
+         * parameters and capabilities are not listed here to avoid having
+         * to enumerate them all, see test_hmp_completion().
+         */
+        TEST("migrate_set_parameter \t", "migrate_set_parameter ", "@params@"),
+        TEST("migrate_set_capability \t", "migrate_set_capability ", "@caps@"),
+    };
+
+    assert(!qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds));
+    qemu_clear_cloexec(sockfds[1]);
+
+    cmdline = g_strdup_printf("-chardev socket,id=mon0,fd=%d "
+                              "-mon chardev=mon0,mode=readline -S",
+                              sockfds[1]);
+    qts = qtest_init(cmdline);
+    close(sockfds[1]);
+
+    /* read HMP banner */
+    hmp_sock_read(sockfds[0], buf, sizeof(buf));
+
+    for (int i = 0; i < G_N_ELEMENTS(completion_cases); i++) {
+        hmp_completion_single(qts, sockfds[0], &completion_cases[i]);
+    }
+
+    close(sockfds[0]);
+    qtest_quit(qts);
+}
 #endif /* CONFIG_HMP */
 
 static void test_baddest(char *name, MigrateCommon *args)
@@ -459,5 +673,7 @@ void migration_test_add_misc(MigrationTestEnv *env)
 #ifdef CONFIG_HMP
     migration_test_add("/migration/hmp/parameters",
                        test_hmp_migration_parameters);
+    migration_test_add("/migration/hmp/completion",
+                       test_hmp_completion);
 #endif
 }
-- 
2.53.0


Reply via email to