Browse Source

Merge remote-tracking branch 'remotes/armbru/tags/pull-qapi-2019-06-12' into staging

QAPI patches for 2019-06-12

# gpg: Signature made Wed 12 Jun 2019 17:44:50 BST
# gpg:                using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653
# gpg:                issuer "armbru@redhat.com"
# gpg: Good signature from "Markus Armbruster <armbru@redhat.com>" [full]
# gpg:                 aka "Markus Armbruster <armbru@pond.sub.org>" [full]
# Primary key fingerprint: 354B C8B3 D7EB 2A6B 6867  4E5F 3870 B400 EB91 8653

* remotes/armbru/tags/pull-qapi-2019-06-12:
  qapi: Simplify how QAPIDoc implements its state machine
  file-posix: Add dynamic-auto-read-only QAPI feature
  qapi: Allow documentation for features
  qapi: Disentangle QAPIDoc code
  tests/qapi-schema: Error case tests for features in structs
  tests/qapi-schema: Test for good feature lists in structs
  qapi: Add feature flags to struct types
  block/gluster: update .help of BLOCK_OPT_PREALLOC option
  block/file-posix: update .help of BLOCK_OPT_PREALLOC option
  qapi/block-core: update documentation of preallocation parameter
  qdev: Delete unused LostTickPolicy "merge"

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
Peter Maydell 6 years ago
parent
commit
4747524f9f
42 changed files with 447 additions and 59 deletions
  1. 5 1
      block/file-posix.c
  2. 8 1
      block/gluster.c
  3. 38 0
      docs/devel/qapi-code-gen.txt
  4. 26 7
      qapi/block-core.json
  5. 5 1
      qapi/introspect.json
  6. 1 5
      qapi/misc.json
  7. 206 37
      scripts/qapi/common.py
  8. 14 1
      scripts/qapi/doc.py
  9. 5 1
      scripts/qapi/introspect.py
  10. 2 1
      scripts/qapi/types.py
  11. 2 1
      scripts/qapi/visit.py
  12. 6 0
      tests/Makefile.include
  13. 1 1
      tests/qapi-schema/double-type.err
  14. 1 0
      tests/qapi-schema/features-bad-type.err
  15. 1 0
      tests/qapi-schema/features-bad-type.exit
  16. 3 0
      tests/qapi-schema/features-bad-type.json
  17. 0 0
      tests/qapi-schema/features-bad-type.out
  18. 1 0
      tests/qapi-schema/features-duplicate-name.err
  19. 1 0
      tests/qapi-schema/features-duplicate-name.exit
  20. 3 0
      tests/qapi-schema/features-duplicate-name.json
  21. 0 0
      tests/qapi-schema/features-duplicate-name.out
  22. 1 0
      tests/qapi-schema/features-missing-name.err
  23. 1 0
      tests/qapi-schema/features-missing-name.exit
  24. 3 0
      tests/qapi-schema/features-missing-name.json
  25. 0 0
      tests/qapi-schema/features-missing-name.out
  26. 1 0
      tests/qapi-schema/features-name-bad-type.err
  27. 1 0
      tests/qapi-schema/features-name-bad-type.exit
  28. 3 0
      tests/qapi-schema/features-name-bad-type.json
  29. 0 0
      tests/qapi-schema/features-name-bad-type.out
  30. 1 0
      tests/qapi-schema/features-no-list.err
  31. 1 0
      tests/qapi-schema/features-no-list.exit
  32. 3 0
      tests/qapi-schema/features-no-list.json
  33. 0 0
      tests/qapi-schema/features-no-list.out
  34. 2 0
      tests/qapi-schema/features-unknown-key.err
  35. 1 0
      tests/qapi-schema/features-unknown-key.exit
  36. 3 0
      tests/qapi-schema/features-unknown-key.json
  37. 0 0
      tests/qapi-schema/features-unknown-key.out
  38. 39 0
      tests/qapi-schema/qapi-schema-test.json
  39. 43 0
      tests/qapi-schema/qapi-schema-test.out
  40. 6 1
      tests/qapi-schema/test-qapi.py
  41. 1 1
      tests/qapi-schema/unknown-expr-key.err
  42. 8 0
      tests/test-qmp-cmds.c

+ 5 - 1
block/file-posix.c

@@ -2752,7 +2752,11 @@ static QemuOptsList raw_create_opts = {
         {
         {
             .name = BLOCK_OPT_PREALLOC,
             .name = BLOCK_OPT_PREALLOC,
             .type = QEMU_OPT_STRING,
             .type = QEMU_OPT_STRING,
-            .help = "Preallocation mode (allowed values: off, falloc, full)"
+            .help = "Preallocation mode (allowed values: off"
+#ifdef CONFIG_POSIX_FALLOCATE
+                    ", falloc"
+#endif
+                    ", full)"
         },
         },
         { /* end of list */ }
         { /* end of list */ }
     }
     }

+ 8 - 1
block/gluster.c

@@ -98,7 +98,14 @@ static QemuOptsList qemu_gluster_create_opts = {
         {
         {
             .name = BLOCK_OPT_PREALLOC,
             .name = BLOCK_OPT_PREALLOC,
             .type = QEMU_OPT_STRING,
             .type = QEMU_OPT_STRING,
-            .help = "Preallocation mode (allowed values: off, full)"
+            .help = "Preallocation mode (allowed values: off"
+#ifdef CONFIG_GLUSTERFS_FALLOCATE
+                    ", falloc"
+#endif
+#ifdef CONFIG_GLUSTERFS_ZEROFILL
+                    ", full"
+#endif
+                    ")"
         },
         },
         {
         {
             .name = GLUSTER_OPT_DEBUG,
             .name = GLUSTER_OPT_DEBUG,

+ 38 - 0
docs/devel/qapi-code-gen.txt

@@ -719,6 +719,34 @@ any non-empty complex type (struct, union, or alternate), and a
 pointer to that QAPI type is passed as a single argument.
 pointer to that QAPI type is passed as a single argument.
 
 
 
 
+=== Features ===
+
+Sometimes, the behaviour of QEMU changes compatibly, but without a
+change in the QMP syntax (usually by allowing values or operations that
+previously resulted in an error). QMP clients may still need to know
+whether the extension is available.
+
+For this purpose, a list of features can be specified for a struct type.
+This is exposed to the client as a list of string, where each string
+signals that this build of QEMU shows a certain behaviour.
+
+In the schema, features can be specified as simple strings, for example:
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ 'allow-negative-numbers' ] }
+
+Another option is to specify features as dictionaries, where the key
+'name' specifies the feature string to be exposed to clients:
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ { 'name': 'allow-negative-numbers' } ] }
+
+This expanded form is necessary if you want to make the feature
+conditional (see below in "Configuring the schema").
+
+
 === Downstream extensions ===
 === Downstream extensions ===
 
 
 QAPI schema names that are externally visible, say in the Client JSON
 QAPI schema names that are externally visible, say in the Client JSON
