diff --git a/tests/Makefile.am b/tests/Makefile.am index fe8847386ef2060c774ab15acda25161b2ab826d..cf254f65c31f4a9447d07efe26177a86400252ac 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -616,6 +616,7 @@ qemumonitorjsontest_SOURCES = \ qemumonitorjsontest.c \ testutils.c testutils.h \ testutilsqemu.c testutilsqemu.h \ + testutilsqemuschema.c testutilsqemuschema.h \ $(NULL) qemumonitorjsontest_LDADD = libqemumonitortestutils.la \ $(qemu_LDADDS) $(LDADDS) @@ -694,6 +695,7 @@ else ! WITH_QEMU EXTRA_DIST += qemuxml2argvtest.c qemuxml2xmltest.c qemuargv2xmltest.c \ qemuhelptest.c domainsnapshotxml2xmltest.c \ qemumonitortest.c testutilsqemu.c testutilsqemu.h \ + testutilsqemuschema.c testutilsqemuschema.h \ qemumonitorjsontest.c qemuhotplugtest.c \ qemuagenttest.c qemucapabilitiestest.c \ qemucaps2xmltest.c qemucommandutiltest.c \ diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index 908ec3a3c86c6c01086e576dcf7a942165c058a7..7048a15b6eb1396d80840be09a54b901f8b9ae90 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -21,10 +21,12 @@ #include "testutils.h" #include "testutilsqemu.h" +#include "testutilsqemuschema.h" #include "qemumonitortestutils.h" #include "qemu/qemu_domain.h" #include "qemu/qemu_block.h" #include "qemu/qemu_monitor_json.h" +#include "qemu/qemu_qapi.h" #include "virthread.h" #include "virerror.h" #include "virstring.h" @@ -2828,12 +2830,60 @@ testBlockNodeNameDetect(const void *opaque) } +struct testQAPISchemaData { + virHashTablePtr schema; + const char *name; + const char *query; + const char *json; + bool success; +}; + + +static int +testQAPISchema(const void *opaque) +{ + const struct testQAPISchemaData *data = opaque; + virBuffer debug = VIR_BUFFER_INITIALIZER; + virJSONValuePtr schemaroot; + virJSONValuePtr json = NULL; + int ret = -1; + + if (virQEMUQAPISchemaPathGet(data->query, data->schema, &schemaroot) < 0) + goto cleanup; + + if (!(json = virJSONValueFromString(data->json))) + goto cleanup; + + if ((testQEMUSchemaValidate(json, schemaroot, data->schema, &debug) == 0) != data->success) { + if (!data->success) + VIR_TEST_VERBOSE("\nschema validation should have failed\n"); + } else { + ret = 0; + } + + if (virTestGetDebug() || + (ret < 0 && virTestGetVerbose())) { + char *debugstr = virBufferContentAndReset(&debug); + fprintf(stderr, "\n%s\n", debugstr); + VIR_FREE(debugstr); + } + + + cleanup: + virBufferFreeAndReset(&debug); + virJSONValueFree(json); + return ret; +} + + static int mymain(void) { int ret = 0; virQEMUDriver driver; testQemuMonitorJSONSimpleFuncData simpleFunc; + struct testQAPISchemaData qapiData; + char *metaschema = NULL; #if !WITH_YAJL fputs("libvirt not compiled with yajl, skipping this test\n", stderr); @@ -2982,8 +3032,64 @@ mymain(void) #undef DO_TEST_BLOCK_NODE_DETECT - qemuTestDriverFree(&driver); +#define DO_TEST_QAPI_SCHEMA(nme, rootquery, scc, jsonstr) \ + do { \ + qapiData.name = nme; \ + qapiData.query = rootquery; \ + qapiData.success = scc; \ + qapiData.json = jsonstr; \ + if (virTestRun("qapi schema " nme, testQAPISchema, &qapiData) < 0)\ + ret = -1; \ + } while (0) + + if (!(qapiData.schema = testQEMUSchemaLoad())) { + VIR_TEST_VERBOSE("failed to load qapi schema\n"); + ret = -1; + goto cleanup; + } + DO_TEST_QAPI_SCHEMA("string", "trace-event-get-state/arg-type", true, + "{\"name\":\"test\"}"); + DO_TEST_QAPI_SCHEMA("all attrs", "trace-event-get-state/arg-type", true, + "{\"name\":\"test\", \"vcpu\":123}"); + DO_TEST_QAPI_SCHEMA("attr type mismatch", "trace-event-get-state/arg-type", false, + "{\"name\":123}"); + DO_TEST_QAPI_SCHEMA("missing mandatory attr", "trace-event-get-state/arg-type", false, + "{\"vcpu\":123}"); + DO_TEST_QAPI_SCHEMA("attr name not present", "trace-event-get-state/arg-type", false, + "{\"name\":\"test\", \"blah\":123}"); + DO_TEST_QAPI_SCHEMA("variant", "blockdev-add/arg-type", true, + "{\"driver\":\"file\", \"filename\":\"ble\"}"); + DO_TEST_QAPI_SCHEMA("variant wrong", "blockdev-add/arg-type", false, + "{\"driver\":\"filefilefilefile\", \"filename\":\"ble\"}"); + DO_TEST_QAPI_SCHEMA("variant missing mandatory", "blockdev-add/arg-type", false, + "{\"driver\":\"file\", \"pr-manager\":\"ble\"}"); + DO_TEST_QAPI_SCHEMA("variant missing discriminator", "blockdev-add/arg-type", false, + "{\"node-name\":\"dfgfdg\"}"); + DO_TEST_QAPI_SCHEMA("alternate 1", "blockdev-add/arg-type", true, + "{\"driver\":\"qcow2\"," + "\"file\": { \"driver\":\"file\", \"filename\":\"ble\"}}"); + DO_TEST_QAPI_SCHEMA("alternate 2", "blockdev-add/arg-type", true, + "{\"driver\":\"qcow2\",\"file\": \"somepath\"}"); + DO_TEST_QAPI_SCHEMA("alternate 2", "blockdev-add/arg-type", false, + "{\"driver\":\"qcow2\",\"file\": 1234}"); + + if (!(metaschema = virTestLoadFilePath("qemuqapischema.json", NULL))) { + VIR_TEST_VERBOSE("failed to load qapi schema\n"); + ret = -1; + goto cleanup; + } + + DO_TEST_QAPI_SCHEMA("schema-meta", "query-qmp-schema/ret-type", true, + metaschema); + + +#undef DO_TEST_QAPI_SCHEMA + + cleanup: + VIR_FREE(metaschema); + virHashFree(qapiData.schema); + qemuTestDriverFree(&driver); return (ret == 0) ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/testutilsqemuschema.c b/tests/testutilsqemuschema.c new file mode 100644 index 0000000000000000000000000000000000000000..21f5d119e82c6ee2b68e0d8850784295858e3c6f --- /dev/null +++ b/tests/testutilsqemuschema.c @@ -0,0 +1,536 @@ +/* + * testutilsqemuschema.c: helper functions for QEMU QAPI schema testing + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * . + */ +#include +#include "testutils.h" +#include "testutilsqemuschema.h" +#include "qemu/qemu_qapi.h" + +static int +testQEMUSchemaValidateRecurse(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug); + +static int +testQEMUSchemaValidateArrayBuiltin(virJSONValuePtr obj, + virJSONValuePtr root, + virBufferPtr debug) +{ + const char *t = virJSONValueObjectGetString(root, "json-type"); + const char *s = NULL; + bool b = false; + int ret = -1; + + if (STREQ_NULLABLE(t, "value")) { + s = "{any}"; + ret = 0; + goto cleanup; + } + + switch (virJSONValueGetType(obj)) { + case VIR_JSON_TYPE_STRING: + if (STRNEQ_NULLABLE(t, "string")) + goto cleanup; + s = virJSONValueGetString(obj); + break; + + case VIR_JSON_TYPE_NUMBER: + if (STRNEQ_NULLABLE(t, "int") && + STRNEQ_NULLABLE(t, "number")) + goto cleanup; + s = "{number}"; + break; + + case VIR_JSON_TYPE_BOOLEAN: + if (STRNEQ_NULLABLE(t, "boolean")) + goto cleanup; + virJSONValueGetBoolean(obj, &b); + if (b) + s = "true"; + else + s = "false"; + break; + + case VIR_JSON_TYPE_NULL: + if (STRNEQ_NULLABLE(t, "null")) + goto cleanup; + break; + + case VIR_JSON_TYPE_OBJECT: + case VIR_JSON_TYPE_ARRAY: + goto cleanup; + } + + ret = 0; + + cleanup: + if (ret == 0) + virBufferAsprintf(debug, "'%s': OK", s); + else + virBufferAsprintf(debug, "ERROR: expected type '%s', actual type %d", + t, virJSONValueGetType(obj)); + return ret; +} + +struct testQEMUSchemaValidateObjectMemberData { + virJSONValuePtr rootmembers; + virHashTablePtr schema; + virBufferPtr debug; + bool missingMandatory; +}; + + +static virJSONValuePtr +testQEMUSchemaStealObjectMemberByName(const char *name, + virJSONValuePtr members) +{ + virJSONValuePtr member; + virJSONValuePtr ret = NULL; + size_t n; + size_t i; + + n = virJSONValueArraySize(members); + for (i = 0; i < n; i++) { + member = virJSONValueArrayGet(members, i); + + if (STREQ_NULLABLE(name, virJSONValueObjectGetString(member, "name"))) { + ret = virJSONValueArraySteal(members, i); + break; + } + } + + return ret; +} + + +static int +testQEMUSchemaValidateObjectMember(const char *key, + virJSONValuePtr value, + void *opaque) +{ + struct testQEMUSchemaValidateObjectMemberData *data = opaque; + virJSONValuePtr keymember = NULL; + const char *keytype; + virJSONValuePtr keyschema = NULL; + int ret = -1; + + virBufferStrcat(data->debug, key, ": ", NULL); + + /* lookup 'member' entry for key */ + if (!(keymember = testQEMUSchemaStealObjectMemberByName(key, data->rootmembers))) { + virBufferAddLit(data->debug, "ERROR: attribute not in schema"); + goto cleanup; + } + + /* lookup schema entry for keytype */ + if (!(keytype = virJSONValueObjectGetString(keymember, "type")) || + !(keyschema = virHashLookup(data->schema, keytype))) { + virBufferAsprintf(data->debug, "ERROR: can't find schema for type '%s'", + NULLSTR(keytype)); + ret = -2; + goto cleanup; + } + + /* recurse */ + ret = testQEMUSchemaValidateRecurse(value, keyschema, data->schema, + data->debug); + + cleanup: + virBufferAddLit(data->debug, "\n"); + virJSONValueFree(keymember); + return ret; +} + + +static int +testQEMUSchemaValidateObjectMergeVariantMember(size_t pos ATTRIBUTE_UNUSED, + virJSONValuePtr item, + void *opaque) +{ + virJSONValuePtr array = opaque; + virJSONValuePtr copy; + + if (!(copy = virJSONValueCopy(item))) + return -1; + + if (virJSONValueArrayAppend(array, copy) < 0) + return -1; + + return 1; +} + + +/** + * testQEMUSchemaValidateObjectMergeVariant: + * + * Merges schema of variant @variantname in @root into @root and removes the + * 'variants' array from @root. + */ +static int +testQEMUSchemaValidateObjectMergeVariant(virJSONValuePtr root, + const char *variantfield, + const char *variantname, + virHashTablePtr schema, + virBufferPtr debug) +{ + size_t n; + size_t i; + virJSONValuePtr variants = NULL; + virJSONValuePtr variant; + virJSONValuePtr variantschema; + virJSONValuePtr variantschemamembers; + virJSONValuePtr rootmembers; + const char *varianttype = NULL; + int ret = -1; + + if (!(variants = virJSONValueObjectStealArray(root, "variants"))) { + virBufferAddLit(debug, "ERROR: missing 'variants' in schema\n"); + return -2; + } + + n = virJSONValueArraySize(variants); + for (i = 0; i < n; i++) { + variant = virJSONValueArrayGet(variants, i); + + if (STREQ_NULLABLE(variantname, + virJSONValueObjectGetString(variant, "case"))) { + varianttype = virJSONValueObjectGetString(variant, "type"); + break; + } + } + + if (!varianttype) { + virBufferAsprintf(debug, "ERROR: variant '%s' for discriminator '%s' not found\n", + variantname, variantfield); + goto cleanup; + + } + + if (!(variantschema = virHashLookup(schema, varianttype)) || + !(variantschemamembers = virJSONValueObjectGetArray(variantschema, "members"))) { + virBufferAsprintf(debug, + "ERROR: missing schema or schema members for variant '%s'(%s)\n", + variantname, varianttype); + ret = -2; + goto cleanup; + } + + rootmembers = virJSONValueObjectGetArray(root, "members"); + + if (virJSONValueArrayForeachSteal(variantschemamembers, + testQEMUSchemaValidateObjectMergeVariantMember, + rootmembers) < 0) { + ret = -2; + goto cleanup; + } + + ret = 0; + + cleanup: + virJSONValueFree(variants); + return ret; +} + + +static int +testQEMUSchemaValidateObjectMandatoryMember(size_t pos ATTRIBUTE_UNUSED, + virJSONValuePtr item, + void *opaque ATTRIBUTE_UNUSED) +{ + struct testQEMUSchemaValidateObjectMemberData *data = opaque; + + if (virJSONValueObjectHasKey(item, "default") != 1) { + virBufferAsprintf(data->debug, "ERROR: missing mandatory attribute '%s'\n", + NULLSTR(virJSONValueObjectGetString(item, "name"))); + data->missingMandatory = true; + } + + return 1; +} + + +static int +testQEMUSchemaValidateObject(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug) +{ + struct testQEMUSchemaValidateObjectMemberData data = { NULL, schema, + debug, false }; + virJSONValuePtr localroot = NULL; + const char *variantfield; + const char *variantname; + int ret = -1; + + if (virJSONValueGetType(obj) != VIR_JSON_TYPE_OBJECT) { + virBufferAddLit(debug, "ERROR: not an object"); + return -1; + } + + virBufferAddLit(debug, "{\n"); + virBufferAdjustIndent(debug, 3); + + /* copy schema */ + if (!(localroot = virJSONValueCopy(root))) { + ret = -2; + goto cleanup; + } + + /* remove variant */ + if ((variantfield = virJSONValueObjectGetString(localroot, "tag"))) { + if (!(variantname = virJSONValueObjectGetString(obj, variantfield))) { + virBufferAsprintf(debug, "ERROR: missing variant discriminator attribute '%s'\n", + variantfield); + goto cleanup; + } + + if (testQEMUSchemaValidateObjectMergeVariant(localroot, variantfield, + variantname, + schema, debug) < 0) + goto cleanup; + } + + + /* validate members */ + data.rootmembers = virJSONValueObjectGetArray(localroot, "members"); + if (virJSONValueObjectForeachKeyValue(obj, + testQEMUSchemaValidateObjectMember, + &data) < 0) + goto cleanup; + + /* check missing mandatory values */ + if (virJSONValueArrayForeachSteal(data.rootmembers, + testQEMUSchemaValidateObjectMandatoryMember, + &data) < 0) { + ret = -2; + goto cleanup; + } + + if (data.missingMandatory) + goto cleanup; + + virBufferAdjustIndent(debug, -3); + virBufferAddLit(debug, "} OK"); + ret = 0; + + cleanup: + virJSONValueFree(localroot); + return ret; +} + + +static int +testQEMUSchemaValidateEnum(virJSONValuePtr obj, + virJSONValuePtr root, + virBufferPtr debug) +{ + const char *objstr; + virJSONValuePtr values = NULL; + virJSONValuePtr value; + size_t n; + size_t i; + + if (virJSONValueGetType(obj) != VIR_JSON_TYPE_STRING) { + virBufferAddLit(debug, "ERROR: not a string"); + return -1; + } + + objstr = virJSONValueGetString(obj); + + if (!(values = virJSONValueObjectGetArray(root, "values"))) { + virBufferAsprintf(debug, "ERROR: missing enum values in schema '%s'", + NULLSTR(virJSONValueObjectGetString(root, "name"))); + return -2; + } + + n = virJSONValueArraySize(values); + for (i = 0; i < n; i++) { + value = virJSONValueArrayGet(values, i); + + if (STREQ_NULLABLE(objstr, virJSONValueGetString(value))) { + virBufferAsprintf(debug, "'%s' OK", NULLSTR(objstr)); + return 0; + } + } + + virBufferAsprintf(debug, "ERROR: enum value '%s' is not in schema", + NULLSTR(objstr)); + return -1; +} + + +static int +testQEMUSchemaValidateArray(virJSONValuePtr objs, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug) +{ + const char *elemtypename = virJSONValueObjectGetString(root, "element-type"); + virJSONValuePtr elementschema; + virJSONValuePtr obj; + size_t n; + size_t i; + + if (virJSONValueGetType(objs) != VIR_JSON_TYPE_ARRAY) { + virBufferAddLit(debug, "ERROR: not an array\n"); + return -1; + } + + if (!elemtypename || + !(elementschema = virHashLookup(schema, elemtypename))) { + virBufferAsprintf(debug, "ERROR: missing schema for array element type '%s'", + NULLSTR(elemtypename)); + return -2; + } + + virBufferAddLit(debug, "[\n"); + virBufferAdjustIndent(debug, 3); + + n = virJSONValueArraySize(objs); + for (i = 0; i < n; i++) { + obj = virJSONValueArrayGet(objs, i); + + if (testQEMUSchemaValidateRecurse(obj, elementschema, schema, debug) < 0) + return -1; + virBufferAddLit(debug, ",\n"); + } + virBufferAddLit(debug, "] OK"); + virBufferAdjustIndent(debug, -3); + + return 0; +} + +static int +testQEMUSchemaValidateAlternate(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug) +{ + virJSONValuePtr members; + virJSONValuePtr member; + size_t n; + size_t i; + const char *membertype; + virJSONValuePtr memberschema; + int indent; + int rc; + + if (!(members = virJSONValueObjectGetArray(root, "members"))) { + virBufferAddLit(debug, "ERROR: missing 'members' for alternate schema"); + return -2; + } + + virBufferAddLit(debug, "(\n"); + virBufferAdjustIndent(debug, 3); + indent = virBufferGetIndent(debug, false); + + n = virJSONValueArraySize(members); + for (i = 0; i < n; i++) { + membertype = NULL; + + /* P != NP */ + virBufferAsprintf(debug, "(alternate %zu/%zu)\n", i + 1, n); + virBufferAdjustIndent(debug, 3); + + if (!(member = virJSONValueArrayGet(members, i)) || + !(membertype = virJSONValueObjectGetString(member, "type")) || + !(memberschema = virHashLookup(schema, membertype))) { + virBufferAsprintf(debug, "ERROR: missing schema for alternate type '%s'", + NULLSTR(membertype)); + return -2; + } + + rc = testQEMUSchemaValidateRecurse(obj, memberschema, schema, debug); + + virBufferAddLit(debug, "\n"); + virBufferSetIndent(debug, indent); + virBufferAsprintf(debug, "(/alternate %zu/%zu)\n", i + 1, n); + + if (rc == 0) { + virBufferAdjustIndent(debug, -3); + virBufferAddLit(debug, ") OK"); + return 0; + } + } + + virBufferAddLit(debug, "ERROR: no alternate type was matched"); + return -1; +} + + +static int +testQEMUSchemaValidateRecurse(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug) +{ + const char *n = virJSONValueObjectGetString(root, "name"); + const char *t = virJSONValueObjectGetString(root, "meta-type"); + + if (STREQ_NULLABLE(t, "builtin")) { + return testQEMUSchemaValidateArrayBuiltin(obj, root, debug); + } else if (STREQ_NULLABLE(t, "object")) { + return testQEMUSchemaValidateObject(obj, root, schema, debug); + } else if (STREQ_NULLABLE(t, "enum")) { + return testQEMUSchemaValidateEnum(obj, root, debug); + } else if (STREQ_NULLABLE(t, "array")) { + return testQEMUSchemaValidateArray(obj, root, schema, debug); + } else if (STREQ_NULLABLE(t, "alternate")) { + return testQEMUSchemaValidateAlternate(obj, root, schema, debug); + } + + virBufferAsprintf(debug, + "qapi schema meta-type '%s' of type '%s' not handled\n", + NULLSTR(t), NULLSTR(n)); + return -2; +} + + +/** + * testQEMUSchemaValidate: + * @obj: object to validate + * @root: schema entry to start from + * @schema: hash table containing schema entries + * @debug: a virBuffer which will be filled with debug information if provided + * + * Validates whether @obj conforms to the QAPI schema passed in via @schema, + * starting from the node @root. Returns 0, if @obj matches @schema, -1 if it + * does not and -2 if there is a problem with the schema or with internals. + * + * @debug is filled with information regarding the validation process + */ +int +testQEMUSchemaValidate(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug) +{ + return testQEMUSchemaValidateRecurse(obj, root, schema, debug); +} + + +virHashTablePtr +testQEMUSchemaLoad(void) +{ + virJSONValuePtr schemajson; + + if (!(schemajson = virTestLoadFileJSON("qemuqapischema.json", NULL))) + return NULL; + + return virQEMUQAPISchemaConvert(schemajson); +} diff --git a/tests/testutilsqemuschema.h b/tests/testutilsqemuschema.h new file mode 100644 index 0000000000000000000000000000000000000000..cb383db174b8b92a6fe9b40e8ce0425c6b4ab7f9 --- /dev/null +++ b/tests/testutilsqemuschema.h @@ -0,0 +1,30 @@ +/* + * testutilsqemuschema.h: helper functions for QEMU QAPI schema testing + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * . + */ + +#include "virhash.h" +#include "virjson.h" +#include "virbuffer.h" + +int +testQEMUSchemaValidate(virJSONValuePtr obj, + virJSONValuePtr root, + virHashTablePtr schema, + virBufferPtr debug); + +virHashTablePtr +testQEMUSchemaLoad(void);