#!/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 getProperty(schema: dict, name: str) -> dict:
  """Gets the property dictionary with the specified name from the schema.

  Args:
    schema: The processed API schema dictionary to look for the property in.
    name: The name of the property to look for.

  Returns:
    The dictionary for the property with the specified name.
  """
  return schema['properties'][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': 'returnsNullableCustomType',
        'optional': True,
        '$ref': 'ExampleType',
      },
      getFunctionReturn(schema, 'returnsNullableCustomType'),
    )
    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',
        'parameters': [{'type': 'string'}],
      },
      getFunctionAsyncReturn(schema, 'stringPromiseReturn'),
    )
    self.assertEqual(
      {
        'name': 'callback',
        'parameters': [{'optional': True, 'type': 'string'}],
      },
      getFunctionAsyncReturn(schema, 'nullablePromiseReturn'),
    )
    self.assertEqual(
      {
        'name': 'callback',
        'parameters': [{'$ref': 'ExampleType'}],
      },
      getFunctionAsyncReturn(schema, 'customTypePromiseReturn'),
    )
    self.assertEqual(
      {
        'name': 'callback',
        'parameters': [],
      },
      getFunctionAsyncReturn(schema, 'undefinedPromiseReturn'),
    )
    self.assertEqual(
      {
        'name': 'callback',
        'parameters': [{'type': 'array', 'items': {'type': 'integer'}}],
      },
      getFunctionAsyncReturn(schema, 'longSequencePromiseReturn'),
    )
    self.assertEqual(
      {
        'name': 'callback',
        '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',
        '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',
        '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'],
    )

    event_max_listeners = getEvent(schema, 'onTestMaxListeners')
    self.assertEqual('onTestMaxListeners', event_max_listeners.get('name'))
    self.assertEqual({'maxListeners': 1}, event_max_listeners.get('options'))

    event_supports_filters = getEvent(schema, 'onTestSupportsFilters')
    self.assertEqual(
      'onTestSupportsFilters', event_supports_filters.get('name')
    )
    self.assertEqual(
      {'supportsFilters': True}, event_supports_filters.get('options')
    )

  # 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'],
    )

    expected_enum = {
      'enum': [
        {'name': 'name1', 'description': 'Comment1.'},
        {'name': 'name2'},
      ],
      'description': 'Enum description.',
      'type': 'string',
      'id': 'EnumType',
    }
    self.assertEqual(expected_enum, getType(schema, 'EnumType'))

    expected_single_line_enum = {
      'enum': [{'name': 'value1'}, {'name': 'value2'}],
      'description': 'Single line enum.',
      'type': 'string',
      'id': 'SingleLineEnum',
    }
    self.assertEqual(
      expected_single_line_enum, getType(schema, 'SingleLineEnum')
    )

    expected_argument_callback_type_member = {
      'name': 'argumentCallback',
      'type': 'function',
      'parameters': [{'name': 'stringArgument', 'type': 'string'}],
    }
    self.assertEqual(
      expected_argument_callback_type_member,
      getType(schema, 'DictionaryWithCallbackMembers')['properties'][
        'argumentCallback'
      ],
    )

    expected_return_callback_type_member = {
      'name': 'returnCallback',
      'type': 'function',
      'parameters': [],
      'returns': {'name': 'returnCallback', 'type': 'boolean'},
    }
    self.assertEqual(
      expected_return_callback_type_member,
      getType(schema, 'DictionaryWithCallbackMembers')['properties'][
        'returnCallback'
      ],
    )

  def testIgnoresAdditionalPropertiesOnType(self):
    self.assertTrue(
      getType(self.idl_basics, 'IgnoreAdditionalPropertiesType')[
        'ignoreAdditionalProperties'
      ]
    )

  # Tests that a schema that references a custom type that has not been defined
  # causes an error to be thrown.
  # TODO(crbug.com/450443604): This will likely have to change when adding
  # support for shared types between schema files in WebIDL.
  def testCustomTypeNotFound(self):
    expected_error_regex = (
      r'.* Typeref\(MissingType\): Could not find definition of referenced'
      r' type "MissingType" for node.'
    )
    self.assertRaisesRegex(
      SchemaCompilerError,
      expected_error_regex,
      web_idl_schema.Load,
      'test/web_idl/custom_type_not_found.idl',
    )

  # Tests that a schema referencing a typeref style type whose definition is not
  # one of the valid IDL node classes (Dictionary, Enum, Callback) causes an
  # error to be thrown.
  def testInvalidTyperefType(self):
    expected_error_regex = (
      r'.* Typeref\(OnTestEvent\): Found a Typeref node referencing a node of'
      r' type "Interface", but we only support Typerefs that reference'
      r' Dictionary, Enum or Callback class nodes.'
    )
    self.assertRaisesRegex(
      SchemaCompilerError,
      expected_error_regex,
      web_idl_schema.Load,
      'test/web_idl/invalid_typeref_type.idl',
    )

  # 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 constants defined on an API Interface are processed into the
  # 'properties' object.
  def testConstantProperties(self):
    schema = self.idl_basics
    # Properties are an ordered dict, so ordering matches order in the IDL file.
    self.assertEqual(
      [
        'CONSTANT_LONG',
        'CONSTANT_DOUBLE',
        'CONSTANT_STRING',
        'DESCRIBED_CONSTANT',
      ],
      list(schema['properties'].keys()),
    )
    self.assertEqual(
      {'type': 'integer', 'value': 39}, getProperty(schema, 'CONSTANT_LONG')
    )
    self.assertEqual(
      {'type': 'number', 'value': 3.9}, getProperty(schema, 'CONSTANT_DOUBLE')
    )
    self.assertEqual(
      {'type': 'string', 'value': 'Foo'}, getProperty(schema, 'CONSTANT_STRING')
    )
    self.assertEqual(
      {
        'type': 'integer',
        'value': 9,
        'description': 'Comment on a constant property with a value.',
      },
      getProperty(schema, 'DESCRIBED_CONSTANT'),
    )

  # Tests that a const DOMString defined on an API Interface which is missing
  # the StringValue extended attribute will throw an error. It's unfortunate
  # that we need to hack this in with an extended attribute, but WebIDL does not
  # support specifying an actual string for the value of a const.
  def testConstStringMissingExtendedAttribute(self):
    expected_error_regex = (
      r'.* Const\(foo\): If using a const of type DOMString, you must specify'
      r' the extended attribute "StringValue" for the value.'
    )
    self.assertRaisesRegex(
      SchemaCompilerError,
      expected_error_regex,
      web_idl_schema.Load,
      'test/web_idl/const_string_missing_extended_attribute.idl',
    )

  # 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'])

  # Test loading a schema for an API with a nested namespace comes through with
  # a dot separator in its name after processing (nested.example).
  def testNestedNamespaceName(self):
    idl = web_idl_schema.Load('test/web_idl/nested_namespace.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    self.assertEqual('nested.example', schema['namespace'])
    self.assertEqual(
      'Schema to test nested namespace names types in IDL (API names with a'
      ' dot in them). API should end up on `<Browser>.nested.example`.',
      schema['description'],
    )

  # Similar to above, but testing a triple nested namespace with 2 dots
  # (multi.nested.example).
  def testMultiNestedNamespaceName(self):
    idl = web_idl_schema.Load('test/web_idl/multi_nested_namespace.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    self.assertEqual('multi.nested.example', schema['namespace'])
    self.assertEqual(
      'Schema to test triple nested namespace names types in IDL (API names'
      ' with 2 dots in them). API should end up on `<Browser>.multi.nested.'
      'example`.',
      schema['description'],
    )

  # 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\): Schema must'
      r' contain either a partial Browser interface \(for APIs\) or a'
      r' partial ExtensionManifest dictionary \(for manifest stubs\)\.'
    )
    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'.* FrozenArray\(\): 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 the nodoc extended attribute used in various places gets the
  # related attribute set to True after processing.
  def testNoDocExtendedAttribute(self):
    idl = web_idl_schema.Load('test/web_idl/nodoc_examples.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # Top level namespace:
    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 a variety of nodoc extended'
      ' attribute usage.',
      schema['description'],
    )

    # Enums:
    nodoc_enum = getType(schema, 'EnumWithNoDoc')
    self.assertTrue(nodoc_enum['nodoc'])
    normal_enum = getType(schema, 'NormalEnum')
    self.assertFalse(hasattr(normal_enum, 'nodoc'))

    # Dictionaries:
    nodoc_dict = getType(schema, 'DictionaryWithNoDoc')
    self.assertTrue(nodoc_dict['nodoc'])
    normal_dict = getType(schema, 'NormalDictionary')
    self.assertFalse(hasattr(normal_dict, 'nodoc'))

    # Dictionary members:
    nodoc_dict_member = getType(schema, 'DictionaryWithNoDocMember')
    self.assertTrue(nodoc_dict_member['properties']['nodocMember']['nodoc'])
    self.assertFalse(
      hasattr(nodoc_dict_member['properties']['normalMember'], 'nodoc')
    )

    # Functions:
    nodoc_function = getFunction(schema, 'functionWithNoDoc')
    self.assertTrue(nodoc_function['nodoc'])
    normal_function = getFunction(schema, 'normalFunction')
    self.assertFalse(hasattr(normal_function, 'nodoc'))

    # Events:
    nodoc_event = getEvent(schema, 'noDocEvent')
    self.assertTrue(nodoc_event['nodoc'])
    normal_event = getEvent(schema, 'normalEvent')
    self.assertFalse(hasattr(normal_event, 'nodoc'))

    # Properties:
    nodoc_property = getProperty(schema, 'PROPERTY_WITH_NODOC')
    self.assertTrue(nodoc_property['nodoc'])
    normal_property = getProperty(schema, 'NORMAL_PROPERTY')
    self.assertFalse(hasattr(normal_property, 'nodoc'))

  # Tests that the nocompile extended attribute used in various places gets the
  # related attribute set to True after processing.
  # Note: Practically with how nocompile works during actual schema compilation
  # this schema doesn't really make sense, as the top level schema node would be
  # deleted, making all the other 'nocompile's redundant. However we can still
  # use this schema to check that the attribute is set correctly on each node
  # after just the parsing and processing steps (before compilation).
  def testNoCompileExtendedAttribute(self):
    idl = web_idl_schema.Load('test/web_idl/nocompile_examples.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # Top level namespace:
    self.assertEqual('nocompileAPI', schema['namespace'])
    self.assertTrue(schema['nocompile'])

    # Enums:
    nocompile_enum = getType(schema, 'EnumWithNoCompile')
    self.assertTrue(nocompile_enum['nocompile'])
    normal_enum = getType(schema, 'NormalEnum')
    self.assertFalse(hasattr(normal_enum, 'nocompile'))

    # Dictionaries:
    nocompile_dict = getType(schema, 'DictionaryWithNoCompile')
    self.assertTrue(nocompile_dict['nocompile'])
    normal_dict = getType(schema, 'NormalDictionary')
    self.assertFalse(hasattr(normal_dict, 'nocompile'))

    # Dictionary members:
    nocompile_dict_member = getType(schema, 'DictionaryWithNoCompileMember')
    self.assertTrue(
      nocompile_dict_member['properties']['nocompileMember']['nocompile']
    )
    self.assertFalse(
      hasattr(nocompile_dict_member['properties']['normalMember'], 'nocompile')
    )

    # Functions:
    nocompile_function = getFunction(schema, 'functionWithNoCompile')
    self.assertTrue(nocompile_function['nocompile'])
    normal_function = getFunction(schema, 'normalFunction')
    self.assertFalse(hasattr(normal_function, 'nocompile'))

    # Events:
    nocompile_event = getEvent(schema, 'noCompileEvent')
    self.assertTrue(nocompile_event['nocompile'])
    normal_event = getEvent(schema, 'normalEvent')
    self.assertFalse(hasattr(normal_event, 'nocompile'))

    # Properties:
    nocompile_property = getProperty(schema, 'PROPERTY_WITH_NOCOMPILE')
    self.assertTrue(nocompile_property['nocompile'])
    normal_property = getProperty(schema, 'NORMAL_PROPERTY')
    self.assertFalse(hasattr(normal_property, 'nocompile'))

  # Tests that the deprecated extended attribute used in various places get the
  # related attribute set to the provided string after processing.
  # TODO(crbug.com/340297705): Enum values are not allowed to have extended
  # attributes preceding them, so we need to find some other way to mark a
  # specific enum value as deprecated.
  def testDeprecatedExtendedAttribute(self):
    idl = web_idl_schema.Load('test/web_idl/deprecated_examples.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # Top level Namespace:
    self.assertEqual('This API is deprecated', schema['deprecated'])

    # Enums:
    deprecated_enum = getType(schema, 'DeprecatedEnum')
    self.assertEqual('This enum is deprecated', deprecated_enum['deprecated'])

    # Dictionaries:
    deprecated_dict = getType(schema, 'DeprecatedDictionary')
    self.assertEqual('This dict is deprecated', deprecated_dict['deprecated'])

    # Dictionary members:
    deprecated_dict_member = getType(schema, 'DictionaryWithDeprecatedMember')
    self.assertEqual(
      'This dict member is deprecated',
      deprecated_dict_member['properties']['deprecatedMember']['deprecated'],
    )

    # Functions:
    deprecated_function = getFunction(schema, 'deprecatedFunction')
    self.assertEqual(
      'This function is deprecated and it has such a long message that\n  it'
      ' requires line wrapping',
      deprecated_function['deprecated'],
    )

    # Events:
    deprecated_event = getEvent(schema, 'onDeprecatedEvent')
    self.assertEqual('This event is deprecated', deprecated_event['deprecated'])

    # Properties:
    deprecated_property = getProperty(schema, 'DEPRECATED_PROPERTY')
    self.assertEqual(
      'This property is deprecated', deprecated_property['deprecated']
    )

  # 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 and a function definition with chromeos listed
  # in the platforms extended attribute has the associated attribute set after
  # processing.
  # Note: Platform restrictions should generally be defined using Extension
  # Features Files (see: chrome/common/extensions/api/_features.md), but for
  # legacy reasons we have to allow this extended attribute on namespace and
  # function definitions.
  def testPlatformsExtendedAttribute(self):
    idl = web_idl_schema.Load('test/web_idl/platforms_examples.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    self.assertEqual('chromeOSPlatformsAPI', schema['namespace'])
    expected = ['chromeos']
    self.assertEqual(expected, schema['platforms'])

    platforms_function = getFunction(schema, 'chromeOSOnlyFunction')
    self.assertEqual(expected, platforms_function['platforms'])

    normal_function = getFunction(schema, 'normalFunction')
    self.assertFalse(hasattr(normal_function, 'platforms'))

  # Tests that the 'implemented_in' extended attribute on an interface
  # definition is copied into the resulting namespace after processing.
  def testImplementedInExtendedAttribute(self):
    idl = web_idl_schema.Load('test/web_idl/implemented_in.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    self.assertEqual('implementedInAPI', schema['namespace'])
    self.assertEqual(
      'path/to/implementation.h', schema['compiler_options']['implemented_in']
    )

  # Tests that an API interface using the 'generate_error_messages' extended
  # attribute has the associated attribute set to true after processing.
  def testGenerateErrorMessages(self):
    idl = web_idl_schema.Load('test/web_idl/generate_error_messages.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    self.assertEqual('generateErrorMessagesAPI', schema['namespace'])
    self.assertTrue(schema['compiler_options']['generate_error_messages'])

  # 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,
    )

  # Tests that Enum and Dictionary types defined in a schema file retain their
  # ordering in the resulting processed API object.
  def testEnumAndTypeOrdering(self):
    idl = web_idl_schema.Load('test/web_idl/enum_and_type_ordering.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]
    # Types are interleaved in the schema.
    types = schema['types']
    self.assertEqual('ExampleDictOne', types[0]['id'])
    self.assertEqual('EnumTypeOne', types[1]['id'])
    self.assertEqual('ExampleDictTwo', types[2]['id'])
    self.assertEqual('EnumTypeTwo', types[3]['id'])

  # Tests various 'any' and 'object' types on Dictionaries.
  def testObjectTypes(self):
    idl = web_idl_schema.Load('test/web_idl/object_types.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    object_dict = getType(schema, 'ObjectDict')
    self.assertEqual('object', object_dict['type'])
    self.assertEqual(
      {
        'type': 'object',
        'name': 'requiredObject',
        'additionalProperties': {'type': 'any'},
      },
      object_dict['properties']['requiredObject'],
    )
    self.assertEqual(
      {
        'type': 'object',
        'optional': True,
        'name': 'optionalObject',
        'additionalProperties': {'type': 'any'},
      },
      object_dict['properties']['optionalObject'],
    )
    self.assertEqual(
      {
        'type': 'object',
        'name': 'instanceOfObject',
        'additionalProperties': {'type': 'any'},
        'isInstanceOf': 'Blob',
      },
      object_dict['properties']['instanceOfObject'],
    )

    any_dict = getType(schema, 'AnyDict')
    self.assertEqual('object', any_dict['type'])
    self.assertEqual(
      {'type': 'any', 'name': 'requiredAny'},
      any_dict['properties']['requiredAny'],
    )
    self.assertEqual(
      {'type': 'any', 'optional': True, 'name': 'optionalAny'},
      any_dict['properties']['optionalAny'],
    )

  # Tests 'object' and 'any' types used as function parameters.
  def testObjectFunctionParams(self):
    idl = web_idl_schema.Load('test/web_idl/object_types.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    object_params = getFunctionParameters(schema, 'objectParamFunction')
    self.assertEqual(
      {
        'type': 'object',
        'name': 'requiredObjectParam',
        'additionalProperties': {'type': 'any'},
      },
      object_params[0],
    )
    self.assertEqual(
      {
        'type': 'object',
        'optional': True,
        'name': 'optionalObjectParam',
        'additionalProperties': {'type': 'any'},
      },
      object_params[1],
    )

    any_params = getFunctionParameters(schema, 'anyParamFunction')
    self.assertEqual({'type': 'any', 'name': 'requiredAnyParam'}, any_params[0])
    self.assertEqual(
      {'type': 'any', 'optional': True, 'name': 'optionalAnyParam'},
      any_params[1],
    )

    instance_of_params = getFunctionParameters(
      schema, 'instanceOfFunctionParam'
    )
    self.assertEqual(
      {
        'type': 'object',
        'name': 'instanceOfParam',
        'additionalProperties': {'type': 'any'},
        'isInstanceOf': 'Entry',
      },
      instance_of_params[0],
    )

  # Tests various Union types on Dictionaries.
  def testUnionTypes(self):
    schema = self.idl_basics
    union_dict = getType(schema, 'UnionTypes')

    self.assertEqual(
      {
        'name': 'stringLongOrBoolean',
        'choices': [
          {'type': 'string'},
          {'type': 'integer'},
          {'type': 'boolean'},
        ],
      },
      union_dict['properties']['stringLongOrBoolean'],
    )
    self.assertEqual(
      {
        'name': 'stringOrEnum',
        'choices': [
          {'type': 'string'},
          {'$ref': 'EnumType'},
        ],
      },
      union_dict['properties']['stringOrEnum'],
    )
    self.assertEqual(
      {
        'name': 'optionalEnumOrString',
        'optional': True,
        'choices': [
          {'$ref': 'EnumType'},
          {'type': 'string'},
        ],
      },
      union_dict['properties']['optionalEnumOrString'],
    )
    self.assertEqual(
      {
        'name': 'optionalStringOrStringSequence',
        'optional': True,
        'choices': [
          {'type': 'string'},
          {'type': 'array', 'items': {'type': 'string'}},
        ],
      },
      union_dict['properties']['optionalStringOrStringSequence'],
    )
    self.assertEqual(
      {
        'name': 'dictTypeOrLong',
        'choices': [{'$ref': 'ExampleType'}, {'type': 'integer'}],
      },
      union_dict['properties']['dictTypeOrLong'],
    )

  # Tests supported 'binary' buffer source types in dictionaries and as function
  # parameters. Currently only ArrayBuffer and Uint8Array.
  def testBufferSourceTypes(self):
    idl = web_idl_schema.Load('test/web_idl/buffer_source_types.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    array_buffer_dict = getType(schema, 'ArrayBufferDict')
    self.assertEqual('object', array_buffer_dict['type'])
    self.assertEqual(
      {
        'type': 'binary',
        'name': 'requiredArrayBuffer',
        'isInstanceOf': 'ArrayBuffer',
      },
      array_buffer_dict['properties']['requiredArrayBuffer'],
    )
    self.assertEqual(
      {
        'type': 'binary',
        'optional': True,
        'name': 'optionalArrayBuffer',
        'isInstanceOf': 'ArrayBuffer',
      },
      array_buffer_dict['properties']['optionalArrayBuffer'],
    )

    uint_8_array_dict = getType(schema, 'Uint8ArrayDict')
    self.assertEqual('object', uint_8_array_dict['type'])
    self.assertEqual(
      {
        'type': 'binary',
        'name': 'requiredUint8Array',
        'isInstanceOf': 'Uint8Array',
      },
      uint_8_array_dict['properties']['requiredUint8Array'],
    )
    self.assertEqual(
      {
        'type': 'binary',
        'optional': True,
        'name': 'optionalUint8Array',
        'isInstanceOf': 'Uint8Array',
      },
      uint_8_array_dict['properties']['optionalUint8Array'],
    )

    array_buffer_params = getFunctionParameters(
      schema, 'arrayBufferParamFunction'
    )
    self.assertEqual(
      {
        'type': 'binary',
        'name': 'requiredArrayBufferParam',
        'isInstanceOf': 'ArrayBuffer',
      },
      array_buffer_params[0],
    )
    self.assertEqual(
      {
        'type': 'binary',
        'optional': True,
        'name': 'optionalArrayBufferParam',
        'isInstanceOf': 'ArrayBuffer',
      },
      array_buffer_params[1],
    )

    uint_8_array_params = getFunctionParameters(
      schema, 'uint8ArrayParamFunction'
    )
    self.assertEqual(
      {
        'type': 'binary',
        'name': 'requiredUint8ArrayParam',
        'isInstanceOf': 'Uint8Array',
      },
      uint_8_array_params[0],
    )
    self.assertEqual(
      {
        'type': 'binary',
        'optional': True,
        'name': 'optionalUint8ArrayParam',
        'isInstanceOf': 'Uint8Array',
      },
      uint_8_array_params[1],
    )

  # Tests using the ExternalExtensionType extended attribute to indicate a type
  # has the actual definition in another schema file from another API
  # 'namespace'.
  def testExternalExtensionTypes(self):
    idl = web_idl_schema.Load('test/web_idl/uses_shared_types.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # Using a normal locally defined type just gets a normal $ref value with the
    # local type name.
    local_type_params = getFunctionParameters(schema, 'localTypeFunction')
    self.assertEqual(
      {'name': 'localTypeParam', '$ref': 'LocalType'}, local_type_params[0]
    )

    # The $ref value for the shared type usage should have the name specified by
    # the extended attribute.
    shared_type_params = getFunctionParameters(schema, 'sharedTypeFunction')
    self.assertEqual(
      {'name': 'sharedTypeParam', '$ref': 'basics.ExampleType'},
      shared_type_params[0],
    )

    # Only the locally defined type should be in the list of Types for the API
    # and not the typedef for the shared type it uses.
    self.assertEqual(1, len(schema['types']))
    self.assertEqual('LocalType', schema['types'][0]['id'])

  # Tests that a Typedef in a schema without the ExternalExtensionType extended
  # attribute is processed as a local alias of the underlying type.
  def testTypedefAliases(self):
    idl = web_idl_schema.Load('test/web_idl/typedef_alias.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # takesFileAlias(FileAlias file)
    # FileAlias is a typedef of object with [instanceOf=File]
    file_alias_params = getFunctionParameters(schema, 'takesFileAlias')
    self.assertEqual(
      {
        'name': 'file',
        'type': 'object',
        'additionalProperties': {'type': 'any'},
        'isInstanceOf': 'File',
      },
      file_alias_params[0],
    )

    # takesLongAlias(LongAlias count)
    # LongAlias is a typedef of long
    long_alias_params = getFunctionParameters(schema, 'takesLongAlias')
    self.assertEqual({'name': 'count', 'type': 'integer'}, long_alias_params[0])

  # Tests Manifest keys defined on a partial 'ExtensionManifest' dictionary are
  # extracted and put into the manifest keys details and not into the Types.
  def testManifestKeysOnApiSchema(self):
    schema = self.idl_basics
    # The 'Manifest' dictionary shouldn't get put into the custom types.
    self.assertFalse(any(obj['id'] == 'Manifest' for obj in schema['types']))
    manifest_keys = schema['manifest_keys']

    # We should have 3 manifest keys of varying types.
    self.assertEqual(
      ['string_key', 'custom_type_key', 'union_type_key'],
      list(manifest_keys.keys()),
    )
    self.assertEqual(
      {
        'type': 'string',
        'name': 'string_key',
        'description': 'Description of a manifest key.',
      },
      manifest_keys['string_key'],
    )
    self.assertEqual(
      {'$ref': 'ExampleType', 'name': 'custom_type_key'},
      manifest_keys['custom_type_key'],
    )
    self.assertEqual(
      {
        'choices': [{'type': 'string'}, {'type': 'integer'}],
        'name': 'union_type_key',
      },
      manifest_keys['union_type_key'],
    )

  # Tests that if 'partial' is left off the 'ExtensionManifest' dictionary, we
  # throw an error.
  def testNonPartialManifestDictError(self):
    expected_error_regex = (
      r'.* Dictionary\(ExtensionManifest\): If using an "ExtensionManifest"'
      r' dictionary to define manifest keys, it must be declared "partial"\.'
    )
    self.assertRaisesRegex(
      SchemaCompilerError,
      expected_error_regex,
      web_idl_schema.Load,
      'test/web_idl/non_partial_manifest_dict.idl',
    )

  # Tests that the 'ExtensionManifest' dictionary can be defined as a "stub"
  # schema file which is just defining manifest key details to be used for type
  # checking.
  def testManifestKeysOnStubSchema(self):
    # Loading the stub should load fine, even though it has no 'Browser'
    # interface.
    idl = web_idl_schema.Load('test/web_idl/stub_extension_manifest.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # The name specified by the 'Namespace' extended attribute should be set.
    self.assertEqual('ExampleManifestStub', schema['namespace'])

    # There should be two keys, one just a string and the other a sequence of a
    # custom type.
    manifest_keys = schema['manifest_keys']
    self.assertEqual(['string_key', 'item_list'], list(manifest_keys.keys()))
    self.assertEqual(
      {
        'type': 'string',
        'name': 'string_key',
      },
      manifest_keys['string_key'],
    )
    self.assertEqual(
      {'type': 'array', 'name': 'item_list', 'items': {'$ref': 'Item'}},
      manifest_keys['item_list'],
    )

    # In a manifest key stub schema, there are no functions or events.
    self.assertFalse(schema['functions'])
    self.assertFalse(schema['events'])

  # Tests that trying to load a "stub" manifest key schema fails if the
  # "ExtensionManifest" dictionary doesn't include the extended attribute to
  # specify the namespace name associated with it.
  def testManifestKeyStubMissingNamespaceName(self):
    expected_error_regex = (
      r'.* Error processing node Dictionary\(ExtensionManifest\):'
      r' ExtensionManifest stub schemas must specify a \[Namespace=...\]'
      r' extended attribute.'
    )
    self.assertRaisesRegex(
      SchemaCompilerError,
      expected_error_regex,
      web_idl_schema.Load,
      'test/web_idl/stub_extension_manifest_missing_namespace.idl',
    )

  # Tests that descriptions are correctly extracted for nodes where the
  # identifier or type might be pushed onto a new line due to length.
  def testLongIdentifierComments(self):
    loaded = web_idl_schema.Load('test/web_idl/long_identifiers.idl')
    self.assertEqual(1, len(loaded))
    schema = loaded[0]

    # Check dictionary member.
    long_identifier_type = getType(schema, 'LongIdentifierType')
    long_identifier_member = long_identifier_type['properties'][
      'longIdentifierMember'
    ]
    self.assertEqual(
      'Description for longIdentifierMember.',
      long_identifier_member.get('description'),
    )

    member_with_ext_attr = long_identifier_type['properties'][
      'memberWithExtendedAttribute'
    ]
    self.assertEqual(
      'Description for memberWithExtendedAttribute.',
      member_with_ext_attr.get('description'),
    )

    # Check event.
    long_identifier_event = getEvent(schema, 'onLongIdentifierEvent')
    self.assertEqual(
      'Description for onLongIdentifierEvent.',
      long_identifier_event.get('description'),
    )
    self.assertEqual(1, len(long_identifier_event['parameters']))
    self.assertEqual(
      'Description for param.',
      long_identifier_event['parameters'][0].get('description'),
    )

    # Check function.
    long_identifier_function = getFunction(schema, 'longIdentifierFunction')
    self.assertEqual(
      'Description for longIdentifierFunction.',
      long_identifier_function.get('description'),
    )
    self.assertEqual(1, len(long_identifier_function['parameters']))
    self.assertEqual(
      'Description for longIdentifierParameter.',
      long_identifier_function['parameters'][0].get('description'),
    )

  # Various tests for trailing callback parameters on function definitions.
  def testTrailingCallbackParameters(self):
    idl = web_idl_schema.Load('test/web_idl/trailing_callback_parameters.idl')
    self.assertEqual(1, len(idl))
    schema = idl[0]

    # Verifies an optional callback parameter is marked as optional and as not
    # supporting promises, along with various related descritiptions come
    # through as expected.
    foo_function = getFunction(schema, 'foo')
    self.assertEqual('Foo description.', foo_function.get('description'))
    self.assertEqual(1, len(foo_function['parameters']))
    self.assertEqual(
      'The arg.', foo_function['parameters'][0].get('description')
    )

    foo_async_return = getFunctionAsyncReturn(schema, 'foo')
    self.assertTrue(foo_async_return.get('does_not_support_promises'))
    self.assertTrue(foo_async_return.get('optional'))
    self.assertEqual('callback', foo_async_return.get('name'))
    self.assertEqual('The callback.', foo_async_return.get('description'))

    self.assertEqual(1, len(foo_async_return['parameters']))
    self.assertEqual(
      'The x value.', foo_async_return['parameters'][0].get('description')
    )

    # Verifies a required callback parameter isn't marked optional and is marked
    # as not supporting promises.
    bar_async_return = getFunctionAsyncReturn(schema, 'bar')
    self.assertTrue(bar_async_return.get('does_not_support_promises'))
    self.assertNotIn('optional', bar_async_return)

    # Verifies a function with both callback and a synchronous return gets both
    # of those processed as expected.
    sync_and_async_function = getFunction(schema, 'syncAndAsync')
    self.assertEqual(
      'Description for syncAndAsync.',
      sync_and_async_function.get('description'),
    )

    sync_return = getFunctionReturn(schema, 'syncAndAsync')
    self.assertEqual('integer', sync_return.get('type'))
    self.assertEqual('Sync return description.', sync_return.get('description'))

    async_return = getFunctionAsyncReturn(schema, 'syncAndAsync')
    self.assertTrue(async_return.get('does_not_support_promises'))
    self.assertEqual('callback', async_return.get('name'))
    self.assertEqual(
      'Async return description.', async_return.get('description')
    )

    # Verifies that the [trailingCallbackIsFunctionParameter] extended attribute
    # on a function leaves the trailing callback as a normal function parameter
    # and doesn't get turned into an async return.
    with_ext_attr_function = getFunction(schema, 'withExtAttr')
    self.assertEqual(
      'Description for withExtAttr.', with_ext_attr_function.get('description')
    )
    self.assertEqual(2, len(with_ext_attr_function['parameters']))
    self.assertEqual(
      'The arg.', with_ext_attr_function['parameters'][0].get('description')
    )
    self.assertEqual(
      'The callback as a normal parameter.',
      with_ext_attr_function['parameters'][1].get('description'),
    )
    self.assertEqual(
      'function', with_ext_attr_function['parameters'][1].get('type')
    )
    self.assertIsNone(getFunctionAsyncReturn(schema, 'withExtAttr'))

    # Verifies that a Callback definition with a trailing callback does not pop
    # it into returns_async. We test this by getting the type on a dictionary
    # member which references the callback.
    callback_type = getType(schema, 'CallbackDictionary')['properties'][
      'trailingCallback'
    ]
    self.assertEqual('function', callback_type.get('type'))
    self.assertEqual(1, len(callback_type['parameters']))
    self.assertEqual('callback', callback_type['parameters'][0].get('name'))
    self.assertEqual('function', callback_type['parameters'][0].get('type'))
    self.assertEqual(
      'The trailing callback.',
      callback_type['parameters'][0].get('description'),
    )
    self.assertNotIn('returns_async', callback_type)


if __name__ == '__main__':
  unittest.main()
