libvirt/tests/testutilsqemuschema.c
Peter Krempa 98b6087e83 tests: qemu: Add infrastructure for QAPI schema testing
Add a function which will allow to test whether a JSON object conforms
to the QAPI schema. This greatly helps when developing formatters for
new JSON objects and will help make sure that the code will not break in
cases which have unit tests but were actually not function-tested
(mostly various disk access protocols).

Signed-off-by: Peter Krempa <pkrempa@redhat.com>
2018-03-23 14:52:07 +01:00

537 lines
16 KiB
C

/*
* 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
* <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#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);
}