diff mbox

[v4,08/19] qapi: Better error messages for bad expressions

Message ID 1411165504-18198-9-git-send-email-eblake@redhat.com
State New
Headers show

Commit Message

Eric Blake Sept. 19, 2014, 10:24 p.m. UTC
The previous commit demonstrated that the generator overlooked some
fairly basic broken expressions:
- missing metataype
- metatype key has a non-string value
- unknown key in relation to the metatype
- conflicting metatype (this patch treats the second metatype as an
unknown key of the first key visited, which is not necessarily the
first key the user typed)

Add check_keys to cover these situations, and update testcases to
match.  A couple other tests (enum-missing-data, indented-expr) had
to change since the validation added here occurs so early.

While valid .json files won't trigger any of these cases, we might
as well be nicer to developers that make a typo while trying to add
new QAPI code.

Signed-off-by: Eric Blake <eblake@redhat.com>
---
 scripts/qapi.py                         | 76 +++++++++++++++++++++++++--------
 tests/qapi-schema/bad-type-dict.err     |  1 +
 tests/qapi-schema/bad-type-dict.exit    |  2 +-
 tests/qapi-schema/bad-type-dict.json    |  2 +-
 tests/qapi-schema/bad-type-dict.out     |  3 --
 tests/qapi-schema/double-type.err       |  1 +
 tests/qapi-schema/double-type.exit      |  2 +-
 tests/qapi-schema/double-type.json      |  2 +-
 tests/qapi-schema/double-type.out       |  3 --
 tests/qapi-schema/enum-missing-data.err |  2 +-
 tests/qapi-schema/indented-expr.json    |  4 +-
 tests/qapi-schema/indented-expr.out     |  2 +-
 tests/qapi-schema/missing-type.err      |  1 +
 tests/qapi-schema/missing-type.exit     |  2 +-
 tests/qapi-schema/missing-type.json     |  2 +-
 tests/qapi-schema/missing-type.out      |  3 --
 tests/qapi-schema/unknown-expr-key.err  |  1 +
 tests/qapi-schema/unknown-expr-key.exit |  2 +-
 tests/qapi-schema/unknown-expr-key.json |  2 +-
 tests/qapi-schema/unknown-expr-key.out  |  3 --
 20 files changed, 75 insertions(+), 41 deletions(-)

Comments

Markus Armbruster Sept. 23, 2014, 2:56 p.m. UTC | #1
Eric Blake <eblake@redhat.com> writes:

> The previous commit demonstrated that the generator overlooked some
> fairly basic broken expressions:
> - missing metataype
> - metatype key has a non-string value
> - unknown key in relation to the metatype
> - conflicting metatype (this patch treats the second metatype as an
> unknown key of the first key visited, which is not necessarily the
> first key the user typed)

The whole thing is a Saturday afternoon hack, with extra hacks bolted on
left & right.

> Add check_keys to cover these situations, and update testcases to
> match.  A couple other tests (enum-missing-data, indented-expr) had
> to change since the validation added here occurs so early.
>
> While valid .json files won't trigger any of these cases, we might
> as well be nicer to developers that make a typo while trying to add
> new QAPI code.
>
> Signed-off-by: Eric Blake <eblake@redhat.com>

> ---
>  scripts/qapi.py                         | 76 +++++++++++++++++++++++++--------
>  tests/qapi-schema/bad-type-dict.err     |  1 +
>  tests/qapi-schema/bad-type-dict.exit    |  2 +-
>  tests/qapi-schema/bad-type-dict.json    |  2 +-
>  tests/qapi-schema/bad-type-dict.out     |  3 --
>  tests/qapi-schema/double-type.err       |  1 +
>  tests/qapi-schema/double-type.exit      |  2 +-
>  tests/qapi-schema/double-type.json      |  2 +-
>  tests/qapi-schema/double-type.out       |  3 --
>  tests/qapi-schema/enum-missing-data.err |  2 +-
>  tests/qapi-schema/indented-expr.json    |  4 +-
>  tests/qapi-schema/indented-expr.out     |  2 +-
>  tests/qapi-schema/missing-type.err      |  1 +
>  tests/qapi-schema/missing-type.exit     |  2 +-
>  tests/qapi-schema/missing-type.json     |  2 +-
>  tests/qapi-schema/missing-type.out      |  3 --
>  tests/qapi-schema/unknown-expr-key.err  |  1 +
>  tests/qapi-schema/unknown-expr-key.exit |  2 +-
>  tests/qapi-schema/unknown-expr-key.json |  2 +-
>  tests/qapi-schema/unknown-expr-key.out  |  3 --
>  20 files changed, 75 insertions(+), 41 deletions(-)
>
> diff --git a/scripts/qapi.py b/scripts/qapi.py
> index 85aa8bf..8fbc45f 100644
> --- a/scripts/qapi.py
> +++ b/scripts/qapi.py
> @@ -348,7 +348,29 @@ def check_exprs(schema):
>          if expr.has_key('enum'):
>              check_enum(expr, info)
>
> +def check_keys(expr_elem, meta, required, optional=[]):
> +    expr = expr_elem['expr']
> +    info = expr_elem['info']
> +    name = expr[meta]