@@ -771,6 +799,16 @@ Example: a conditional 'bar' enum member.
   [ 'foo',
   [ 'foo',
     { 'name' : 'bar', 'if': 'defined(IFCOND)' } ] }
     { 'name' : 'bar', 'if': 'defined(IFCOND)' } ] }
 
 
+Similarly, features can be specified as a dictionary with a 'name' and
+an 'if' key.
+
+Example: a conditional 'allow-negative-numbers' feature
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ { 'name': 'allow-negative-numbers',
+                  'if' 'defined(IFCOND)' } ] }
+
 Please note that you are responsible to ensure that the C code will
 Please note that you are responsible to ensure that the C code will
 compile with an arbitrary combination of conditions, since the
 compile with an arbitrary combination of conditions, since the
 generators are unable to check it at this point.
 generators are unable to check it at this point.

+ 26 - 7
qapi/block-core.json

@@ -2859,6 +2859,15 @@
 #                         file is large, do not use in production.
 #                         file is large, do not use in production.
 #                         (default: off) (since: 3.0)
 #                         (default: off) (since: 3.0)
 #
 #
+# Features:
+# @dynamic-auto-read-only: If present, enabled auto-read-only means that the
+#                          driver will open the image read-only at first,
+#                          dynamically reopen the image file read-write when
+#                          the first writer is attached to the node and reopen
+#                          read-only when the last writer is detached. This
+#                          allows giving QEMU write permissions only on demand
+#                          when an operation actually needs write access.
+#
 # Since: 2.9
 # Since: 2.9
 ##
 ##
 { 'struct': 'BlockdevOptionsFile',
 { 'struct': 'BlockdevOptionsFile',
@@ -2868,7 +2877,9 @@
             '*aio': 'BlockdevAioOptions',
             '*aio': 'BlockdevAioOptions',
 	    '*drop-cache': {'type': 'bool',
 	    '*drop-cache': {'type': 'bool',
 	                    'if': 'defined(CONFIG_LINUX)'},
 	                    'if': 'defined(CONFIG_LINUX)'},
-            '*x-check-cache-dropped': 'bool' } }
+            '*x-check-cache-dropped': 'bool' },
+  'features': [ { 'name': 'dynamic-auto-read-only',
+                  'if': 'defined(CONFIG_POSIX)' } ] }
 
 
 ##
 ##
 # @BlockdevOptionsNull:
 # @BlockdevOptionsNull:
@@ -4121,7 +4132,10 @@
 #
 #
 # @filename         Filename for the new image file
 # @filename         Filename for the new image file
 # @size             Size of the virtual disk in bytes
 # @size             Size of the virtual disk in bytes
-# @preallocation    Preallocation mode for the new image (default: off)
+# @preallocation    Preallocation mode for the new image (default: off;
+#                   allowed values: off,
+#                   falloc (if defined CONFIG_POSIX_FALLOCATE),
+#                   full (if defined CONFIG_POSIX))
 # @nocow            Turn off copy-on-write (valid only on btrfs; default: off)
 # @nocow            Turn off copy-on-write (valid only on btrfs; default: off)
 #
 #
 # Since: 2.12
 # Since: 2.12
@@ -4139,7 +4153,10 @@
 #
 #
 # @location         Where to store the new image file
 # @location         Where to store the new image file
 # @size             Size of the virtual disk in bytes
 # @size             Size of the virtual disk in bytes
-# @preallocation    Preallocation mode for the new image (default: off)
+# @preallocation    Preallocation mode for the new image (default: off;
+#                   allowed values: off,
+#                   falloc (if defined CONFIG_GLUSTERFS_FALLOCATE),
+#                   full (if defined CONFIG_GLUSTERFS_ZEROFILL))
 #
 #
 # Since: 2.12
 # Since: 2.12
 ##
 ##
@@ -4243,7 +4260,8 @@
 # @backing-fmt      Name of the block driver to use for the backing file
 # @backing-fmt      Name of the block driver to use for the backing file
 # @encrypt          Encryption options if the image should be encrypted
 # @encrypt          Encryption options if the image should be encrypted
 # @cluster-size     qcow2 cluster size in bytes (default: 65536)
 # @cluster-size     qcow2 cluster size in bytes (default: 65536)
-# @preallocation    Preallocation mode for the new image (default: off)
+# @preallocation    Preallocation mode for the new image (default: off;
+#                   allowed values: off, falloc, full, metadata)
 # @lazy-refcounts   True if refcounts may be updated lazily (default: off)
 # @lazy-refcounts   True if refcounts may be updated lazily (default: off)
 # @refcount-bits    Width of reference counts in bits (default: 16)
 # @refcount-bits    Width of reference counts in bits (default: 16)
 #
 #
@@ -4426,7 +4444,8 @@
 # @location         Where to store the new image file
 # @location         Where to store the new image file
 # @size             Size of the virtual disk in bytes
 # @size             Size of the virtual disk in bytes
 # @backing-file     File name of a base image
 # @backing-file     File name of a base image
-# @preallocation    Preallocation mode (allowed values: off, full)
+# @preallocation    Preallocation mode for the new image (default: off;
+#                   allowed values: off, full)
 # @redundancy       Redundancy of the image
 # @redundancy       Redundancy of the image
 # @object-size      Object size of the image
 # @object-size      Object size of the image
 #
 #
@@ -4461,8 +4480,8 @@
 #
 #
 # @file             Node to create the image format on
 # @file             Node to create the image format on
 # @size             Size of the virtual disk in bytes
 # @size             Size of the virtual disk in bytes
