Skip to content

Commit

Permalink
qapi: allow unions to contain further unions
Browse files Browse the repository at this point in the history
This extends the QAPI schema validation to permit unions inside unions,
provided the checks for clashing fields pass.

Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
Message-Id: <20230420102619.348173-4-berrange@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
  • Loading branch information
berrange authored and Markus Armbruster committed Apr 26, 2023
1 parent 1e148b5 commit a17dbc4
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 3 deletions.
6 changes: 3 additions & 3 deletions scripts/qapi/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,10 @@ def check(self, schema):
# on behalf of info, which is not necessarily self.info
def check_clash(self, info, seen):
assert self._checked
assert not self.variants # not implemented
for m in self.members:
m.check_clash(info, seen)
if self.variants:
self.variants.check_clash(info, seen)

def connect_doc(self, doc=None):
super().connect_doc(doc)
Expand Down Expand Up @@ -656,8 +657,7 @@ def check(self, schema, seen):
self.info,
"branch '%s' is not a value of %s"
% (v.name, self.tag_member.type.describe()))
if (not isinstance(v.type, QAPISchemaObjectType)
or v.type.variants):
if not isinstance(v.type, QAPISchemaObjectType):
raise QAPISemError(
self.info,
"%s cannot use %s"
Expand Down
2 changes: 2 additions & 0 deletions tests/qapi-schema/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ schemas = [
'union-invalid-data.json',
'union-invalid-discriminator.json',
'union-invalid-if-discriminator.json',
'union-invalid-union-subfield.json',
'union-invalid-union-subtype.json',
'union-no-base.json',
'union-optional-discriminator.json',
'union-string-discriminator.json',
Expand Down
32 changes: 32 additions & 0 deletions tests/qapi-schema/qapi-schema-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@
{ 'struct': 'UserDefC',
'data': { 'string1': 'str', 'string2': 'str' } }

# this tests that unions can contain other unions in their branches
{ 'enum': 'TestUnionEnum',
'data': [ 'value-a', 'value-b' ] }

{ 'enum': 'TestUnionEnumA',
'data': [ 'value-a1', 'value-a2' ] }

{ 'struct': 'TestUnionTypeA1',
'data': { 'integer': 'int',
'name': 'str'} }

{ 'struct': 'TestUnionTypeA2',
'data': { 'integer': 'int',
'size': 'int' } }

{ 'union': 'TestUnionTypeA',
'base': { 'type-a': 'TestUnionEnumA' },
'discriminator': 'type-a',
'data': { 'value-a1': 'TestUnionTypeA1',
'value-a2': 'TestUnionTypeA2' } }

{ 'struct': 'TestUnionTypeB',
'data': { 'integer': 'int',
'onoff': 'bool' } }

{ 'union': 'TestUnionInUnion',
'base': { 'type': 'TestUnionEnum' },
'discriminator': 'type',
'data': { 'value-a': 'TestUnionTypeA',
'value-b': 'TestUnionTypeB' } }


# for testing use of 'number' within alternates
{ 'alternate': 'AltEnumBool', 'data': { 'e': 'EnumOne', 'b': 'bool' } }
{ 'alternate': 'AltEnumNum', 'data': { 'e': 'EnumOne', 'n': 'number' } }
Expand Down
29 changes: 29 additions & 0 deletions tests/qapi-schema/qapi-schema-test.out
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ alternate UserDefAlternate
object UserDefC
member string1: str optional=False
member string2: str optional=False
enum TestUnionEnum
member value-a
member value-b
enum TestUnionEnumA
member value-a1
member value-a2
object TestUnionTypeA1
member integer: int optional=False
member name: str optional=False
object TestUnionTypeA2
member integer: int optional=False
member size: int optional=False
object q_obj_TestUnionTypeA-base
member type-a: TestUnionEnumA optional=False
object TestUnionTypeA
base q_obj_TestUnionTypeA-base
tag type-a
case value-a1: TestUnionTypeA1
case value-a2: TestUnionTypeA2
object TestUnionTypeB
member integer: int optional=False
member onoff: bool optional=False
object q_obj_TestUnionInUnion-base
member type: TestUnionEnum optional=False
object TestUnionInUnion
base q_obj_TestUnionInUnion-base
tag type
case value-a: TestUnionTypeA
case value-b: TestUnionTypeB
alternate AltEnumBool
tag type
case e: EnumOne
Expand Down
2 changes: 2 additions & 0 deletions tests/qapi-schema/union-invalid-union-subfield.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
union-invalid-union-subfield.json: In union 'TestUnion':
union-invalid-union-subfield.json:25: member 'teeth' of type 'TestTypeFish' collides with base member 'teeth'
30 changes: 30 additions & 0 deletions tests/qapi-schema/union-invalid-union-subfield.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Clash between common member and union variant's variant member
# Base's member 'teeth' clashes with TestTypeFish's

{ 'enum': 'TestEnum',
'data': [ 'animals', 'plants' ] }

{ 'enum': 'TestAnimals',
'data': [ 'fish', 'birds'] }

{ 'struct': 'TestTypeFish',
'data': { 'scales': 'int', 'teeth': 'int' } }

{ 'struct': 'TestTypeBirds',
'data': { 'feathers': 'int' } }

{ 'union': 'TestTypeAnimals',
'base': { 'atype': 'TestAnimals' },
'discriminator': 'atype',
'data': { 'fish': 'TestTypeFish',
'birds': 'TestTypeBirds' } }

{ 'struct': 'TestTypePlants',
'data': { 'integer': 'int' } }

{ 'union': 'TestUnion',
'base': { 'type': 'TestEnum',
'teeth': 'int' },
'discriminator': 'type',
'data': { 'animals': 'TestTypeAnimals',
'plants': 'TestTypePlants' } }
Empty file.
2 changes: 2 additions & 0 deletions tests/qapi-schema/union-invalid-union-subtype.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
union-invalid-union-subtype.json: In union 'TestUnion':
union-invalid-union-subtype.json:25: base member 'type' of type 'TestTypeA' collides with base member 'type'
29 changes: 29 additions & 0 deletions tests/qapi-schema/union-invalid-union-subtype.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Clash between common member and union variant's common member
# Base's member 'type' clashes with TestTypeA's

{ 'enum': 'TestEnum',
'data': [ 'value-a', 'value-b' ] }

{ 'enum': 'TestEnumA',
'data': [ 'value-a1', 'value-a2' ] }

{ 'struct': 'TestTypeA1',
'data': { 'integer': 'int' } }

{ 'struct': 'TestTypeA2',
'data': { 'integer': 'int' } }

{ 'union': 'TestTypeA',
'base': { 'type': 'TestEnumA' },
'discriminator': 'type',
'data': { 'value-a1': 'TestTypeA1',
'value-a2': 'TestTypeA2' } }

{ 'struct': 'TestTypeB',
'data': { 'integer': 'int' } }

{ 'union': 'TestUnion',
'base': { 'type': 'TestEnum' },
'discriminator': 'type',
'data': { 'value-a': 'TestTypeA',
'value-b': 'TestTypeB' } }
Empty file.
47 changes: 47 additions & 0 deletions tests/unit/test-qobject-input-visitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,51 @@ static void test_visitor_in_union_flat(TestInputVisitorData *data,
g_assert(&base->enum1 == &tmp->enum1);
}

static void test_visitor_in_union_in_union(TestInputVisitorData *data,
const void *unused)
{
Visitor *v;
g_autoptr(TestUnionInUnion) tmp = NULL;

v = visitor_input_test_init(data,
"{ 'type': 'value-a', "
" 'type-a': 'value-a1', "
" 'integer': 2, "
" 'name': 'fish' }");

visit_type_TestUnionInUnion(v, NULL, &tmp, &error_abort);
g_assert_cmpint(tmp->type, ==, TEST_UNION_ENUM_VALUE_A);
g_assert_cmpint(tmp->u.value_a.type_a, ==, TEST_UNION_ENUMA_VALUE_A1);
g_assert_cmpint(tmp->u.value_a.u.value_a1.integer, ==, 2);
g_assert_cmpint(strcmp(tmp->u.value_a.u.value_a1.name, "fish"), ==, 0);

qapi_free_TestUnionInUnion(tmp);

v = visitor_input_test_init(data,
"{ 'type': 'value-a', "
" 'type-a': 'value-a2', "
" 'integer': 1729, "
" 'size': 87539319 }");

visit_type_TestUnionInUnion(v, NULL, &tmp, &error_abort);
g_assert_cmpint(tmp->type, ==, TEST_UNION_ENUM_VALUE_A);
g_assert_cmpint(tmp->u.value_a.type_a, ==, TEST_UNION_ENUMA_VALUE_A2);
g_assert_cmpint(tmp->u.value_a.u.value_a2.integer, ==, 1729);
g_assert_cmpint(tmp->u.value_a.u.value_a2.size, ==, 87539319);

qapi_free_TestUnionInUnion(tmp);

v = visitor_input_test_init(data,
"{ 'type': 'value-b', "
" 'integer': 1729, "
" 'onoff': true }");

visit_type_TestUnionInUnion(v, NULL, &tmp, &error_abort);
g_assert_cmpint(tmp->type, ==, TEST_UNION_ENUM_VALUE_B);
g_assert_cmpint(tmp->u.value_b.integer, ==, 1729);
g_assert_cmpint(tmp->u.value_b.onoff, ==, true);
}

static void test_visitor_in_alternate(TestInputVisitorData *data,
const void *unused)
{
Expand Down Expand Up @@ -1216,6 +1261,8 @@ int main(int argc, char **argv)
NULL, test_visitor_in_null);
input_visitor_test_add("/visitor/input/union-flat",
NULL, test_visitor_in_union_flat);
input_visitor_test_add("/visitor/input/union-in-union",
NULL, test_visitor_in_union_in_union);
input_visitor_test_add("/visitor/input/alternate",
NULL, test_visitor_in_alternate);
input_visitor_test_add("/visitor/input/errors",
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/test-qobject-output-visitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,62 @@ static void test_visitor_out_union_flat(TestOutputVisitorData *data,
qapi_free_UserDefFlatUnion(tmp);
}

static void test_visitor_out_union_in_union(TestOutputVisitorData *data,
const void *unused)
{
QDict *qdict;

TestUnionInUnion *tmp = g_new0(TestUnionInUnion, 1);
tmp->type = TEST_UNION_ENUM_VALUE_A;
tmp->u.value_a.type_a = TEST_UNION_ENUMA_VALUE_A1;
tmp->u.value_a.u.value_a1.integer = 42;
tmp->u.value_a.u.value_a1.name = g_strdup("fish");

visit_type_TestUnionInUnion(data->ov, NULL, &tmp, &error_abort);
qdict = qobject_to(QDict, visitor_get(data));
g_assert(qdict);
g_assert_cmpstr(qdict_get_str(qdict, "type"), ==, "value-a");
g_assert_cmpstr(qdict_get_str(qdict, "type-a"), ==, "value-a1");
g_assert_cmpint(qdict_get_int(qdict, "integer"), ==, 42);
g_assert_cmpstr(qdict_get_str(qdict, "name"), ==, "fish");

qapi_free_TestUnionInUnion(tmp);


visitor_reset(data);
tmp = g_new0(TestUnionInUnion, 1);
tmp->type = TEST_UNION_ENUM_VALUE_A;
tmp->u.value_a.type_a = TEST_UNION_ENUMA_VALUE_A2;
tmp->u.value_a.u.value_a2.integer = 1729;
tmp->u.value_a.u.value_a2.size = 87539319;

visit_type_TestUnionInUnion(data->ov, NULL, &tmp, &error_abort);
qdict = qobject_to(QDict, visitor_get(data));
g_assert(qdict);
g_assert_cmpstr(qdict_get_str(qdict, "type"), ==, "value-a");
g_assert_cmpstr(qdict_get_str(qdict, "type-a"), ==, "value-a2");
g_assert_cmpint(qdict_get_int(qdict, "integer"), ==, 1729);
g_assert_cmpint(qdict_get_int(qdict, "size"), ==, 87539319);

qapi_free_TestUnionInUnion(tmp);


visitor_reset(data);
tmp = g_new0(TestUnionInUnion, 1);
tmp->type = TEST_UNION_ENUM_VALUE_B;
tmp->u.value_b.integer = 1729;
tmp->u.value_b.onoff = true;

visit_type_TestUnionInUnion(data->ov, NULL, &tmp, &error_abort);
qdict = qobject_to(QDict, visitor_get(data));
g_assert(qdict);
g_assert_cmpstr(qdict_get_str(qdict, "type"), ==, "value-b");
g_assert_cmpint(qdict_get_int(qdict, "integer"), ==, 1729);
g_assert_cmpint(qdict_get_bool(qdict, "onoff"), ==, true);

qapi_free_TestUnionInUnion(tmp);
}

static void test_visitor_out_alternate(TestOutputVisitorData *data,
const void *unused)
{
Expand Down Expand Up @@ -586,6 +642,8 @@ int main(int argc, char **argv)
&out_visitor_data, test_visitor_out_list_qapi_free);
output_visitor_test_add("/visitor/output/union-flat",
&out_visitor_data, test_visitor_out_union_flat);
output_visitor_test_add("/visitor/output/union-in-union",
&out_visitor_data, test_visitor_out_union_in_union);
output_visitor_test_add("/visitor/output/alternate",
&out_visitor_data, test_visitor_out_alternate);
output_visitor_test_add("/visitor/output/null",
Expand Down

0 comments on commit a17dbc4

Please sign in to comment.