Caller ensures expr[meta] exists.  Okay.

> +    if not isinstance(name, basestring):

I'm a Python noob: why basestring and not str?

> +        raise QAPIExprError(info,
> +                            "%s key must have a string value" % meta)
> +    expr_elem['name'] = name

Where is this used?

> +    required.append(meta)

Ugly side effect.  To avoid, either make a new list here

    required = required + [ meta ]

or do nothing here and...

> +    for (key, value) in expr.items():
> +        if not key in required and not key in optional:

... add "and key != meta" to this condition.

> +            raise QAPIExprError(info,
> +                                "%s '%s' has unknown key '%s'"
> +                                % (meta, name, key))
> +    for key in required:
> +        if not expr.has_key(key):
> +            raise QAPIExprError(info,
> +                                "%s '%s' is missing key '%s'"
> +                                % (meta, name, key))
> +
> +
>  def parse_schema(input_file):
> +    # First pass: read entire file into memory
>      try:
>          schema = QAPISchema(open(input_file, "r"))
>      except (QAPISchemaError, QAPIExprError), e:
> @@ -357,24 +379,44 @@ def parse_schema(input_file):
>
>      exprs = []
>
> -    for expr_elem in schema.exprs:
> -        expr = expr_elem['expr']
> -        if expr.has_key('enum'):
> -            add_enum(expr['enum'], expr.get('data'))
> -        elif expr.has_key('union'):
> -            add_union(expr)
> -        elif expr.has_key('type'):
> -            add_struct(expr)
> -        exprs.append(expr)
> -
> -    # Try again for hidden UnionKind enum
> -    for expr_elem in schema.exprs:
> -        expr = expr_elem['expr']
> -        if expr.has_key('union'):
> -            if not discriminator_find_enum_define(expr):
> -                add_enum('%sKind' % expr['union'])
> -
>      try:
> +        # Next pass: learn the types and check for valid expression keys. At
> +        # this point, top-level 'include' has already been flattened.
> +        for expr_elem in schema.exprs:
> +            expr = expr_elem['expr']
> +            if expr.has_key('enum'):
> +                check_keys(expr_elem, 'enum', ['data'])
> +                add_enum(expr['enum'], expr['data'])
> +            elif expr.has_key('union'):
> +                # Two styles of union, based on discriminator
> +                discriminator = expr.get('discriminator')
> +                if discriminator == {}:
> +                    check_keys(expr_elem, 'union', ['data', 'discriminator'])
> +                else:
> +                    check_keys(expr_elem, 'union', ['data'],
> +                               ['base', 'discriminator'])
> +                add_union(expr)
> +            elif expr.has_key('type'):
> +                check_keys(expr_elem, 'type', ['data'], ['base'])
> +                add_struct(expr)
> +            elif expr.has_key('command'):
> +                check_keys(expr_elem, 'command', [],
> +                           ['data', 'returns', 'gen', 'success-response'])
> +            elif expr.has_key('event'):
> +                check_keys(expr_elem, 'event', [], ['data'])
> +            else:
> +                raise QAPIExprError(expr_elem['info'],
> +                                    "expression is missing metatype")
> +            exprs.append(expr)
> +
> +        # Try again for hidden UnionKind enum
> +        for expr_elem in schema.exprs:
> +            expr = expr_elem['expr']
> +            if expr.has_key('union'):
> +                if not discriminator_find_enum_define(expr):
> +                    add_enum('%sKind' % expr['union'])
> +
> +        # Final pass - validate that exprs make sense
>          check_exprs(schema)
>      except QAPIExprError, e:
>          print >>sys.stderr, e

This hunk is easier to review with whitespace ignored:

  @@ -356,13 +356,34 @@

       exprs = []
  -    for expr_elem in schema.exprs:
  +    try:
  +        # Next pass: learn the types and check for valid expression keys. At
  +        # this point, top-level 'include' has already been flattened.
  +        for expr_elem in schema.exprs:
           expr = expr_elem['expr']
           if expr.has_key('enum'):
  -            add_enum(expr['enum'], expr.get('data'))
  +                check_keys(expr_elem, 'enum', ['data'])
  +                add_enum(expr['enum'], expr['data'])
           elif expr.has_key('union'):
  +                # Two styles of union, based on discriminator
  +                discriminator = expr.get('discriminator')
  +                if discriminator == {}:
  +                    check_keys(expr_elem, 'union', ['data', 'discriminator'])
  +                else:
  +                    check_keys(expr_elem, 'union', ['data'],
  +                               ['base', 'discriminator'])
               add_union(expr)
           elif expr.has_key('type'):
  +                check_keys(expr_elem, 'type', ['data'], ['base'])
               add_struct(expr)
  +            elif expr.has_key('command'):
  +                check_keys(expr_elem, 'command', [],
  +                           ['data', 'returns', 'gen', 'success-response'])
  +            elif expr.has_key('event'):
  +                check_keys(expr_elem, 'event', [], ['data'])
  +            else:
  +                raise QAPIExprError(expr_elem['info'],
  +                                    "expression is missing metatype")
           exprs.append(expr)

       # Try again for hidden UnionKind enum
  @@ -372,7 +393,7 @@
               if not discriminator_find_enum_define(expr):
                   add_enum('%sKind' % expr['union'])

  -    try:
  +        # Final pass - validate that exprs make sense
           check_exprs(schema)
       except QAPIExprError, e:
           print >>sys.stderr, e