-# @preallocation    Preallocation mode for the new image (allowed values: off,
-#                   metadata; default: off)
+# @preallocation    Preallocation mode for the new image (default: off;
+#                   allowed values: off, metadata)
 #
 #
 # Since: 2.12
 # Since: 2.12
 ##
 ##

+ 5 - 1
qapi/introspect.json

@@ -174,6 +174,9 @@
 #            and may even differ from the order of the values of the
 #            and may even differ from the order of the values of the
 #            enum type of the @tag.
 #            enum type of the @tag.
 #
 #
+# @features: names of features associated with the type, in no particular
+#            order. (since: 4.1)
+#
 # Values of this type are JSON object on the wire.
 # Values of this type are JSON object on the wire.
 #
 #
 # Since: 2.5
 # Since: 2.5
@@ -181,7 +184,8 @@
 { 'struct': 'SchemaInfoObject',
 { 'struct': 'SchemaInfoObject',
   'data': { 'members': [ 'SchemaInfoObjectMember' ],
   'data': { 'members': [ 'SchemaInfoObjectMember' ],
             '*tag': 'str',
             '*tag': 'str',
-            '*variants': [ 'SchemaInfoObjectVariant' ] } }
+            '*variants': [ 'SchemaInfoObjectVariant' ],
+            '*features': [ 'str' ] } }
 
 
 ##
 ##
 # @SchemaInfoObjectMember:
 # @SchemaInfoObjectMember:

+ 1 - 5
qapi/misc.json

@@ -172,17 +172,13 @@
 # @delay: continue to deliver ticks at the normal rate.  Guest time will be
 # @delay: continue to deliver ticks at the normal rate.  Guest time will be
 #         delayed due to the late tick
 #         delayed due to the late tick
 #
 #
-# @merge: merge the missed tick(s) into one tick and inject.  Guest time
-#         may be delayed, depending on how the OS reacts to the merging
-#         of ticks
-#
 # @slew: deliver ticks at a higher rate to catch up with the missed tick. The
 # @slew: deliver ticks at a higher rate to catch up with the missed tick. The
 #        guest time should not be delayed once catchup is complete.
 #        guest time should not be delayed once catchup is complete.
 #
 #
 # Since: 2.0
 # Since: 2.0
 ##
 ##
 { 'enum': 'LostTickPolicy',
 { 'enum': 'LostTickPolicy',
-  'data': ['discard', 'delay', 'merge', 'slew' ] }
+  'data': ['discard', 'delay', 'slew' ] }
 
 
 ##
 ##
 # @add_client:
 # @add_client:

+ 206 - 37
scripts/qapi/common.py

@@ -102,6 +102,24 @@ def __init__(self, info, msg):
 
 
 
 
 class QAPIDoc(object):
 class QAPIDoc(object):
+    """
+    A documentation comment block, either expression or free-form
+
+    Expression documentation blocks consist of
+
+    * a body section: one line naming the expression, followed by an
+      overview (any number of lines)
+
+    * argument sections: a description of each argument (for commands
+      and events) or member (for structs, unions and alternates)
+
+    * features sections: a description of each feature flag
+
+    * additional (non-argument) sections, possibly tagged
+
+    Free-form documentation blocks consist only of a body section.
+    """
+
     class Section(object):
     class Section(object):
         def __init__(self, name=None):
         def __init__(self, name=None):
             # optional section name (argument/member or section name)
             # optional section name (argument/member or section name)
@@ -131,10 +149,12 @@ def __init__(self, parser, info):
         self.body = QAPIDoc.Section()
         self.body = QAPIDoc.Section()
         # dict mapping parameter name to ArgSection
         # dict mapping parameter name to ArgSection
         self.args = OrderedDict()
         self.args = OrderedDict()
+        self.features = OrderedDict()
         # a list of Section
         # a list of Section
         self.sections = []
         self.sections = []
         # the current section
         # the current section
         self._section = self.body
         self._section = self.body
+        self._append_line = self._append_body_line
 
 
     def has_section(self, name):
     def has_section(self, name):
         """Return True if we have a section with this name."""
         """Return True if we have a section with this name."""
@@ -144,7 +164,16 @@ def has_section(self, name):
         return False
         return False
 
 
     def append(self, line):
     def append(self, line):
-        """Parse a comment line and add it to the documentation."""
+        """
+        Parse a comment line and add it to the documentation.
+
+        The way that the line is dealt with depends on which part of
+        the documentation we're parsing right now:
+        * The body section: ._append_line is ._append_body_line
+        * An argument section: ._append_line is ._append_args_line
+        * A features section: ._append_line is ._append_features_line
+        * An additional section: ._append_line is ._append_various_line
+        """
         line = line[1:]
         line = line[1:]
         if not line:
         if not line:
             self._append_freeform(line)
             self._append_freeform(line)
@@ -153,54 +182,155 @@ def append(self, line):
         if line[0] != ' ':
         if line[0] != ' ':
             raise QAPIParseError(self._parser, "Missing space after #")
             raise QAPIParseError(self._parser, "Missing space after #")
         line = line[1:]
         line = line[1:]
+        self._append_line(line)
+
+    def end_comment(self):
+        self._end_section()
 
 
+    @staticmethod
+    def _is_section_tag(name):
+        return name in ('Returns:', 'Since:',
+                        # those are often singular or plural
+                        'Note:', 'Notes:',
+                        'Example:', 'Examples:',
+                        'TODO:')
+
+    def _append_body_line(self, line):
+        """
+        Process a line of documentation text in the body section.
+
+        If this a symbol line and it is the section's first line, this
+        is an expression documentation block for that symbol.
+
+        If it's an expression documentation block, another symbol line
+        begins the argument section for the argument named by it, and
+        a section tag begins an additional section.  Start that
+        section and append the line to it.
+
+        Else, append the line to the current section.
+        """
+        name = line.split(' ', 1)[0]
         # FIXME not nice: things like '#  @foo:' and '# @foo: ' aren't
         # FIXME not nice: things like '#  @foo:' and '# @foo: ' aren't
         # recognized, and get silently treated as ordinary text
         # recognized, and get silently treated as ordinary text
