blob: 8dd6e5fcf60ac945e40c3189649d83ea0b978e4e [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
import web_idl_schema
from web_idl_schema import SchemaCompilerError
# Helper functions for fetching specific parts of a processed API schema
# dictionary.
def getFunction(schema: dict, name: str) -> dict:
"""Gets the function dictionary with the specified name from the schema.
Args:
schema: The processed API schema dictionary to look for the function in.
name: The name of the function to look for.
Returns:
The dictionary for the function with the specified name.
Raises:
KeyError: If the given function name was not found in the list of functions.
"""
for item in schema['functions']:
if item['name'] == name:
return item
raise KeyError('Could not find "function" with name "%s" in schema' % name)
def getType(schema: dict, name: str) -> dict:
"""Gets the custom type dictionary with the specified name from the schema.
Args:
schema: The processed API schema dictionary to look for the type in.
name: The name of the custom type to look for.
Returns:
The dictionary for the custom type with the specified name.
Raises:
KeyError: If the given type name was not found in the list of types.
"""
for item in schema['types']:
if item['id'] == name:
return item
raise KeyError('Could not find "type" with id "%s" in schema' % name)
def getEvent(schema: dict, name: str) -> dict:
"""Gets the event dictionary with the specified name from the schema.
Args:
schema: The processed API schema dictionary to look for the event in.
name: The name of the event to look for.
Returns:
The dictionary for the event with the specified name.
Raises:
KeyError: If the given event name was not found in the list of events.
"""
for item in schema['events']:
if item['name'] == name:
return item
raise KeyError('Could not find "event" with name "%s" in schema' % name)
def getFunctionReturn(schema: dict, name: str) -> dict:
"""Gets the return dictionary for the function with the specified name.
Args:
schema: The processed API schema dictionary to look for the function in.
name: The name of the function to get the return value from.
Returns:
The dictionary representing the return for the specified function name if it
has a return, otherwise None if it does not.
"""
function = getFunction(schema, name)
return function.get('returns', None)
def getFunctionAsyncReturn(schema: dict, name: str) -> dict:
"""Gets the async return dictionary for the function with the specified name.
Args:
schema: The processed API schema dictionary to look for the function in.
name: The name of the function to get the async return value from.
Returns:
The dictionary representing the async return for the function with the
specified name if it has one, otherwise None if it does not.
"""
function = getFunction(schema, name)
return function.get('returns_async', None)
def getFunctionParameters(schema: dict, name: str) -> dict:
"""Gets the list of parameters for the function with the specified name.
Args:
schema: The processed API schema dictionary to look for the function in.
name: The name of the function to get the parameters list from.
Returns:
The list of dictionaries representing the function parameters for the
function with the specified name if it has any, otherwise None if it does
not.
"""
# TODO(crbug.com/340297705): All functions should have the 'parameters' key,
# so we shouldn't have a None fallback and just raise a KeyError if it isn't
# present.
function = getFunction(schema, name)
return function.get('parameters', None)
class WebIdlSchemaTest(unittest.TestCase):
def setUp(self):
loaded = web_idl_schema.Load('test/web_idl/basics.idl')
self.assertEqual(1, len(loaded))
self.assertEqual('testWebIdl', loaded[0]['namespace'])
self.idl_basics = loaded[0]
def testFunctionBasics(self):
function = getFunction(self.idl_basics, 'returnsUndefined')
self.assertEqual('returnsUndefined', function.get('name'))
self.assertEqual([], function.get('parameters'))
self.assertEqual('function', function.get('type'))
def testFunctionReturnTypes(self):
schema = self.idl_basics
# Test basic types.
self.assertEqual(
None,
getFunctionReturn(schema, 'returnsUndefined'),
)
self.assertEqual(
{
'name': 'returnsBoolean',
'type': 'boolean'
},
getFunctionReturn(schema, 'returnsBoolean'),
)
self.assertEqual(
{
'name': 'returnsDouble',
'type': 'number'
},
getFunctionReturn(schema, 'returnsDouble'),
)
self.assertEqual(
{
'name': 'returnsLong',
'type': 'integer'
},
getFunctionReturn(schema, 'returnsLong'),
)
self.assertEqual(
{
'name': 'returnsDOMString',
'type': 'string'
},
getFunctionReturn(schema, 'returnsDOMString'),
)
self.assertEqual({
'name': 'returnsCustomType',
'$ref': 'ExampleType'
}, getFunctionReturn(schema, 'returnsCustomType'))
self.assertEqual(
{
'name': 'returnsDOMStringSequence',
'type': 'array',
'items': {
'type': 'string'
}
}, getFunctionReturn(schema, 'returnsDOMStringSequence'))
self.assertEqual(
{
'name': 'returnsCustomTypeSequence',
'type': 'array',
'items': {
'$ref': 'ExampleType'
}
}, getFunctionReturn(schema, 'returnsCustomTypeSequence'))
def testPromiseBasedReturn(self):
schema = self.idl_basics
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'type': 'string'
}],
}, getFunctionAsyncReturn(schema, 'stringPromiseReturn'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'optional': True,
'type': 'string'
}],
}, getFunctionAsyncReturn(schema, 'nullablePromiseReturn'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'$ref': 'ExampleType'
}],
}, getFunctionAsyncReturn(schema, 'customTypePromiseReturn'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [],
}, getFunctionAsyncReturn(schema, 'undefinedPromiseReturn'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'type': 'array',
'items': {
'type': 'integer'
}
}],
}, getFunctionAsyncReturn(schema, 'longSequencePromiseReturn'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'type': 'array',
'items': {
'$ref': 'ExampleType'
}
}],
}, getFunctionAsyncReturn(schema, 'customTypeSequencePromiseReturn'))
# Tests function parameters are processed as expected.
def testFunctionParameters(self):
schema = self.idl_basics
# A function with no arguments has an empty array on the "parameters" key.
self.assertEqual([], getFunctionParameters(schema, 'takesNoArguments'))
self.assertEqual([{
'name': 'stringArgument',
'type': 'string'
}], getFunctionParameters(schema, 'takesDOMString'))
self.assertEqual([{
'name': 'optionalBoolean',
'optional': True,
'type': 'boolean'
}], getFunctionParameters(schema, 'takesOptionalBoolean'))
self.assertEqual([{
'name': 'argument1',
'type': 'string'
}, {
'name': 'argument2',
'optional': True,
'type': 'number'
}], getFunctionParameters(schema, 'takesMultipleArguments'))
self.assertEqual([{
'name': 'first',
'type': 'string'
}, {
'name': 'optionalInner',
'optional': True,
'type': 'string'
}, {
'name': 'last',
'type': 'string'
}], getFunctionParameters(schema, 'takesOptionalInnerArgument'))
self.assertEqual([{
'name': 'sequenceArgument',
'type': 'array',
'items': {
'type': 'boolean'
}
}], getFunctionParameters(schema, 'takesSequenceArgument'))
self.assertEqual([{
'name': 'optionalSequenceArgument',
'type': 'array',
'optional': True,
'items': {
'type': 'boolean'
}
}], getFunctionParameters(schema, 'takesOptionalSequenceArgument'))
self.assertEqual([{
'name': 'customTypeArgument',
'$ref': 'ExampleType'
}], getFunctionParameters(schema, 'takesCustomType'))
self.assertEqual([{
'name': 'optionalCustomTypeArgument',
'optional': True,
'$ref': 'ExampleType'
}], getFunctionParameters(schema, 'takesOptionalCustomType'))
self.assertEqual([{
'name': 'enumArgument',
'$ref': 'EnumType'
}], getFunctionParameters(schema, 'takesEnum'))
# Tests function descriptions are processed as expected.
def testFunctionDescriptions(self):
schema = self.idl_basics
# A function without a preceding comment has no 'description' key.
self.assertTrue('description' not in getFunction(schema, 'noDescription'))
# Basic single and multi-line function comments.
self.assertEqual(
'One line description.',
getFunction(schema, 'oneLineDescription').get('description'))
self.assertEqual(
'Multi line description. Split over. Multiple lines.',
getFunction(schema, 'multiLineDescription').get('description'))
self.assertEqual(
'<p>Paragraphed description.</p><p>With blank comment line for'
' paragraph tags.</p>',
getFunction(schema, 'paragraphedDescription').get('description'))
# Function with parameter comments.
function = getFunction(schema, 'parameterComments')
self.assertEqual('This function has parameter comments.',
function.get('description'))
function_parameters = getFunctionParameters(schema, 'parameterComments')
self.assertEqual(2, len(function_parameters))
self.assertEqual(
{
'description':
('This comment about the argument is split across multiple lines'
' and contains <em>HTML tags</em>.'),
'name':
'arg1',
'type':
'boolean',
},
function_parameters[0],
)
self.assertEqual(
{
'description': 'This second argument uses a custom type.',
'name': 'arg2',
'$ref': 'ExampleType'
}, function_parameters[1])
# Basic descriptions on a promise returning async function.
promise_function = getFunction(schema, 'describedPromiseReturn')
self.assertEqual(
('Promise returning function, with a comment that provides the name and'
' description of the value the promise resolves to.'),
promise_function.get('description'))
promise_function_parameters = getFunctionParameters(
schema, 'describedPromiseReturn')
self.assertEqual(1, len(promise_function_parameters))
self.assertEqual(
{
'description': 'This is a normal argument comment.',
'name': 'arg1',
'type': 'boolean',
},
promise_function_parameters[0],
)
promise_function_async_return = getFunctionAsyncReturn(
schema, 'describedPromiseReturn')
self.assertEqual(
{
'name':
'callback',
'optional':
True,
'description':
'General description for the promise return.',
'parameters': [{
'$ref':
'ExampleType',
'name':
'returnValueName',
'description':
('A description for the value the promise resolves to: with'
' an extra colon for good measure.'),
}],
}, promise_function_async_return)
# Promise returning function with just a name for the promise value and no
# further description.
named_promise_function_async_return = getFunctionAsyncReturn(
schema, 'namedPromiseReturn')
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'type': 'boolean',
'name': 'justAName'
}],
}, named_promise_function_async_return)
# Function with a return and simple comment describing it.
return_function = getFunction(schema, 'describedReturnFunction')
self.assertEqual(
'General function description for the describedReturnFunction.',
return_function.get('description'))
return_function_returns_value = getFunctionReturn(
schema, 'describedReturnFunction')
self.assertEqual('Description for the returns object itself.',
return_function_returns_value.get('description'))
# Tests that API events are processed as expected.
def testEvents(self):
schema = self.idl_basics
event_one = getEvent(schema, 'onTestOne')
# This is a bit of a tautology for now, as getEvent() uses name to retrieve
# the object and raises a KeyError if it is not found.
self.assertEqual('onTestOne', event_one.get('name'))
self.assertEqual('function', event_one.get('type'))
self.assertEqual(
'Comment that acts as a description for onTestOne. Parameter specific'
' comments are down below before the associated callback definition.',
event_one.get('description'))
self.assertEqual(
[{
'name': 'argument1',
'type': 'string',
'description': 'Parameter description for argument1.'
}, {
'name': 'argument2',
'optional': True,
'type': 'number',
'description': 'Another description, this time for argment2.'
}], event_one['parameters'])
event_two = getEvent(schema, 'onTestTwo')
self.assertEqual('onTestTwo', event_two.get('name'))
self.assertEqual('function', event_two.get('type'))
self.assertEqual('Comment for onTestTwo.', event_two.get('description'))
self.assertEqual(
[{
'name': 'customType',
'$ref': 'ExampleType',
'description': 'An ExampleType passed to the event listener.'
}], event_two['parameters'])
# Tests that Dictionaries and Enums defined on the top level of the IDL file
# are processed into types on the resulting namespace.
def testApiTypesOnNamespace(self):
schema = self.idl_basics
custom_type = getType(schema, 'ExampleType')
self.assertEqual('ExampleType', custom_type['id'])
self.assertEqual('object', custom_type['type'])
self.assertEqual(
{
'name': 'someString',
'type': 'string',
'description':
'Attribute comment attached to ExampleType.someString.'
}, custom_type['properties']['someString'])
self.assertEqual(
{
'name': 'someNumber',
'type': 'number',
'description':
'Comment where <var>someNumber</var> has some markup.'
}, custom_type['properties']['someNumber'])
# TODO(crbug.com/379052294): using HTML comments like this is a bit of a
# hack to allow us to add comments in IDL files (e.g. for TODOs) and to not
# have them end up on the documentation site. We should probably just filter
# them out during compilation.
self.assertEqual(
{
'name':
'optionalBoolean',
'type':
'boolean',
'optional':
True,
'description':
'Comment with HTML comment. <!-- Which should get through -->'
}, custom_type['properties']['optionalBoolean'])
self.assertEqual(
{
'name': 'booleanSequence',
'type': 'array',
'items': {
'type': 'boolean'
},
'description': 'Comment on sequence type.',
}, custom_type['properties']['booleanSequence'])
enum_expected = {
'enum': [{
'name': 'name1',
'description': 'Comment1.'
}, {
'name': 'name2'
}],
'description': 'Enum description.',
'type': 'string',
'id': 'EnumType'
}
self.assertEqual(enum_expected, getType(schema, 'EnumType'))
# Tests that a top level API comment is processed into a description
# attribute, with HTML paragraph nodes added due to the blank commented line.
def testApiDescriptionComment(self):
schema = self.idl_basics
expected_description = (
'<p>This comment is an example of a top level API description, which'
' will be extracted and added to the processed python dictionary as a'
' description.</p><p>Note: All comment lines preceding the thing they'
' are attached to will be part of the description, until a blank new'
' line or non-comment is reached.</p>')
self.assertEqual(expected_description, schema['description'])
# Tests that if the nodoc extended attribute is not specified on the API
# interface the related attribute is set to false after processing.
def testNodocUnspecifiedOnNamespace(self):
self.assertFalse(self.idl_basics['nodoc'])
# TODO(crbug.com/340297705): This will eventually be relaxed when adding
# support for shared types to the new parser.
def testMissingBrowserInterfaceError(self):
expected_error_regex = (
r'.* File\(test\/web_idl\/missing_browser_interface.idl\): Required'
r' partial Browser interface not found in schema\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_browser_interface.idl',
)
# Tests that having a Browser interface on an API definition with no attribute
# throws an error.
def testMissingAttributeOnBrowserError(self):
expected_error_regex = (
r'.* Interface\(Browser\): The partial Browser interface should have'
r' exactly one attribute for the name the API will be exposed under\.')
self.assertRaisesRegex(
Exception,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_attribute_on_browser.idl',
)
# Tests that using a valid basic WebIDL type with a "name" the schema compiler
# doesn't support yet throws an error.
def testUnsupportedBasicTypeError(self):
expected_error_regex = (
r'.* PrimitiveType\(float\): Unsupported basic type found when'
r' processing type\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/unsupported_basic_type.idl',
)
# Tests that using a valid WebIDL type with a node "class" the schema compiler
# doesn't support yet throws an error.
def testUnsupportedTypeClassError(self):
expected_error_regex = (
r'.* Any\(\): Unsupported type class when processing type\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/unsupported_type_class.idl',
)
# Tests that an event trying to say it uses an Interface that is not defined
# in the IDL file will throw an error. This is largely in place to help catch
# spelling mistakes in event names or forgetting to add the Interface
# definition.
def testMissingEventInterface(self):
expected_error_regex = (
r'.* Error processing node Attribute\(onTestTwo\): Could not find'
r' Interface definition for event\.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_event_interface.idl',
)
# Various tests that ensure validation on event interface definitions.
# Specifically checks that not defining any of the required add/remove/has
# Operations or forgetting the ExtensionEvent inheritance will throw an error.
def testMissingEventInheritance(self):
expected_error_regex = (
r'.* Error processing node Interface\(OnMissingInheritanceEvent\):'
r' Event Interface missing ExtensionEvent Inheritance.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_event_inheritance.idl',
)
def testMissingEventAddListener(self):
expected_error_regex = (
r'.* Error processing node Interface\(OnMissingAddListenerEvent\):'
r' Event Interface missing addListener Operation definition.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_event_add_listener.idl',
)
def testMissingEventRemoveListener(self):
expected_error_regex = (
r'.* Error processing node Interface\(OnMissingRemoveListenerEvent\):'
r' Event Interface missing removeListener Operation definition.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_event_remove_listener.idl',
)
def testMissingEventHasListener(self):
expected_error_regex = (
r'.* Error processing node Interface\(OnMissingHasListenerEvent\):'
r' Event Interface missing hasListener Operation definition.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/missing_event_has_listener.idl',
)
# Tests that if description parsing from file comments reaches the top of the
# file, a schema compiler error is thrown (as the top of the file should
# always be copyright lines and not part of the description).
def testDocumentationCommentReachedTopOfFileError(self):
expected_error_regex = (
r'.* Reached top of file when trying to parse description from file'
r' comment. Make sure there is a blank line before the comment.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/documentation_comment_top_of_file.idl',
)
# Tests that usage of the 'void' type will result in a schema compiler error.
# 'void' has been deprecated and 'undefined' should be used instead.
def testVoidUsageTriggersError(self):
expected_error_regex = (
r'Error processing node PrimitiveType\(void\): Usage of "void" in IDL'
r' is deprecated, use "Undefined" instead.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/void_unsupported.idl',
)
# Tests that a namespace with an extended attribute that we don't have
# processing for results in a schema compiler error.
def testUnknownNamespaceExtendedAttributeNameError(self):
expected_error_regex = (
r'.* Interface\(TestWebIdl\): Unknown extended attribute with name'
r' "UnknownExtendedAttribute" when processing namespace.')
self.assertRaisesRegex(
SchemaCompilerError,
expected_error_regex,
web_idl_schema.Load,
'test/web_idl/unknown_namespace_extended_attribute.idl',
)
# Tests that an API interface that uses the nodoc extended attribute has the
# related nodoc attribute set to true after processing.
def testNoDocOnNamespace(self):
idl = web_idl_schema.Load('test/web_idl/nodoc_on_namespace.idl')
self.assertEqual(1, len(idl))
schema = idl[0]
self.assertEqual('nodocAPI', schema['namespace'])
self.assertTrue(schema['nodoc'])
# Also ensure the description comes through correctly on the node with
# 'nodoc' as an extended attribute.
self.assertEqual(
'The nodoc API. This exists to demonstrate nodoc on the main interface'
' itself.',
schema['description'],
)
# Tests that an enum that uses the nodoc extended attribute has the related
# nodoc attribute set to true after processing.
def testNoDocOnEnum(self):
schema = self.idl_basics
nodoc_enum = getType(schema, 'EnumTypeWithNoDoc')
self.assertEqual(True, nodoc_enum['nodoc'])
# An enum without the extended attribute will not have a nodoc attribute.
no_nodoc_enum = getType(schema, 'EnumType')
self.assertFalse(hasattr(no_nodoc_enum, 'nodoc'))
# Tests that an enum with the deprecated extended attribute has the related
# deprecated attribute set to the provided string value after processing.
# TODO(crbug.com/340297705): expand this out to the other various places
# deprecated can be specified.
def testDeprecatedExtendedAttribute(self):
idl = web_idl_schema.Load('test/web_idl/deprecated.idl')
self.assertEqual(1, len(idl))
schema = idl[0]
deprecated_enum = getType(schema, 'DeprecatedEnum')
self.assertEqual('This enum is deprecated', deprecated_enum['deprecated'])
# Tests that a function defined with the requiredCallback extended attribute
# does not have the returns_async field marked as optional after processing.
# Note: These are only relevant to contexts which don't support promise based
# calls, or for specific functions which still do not support promises.
def testRequiredCallbackFunction(self):
idl = web_idl_schema.Load('test/web_idl/required_callback_function.idl')
self.assertEqual(1, len(idl))
self.assertEqual(
{
'name': 'callback',
'parameters': [{
'type': 'string'
}],
}, getFunctionAsyncReturn(idl[0], 'requiredCallbackFunction'))
self.assertEqual(
{
'name': 'callback',
'optional': True,
'parameters': [{
'type': 'string'
}],
}, getFunctionAsyncReturn(idl[0], 'notRequiredCallbackFunction'))
# Tests that extended attributes being listed on the the line previous to a
# node come through correctly and don't throw off and associated descriptions.
def testPreviousLineExtendedAttributes(self):
idl = web_idl_schema.Load('test/web_idl/preceding_extended_attributes.idl')
self.assertEqual(1, len(idl))
schema = idl[0]
self.assertEqual('precedingExtendedAttributes', schema['namespace'])
self.assertTrue(schema['nodoc'])
self.assertEqual(
'Comment on a schema that has extended attributes on a previous line.',
schema['description'],
)
function = getFunction(schema, 'functionExample')
self.assertEqual('Description on a function.', function.get('description'))
async_return = getFunctionAsyncReturn(schema, 'functionExample')
# The extended attribute on the function causes 'optional': True to not be
# present on the async return.
self.assertNotIn('optional', async_return)
self.assertEqual('Promise return description.',
async_return.get('description'))
# Tests that an API interface with the platforms extended attribute has these
# values in a platforms attribute after processing.
def testAllPlatformsOnNamespace(self):
platforms_schema = web_idl_schema.Load(
'test/web_idl/all_platforms_on_namespace.idl')
self.assertEqual(1, len(platforms_schema))
self.assertEqual('allPlatformsAPI', platforms_schema[0]['namespace'])
expected = ['chromeos', 'desktop_android', 'linux', 'mac', 'win']
self.assertEqual(expected, platforms_schema[0]['platforms'])
# Tests that an API interface with just chromeos listed in the platforms
# extended attribute just has that after processing.
def testChromeOSPlatformsOnNamespace(self):
platforms_schema = web_idl_schema.Load(
'test/web_idl/chromeos_platforms_on_namespace.idl')
self.assertEqual(1, len(platforms_schema))
self.assertEqual('chromeOSPlatformsAPI', platforms_schema[0]['namespace'])
expected = ['chromeos']
self.assertEqual(expected, platforms_schema[0]['platforms'])
# Tests a variety of default values that are set on an API namespace when they
# are not specified in the source IDL file.
def testNonSpecifiedDefaultValues(self):
defaults_schema = web_idl_schema.Load('test/web_idl/defaults.idl')[0]
self.assertEqual(
{
'compiler_options': {},
'deprecated': None,
'description': '',
'events': [],
'functions': [],
'manifest_keys': None,
'namespace': 'defaultsOnlyWebIdl',
'nodoc': False,
'platforms': None,
'properties': {},
'types': [],
}, defaults_schema)
if __name__ == '__main__':
unittest.main()