Looks good to me.  The tests, too.

[...]
Eric Blake Sept. 23, 2014, 4:11 p.m. UTC | #2
On 09/23/2014 08:56 AM, Markus Armbruster wrote:
> Eric Blake <eblake@redhat.com> writes:
> 
>> The previous commit demonstrated that the generator overlooked some
>> fairly basic broken expressions:
>> - missing metataype
>> - metatype key has a non-string value
>> - unknown key in relation to the metatype
>> - conflicting metatype (this patch treats the second metatype as an
>> unknown key of the first key visited, which is not necessarily the
>> first key the user typed)
> 
> The whole thing is a Saturday afternoon hack, with extra hacks bolted on
> left & right.

And this series is a sequence of MY Saturday afternoon hacks in cleaning
it up :)

> 
>> Add check_keys to cover these situations, and update testcases to
>> match.  A couple other tests (enum-missing-data, indented-expr) had
>> to change since the validation added here occurs so early.
>>
>> While valid .json files won't trigger any of these cases, we might
>> as well be nicer to developers that make a typo while trying to add
>> new QAPI code.
>>
>> Signed-off-by: Eric Blake <eblake@redhat.com>
> 

>> +def check_keys(expr_elem, meta, required, optional=[]):
>> +    expr = expr_elem['expr']
>> +    info = expr_elem['info']
>> +    name = expr[meta]
> 
> Caller ensures expr[meta] exists.  Okay.
> 
>> +    if not isinstance(name, basestring):
> 
> I'm a Python noob: why basestring and not str?

Me too.  No clue.  Copy and paste from existing code.
http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361

> 
>> +        raise QAPIExprError(info,
>> +                            "%s key must have a string value" % meta)
>> +    expr_elem['name'] = name
> 
> Where is this used?

Hmm, I know I used it at one point in my series (to be able to print the
name of an expression in check_type added in 13/19, without having to
repeat the dance of if enum: expr['enum'] elif union: expr['union'] etc.
in multiple places).  Although in my refactoring, I may have eliminated
the need for it after all.  If it's not in use at the end of the series,
I can drop it.

> 
>> +    required.append(meta)
> 
> Ugly side effect.  To avoid, either make a new list here
> 
>     required = required + [ meta ]
> 
> or do nothing here and...
> 
>> +    for (key, value) in expr.items():
>> +        if not key in required and not key in optional:
> 
> ... add "and key != meta" to this condition.

Shows my inexperience in python.  Sure, I can fix.  Looks like I get to
spin a v5.

> 
> This hunk is easier to review with whitespace ignored:

Indeed. Alas, python is a stickler about whitespace reindentation.
Since I have to respin, I'll probably split into two pieces (one with a
no-op change that reindents, the other for the improvements).
Markus Armbruster Sept. 24, 2014, 7:34 a.m. UTC | #3
Eric Blake <eblake@redhat.com> writes:

> On 09/23/2014 08:56 AM, Markus Armbruster wrote:
>> Eric Blake <eblake@redhat.com> writes:
>> 
>>> The previous commit demonstrated that the generator overlooked some
>>> fairly basic broken expressions:
>>> - missing metataype
>>> - metatype key has a non-string value
>>> - unknown key in relation to the metatype
>>> - conflicting metatype (this patch treats the second metatype as an
>>> unknown key of the first key visited, which is not necessarily the
>>> first key the user typed)
>> 
>> The whole thing is a Saturday afternoon hack, with extra hacks bolted on
>> left & right.
>
> And this series is a sequence of MY Saturday afternoon hacks in cleaning
> it up :)

Much appreciated!

>>> Add check_keys to cover these situations, and update testcases to
>>> match.  A couple other tests (enum-missing-data, indented-expr) had
>>> to change since the validation added here occurs so early.
>>>
>>> While valid .json files won't trigger any of these cases, we might
>>> as well be nicer to developers that make a typo while trying to add
>>> new QAPI code.
>>>
>>> Signed-off-by: Eric Blake <eblake@redhat.com>
>> 
>
>>> +def check_keys(expr_elem, meta, required, optional=[]):
>>> +    expr = expr_elem['expr']
>>> +    info = expr_elem['info']
>>> +    name = expr[meta]
>> 
>> Caller ensures expr[meta] exists.  Okay.
>> 
>>> +    if not isinstance(name, basestring):
>> 
>> I'm a Python noob: why basestring and not str?
>
> Me too.  No clue.  Copy and paste from existing code.
> http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361

Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
str).