-        if self.symbol:
-            self._append_symbol_line(line)
-        elif not self.body.text and line.startswith('@'):
+        if not self.symbol and not self.body.text and line.startswith('@'):
             if not line.endswith(':'):
             if not line.endswith(':'):
                 raise QAPIParseError(self._parser, "Line should end with :")
                 raise QAPIParseError(self._parser, "Line should end with :")
             self.symbol = line[1:-1]
             self.symbol = line[1:-1]
             # FIXME invalid names other than the empty string aren't flagged
             # FIXME invalid names other than the empty string aren't flagged
             if not self.symbol:
             if not self.symbol:
                 raise QAPIParseError(self._parser, "Invalid name")
                 raise QAPIParseError(self._parser, "Invalid name")
+        elif self.symbol:
+            # This is an expression documentation block
+            if name.startswith('@') and name.endswith(':'):
+                self._append_line = self._append_args_line
+                self._append_args_line(line)
+            elif line == 'Features:':
+                self._append_line = self._append_features_line
+            elif self._is_section_tag(name):
+                self._append_line = self._append_various_line
+                self._append_various_line(line)
+            else:
+                self._append_freeform(line.strip())
         else:
         else:
-            self._append_freeform(line)
+            # This is a free-form documentation block
+            self._append_freeform(line.strip())
 
 
-    def end_comment(self):
-        self._end_section()
+    def _append_args_line(self, line):
+        """
+        Process a line of documentation text in an argument section.
+
+        A symbol line begins the next argument section, a section tag
+        section or a non-indented line after a blank line begins an
+        additional section.  Start that section and append the line to
+        it.
+
+        Else, append the line to the current section.
 
 
-    def _append_symbol_line(self, line):
+        """
         name = line.split(' ', 1)[0]
         name = line.split(' ', 1)[0]
 
 
         if name.startswith('@') and name.endswith(':'):
         if name.startswith('@') and name.endswith(':'):
             line = line[len(name)+1:]
             line = line[len(name)+1:]
             self._start_args_section(name[1:-1])
             self._start_args_section(name[1:-1])
-        elif name in ('Returns:', 'Since:',
-                      # those are often singular or plural
-                      'Note:', 'Notes:',
-                      'Example:', 'Examples:',
-                      'TODO:'):
+        elif self._is_section_tag(name):
+            self._append_line = self._append_various_line
+            self._append_various_line(line)
+            return
+        elif (self._section.text.endswith('\n\n')
+              and line and not line[0].isspace()):
+            if line == 'Features:':
+                self._append_line = self._append_features_line
+            else:
+                self._start_section()
+                self._append_line = self._append_various_line
+                self._append_various_line(line)
+            return
+
+        self._append_freeform(line.strip())
+
+    def _append_features_line(self, line):
+        name = line.split(' ', 1)[0]
+
+        if name.startswith('@') and name.endswith(':'):
+            line = line[len(name)+1:]
+            self._start_features_section(name[1:-1])
+        elif self._is_section_tag(name):
+            self._append_line = self._append_various_line
+            self._append_various_line(line)
+            return
+        elif (self._section.text.endswith('\n\n')
+              and line and not line[0].isspace()):
+            self._start_section()
+            self._append_line = self._append_various_line
+            self._append_various_line(line)
+            return
+
+        self._append_freeform(line.strip())
+
+    def _append_various_line(self, line):
+        """
+        Process a line of documentation text in an additional section.
+
+        A symbol line is an error.
+
+        A section tag begins an additional section.  Start that
+        section and append the line to it.
+
+        Else, append the line to the current section.
+        """
+        name = line.split(' ', 1)[0]
+
+        if name.startswith('@') and name.endswith(':'):
+            raise QAPIParseError(self._parser,
+                                 "'%s' can't follow '%s' section"
+                                 % (name, self.sections[0].name))
+        elif self._is_section_tag(name):
             line = line[len(name)+1:]
             line = line[len(name)+1:]
             self._start_section(name[:-1])
             self._start_section(name[:-1])
 
 
+        if (not self._section.name or
+                not self._section.name.startswith('Example')):
+            line = line.strip()
+
         self._append_freeform(line)
         self._append_freeform(line)
 
 
-    def _start_args_section(self, name):
+    def _start_symbol_section(self, symbols_dict, name):
         # FIXME invalid names other than the empty string aren't flagged
         # FIXME invalid names other than the empty string aren't flagged
         if not name:
         if not name:
             raise QAPIParseError(self._parser, "Invalid parameter name")
             raise QAPIParseError(self._parser, "Invalid parameter name")
-        if name in self.args:
+        if name in symbols_dict:
             raise QAPIParseError(self._parser,
             raise QAPIParseError(self._parser,
                                  "'%s' parameter name duplicated" % name)
                                  "'%s' parameter name duplicated" % name)
-        if self.sections:
-            raise QAPIParseError(self._parser,
-                                 "'@%s:' can't follow '%s' section"
-                                 % (name, self.sections[0].name))
+        assert not self.sections
         self._end_section()
         self._end_section()
         self._section = QAPIDoc.ArgSection(name)
         self._section = QAPIDoc.ArgSection(name)
-        self.args[name] = self._section
+        symbols_dict[name] = self._section
+
+    def _start_args_section(self, name):
+        self._start_symbol_section(self.args, name)
+
+    def _start_features_section(self, name):
+        self._start_symbol_section(self.features, name)
 
 
     def _start_section(self, name=None):
     def _start_section(self, name=None):
         if name in ('Returns', 'Since') and self.has_section(name):
         if name in ('Returns', 'Since') and self.has_section(name):
@@ -219,13 +349,6 @@ def _end_section(self):
             self._section = None
             self._section = None
 
 
     def _append_freeform(self, line):
     def _append_freeform(self, line):
