A command is a query if it has no side effect and yields a result. Such commands are typically named query-FOO, but there are exceptions. The basic idea is to find candidates with query-qmp-schema, filter out the ones that aren't queries with an explicit blacklist, and test the remaining ones against a QEMU with no special arguments. The current blacklist is just add-fd. The test can't do queries with arguments, because it knows nothing about the arguments. No coverage for query-cpu-model-baseline, query-cpu-model-comparison, query-cpu-model-expansion, query-rocker, query-rocker-ports, query-rocker-of-dpa-flows, and query-rocker-of-dpa-groups. Most tested commands are expected to succeed. The test does not check the return value then. query-balloon and query-vm-generation-id are expected to fail because they need a virtio-balloon / vmgenid device to succeed, and this test is too dumb to set one up. Could be addressed later. query-acpi-ospm-status and query-hotpluggable-cpus are expected to fail because they require features provided only by special machine types, and this test is too dumb to set that up. Could also be addressed later. Several commands may either be functional or stubs that always fail, depending on build configuration. Ideally, the stubs shouldn't be in query-qmp-schema, but that requires QAPI schema compile-time configuration, which we don't have, yet. Until we do, we need to figure out whether a command is a stub. When we have a suitable CONFIG_FOO preprocessor symbol is available, use that. Else, simply blacklist the command for now. We get basic test coverage for the following commands, except as noted: qom-list-types query-acpi-ospm-status (expected to fail) query-balloon (expected to fail) query-block query-block-jobs query-blockstats query-chardev query-chardev-backends query-command-line-options query-commands query-cpu-definitions (blacklisted for now) query-cpus query-dump query-dump-guest-memory-capability query-events query-fdsets query-gic-capabilities (blacklisted for now) query-hotpluggable-cpus (expected to fail) query-iothreads query-kvm query-machines query-memdev query-memory-devices query-mice query-migrate query-migrate-cache-size query-migrate-capabilities query-migrate-parameters query-name query-named-block-nodes query-pci (blacklisted for now) query-qmp-schema query-rx-filter query-spice query-status query-target query-tpm query-tpm-models query-tpm-types query-uuid query-version query-vm-generation-id (expected to fail) query-vnc query-vnc-servers query-xen-replication-status Signed-off-by: Markus Armbruster <armbru@redhat.com> Message-Id: <1502461148-10154-1-git-send-email-armbru@redhat.com> Reviewed-by: Eric Blake <eblake@redhat.com> [Typos in code under #ifndef and in the commit message fixed]
320 lines
8.4 KiB
C
320 lines
8.4 KiB
C
/*
|
|
* QMP protocol test cases
|
|
*
|
|
* Copyright (c) 2017 Red Hat Inc.
|
|
*
|
|
* Authors:
|
|
* Markus Armbruster <armbru@redhat.com>,
|
|
*
|
|
* This work is licensed under the terms of the GNU GPL, version 2 or later.
|
|
* See the COPYING file in the top-level directory.
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "libqtest.h"
|
|
#include "qapi-visit.h"
|
|
#include "qapi/error.h"
|
|
#include "qapi/qobject-input-visitor.h"
|
|
#include "qapi/util.h"
|
|
#include "qapi/visitor.h"
|
|
|
|
const char common_args[] = "-nodefaults -machine none";
|
|
|
|
static const char *get_error_class(QDict *resp)
|
|
{
|
|
QDict *error = qdict_get_qdict(resp, "error");
|
|
const char *desc = qdict_get_try_str(error, "desc");
|
|
|
|
g_assert(desc);
|
|
return error ? qdict_get_try_str(error, "class") : NULL;
|
|
}
|
|
|
|
static void test_version(QObject *version)
|
|
{
|
|
Visitor *v;
|
|
VersionInfo *vinfo;
|
|
|
|
g_assert(version);
|
|
v = qobject_input_visitor_new(version);
|
|
visit_type_VersionInfo(v, "version", &vinfo, &error_abort);
|
|
qapi_free_VersionInfo(vinfo);
|
|
visit_free(v);
|
|
}
|
|
|
|
static void test_malformed(void)
|
|
{
|
|
QDict *resp;
|
|
|
|
/* Not even a dictionary */
|
|
resp = qmp("null");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
QDECREF(resp);
|
|
|
|
/* No "execute" key */
|
|
resp = qmp("{}");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
QDECREF(resp);
|
|
|
|
/* "execute" isn't a string */
|
|
resp = qmp("{ 'execute': true }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
QDECREF(resp);
|
|
|
|
/* "arguments" isn't a dictionary */
|
|
resp = qmp("{ 'execute': 'no-such-cmd', 'arguments': [] }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
QDECREF(resp);
|
|
|
|
/* extra key */
|
|
resp = qmp("{ 'execute': 'no-such-cmd', 'extra': true }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
QDECREF(resp);
|
|
}
|
|
|
|
static void test_qmp_protocol(void)
|
|
{
|
|
QDict *resp, *q, *ret;
|
|
QList *capabilities;
|
|
|
|
global_qtest = qtest_init_without_qmp_handshake(common_args);
|
|
|
|
/* Test greeting */
|
|
resp = qmp_receive();
|
|
q = qdict_get_qdict(resp, "QMP");
|
|
g_assert(q);
|
|
test_version(qdict_get(q, "version"));
|
|
capabilities = qdict_get_qlist(q, "capabilities");
|
|
g_assert(capabilities && qlist_empty(capabilities));
|
|
QDECREF(resp);
|
|
|
|
/* Test valid command before handshake */
|
|
resp = qmp("{ 'execute': 'query-version' }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "CommandNotFound");
|
|
QDECREF(resp);
|
|
|
|
/* Test malformed commands before handshake */
|
|
test_malformed();
|
|
|
|
/* Test handshake */
|
|
resp = qmp("{ 'execute': 'qmp_capabilities' }");
|
|
ret = qdict_get_qdict(resp, "return");
|
|
g_assert(ret && !qdict_size(ret));
|
|
QDECREF(resp);
|
|
|
|
/* Test repeated handshake */
|
|
resp = qmp("{ 'execute': 'qmp_capabilities' }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "CommandNotFound");
|
|
QDECREF(resp);
|
|
|
|
/* Test valid command */
|
|
resp = qmp("{ 'execute': 'query-version' }");
|
|
test_version(qdict_get(resp, "return"));
|
|
QDECREF(resp);
|
|
|
|
/* Test malformed commands */
|
|
test_malformed();
|
|
|
|
/* Test 'id' */
|
|
resp = qmp("{ 'execute': 'query-name', 'id': 'cookie#1' }");
|
|
ret = qdict_get_qdict(resp, "return");
|
|
g_assert(ret);
|
|
g_assert_cmpstr(qdict_get_try_str(resp, "id"), ==, "cookie#1");
|
|
QDECREF(resp);
|
|
|
|
/* Test command failure with 'id' */
|
|
resp = qmp("{ 'execute': 'human-monitor-command', 'id': 2 }");
|
|
g_assert_cmpstr(get_error_class(resp), ==, "GenericError");
|
|
g_assert_cmpint(qdict_get_int(resp, "id"), ==, 2);
|
|
QDECREF(resp);
|
|
|
|
qtest_end();
|
|
}
|
|
|
|
static int query_error_class(const char *cmd)
|
|
{
|
|
static struct {
|
|
const char *cmd;
|
|
int err_class;
|
|
} fails[] = {
|
|
/* Success depends on build configuration: */
|
|
#ifndef CONFIG_SPICE
|
|
{ "query-spice", ERROR_CLASS_COMMAND_NOT_FOUND },
|
|
#endif
|
|
#ifndef CONFIG_VNC
|
|
{ "query-vnc", ERROR_CLASS_GENERIC_ERROR },
|
|
{ "query-vnc-servers", ERROR_CLASS_GENERIC_ERROR },
|
|
#endif
|
|
#ifndef CONFIG_REPLICATION
|
|
{ "query-xen-replication-status", ERROR_CLASS_COMMAND_NOT_FOUND },
|
|
#endif
|
|
/* Likewise, and require special QEMU command-line arguments: */
|
|
{ "query-acpi-ospm-status", ERROR_CLASS_GENERIC_ERROR },
|
|
{ "query-balloon", ERROR_CLASS_DEVICE_NOT_ACTIVE },
|
|
{ "query-hotpluggable-cpus", ERROR_CLASS_GENERIC_ERROR },
|
|
{ "query-vm-generation-id", ERROR_CLASS_GENERIC_ERROR },
|
|
{ NULL, -1 }
|
|
};
|
|
int i;
|
|
|
|
for (i = 0; fails[i].cmd; i++) {
|
|
if (!strcmp(cmd, fails[i].cmd)) {
|
|
return fails[i].err_class;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void test_query(const void *data)
|
|
{
|
|
const char *cmd = data;
|
|
int expected_error_class = query_error_class(cmd);
|
|
QDict *resp, *error;
|
|
const char *error_class;
|
|
|
|
qtest_start(common_args);
|
|
|
|
resp = qmp("{ 'execute': %s }", cmd);
|
|
error = qdict_get_qdict(resp, "error");
|
|
error_class = error ? qdict_get_str(error, "class") : NULL;
|
|
|
|
if (expected_error_class < 0) {
|
|
g_assert(qdict_haskey(resp, "return"));
|
|
} else {
|
|
g_assert(error);
|
|
g_assert_cmpint(qapi_enum_parse(QapiErrorClass_lookup, error_class,
|
|
QAPI_ERROR_CLASS__MAX, -1,
|
|
&error_abort),
|
|
==, expected_error_class);
|
|
}
|
|
QDECREF(resp);
|
|
|
|
qtest_end();
|
|
}
|
|
|
|
static bool query_is_blacklisted(const char *cmd)
|
|
{
|
|
const char *blacklist[] = {
|
|
/* Not actually queries: */
|
|
"add-fd",
|
|
/* Success depends on target arch: */
|
|
"query-cpu-definitions", /* arm, i386, ppc, s390x */
|
|
"query-gic-capabilities", /* arm */
|
|
/* Success depends on target-specific build configuration: */
|
|
"query-pci", /* CONFIG_PCI */
|
|
NULL
|
|
};
|
|
int i;
|
|
|
|
for (i = 0; blacklist[i]; i++) {
|
|
if (!strcmp(cmd, blacklist[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
typedef struct {
|
|
SchemaInfoList *list;
|
|
GHashTable *hash;
|
|
} QmpSchema;
|
|
|
|
static void qmp_schema_init(QmpSchema *schema)
|
|
{
|
|
QDict *resp;
|
|
Visitor *qiv;
|
|
SchemaInfoList *tail;
|
|
|
|
qtest_start(common_args);
|
|
resp = qmp("{ 'execute': 'query-qmp-schema' }");
|
|
|
|
qiv = qobject_input_visitor_new(qdict_get(resp, "return"));
|
|
visit_type_SchemaInfoList(qiv, NULL, &schema->list, &error_abort);
|
|
visit_free(qiv);
|
|
|
|
QDECREF(resp);
|
|
qtest_end();
|
|
|
|
schema->hash = g_hash_table_new(g_str_hash, g_str_equal);
|
|
|
|
/* Build @schema: hash table mapping entity name to SchemaInfo */
|
|
for (tail = schema->list; tail; tail = tail->next) {
|
|
g_hash_table_insert(schema->hash, tail->value->name, tail->value);
|
|
}
|
|
}
|
|
|
|
static SchemaInfo *qmp_schema_lookup(QmpSchema *schema, const char *name)
|
|
{
|
|
return g_hash_table_lookup(schema->hash, name);
|
|
}
|
|
|
|
static void qmp_schema_cleanup(QmpSchema *schema)
|
|
{
|
|
qapi_free_SchemaInfoList(schema->list);
|
|
g_hash_table_destroy(schema->hash);
|
|
}
|
|
|
|
static bool object_type_has_mandatory_members(SchemaInfo *type)
|
|
{
|
|
SchemaInfoObjectMemberList *tail;
|
|
|
|
g_assert(type->meta_type == SCHEMA_META_TYPE_OBJECT);
|
|
|
|
for (tail = type->u.object.members; tail; tail = tail->next) {
|
|
if (!tail->value->has_q_default) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void add_query_tests(QmpSchema *schema)
|
|
{
|
|
SchemaInfoList *tail;
|
|
SchemaInfo *si, *arg_type, *ret_type;
|
|
const char *test_name;
|
|
|
|
/* Test the query-like commands */
|
|
for (tail = schema->list; tail; tail = tail->next) {
|
|
si = tail->value;
|
|
if (si->meta_type != SCHEMA_META_TYPE_COMMAND) {
|
|
continue;
|
|
}
|
|
|
|
if (query_is_blacklisted(si->name)) {
|
|
continue;
|
|
}
|
|
|
|
arg_type = qmp_schema_lookup(schema, si->u.command.arg_type);
|
|
if (object_type_has_mandatory_members(arg_type)) {
|
|
continue;
|
|
}
|
|
|
|
ret_type = qmp_schema_lookup(schema, si->u.command.ret_type);
|
|
if (ret_type->meta_type == SCHEMA_META_TYPE_OBJECT
|
|
&& !ret_type->u.object.members) {
|
|
continue;
|
|
}
|
|
|
|
test_name = g_strdup_printf("qmp/%s", si->name);
|
|
qtest_add_data_func(test_name, si->name, test_query);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QmpSchema schema;
|
|
int ret;
|
|
|
|
g_test_init(&argc, &argv, NULL);
|
|
|
|
qtest_add_func("qmp/protocol", test_qmp_protocol);
|
|
qmp_schema_init(&schema);
|
|
add_query_tests(&schema);
|
|
|
|
ret = g_test_run();
|
|
|
|
qmp_schema_cleanup(&schema);
|
|
return ret;
|
|
}
|