The basestring use comes from Kevin's commit b35284e.  Kevin, why
basestring and not str?

>> 
>>> +        raise QAPIExprError(info,
>>> +                            "%s key must have a string value" % meta)
>>> +    expr_elem['name'] = name
>> 
>> Where is this used?
>
> Hmm, I know I used it at one point in my series (to be able to print the
> name of an expression in check_type added in 13/19, without having to
> repeat the dance of if enum: expr['enum'] elif union: expr['union'] etc.
> in multiple places).  Although in my refactoring, I may have eliminated
> the need for it after all.  If it's not in use at the end of the series,
> I can drop it.

A quick grep comes up empty.

>>> +    required.append(meta)
>> 
>> Ugly side effect.  To avoid, either make a new list here
>> 
>>     required = required + [ meta ]
>> 
>> or do nothing here and...
>> 
>>> +    for (key, value) in expr.items():
>>> +        if not key in required and not key in optional:
>> 
>> ... add "and key != meta" to this condition.
>
> Shows my inexperience in python.  Sure, I can fix.  Looks like I get to
> spin a v5.
>
>> 
>> This hunk is easier to review with whitespace ignored:
>
> Indeed. Alas, python is a stickler about whitespace reindentation.
> Since I have to respin, I'll probably split into two pieces (one with a
> no-op change that reindents, the other for the improvements).

Not necessary for me, I'm comfy with telling Emacs to hide whitespace
differences :)
Kevin Wolf Sept. 24, 2014, 9:25 a.m. UTC | #4
Am 24.09.2014 um 09:34 hat Markus Armbruster geschrieben:
> Eric Blake <eblake@redhat.com> writes:
> 
> > On 09/23/2014 08:56 AM, Markus Armbruster wrote:
> >> Eric Blake <eblake@redhat.com> writes:
> >>> Add check_keys to cover these situations, and update testcases to
> >>> match.  A couple other tests (enum-missing-data, indented-expr) had
> >>> to change since the validation added here occurs so early.
> >>>
> >>> While valid .json files won't trigger any of these cases, we might
> >>> as well be nicer to developers that make a typo while trying to add
> >>> new QAPI code.
> >>>
> >>> Signed-off-by: Eric Blake <eblake@redhat.com>
> >> 
> >
> >>> +def check_keys(expr_elem, meta, required, optional=[]):
> >>> +    expr = expr_elem['expr']
> >>> +    info = expr_elem['info']
> >>> +    name = expr[meta]
> >> 
> >> Caller ensures expr[meta] exists.  Okay.
> >> 
> >>> +    if not isinstance(name, basestring):
> >> 
> >> I'm a Python noob: why basestring and not str?
> >
> > Me too.  No clue.  Copy and paste from existing code.
> > http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361
> 
> Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
> str).
> 
> The basestring use comes from Kevin's commit b35284e.  Kevin, why
> basestring and not str?

Do I look as if I knew what I'm doing when I write Python code? :-)

Apparently basestring is the superclass for ASCII and Unicode strings. I
seem to dimly remember that I did indeed get a Unicode string somewhere
(even though probably no non-ASCII characters in it) and it caused
trouble. Might well have been here.

Kevin
Markus Armbruster Sept. 24, 2014, 11:14 a.m. UTC | #5
Kevin Wolf <kwolf@redhat.com> writes:

> Am 24.09.2014 um 09:34 hat Markus Armbruster geschrieben:
>> Eric Blake <eblake@redhat.com> writes:
>> 
>> > On 09/23/2014 08:56 AM, Markus Armbruster wrote:
>> >> Eric Blake <eblake@redhat.com> writes:
>> >>> Add check_keys to cover these situations, and update testcases to
>> >>> match.  A couple other tests (enum-missing-data, indented-expr) had
>> >>> to change since the validation added here occurs so early.
>> >>>
>> >>> While valid .json files won't trigger any of these cases, we might
>> >>> as well be nicer to developers that make a typo while trying to add
>> >>> new QAPI code.
>> >>>
>> >>> Signed-off-by: Eric Blake <eblake@redhat.com>
>> >> 
>> >
>> >>> +def check_keys(expr_elem, meta, required, optional=[]):
>> >>> +    expr = expr_elem['expr']
>> >>> +    info = expr_elem['info']
>> >>> +    name = expr[meta]
>> >> 
>> >> Caller ensures expr[meta] exists.  Okay.
>> >> 
>> >>> +    if not isinstance(name, basestring):
>> >> 
>> >> I'm a Python noob: why basestring and not str?
>> >
>> > Me too.  No clue.  Copy and paste from existing code.
>> > http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361
>> 
>> Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
>> str).
>> 
>> The basestring use comes from Kevin's commit b35284e.  Kevin, why
>> basestring and not str?
>
> Do I look as if I knew what I'm doing when I write Python code? :-)
>
> Apparently basestring is the superclass for ASCII and Unicode strings. I
> seem to dimly remember that I did indeed get a Unicode string somewhere
> (even though probably no non-ASCII characters in it) and it caused
> trouble. Might well have been here.