-        in_arg = isinstance(self._section, QAPIDoc.ArgSection)
-        if (in_arg and self._section.text.endswith('\n\n')
-                and line and not line[0].isspace()):
-            self._start_section()
-        if (in_arg or not self._section.name
-                or not self._section.name.startswith('Example')):
-            line = line.strip()
         match = re.match(r'(@\S+:)', line)
         match = re.match(r'(@\S+:)', line)
         if match:
         if match:
             raise QAPIParseError(self._parser,
             raise QAPIParseError(self._parser,
@@ -886,12 +1009,26 @@ def check_enum(expr, info):
 def check_struct(expr, info):
 def check_struct(expr, info):
     name = expr['struct']
     name = expr['struct']
     members = expr['data']
     members = expr['data']
+    features = expr.get('features')
 
 
     check_type(info, "'data' for struct '%s'" % name, members,
     check_type(info, "'data' for struct '%s'" % name, members,
                allow_dict=True, allow_optional=True)
                allow_dict=True, allow_optional=True)
     check_type(info, "'base' for struct '%s'" % name, expr.get('base'),
     check_type(info, "'base' for struct '%s'" % name, expr.get('base'),
                allow_metas=['struct'])
                allow_metas=['struct'])
 
 
+    if features:
+        if not isinstance(features, list):
+            raise QAPISemError(info,
+                               "Struct '%s' requires an array for 'features'" %
+                               name)
+        for f in features:
+            assert isinstance(f, dict)
+            check_known_keys(info, "feature of struct %s" % name, f,
+                             ['name'], ['if'])
+
+            check_if(f, info)
+            check_name(info, "Feature of struct %s" % name, f['name'])
+
 
 
 def check_known_keys(info, source, keys, required, optional):
 def check_known_keys(info, source, keys, required, optional):
 
 
@@ -948,6 +1085,12 @@ def normalize_members(members):
             members[key] = {'type': arg}
             members[key] = {'type': arg}
 
 
 
 
+def normalize_features(features):
+    if isinstance(features, list):
+        features[:] = [f if isinstance(f, dict) else {'name': f}
+                       for f in features]
+
+
 def check_exprs(exprs):
 def check_exprs(exprs):
     global all_names
     global all_names
 
 
@@ -986,8 +1129,10 @@ def check_exprs(exprs):
             normalize_members(expr['data'])
             normalize_members(expr['data'])
         elif 'struct' in expr:
         elif 'struct' in expr:
             meta = 'struct'
             meta = 'struct'
-            check_keys(expr_elem, 'struct', ['data'], ['base', 'if'])
+            check_keys(expr_elem, 'struct', ['data'],
+                       ['base', 'if', 'features'])
             normalize_members(expr['data'])
             normalize_members(expr['data'])
+            normalize_features(expr.get('features'))
             struct_types[expr[meta]] = expr
             struct_types[expr[meta]] = expr
         elif 'command' in expr:
         elif 'command' in expr:
             meta = 'command'
             meta = 'command'
@@ -1126,10 +1271,12 @@ def visit_enum_type(self, name, info, ifcond, members, prefix):
     def visit_array_type(self, name, info, ifcond, element_type):
     def visit_array_type(self, name, info, ifcond, element_type):
         pass
         pass
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         pass
         pass
 
 
-    def visit_object_type_flat(self, name, info, ifcond, members, variants):
+    def visit_object_type_flat(self, name, info, ifcond, members, variants,
+                               features):
         pass
         pass
 
 
     def visit_alternate_type(self, name, info, ifcond, variants):
     def visit_alternate_type(self, name, info, ifcond, variants):
@@ -1290,7 +1437,7 @@ def visit(self, visitor):
 
 
 class QAPISchemaObjectType(QAPISchemaType):
 class QAPISchemaObjectType(QAPISchemaType):
     def __init__(self, name, info, doc, ifcond,
     def __init__(self, name, info, doc, ifcond,
-                 base, local_members, variants):
+                 base, local_members, variants, features):
         # struct has local_members, optional base, and no variants
         # struct has local_members, optional base, and no variants
         # flat union has base, variants, and no local_members
         # flat union has base, variants, and no local_members
         # simple union has local_members, variants, and no base
         # simple union has local_members, variants, and no base
@@ -1302,11 +1449,15 @@ def __init__(self, name, info, doc, ifcond,
         if variants is not None:
         if variants is not None:
             assert isinstance(variants, QAPISchemaObjectTypeVariants)
             assert isinstance(variants, QAPISchemaObjectTypeVariants)
             variants.set_owner(name)
             variants.set_owner(name)
+        for f in features:
+            assert isinstance(f, QAPISchemaFeature)
+            f.set_owner(name)
         self._base_name = base
         self._base_name = base
         self.base = None
         self.base = None
         self.local_members = local_members
         self.local_members = local_members
         self.variants = variants
         self.variants = variants
         self.members = None
         self.members = None
+        self.features = features
 
 
     def check(self, schema):
     def check(self, schema):
         QAPISchemaType.check(self, schema)
         QAPISchemaType.check(self, schema)
@@ -1332,6 +1483,12 @@ def check(self, schema):
             self.variants.check(schema, seen)
             self.variants.check(schema, seen)
             assert self.variants.tag_member in self.members
             assert self.variants.tag_member in self.members
             self.variants.check_clash(self.info, seen)
             self.variants.check_clash(self.info, seen)
+
+        # Features are in a name space separate from members
+        seen = {}
+        for f in self.features:
+            f.check_clash(self.info, seen)
+
         if self.doc:
         if self.doc:
             self.doc.check()
             self.doc.check()
 
 
@@ -1368,12 +1525,15 @@ def json_type(self):
 
 
     def visit(self, visitor):
     def visit(self, visitor):
         visitor.visit_object_type(self.name, self.info, self.ifcond,
         visitor.visit_object_type(self.name, self.info, self.ifcond,
-                                  self.base, self.local_members, self.variants)
+                                  self.base, self.local_members, self.variants,
+                                  self.features)
         visitor.visit_object_type_flat(self.name, self.info, self.ifcond,
         visitor.visit_object_type_flat(self.name, self.info, self.ifcond,
-                                       self.members, self.variants)
+                                       self.members, self.variants,
+                                       self.features)
 
 
 
 
 class QAPISchemaMember(object):
 class QAPISchemaMember(object):
+    """ Represents object members, enum members and features """
     role = 'member'
     role = 'member'
 
 
     def __init__(self, name, ifcond=None):
     def __init__(self, name, ifcond=None):
@@ -1419,6 +1579,10 @@ def describe(self):
         return "'%s' %s" % (self.name, self._pretty_owner())
         return "'%s' %s" % (self.name, self._pretty_owner())
 
 
 
 
+class QAPISchemaFeature(QAPISchemaMember):
+    role = 'feature'
+
+
 class QAPISchemaObjectTypeMember(QAPISchemaMember):
 class QAPISchemaObjectTypeMember(QAPISchemaMember):
     def __init__(self, name, typ, optional, ifcond=None):
     def __init__(self, name, typ, optional, ifcond=None):
         QAPISchemaMember.__init__(self, name, ifcond)
         QAPISchemaMember.__init__(self, name, ifcond)
@@ -1675,7 +1839,7 @@ def _def_predefineds(self):
                   ('null',   'null',    'QNull' + pointer_suffix)]:
                   ('null',   'null',    'QNull' + pointer_suffix)]:
             self._def_builtin_type(*t)
             self._def_builtin_type(*t)
         self.the_empty_object_type = QAPISchemaObjectType(
         self.the_empty_object_type = QAPISchemaObjectType(
-            'q_empty', None, None, None, None, [], None)
+            'q_empty', None, None, None, None, [], None, [])
         self._def_entity(self.the_empty_object_type)
         self._def_entity(self.the_empty_object_type)
 
 
         qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
         qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
@@ -1685,6 +1849,9 @@ def _def_predefineds(self):
         self._def_entity(QAPISchemaEnumType('QType', None, None, None,
         self._def_entity(QAPISchemaEnumType('QType', None, None, None,
                                             qtype_values, 'QTYPE'))
                                             qtype_values, 'QTYPE'))
 
 
+    def _make_features(self, features):
+        return [QAPISchemaFeature(f['name'], f.get('if')) for f in features]
+
     def _make_enum_members(self, values):
     def _make_enum_members(self, values):
         return [QAPISchemaMember(v['name'], v.get('if')) for v in values]
         return [QAPISchemaMember(v['name'], v.get('if')) for v in values]
 
 
@@ -1721,7 +1888,7 @@ def _make_implicit_object_type(self, name, info, doc, ifcond,
             assert ifcond == typ._ifcond # pylint: disable=protected-access
             assert ifcond == typ._ifcond # pylint: disable=protected-access
         else:
         else:
             self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond,
             self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond,
-                                                  None, members, None))
+                                                  None, members, None, []))
         return name
         return name
 
 
     def _def_enum_type(self, expr, info, doc):
     def _def_enum_type(self, expr, info, doc):
