Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/idl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# WebIDL comparison

Compares the public API of the `webrtc` package with the WebIDL of the specifications it implements, taken from
`wpt/interfaces/`. Updating the WPT checkout brings in spec changes, and new differences fail the tests.

It needs the WPT checkout and PythonMonkey (see [tests/wpt/README.md](../wpt/README.md)) and Python 3.10 or later;
otherwise the tests are skipped. The IDL is parsed by `webidl2.js` of the checkout, the parser of idlharness, in a
child process.

## Running

```sh
uv run pytest tests/idl # compare every definition with expectations.json
uv run python -m tests.idl # print every difference, + for new ones and - for gone ones
uv run python -m tests.idl update # record the current differences as expected
```

## What is compared

`spec.FILES` lists the IDL files and which definitions to take from each. Every interface, dictionary and enum maps
to the object of the same name in `webrtc`:

- enums: the values.
- dictionaries: every member has a snake_case name and a camelCase alias, required members have no default, and
annotations name the IDL types the member refers to.
- interfaces: attributes and methods by name and alias, read-only or writable attributes, static and async methods
(a promise is a coroutine or returns a future), `on<event>` handlers against `_events`, maplike and iterable
declarations against the Python protocols.
- arguments of methods and constructors: names, order, optional and variadic ones, and types. A dictionary argument
is either one parameter or keyword parameters for its members, like `create_offer(*, ice_restart=False)`, whose
members are then checked one by one. Overloads match the one the signature fits best.
- public members the IDL doesn't have.

## Expectations

`expectations.json` lists the differences per definition. A definition fails when its differences change in either
direction, so after aligning the API or updating WPT, run `update` and commit the new expectations with the change.
6 changes: 6 additions & 0 deletions tests/idl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#
# Copyright 2026 Ilya (Marshal) <https://github.com/MarshalX>. All rights reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE.md file in the root of the project.
#
55 changes: 55 additions & 0 deletions tests/idl/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#
# Copyright 2026 Ilya (Marshal) <https://github.com/MarshalX>. All rights reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE.md file in the root of the project.
#

"""Command line for the IDL comparison.

python -m tests.idl print every difference, marking new (+) and gone (-) ones against expectations.json
python -m tests.idl update record the current differences as expected
"""

from __future__ import annotations

import argparse
import logging

import webrtc
from tests.idl import expectations
from tests.idl.compare import compare
from tests.idl.spec import load

logger = logging.getLogger(__name__)


def report(differences: dict[str, list[str]]) -> None:
expected = expectations.load()
for name in sorted(differences.keys() | expected.keys()):
changed = set(expectations.mismatches(expected.get(name, []), differences.get(name, [])))
items = sorted(set(differences.get(name, [])) | set(expected.get(name, [])))
logger.info(name)
for item in items:
mark = '+' if f'+ {item}' in changed else '-' if f'- {item}' in changed else ' '
logger.info(' %s %s', mark, item)
total = sum(map(len, differences.values()))
logger.info('%d differences in %d definitions', total, len(differences))


def main() -> None:
parser = argparse.ArgumentParser(prog='python -m tests.idl')
parser.add_argument('command', nargs='?', choices=['report', 'update'], default='report')
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format='%(message)s')

differences = compare(load(), webrtc)
if args.command == 'update':
expectations.save(differences)
logger.info('recorded %d differences', sum(map(len, differences.values())))
else:
report(differences)


if __name__ == '__main__':
main()
35 changes: 35 additions & 0 deletions tests/idl/child.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#
# Copyright 2026 Ilya (Marshal) <https://github.com/MarshalX>. All rights reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE.md file in the root of the project.
#

"""Parses IDL with webidl2.js of the WPT checkout, the parser idlharness uses.

Reads ``{file name: IDL text}`` as JSON from stdin and writes the JSON AST of every definition, with its file name.
"""

from __future__ import annotations

import json
import sys

import pythonmonkey as pm

from tests.idl.spec import WPT_ROOT


def main() -> None:
pm.eval((WPT_ROOT / 'resources' / 'webidl2' / 'lib' / 'webidl2.js').read_text())
parse = pm.eval('(text) => JSON.stringify(globalThis.WebIDL2.parse(text))')
definitions = []
for name, text in json.load(sys.stdin).items():
for definition in json.loads(parse(text)):
definition['file'] = name
definitions.append(definition)
json.dump(definitions, sys.stdout)


if __name__ == '__main__':
main()
Loading
Loading