Aha.  These links apply:

https://stackoverflow.com/questions/11301138/how-to-check-if-variable-is-string-with-python-2-and-3-compatibility
https://en.wikipedia.org/wiki/File:Pieter_Bruegel_the_Elder_%281568%29_The_Blind_Leading_the_Blind.jpg
Markus Armbruster Sept. 26, 2014, 9:15 a.m. UTC | #6
Kevin Wolf <kwolf@redhat.com> writes:

> Am 24.09.2014 um 09:34 hat Markus Armbruster geschrieben:
>> Eric Blake <eblake@redhat.com> writes:
>> 
>> > On 09/23/2014 08:56 AM, Markus Armbruster wrote:
>> >> Eric Blake <eblake@redhat.com> writes:
>> >>> Add check_keys to cover these situations, and update testcases to
>> >>> match.  A couple other tests (enum-missing-data, indented-expr) had
>> >>> to change since the validation added here occurs so early.
>> >>>
>> >>> While valid .json files won't trigger any of these cases, we might
>> >>> as well be nicer to developers that make a typo while trying to add
>> >>> new QAPI code.
>> >>>
>> >>> Signed-off-by: Eric Blake <eblake@redhat.com>
>> >> 
>> >
>> >>> +def check_keys(expr_elem, meta, required, optional=[]):
>> >>> +    expr = expr_elem['expr']
>> >>> +    info = expr_elem['info']
>> >>> +    name = expr[meta]
>> >> 
>> >> Caller ensures expr[meta] exists.  Okay.
>> >> 
>> >>> +    if not isinstance(name, basestring):
>> >> 
>> >> I'm a Python noob: why basestring and not str?
>> >
>> > Me too.  No clue.  Copy and paste from existing code.
>> > http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361
>> 
>> Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
>> str).
>> 
>> The basestring use comes from Kevin's commit b35284e.  Kevin, why
>> basestring and not str?
>
> Do I look as if I knew what I'm doing when I write Python code? :-)
>
> Apparently basestring is the superclass for ASCII and Unicode strings. I
> seem to dimly remember that I did indeed get a Unicode string somewhere
> (even though probably no non-ASCII characters in it) and it caused
> trouble. Might well have been here.

I think there are two sane ways forward:

1. Declare QAPI schemas to be ASCII only for now.  Replace the one
instance of basestring by plain str.

2. Fix the code to work with both ASCII and Unicode strings even in
Python 2.  Package six could be used.

I'm voting for 1.
Kevin Wolf Sept. 26, 2014, 9:25 a.m. UTC | #7
Am 26.09.2014 um 11:15 hat Markus Armbruster geschrieben:
> Kevin Wolf <kwolf@redhat.com> writes:
> 
> > Am 24.09.2014 um 09:34 hat Markus Armbruster geschrieben:
> >> Eric Blake <eblake@redhat.com> writes:
> >> 
> >> > On 09/23/2014 08:56 AM, Markus Armbruster wrote:
> >> >> Eric Blake <eblake@redhat.com> writes:
> >> >>> Add check_keys to cover these situations, and update testcases to
> >> >>> match.  A couple other tests (enum-missing-data, indented-expr) had
> >> >>> to change since the validation added here occurs so early.
> >> >>>
> >> >>> While valid .json files won't trigger any of these cases, we might
> >> >>> as well be nicer to developers that make a typo while trying to add
> >> >>> new QAPI code.
> >> >>>
> >> >>> Signed-off-by: Eric Blake <eblake@redhat.com>
> >> >> 
> >> >
> >> >>> +def check_keys(expr_elem, meta, required, optional=[]):
> >> >>> +    expr = expr_elem['expr']
> >> >>> +    info = expr_elem['info']
> >> >>> +    name = expr[meta]
> >> >> 
> >> >> Caller ensures expr[meta] exists.  Okay.
> >> >> 
> >> >>> +    if not isinstance(name, basestring):
> >> >> 
> >> >> I'm a Python noob: why basestring and not str?
> >> >
> >> > Me too.  No clue.  Copy and paste from existing code.
> >> > http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361
> >> 
> >> Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
> >> str).
> >> 
> >> The basestring use comes from Kevin's commit b35284e.  Kevin, why
> >> basestring and not str?
> >
> > Do I look as if I knew what I'm doing when I write Python code? :-)
> >
> > Apparently basestring is the superclass for ASCII and Unicode strings. I
> > seem to dimly remember that I did indeed get a Unicode string somewhere
> > (even though probably no non-ASCII characters in it) and it caused
> > trouble. Might well have been here.
> 
> I think there are two sane ways forward:
> 
> 1. Declare QAPI schemas to be ASCII only for now.  Replace the one
> instance of basestring by plain str.
> 
> 2. Fix the code to work with both ASCII and Unicode strings even in
> Python 2.  Package six could be used.
> 
> I'm voting for 1.

As I said, the problem is probably not about the actual value of the
string (I doubt we use anything but ASCII at the moment), but about its
type. We may need to convert something somewhere. If you think it's
worth the effort of finding that place, go ahead.