@@ -1752,9 +1919,11 @@ def _def_struct_type(self, expr, info, doc):
         base = expr.get('base')
         base = expr.get('base')
         data = expr['data']
         data = expr['data']
         ifcond = expr.get('if')
         ifcond = expr.get('if')
+        features = expr.get('features', [])
         self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, base,
         self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, base,
                                               self._make_members(data, info),
                                               self._make_members(data, info),
-                                              None))
+                                              None,
+                                              self._make_features(features)))
 
 
     def _make_variant(self, case, typ, ifcond):
     def _make_variant(self, case, typ, ifcond):
         return QAPISchemaObjectTypeVariant(case, typ, ifcond)
         return QAPISchemaObjectTypeVariant(case, typ, ifcond)
@@ -1795,7 +1964,7 @@ def _def_union_type(self, expr, info, doc):
             QAPISchemaObjectType(name, info, doc, ifcond, base, members,
             QAPISchemaObjectType(name, info, doc, ifcond, base, members,
                                  QAPISchemaObjectTypeVariants(tag_name,
                                  QAPISchemaObjectTypeVariants(tag_name,
                                                               tag_member,
                                                               tag_member,
-                                                              variants)))
+                                                              variants), []))
 
 
     def _def_alternate_type(self, expr, info, doc):
     def _def_alternate_type(self, expr, info, doc):
         name = expr['alternate']
         name = expr['alternate']

+ 14 - 1
scripts/qapi/doc.py

@@ -182,6 +182,17 @@ def texi_members(doc, what, base, variants, member_func):
     return '\n@b{%s:}\n@table @asis\n%s@end table\n' % (what, items)
     return '\n@b{%s:}\n@table @asis\n%s@end table\n' % (what, items)
 
 
 
 
+def texi_features(doc):
+    """Format the table of features"""
+    items = ''
+    for section in doc.features.values():
+        desc = texi_format(section.text)
+        items += '@item @code{%s}\n%s' % (section.name, desc)
+    if not items:
+        return ''
+    return '\n@b{Features:}\n@table @asis\n%s@end table\n' % (items)
+
+
 def texi_sections(doc, ifcond):
 def texi_sections(doc, ifcond):
     """Format additional sections following arguments"""
     """Format additional sections following arguments"""
     body = ''
     body = ''
@@ -201,6 +212,7 @@ def texi_entity(doc, what, ifcond, base=None, variants=None,
                 member_func=texi_member):
                 member_func=texi_member):
     return (texi_body(doc)
     return (texi_body(doc)
             + texi_members(doc, what, base, variants, member_func)
             + texi_members(doc, what, base, variants, member_func)
+            + texi_features(doc)
             + texi_sections(doc, ifcond))
             + texi_sections(doc, ifcond))
 
 
 
 
@@ -220,7 +232,8 @@ def visit_enum_type(self, name, info, ifcond, members, prefix):
                                body=texi_entity(doc, 'Values', ifcond,
                                body=texi_entity(doc, 'Values', ifcond,
                                                 member_func=texi_enum_value)))
                                                 member_func=texi_enum_value)))
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         doc = self.cur_doc
         doc = self.cur_doc
         if base and base.is_implicit():
         if base and base.is_implicit():
             base = None
             base = None

+ 5 - 1
scripts/qapi/introspect.py

@@ -188,11 +188,15 @@ def visit_array_type(self, name, info, ifcond, element_type):
         self._gen_qlit('[' + element + ']', 'array', {'element-type': element},
         self._gen_qlit('[' + element + ']', 'array', {'element-type': element},
                        ifcond)
                        ifcond)
 
 
-    def visit_object_type_flat(self, name, info, ifcond, members, variants):
+    def visit_object_type_flat(self, name, info, ifcond, members, variants,
+                               features):
         obj = {'members': [self._gen_member(m) for m in members]}
         obj = {'members': [self._gen_member(m) for m in members]}
         if variants:
         if variants:
             obj.update(self._gen_variants(variants.tag_member.name,
             obj.update(self._gen_variants(variants.tag_member.name,
                                           variants.variants))
                                           variants.variants))