Kevin
Markus Armbruster Sept. 26, 2014, 11:40 a.m. UTC | #8
Kevin Wolf <kwolf@redhat.com> writes:

> Am 26.09.2014 um 11:15 hat Markus Armbruster geschrieben:
>> Kevin Wolf <kwolf@redhat.com> writes:
>> 
>> > Am 24.09.2014 um 09:34 hat Markus Armbruster geschrieben:
>> >> Eric Blake <eblake@redhat.com> writes:
>> >> 
>> >> > On 09/23/2014 08:56 AM, Markus Armbruster wrote:
>> >> >> Eric Blake <eblake@redhat.com> writes:
>> >> >>> Add check_keys to cover these situations, and update testcases to
>> >> >>> match.  A couple other tests (enum-missing-data, indented-expr) had
>> >> >>> to change since the validation added here occurs so early.
>> >> >>>
>> >> >>> While valid .json files won't trigger any of these cases, we might
>> >> >>> as well be nicer to developers that make a typo while trying to add
>> >> >>> new QAPI code.
>> >> >>>
>> >> >>> Signed-off-by: Eric Blake <eblake@redhat.com>
>> >> >> 
>> >> >
>> >> >>> +def check_keys(expr_elem, meta, required, optional=[]):
>> >> >>> +    expr = expr_elem['expr']
>> >> >>> +    info = expr_elem['info']
>> >> >>> +    name = expr[meta]
>> >> >> 
>> >> >> Caller ensures expr[meta] exists.  Okay.
>> >> >> 
>> >> >>> +    if not isinstance(name, basestring):
>> >> >> 
>> >> >> I'm a Python noob: why basestring and not str?
>> >> >
>> >> > Me too.  No clue.  Copy and paste from existing code.
>> >> > http://git.qemu.org/?p=qemu.git;a=blob;f=scripts/qapi.py;h=77d46aa;hb=769188d3b#l361
>> >> 
>> >> Yes.  It's our only use of basestring.  Other places use isinstance(FOO,
>> >> str).
>> >> 
>> >> The basestring use comes from Kevin's commit b35284e.  Kevin, why
>> >> basestring and not str?
>> >
>> > Do I look as if I knew what I'm doing when I write Python code? :-)
>> >
>> > Apparently basestring is the superclass for ASCII and Unicode strings. I
>> > seem to dimly remember that I did indeed get a Unicode string somewhere
>> > (even though probably no non-ASCII characters in it) and it caused
>> > trouble. Might well have been here.
>> 
>> I think there are two sane ways forward:
>> 
>> 1. Declare QAPI schemas to be ASCII only for now.  Replace the one
>> instance of basestring by plain str.
>> 
>> 2. Fix the code to work with both ASCII and Unicode strings even in
>> Python 2.  Package six could be used.
>> 
>> I'm voting for 1.
>
> As I said, the problem is probably not about the actual value of the
> string (I doubt we use anything but ASCII at the moment), but about its
> type. We may need to convert something somewhere. If you think it's
> worth the effort of finding that place, go ahead.

A value always has a concrete type.  I'd expect all string values in the
qapi scripts to be of type str.

We have to mess with Python 2's basestring only where we need to cope
with string values that may be either of type str or of type unicode.
Since there should not be any values of type unicode, I don't see why
we'd want to do the extra work required for 2.
diff mbox

Patch

diff --git a/scripts/qapi.py b/scripts/qapi.py
index 85aa8bf..8fbc45f 100644
--- a/scripts/qapi.py
+++ b/scripts/qapi.py
@@ -348,7 +348,29 @@  def check_exprs(schema):
         if expr.has_key('enum'):
             check_enum(expr, info)

+def check_keys(expr_elem, meta, required, optional=[]):
+    expr = expr_elem['expr']
+    info = expr_elem['info']
+    name = expr[meta]
+    if not isinstance(name, basestring):
+        raise QAPIExprError(info,
+                            "%s key must have a string value" % meta)
+    expr_elem['name'] = name
+    required.append(meta)
+    for (key, value) in expr.items():
+        if not key in required and not key in optional:
+            raise QAPIExprError(info,
+                                "%s '%s' has unknown key '%s'"
+                                % (meta, name, key))
+    for key in required:
+        if not expr.has_key(key):
+            raise QAPIExprError(info,
+                                "%s '%s' is missing key '%s'"
+                                % (meta, name, key))
+
+
 def parse_schema(input_file):
+    # First pass: read entire file into memory
     try:
         schema = QAPISchema(open(input_file, "r"))
     except (QAPISchemaError, QAPIExprError), e:
@@ -357,24 +379,44 @@  def parse_schema(input_file):

     exprs = []

-    for expr_elem in schema.exprs:
-        expr = expr_elem['expr']
-        if expr.has_key('enum'):
-            add_enum(expr['enum'], expr.get('data'))
-        elif expr.has_key('union'):
-            add_union(expr)
-        elif expr.has_key('type'):
-            add_struct(expr)
-        exprs.append(expr)
-
-    # Try again for hidden UnionKind enum
-    for expr_elem in schema.exprs:
-        expr = expr_elem['expr']
-        if expr.has_key('union'):
-            if not discriminator_find_enum_define(expr):
-                add_enum('%sKind' % expr['union'])
-
     try:
+        # Next pass: learn the types and check for valid expression keys. At
+        # this point, top-level 'include' has already been flattened.
+        for expr_elem in schema.exprs:
+            expr = expr_elem['expr']
+            if expr.has_key('enum'):
+                check_keys(expr_elem, 'enum', ['data'])
+                add_enum(expr['enum'], expr['data'])
+            elif expr.has_key('union'):
+                # Two styles of union, based on discriminator
+                discriminator = expr.get('discriminator')
+                if discriminator == {}:
+                    check_keys(expr_elem, 'union', ['data', 'discriminator'])
+                else:
+                    check_keys(expr_elem, 'union', ['data'],
+                               ['base', 'discriminator'])
+                add_union(expr)
+            elif expr.has_key('type'):
+                check_keys(expr_elem, 'type', ['data'], ['base'])
+                add_struct(expr)
+            elif expr.has_key('command'):
+                check_keys(expr_elem, 'command', [],
+                           ['data', 'returns', 'gen', 'success-response'])
+            elif expr.has_key('event'):
+                check_keys(expr_elem, 'event', [], ['data'])
+            else:
+                raise QAPIExprError(expr_elem['info'],
+                                    "expression is missing metatype")
+            exprs.append(expr)
+
+        # Try again for hidden UnionKind enum
+        for expr_elem in schema.exprs:
+            expr = expr_elem['expr']
+            if expr.has_key('union'):
+                if not discriminator_find_enum_define(expr):
+                    add_enum('%sKind' % expr['union'])
+
+        # Final pass - validate that exprs make sense
         check_exprs(schema)
     except QAPIExprError, e:
         print >>sys.stderr, e
diff --git a/tests/qapi-schema/bad-type-dict.err b/tests/qapi-schema/bad-type-dict.err
index e69de29..b7c73cc 100644
--- a/tests/qapi-schema/bad-type-dict.err
+++ b/tests/qapi-schema/bad-type-dict.err
@@ -0,0 +1 @@ 
+tests/qapi-schema/bad-type-dict.json:2: command key must have a string value
diff --git a/tests/qapi-schema/bad-type-dict.exit b/tests/qapi-schema/bad-type-dict.exit
index 573541a..d00491f 100644
--- a/tests/qapi-schema/bad-type-dict.exit
+++ b/tests/qapi-schema/bad-type-dict.exit
@@ -1 +1 @@ 
-0
+1
diff --git a/tests/qapi-schema/bad-type-dict.json b/tests/qapi-schema/bad-type-dict.json
index 3c392a7..2a91b24 100644
--- a/tests/qapi-schema/bad-type-dict.json
+++ b/tests/qapi-schema/bad-type-dict.json
@@ -1,2 +1,2 @@ 
-# FIXME: we should reject an expression with a metatype that is not a string
+# we reject an expression with a metatype that is not a string
 { 'command': { } }
diff --git a/tests/qapi-schema/bad-type-dict.out b/tests/qapi-schema/bad-type-dict.out
index c62f1ed..e69de29 100644
--- a/tests/qapi-schema/bad-type-dict.out
+++ b/tests/qapi-schema/bad-type-dict.out
@@ -1,3 +0,0 @@ 
-[OrderedDict([('command', OrderedDict())])]
-[]
-[]
diff --git a/tests/qapi-schema/double-type.err b/tests/qapi-schema/double-type.err
index e69de29..394127b 100644
--- a/tests/qapi-schema/double-type.err
+++ b/tests/qapi-schema/double-type.err
@@ -0,0 +1 @@ 
+tests/qapi-schema/double-type.json:2: type 'bar' has unknown key 'command'
diff --git a/tests/qapi-schema/double-type.exit b/tests/qapi-schema/double-type.exit
index 573541a..d00491f 100644
--- a/tests/qapi-schema/double-type.exit
+++ b/tests/qapi-schema/double-type.exit
@@ -1 +1 @@ 
-0
+1
diff --git a/tests/qapi-schema/double-type.json b/tests/qapi-schema/double-type.json
index 6ca96b9..471623a 100644
--- a/tests/qapi-schema/double-type.json
+++ b/tests/qapi-schema/double-type.json
@@ -1,2 +1,2 @@ 
-# FIXME: we should reject an expression with ambiguous metatype
+# we reject an expression with ambiguous metatype
 { 'command': 'foo', 'type': 'bar', 'data': { } }