+        if features:
+            obj['features'] = [(f.name, {'if': f.ifcond}) for f in features]
+
         self._gen_qlit(name, 'object', obj, ifcond)
         self._gen_qlit(name, 'object', obj, ifcond)
 
 
     def visit_alternate_type(self, name, info, ifcond, variants):
     def visit_alternate_type(self, name, info, ifcond, variants):

+ 2 - 1
scripts/qapi/types.py

@@ -227,7 +227,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
             self._genh.add(gen_array(name, element_type))
             self._genh.add(gen_array(name, element_type))
             self._gen_type_cleanup(name)
             self._gen_type_cleanup(name)
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         # Nothing to do for the special empty builtin
         # Nothing to do for the special empty builtin
         if name == 'q_empty':
         if name == 'q_empty':
             return
             return

+ 2 - 1
scripts/qapi/visit.py

@@ -324,7 +324,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
             self._genh.add(gen_visit_decl(name))
             self._genh.add(gen_visit_decl(name))
             self._genc.add(gen_visit_list(name, element_type))
             self._genc.add(gen_visit_list(name, element_type))
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         # Nothing to do for the special empty builtin
         # Nothing to do for the special empty builtin
         if name == 'q_empty':
         if name == 'q_empty':
             return
             return

+ 6 - 0
tests/Makefile.include

@@ -378,6 +378,12 @@ qapi-schema += event-boxed-empty.json
 qapi-schema += event-case.json
 qapi-schema += event-case.json
 qapi-schema += event-member-invalid-dict.json
 qapi-schema += event-member-invalid-dict.json
 qapi-schema += event-nest-struct.json
 qapi-schema += event-nest-struct.json
+qapi-schema += features-bad-type.json
+qapi-schema += features-duplicate-name.json
+qapi-schema += features-missing-name.json
+qapi-schema += features-name-bad-type.json
+qapi-schema += features-no-list.json
+qapi-schema += features-unknown-key.json
 qapi-schema += flat-union-array-branch.json
 qapi-schema += flat-union-array-branch.json
 qapi-schema += flat-union-bad-base.json
 qapi-schema += flat-union-bad-base.json
 qapi-schema += flat-union-bad-discriminator.json
 qapi-schema += flat-union-bad-discriminator.json

+ 1 - 1
tests/qapi-schema/double-type.err

@@ -1,2 +1,2 @@
 tests/qapi-schema/double-type.json:2: Unknown key 'command' in struct 'bar'
 tests/qapi-schema/double-type.json:2: Unknown key 'command' in struct 'bar'
-Valid keys are 'base', 'data', 'if', 'struct'.
+Valid keys are 'base', 'data', 'features', 'if', 'struct'.

+ 1 - 0
tests/qapi-schema/features-bad-type.err

@@ -0,0 +1 @@
+tests/qapi-schema/features-bad-type.json:1: Feature of struct FeatureStruct0 requires a string name

+ 1 - 0
tests/qapi-schema/features-bad-type.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-bad-type.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [ [ 'a feature cannot be an array' ] ] }

+ 0 - 0
tests/qapi-schema/features-bad-type.out


+ 1 - 0
tests/qapi-schema/features-duplicate-name.err

@@ -0,0 +1 @@
+tests/qapi-schema/features-duplicate-name.json:1: 'foo' (feature of FeatureStruct0) collides with 'foo' (feature of FeatureStruct0)

+ 1 - 0
tests/qapi-schema/features-duplicate-name.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-duplicate-name.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [ 'foo', 'bar', 'foo' ] }

+ 0 - 0
tests/qapi-schema/features-duplicate-name.out


+ 1 - 0
tests/qapi-schema/features-missing-name.err

@@ -0,0 +1 @@
+tests/qapi-schema/features-missing-name.json:1: Key 'name' is missing from feature of struct FeatureStruct0

+ 1 - 0
tests/qapi-schema/features-missing-name.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-missing-name.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [ { 'if': 'defined(NAMELESS_FEATURES)' } ] }

+ 0 - 0
tests/qapi-schema/features-missing-name.out


+ 1 - 0
tests/qapi-schema/features-name-bad-type.err

@@ -0,0 +1 @@
+tests/qapi-schema/features-name-bad-type.json:1: Feature of struct FeatureStruct0 requires a string name

+ 1 - 0
tests/qapi-schema/features-name-bad-type.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-name-bad-type.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': { 'feature-type': 'object' } } ] }

+ 0 - 0
tests/qapi-schema/features-name-bad-type.out


+ 1 - 0
tests/qapi-schema/features-no-list.err

@@ -0,0 +1 @@
+tests/qapi-schema/features-no-list.json:1: Struct 'FeatureStruct0' requires an array for 'features'

+ 1 - 0
tests/qapi-schema/features-no-list.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-no-list.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': 'bar' }

+ 0 - 0
tests/qapi-schema/features-no-list.out


+ 2 - 0
tests/qapi-schema/features-unknown-key.err

@@ -0,0 +1,2 @@
+tests/qapi-schema/features-unknown-key.json:1: Unknown key 'colour' in feature of struct FeatureStruct0
+Valid keys are 'if', 'name'.

+ 1 - 0
tests/qapi-schema/features-unknown-key.exit

@@ -0,0 +1 @@
+1

+ 3 - 0
tests/qapi-schema/features-unknown-key.json

@@ -0,0 +1,3 @@
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': 'bar', 'colour': 'red' } ] }

+ 0 - 0
tests/qapi-schema/features-unknown-key.out


+ 39 - 0
tests/qapi-schema/qapi-schema-test.json

@@ -242,3 +242,42 @@
   { 'foo': 'TestIfStruct',
   { 'foo': 'TestIfStruct',
     'bar': { 'type': ['TestIfEnum'], 'if': 'defined(TEST_IF_EVT_BAR)' } },
     'bar': { 'type': ['TestIfEnum'], 'if': 'defined(TEST_IF_EVT_BAR)' } },
   'if': 'defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)' }
   'if': 'defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)' }