diff --git a/tests/qapi-schema/double-type.out b/tests/qapi-schema/double-type.out
index 3e244f5..e69de29 100644
--- a/tests/qapi-schema/double-type.out
+++ b/tests/qapi-schema/double-type.out
@@ -1,3 +0,0 @@ 
-[OrderedDict([('command', 'foo'), ('type', 'bar'), ('data', OrderedDict())])]
-[]
-[OrderedDict([('command', 'foo'), ('type', 'bar'), ('data', OrderedDict())])]
diff --git a/tests/qapi-schema/enum-missing-data.err b/tests/qapi-schema/enum-missing-data.err
index 4b8b59e..62f4a7f 100644
--- a/tests/qapi-schema/enum-missing-data.err
+++ b/tests/qapi-schema/enum-missing-data.err
@@ -1 +1 @@ 
-tests/qapi-schema/enum-missing-data.json:2: enum 'MyEnum' requires an array for 'data'
+tests/qapi-schema/enum-missing-data.json:2: enum 'MyEnum' is missing key 'data'
diff --git a/tests/qapi-schema/indented-expr.json b/tests/qapi-schema/indented-expr.json
index d80af60..7115d31 100644
--- a/tests/qapi-schema/indented-expr.json
+++ b/tests/qapi-schema/indented-expr.json
@@ -1,2 +1,2 @@ 
-{ 'id' : 'eins' }
- { 'id' : 'zwei' }
+{ 'command' : 'eins' }
+ { 'command' : 'zwei' }
diff --git a/tests/qapi-schema/indented-expr.out b/tests/qapi-schema/indented-expr.out
index 98af89a..b5ce915 100644
--- a/tests/qapi-schema/indented-expr.out
+++ b/tests/qapi-schema/indented-expr.out
@@ -1,3 +1,3 @@ 
-[OrderedDict([('id', 'eins')]), OrderedDict([('id', 'zwei')])]
+[OrderedDict([('command', 'eins')]), OrderedDict([('command', 'zwei')])]
 []
 []
diff --git a/tests/qapi-schema/missing-type.err b/tests/qapi-schema/missing-type.err
index e69de29..19b7c49 100644
--- a/tests/qapi-schema/missing-type.err
+++ b/tests/qapi-schema/missing-type.err
@@ -0,0 +1 @@ 
+tests/qapi-schema/missing-type.json:2: expression is missing metatype
diff --git a/tests/qapi-schema/missing-type.exit b/tests/qapi-schema/missing-type.exit
index 573541a..d00491f 100644
--- a/tests/qapi-schema/missing-type.exit
+++ b/tests/qapi-schema/missing-type.exit
@@ -1 +1 @@ 
-0
+1
diff --git a/tests/qapi-schema/missing-type.json b/tests/qapi-schema/missing-type.json
index 1696f5c..ff5349d 100644
--- a/tests/qapi-schema/missing-type.json
+++ b/tests/qapi-schema/missing-type.json
@@ -1,2 +1,2 @@ 
-# FIXME: we should reject an expression with missing metatype
+# we reject an expression with missing metatype
 { 'data': { } }
diff --git a/tests/qapi-schema/missing-type.out b/tests/qapi-schema/missing-type.out
index 67fd4fa..e69de29 100644
--- a/tests/qapi-schema/missing-type.out
+++ b/tests/qapi-schema/missing-type.out
@@ -1,3 +0,0 @@ 
-[OrderedDict([('data', OrderedDict())])]
-[]
-[]
diff --git a/tests/qapi-schema/unknown-expr-key.err b/tests/qapi-schema/unknown-expr-key.err
index e69de29..3b35508 100644
--- a/tests/qapi-schema/unknown-expr-key.err
+++ b/tests/qapi-schema/unknown-expr-key.err
@@ -0,0 +1 @@ 
+tests/qapi-schema/unknown-expr-key.json:2: type 'bar' has unknown key 'bogus'
diff --git a/tests/qapi-schema/unknown-expr-key.exit b/tests/qapi-schema/unknown-expr-key.exit
index 573541a..d00491f 100644
--- a/tests/qapi-schema/unknown-expr-key.exit
+++ b/tests/qapi-schema/unknown-expr-key.exit
@@ -1 +1 @@ 
-0
+1
diff --git a/tests/qapi-schema/unknown-expr-key.json b/tests/qapi-schema/unknown-expr-key.json
index 1e9282d..ba7bdf3 100644
--- a/tests/qapi-schema/unknown-expr-key.json
+++ b/tests/qapi-schema/unknown-expr-key.json
@@ -1,2 +1,2 @@ 
-# FIXME: we should reject an expression with unknown top-level keys
+# we reject an expression with unknown top-level keys
 { 'type': 'bar', 'data': { 'string': 'str'}, 'bogus': { } }
diff --git a/tests/qapi-schema/unknown-expr-key.out b/tests/qapi-schema/unknown-expr-key.out
index c93f020..e69de29 100644
--- a/tests/qapi-schema/unknown-expr-key.out
+++ b/tests/qapi-schema/unknown-expr-key.out
@@ -1,3 +0,0 @@ 
-[OrderedDict([('type', 'bar'), ('data', OrderedDict([('string', 'str')])), ('bogus', OrderedDict())])]
-[]
-[OrderedDict([('type', 'bar'), ('data', OrderedDict([('string', 'str')])), ('bogus', OrderedDict())])]