+
+# test 'features' for structs
+
+{ 'struct': 'FeatureStruct0',
+  'data': { 'foo': 'int' },
+  'features': [] }
+{ 'struct': 'FeatureStruct1',
+  'data': { 'foo': 'int' },
+  'features': [ 'feature1' ] }
+{ 'struct': 'FeatureStruct2',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': 'feature1' } ] }
+{ 'struct': 'FeatureStruct3',
+  'data': { 'foo': 'int' },
+  'features': [ 'feature1', 'feature2' ] }
+{ 'struct': 'FeatureStruct4',
+  'data': { 'namespace-test': 'int' },
+  'features': [ 'namespace-test', 'int', 'name', 'if' ] }
+
+{ 'struct': 'CondFeatureStruct1',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': 'feature1', 'if': 'defined(TEST_IF_FEATURE_1)'} ] }
+{ 'struct': 'CondFeatureStruct2',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': 'feature1', 'if': 'defined(TEST_IF_FEATURE_1)'},
+                { 'name': 'feature2', 'if': 'defined(TEST_IF_FEATURE_2)'} ] }
+{ 'struct': 'CondFeatureStruct3',
+  'data': { 'foo': 'int' },
+  'features': [ { 'name': 'feature1', 'if': [ 'defined(TEST_IF_COND_1)',
+                                              'defined(TEST_IF_COND_2)'] } ] }
+{ 'command': 'test-features',
+  'data': { 'fs0': 'FeatureStruct0',
+            'fs1': 'FeatureStruct1',
+            'fs2': 'FeatureStruct2',
+            'fs3': 'FeatureStruct3',
+            'fs4': 'FeatureStruct4',
+            'cfs1': 'CondFeatureStruct1',
+            'cfs2': 'CondFeatureStruct2',
+            'cfs3': 'CondFeatureStruct3' } }

+ 43 - 0
tests/qapi-schema/qapi-schema-test.out

@@ -354,3 +354,46 @@ object q_obj_TestIfEvent-arg
 event TestIfEvent q_obj_TestIfEvent-arg
 event TestIfEvent q_obj_TestIfEvent-arg
    boxed=False
    boxed=False
     if ['defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)']
     if ['defined(TEST_IF_EVT) && defined(TEST_IF_STRUCT)']
+object FeatureStruct0
+    member foo: int optional=False
+object FeatureStruct1
+    member foo: int optional=False
+    feature feature1
+object FeatureStruct2
+    member foo: int optional=False
+    feature feature1
+object FeatureStruct3
+    member foo: int optional=False
+    feature feature1
+    feature feature2
+object FeatureStruct4
+    member namespace-test: int optional=False
+    feature namespace-test
+    feature int
+    feature name
+    feature if
+object CondFeatureStruct1
+    member foo: int optional=False
+    feature feature1
+        if ['defined(TEST_IF_FEATURE_1)']
+object CondFeatureStruct2
+    member foo: int optional=False
+    feature feature1
+        if ['defined(TEST_IF_FEATURE_1)']
+    feature feature2
+        if ['defined(TEST_IF_FEATURE_2)']
+object CondFeatureStruct3
+    member foo: int optional=False
+    feature feature1
+        if ['defined(TEST_IF_COND_1)', 'defined(TEST_IF_COND_2)']
+object q_obj_test-features-arg
+    member fs0: FeatureStruct0 optional=False
+    member fs1: FeatureStruct1 optional=False
+    member fs2: FeatureStruct2 optional=False
+    member fs3: FeatureStruct3 optional=False
+    member fs4: FeatureStruct4 optional=False
+    member cfs1: CondFeatureStruct1 optional=False
+    member cfs2: CondFeatureStruct2 optional=False
+    member cfs3: CondFeatureStruct3 optional=False
+command test-features q_obj_test-features-arg -> None
+   gen=True success_response=True boxed=False oob=False preconfig=False

+ 6 - 1
tests/qapi-schema/test-qapi.py

@@ -38,7 +38,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
         print('array %s %s' % (name, element_type.name))
         print('array %s %s' % (name, element_type.name))
         self._print_if(ifcond)
         self._print_if(ifcond)
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         print('object %s' % name)
         print('object %s' % name)
         if base:
         if base:
             print('    base %s' % base.name)
             print('    base %s' % base.name)
@@ -48,6 +49,10 @@ def visit_object_type(self, name, info, ifcond, base, members, variants):
             self._print_if(m.ifcond, 8)
             self._print_if(m.ifcond, 8)
         self._print_variants(variants)
         self._print_variants(variants)
         self._print_if(ifcond)
         self._print_if(ifcond)
+        if features:
+            for f in features:
+                print('    feature %s' % f.name)
+                self._print_if(f.ifcond, 8)
 
 
     def visit_alternate_type(self, name, info, ifcond, variants):
     def visit_alternate_type(self, name, info, ifcond, variants):
         print('alternate %s' % name)
         print('alternate %s' % name)

+ 1 - 1
tests/qapi-schema/unknown-expr-key.err

@@ -1,2 +1,2 @@
 tests/qapi-schema/unknown-expr-key.json:2: Unknown keys 'bogus', 'phony' in struct 'bar'
 tests/qapi-schema/unknown-expr-key.json:2: Unknown keys 'bogus', 'phony' in struct 'bar'
-Valid keys are 'base', 'data', 'if', 'struct'.
+Valid keys are 'base', 'data', 'features', 'if', 'struct'.

+ 8 - 0
tests/test-qmp-cmds.c

@@ -43,6 +43,14 @@ void qmp_user_def_cmd1(UserDefOne * ud1, Error **errp)
 {
 {
 }
 }
 
 
+void qmp_test_features(FeatureStruct0 *fs0, FeatureStruct1 *fs1,
+                       FeatureStruct2 *fs2, FeatureStruct3 *fs3,
+                       FeatureStruct4 *fs4, CondFeatureStruct1 *cfs1,
+                       CondFeatureStruct2 *cfs2, CondFeatureStruct3 *cfs3,
+                       Error **errp)
+{
+}
+
 UserDefTwo *qmp_user_def_cmd2(UserDefOne *ud1a,
 UserDefTwo *qmp_user_def_cmd2(UserDefOne *ud1a,
                               bool has_udb1, UserDefOne *ud1b,
                               bool has_udb1, UserDefOne *ud1b,
                               Error **errp)
                               Error **errp)